在 unix/linux shell 中进行模式匹配时,如何使用反向或负通配符?

Posted

技术标签:

【中文标题】在 unix/linux shell 中进行模式匹配时,如何使用反向或负通配符?【英文标题】:How can I use inverse or negative wildcards when pattern matching in a unix/linux shell? 【发布时间】:2010-09-18 00:51:22 【问题描述】:

假设我想复制一个目录的内容,不包括名称中包含“音乐”一词的文件和文件夹。

cp [exclude-matches] *Music* /target_directory

应该用什么代替 [exclude-matches] 来完成此操作?

【问题讨论】:

【参考方案1】:

在 Bash 中,您可以通过启用 extglob 选项来做到这一点,就像这样(将 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

您可以稍后禁用 extglob

shopt -u extglob

【讨论】:

我喜欢这个功能:ls /dir/*/!(base*) 如何包含所有内容 () 并排除 !(b)? 你将如何匹配所有以f 开头的内容,foo 除外? 为什么默认禁用? shopt -o -u histexpand 如果您需要查找带有感叹号的文件 - 默认情况下打开,默认情况下 extglob 处于关闭状态,因此它不会干扰 histexpand,在文档中它解释了为什么会这样。匹配以 f 开头的所有内容,除了 foo:f!(oo),当然 'food' 仍然匹配(你需要 f!(oo*) 来停止以 'foo' 开头的东西,或者,如果你想摆脱某些以 '.foo' 结尾的东西使用 !(.foo) 或前缀:myprefix!(.foo) (匹配 myprefixBLAH 但不匹配 myprefixBLAH.foo)【参考方案2】:

extglob shell 选项在命令行中为您提供更强大的模式匹配。

你用shopt -s extglob打开它,用shopt -u extglob关闭它。

在您的示例中,您最初会这样做:

$ shopt -s extglob
$ cp !(*Music*) /target_directory

全部可用的ext结束的globbing 运算符是(摘自man bash):

如果使用内置 shopt 启用了 extglob shell 选项,则有几个扩展的 可以识别模式匹配运算符。模式列表是由 | 分隔的一个或多个模式的列表。可以使用以下一个或多个子模式形成复合模式:

?(模式列表) 匹配零次或一次给定模式的出现 *(模式列表) 匹配给定模式的零次或多次出现 +(模式列表) 匹配给定模式的一次或多次出现 @(模式列表) 匹配给定模式之一 !(模式列表) 匹配除给定模式之一之外的任何内容

因此,例如,如果您想列出当前目录中不是.c.h 文件的所有文件,您可以这样做:

$ ls -d !(*@(.c|.h))

当然,正常的 shell globing 是有效的,所以最后一个例子也可以写成:

$ ls -d !(*.[ch])

【讨论】:

-d是什么原因? @Koveras 用于.c.h 文件之一是目录的情况。 @DaveKennedy 是列出当前目录D中的所有内容,而不是列出目录D中可能包含的子目录的内容。【参考方案3】:

不在 bash 中(据我所知),但是:

cp `ls | grep -v Music` /target_directory

我知道这不是您想要的,但它会解决您的示例。

【讨论】:

默认 ls 将每行放置多个文件,这可能不会给出正确的结果。 仅当 stdout 是终端时。在管道中使用时,ls 每行打印一个文件名。 ls 仅在输出到终端时每行放置多个文件。自己尝试一下——“ls | less”每行永远不会有多个文件。 它不适用于包含空格(或其他空白字符)的文件名。【参考方案4】:

如果你想避免使用 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/

【讨论】:

【参考方案5】:

我还没有在这里看到的不使用extglobfindgrep 的技巧是将两个文件列表视为集合并“diff”使用comm:

comm -23 <(ls) <(ls *Music*)

commdiff 更可取,因为它没有多余的东西。

这将返回集合 1 ls 的所有元素,它们也在集合 2 ls *Music* 中。这需要两个集合都按排序顺序才能正常工作。 ls 和 glob 扩展没有问题,但如果您使用类似 find 的东西,请务必调用 sort

comm -23 <(find . | sort) <(find . | grep -i '.jpg' | sort)

可能有用。

【讨论】:

排除的好处之一就是一开始就不会遍历目录。此解决方案对子目录进行 两次 遍历 - 一次包含排除项,一次不包含。 非常好,@MarkStosberg。虽然,这种技术的一个附带好处是您可以从实际文件中读取排除项,例如comm -23 &lt;(ls) exclude_these.list【参考方案6】:

在 bash 中,shopt -s extglob 的替代方法是 GLOBIGNORE variable。它并不是真的更好,但我发现它更容易记住。

可能是原始海报想要的示例:

GLOBIGNORE="*techno*"; cp *Music* /only_good_music/

完成后,unset GLOBIGNORE 可以在源目录中rm *techno*

【讨论】:

【参考方案7】:

您也可以使用一个非常简单的for 循环:

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done

【讨论】:

这会进行递归查找,这与 OP 想要的行为不同。 使用-maxdepth 1 进行非递归? 我发现这是最干净的解决方案,无需启用/禁用 shell 选项。在这篇文章中建议使用 -maxdepth 选项以获得 OP 所需的结果,但这完全取决于您要完成的工作。 在反引号中使用 find 如果发现任何不平凡的文件名,将会以令人不快的方式中断。 它使用 2 个循环,永远不要使用它。使用 find 使用 -exec,例如 find . -not -name "*Music*" -exec cp "" /target/dir \;【参考方案8】:

我个人的偏好是使用 grep 和 while 命令。这允许人们编写功能强大但可读的脚本,以确保您最终完全按照您的意愿行事。另外,通过使用 echo 命令,您可以在执行实际操作之前执行空运行。例如:

ls | grep -v "Music" | while read filename
do
echo $filename
done

将打印出您最终要复制的文件。如果列表正确,下一步就是简单地将 echo 命令替换为 copy 命令,如下所示:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done

【讨论】:

只要您的文件名没有任何制表符、换行符、连续多个空格或任何反斜杠,这将起作用。虽然这些都是病态病例,但最好意识到这种可能性。在 bash 你可以使用 while IFS='' read -r filename ,但是换行仍然是个问题。一般情况下最好不要使用ls枚举文件;像find 这样的工具更适合。 无需任何额外工具:for file in *; do case $file in (*Music*) ;; (*) cp "$file" /target_directory ; echo ;; esac; done mywiki.wooledge.org/ParsingLs 列出了一些您应该避免这种情况的其他原因。【参考方案9】:

使用 find 可以找到一个解决方案。

$ mkdir foo bar
$ touch foo/a.txt foo/Music.txt
$ find foo -type f ! -name '*Music*' -exec cp  bar \;
$ ls bar
a.txt

Find 有很多选项,您可以非常具体地了解包含和排除的内容。

编辑:cmets 中的 Adam 指出这是递归的。查找选项 mindepth 和 maxdepth 可用于控制这一点。

【讨论】:

这会进行递归复制,这是不同的行为。它还会为每个文件生成一个新进程,这对于大量文件来说效率非常低。 与复制每个文件生成的所有 IO 相比,生成进程的成本几乎为零。所以我想说这对于偶尔使用来说已经足够了。 进程生成的一些解决方法:***.com/questions/186099/… 使用“-maxdepth 1”来避免递归。 使用反引号得到shell通配符扩展的类比:cp find -maxdepth 1 -not -name '*Music*' /target_directory【参考方案10】:

以下作品列出了当前目录中的所有*.txt文件,除了以数字开头的文件。

这适用于bashdashzsh 和所有其他与 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 语句用于清除不需要的文件。 – $FILE##*/ 表达式从文件名(此处为 /some/dir/)中去除任何前导目录名称组件,以便模式只能匹配文件的基本名称。 (如果您只是根据后缀清除文件名,则可以将其缩短为 $FILE。)

    在第三行中,所有匹配case 模式[0-9]*) 行的文件都将被跳过(continue 语句跳转到for 循环的下一次迭代)。 – 如果你愿意,你可以在这里做一些更有趣的事情,例如比如使用[!a-z]*跳过所有不以字母(a-z)开头的文件,或者您可以使用多种模式来跳过几种文件名,例如[0-9]*|*.bak 跳过 .bak 文件和不以数字开头的文件。

【讨论】:

噢!有一个错误(我匹配*.txt 而不仅仅是*)。现已修复。【参考方案11】:

这将完全排除“音乐”

cp -a ^'Music' /target

这个和那个用于排除诸如音乐之类的东西?*或*?音乐

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target

【讨论】:

MacOS 上的cp 手册页有一个-a 选项,但它的作用完全不同。哪个平台支持这个?

以上是关于在 unix/linux shell 中进行模式匹配时,如何使用反向或负通配符?的主要内容,如果未能解决你的问题,请参考以下文章

shell学习笔记

Unix/Linux shell脚本中 “set -e” 的作用

Unix/Linux shell脚本中 “set -e” 的作用

2 Powershell与Cmd以及Unix/Linux Shell

Linux Shell脚本简介

linux shell都有哪些 类型