在 unix/linux shell 中进行模式匹配时如何使用反通配符或负通配符?
-
03-07-2019 - |
题
假设我想复制目录的内容,不包括名称中包含“音乐”一词的文件和文件夹。
cp [exclude-matches] *Music* /target_directory
应该用什么来代替 [exclude-matches] 来实现此目的?
解决方案
在Bash中,您可以通过启用 \ textglob
选项来完成此操作(将 ls
替换为 cp
并添加目标目录,当然)
~/foobar> shopt extglob
extglob off
~/foobar> ls
abar afoo bbar bfoo
~/foobar> ls !(b*)
-bash: !: event not found
~/foobar> shopt -s extglob # Enables extglob
~/foobar> ls !(b*)
abar afoo
~/foobar> ls !(a*)
bbar bfoo
~/foobar> ls !(*foo)
abar bbar
稍后您可以使用
禁用extglobshopt -u extglob
其他提示
这 extglob
shell 选项为您在命令行中提供更强大的模式匹配。
你用以下命令打开它 shopt -s extglob
, ,然后将其关闭 shopt -u extglob
.
在您的示例中,您最初会执行以下操作:
$ shopt -s extglob
$ cp !(*Music*) /target_directory
完整可用 分机结束了 全局bing 运算符是(摘自 man bash
):
如果使用Shopt内置启用ExtGlob Shell选项,则可以识别几个扩展模式匹配的操作员。在以下描述中,PAT Tern-List是一个或多个由A |隔开的模式的列表。可以使用以下一个或多个子图案形成复合模式:
- ?(模式列表)
匹配零次或一次出现的给定模式- *(模式列表)
匹配零次或多次出现的给定模式- +(模式列表)
匹配给定模式的一次或多次出现- @(模式列表)
匹配给定模式之一- !(模式列表)
匹配除给定模式之一之外的任何内容
因此,例如,如果您想列出当前目录中不存在的所有文件 .c
或者 .h
文件,你会这样做:
$ ls -d !(*@(.c|.h))
当然,普通的 shell globing 是可以工作的,所以最后一个例子也可以写成:
$ ls -d !(*.[ch])
不是bash(我知道),但是:
cp `ls | grep -v Music` /target_directory
我知道这不是你想要的,但它会解决你的例子。
如果你想避免使用exec命令的mem成本,我相信你可以用xargs做得更好。我认为以下是
更有效的替代方案find foo -type f ! -name '*Music*' -exec cp {} bar \; # new proc for each exec
find . -maxdepth 1 -name '*Music*' -prune -o -print0 | xargs -0 -i cp {} dest/
在bash中, shopt -s \ textglob
的替代方法是 GLOBIGNORE
变量。它并不是更好,但我发现它更容易记住。
可能是原始海报想要的一个例子:
GLOBIGNORE="*techno*"; cp *Music* /only_good_music/
完成后,取消设置GLOBIGNORE
,以便能够在源目录中 rm * techno *
。
您还可以使用非常简单的 for
循环:
for f in `find . -not -name "*Music*"`
do
cp $f /target/dir
done
我个人的偏好是使用grep和while命令。这允许您编写功能强大且可读的脚本,确保您最终完成您想要的操作。另外,通过使用echo命令,您可以在执行实际操作之前执行空运行。例如:
ls | grep -v "Music" | while read filename
do
echo $filename
done
将打印出您最终要复制的文件。如果列表正确,则下一步是使用copy命令替换echo命令,如下所示:
ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done
可以通过find找到一个解决方案。
$ mkdir foo bar
$ touch foo/a.txt foo/Music.txt
$ find foo -type f ! -name '*Music*' -exec cp {} bar \;
$ ls bar
a.txt
查找有很多选项,您可以非常具体地了解您包含和排除的内容。
编辑:Adam在评论中指出这是递归的。查找选项mindepth和maxdepth可用于控制它。
我还没有在这里看到的一个技巧,那就是不使用 extglob
, find
或 grep
来处理两个文件使用 comm
列出为集合和" diff" :
comm -23 <(ls) <(ls *Music*)
comm
优于 diff
,因为它没有额外的内容。
这将返回第1组 ls
中的所有元素, not 也在第2组 ls * Music *
中。这要求两个集合按排序顺序才能正常工作。 ls
和glob扩展没问题,但是如果你使用 find
之类的东西,请务必调用 sort
。
comm -23 <(find . | sort) <(find . | grep -i '.jpg' | sort)
可能有用。
以下作品列出了当前目录中的所有 * .txt
文件,但以数字开头的文件除外。
这适用于 bash
, dash
, zsh
以及所有其他POSIX兼容的shell。
for FILE in /some/dir/*.txt; do # for each *.txt file
case "${FILE##*/}" in # if file basename...
[0-9]*) continue ;; # starts with digit: skip
esac
## otherwise, do stuff with $FILE here
done
-
在第一行中,模式
/ some / dir / * .txt
将导致for
循环迭代/ some /中的所有文件dir
,其名称以.txt
。 结尾
-
在第二行中,case语句用于清除不需要的文件。 &#8211;
$ {FILE ## * /}
表达式从文件名中删除任何前导目录名称组件(此处为/ some / dir /
),以便模式只能匹配文件的基名。 (如果您只是根据后缀清除文件名,则可以将其缩短为$ FILE
。) -
在第三行中,将跳过与
case
模式[0-9] *
)行匹配的所有文件(continue
语句跳转到for
循环的下一次迭代。 &#8211;如果你愿意,你可以在这里做一些更有趣的事情,例如比如使用[!a-z] *
跳过所有不以字母(&#8211; z)开头的文件,或者你可以使用多种模式跳过几种文件名,例如[0-9] * | * .bak
跳过文件.bak
文件,以及不以数字开头的文件。
醇>
这样做可以完全排除'音乐'
cp -a ^'Music' /target
这和那些排除音乐等内容?*或*?音乐
cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target