bash 中的扩展星号

Expanding asterisk in bash

我正在尝试 运行 find,并排除数组中列出的几个目录。不过,我在扩展时发现了一些奇怪的行为,这导致了我的问题:

~/tmp> skipDirs=( "./dirB" "./dirC" )
~/tmp> bars=$(find . -name "bar*" -not \( -path "${skipDirs[0]}/*" $(printf -- '-o -path "%s/\*" ' "${skipDirs[@]:1}") \) -prune); echo $bars
./dirC/bar.txt ./dirA/bar.txt

这并没有像我预期的那样跳过 dirC。问题是打印扩展了 "./dirC".

周围的引号
~/tmp> set -x 
+ set -x
~/tmp> bars=$(find . -name "bar*" -not \( -path "${skipDirs[0]}/*" $(printf -- '-o -path "%s/*" ' "${skipDirs[@]:1}") \) -prune); echo $bars
+++ printf -- '-o -path "%s/*" ' ./dirC
++ find . -name 'bar*' -not '(' -path './dirB/*' -o -path '"./dirC/*"' ')' -prune
+ bars='./dirC/bar.txt
./dirA/bar.txt'
+ echo ./dirC/bar.txt ./dirA/bar.txt
./dirC/bar.txt ./dirA/bar.txt

如果我尝试删除 $(print..) 中的引号,那么 * 会立即展开,这也会给出错误的结果。最后,如果我删除引号并尝试转义 *,那么 \ 转义字符将作为文件名的一部分包含在查找中,这也不起作用。我想知道为什么上面的方法不起作用,什么会起作用?如果可能,我尽量避免使用 eval,但目前我没有找到解决方法。

注意:这与:Finding directories with find in bash using a exclude list非常相似,但是,该问题的已发布解决方案似乎存在我上面列出的问题。

这里的问题是您在 "%s/*" 上使用的引号与您认为的不一样。

也就是说,您认为您需要 "%s/*" 上的引号来防止 printf 的结果被全局化,但事实并非如此。在没有目录分隔符和以双引号开头和结尾的文件中尝试同样的事情,你就会明白我的意思。

$ ls
"dirCfoo"
$ skipDirs=( "dirB" "dirC" )
$ printf '%s\n' -- -path "${skipDirs[0]}*" $(printf -- '-o -path "%s*" ' "${skipDirs[@]:1}")
-path
dirB*
-o
-path
"dirCfoo"
$ rm '"dirCfoo"'
$ printf -- '%s\n' -path "${skipDirs[0]}*" $(printf -- '-o -path "%s*" ' "${skipDirs[@]:1}")
-path
dirB*
-o
-path
"dirC*"

明白我的意思了吗? shell 没有专门处理引号。他们只是碰巧不会出现在你的情况下。

这个问题是 http://mywiki.wooledge.org/BashFAQ/050 中讨论的内容不起作用的部分原因。

要在这里做你想做的事,我相信你需要手动创建查找参数数组。

sD=(-path /dev/null)
for dir in "${skipDirs}"; do
    sD+=(-o -path "$dir")
done

然后在find命令行(-not \( "${sD[@]}" \)左右)展开“${sD[@]}”。

是的,我相信这会使您链接到的答案不正确(尽管另一个答案可能有效(对于非空白等文件),因为正在进行数组间接寻址。

安全的方法是显式构建数组:

#!/bin/bash

skipdirs=( "./dirB" "./dirC" )

skipdirs_args=( -false )
for i in "${skipdirs[@]}"; do
    args+=( -o -type d -path "$i" )
done

find . \! \( \( "${skipdirs_args[@]}" \) -prune \) -name 'bar*'

我稍微修改了你发现的逻辑,因为你在那里有一个轻微的(逻辑)错误:你的命令是:

find -name 'bar*' -not stuff_to_prune_the_dirs

find如何进行?它将解析文件树,当它找到匹配 bar* 的文件(或目录)时,它将应用 -not ... 部分。那真的不是你想要的!您的 -prune 永远不会被应用!

看看这个:

find . \! \( -type d -path './dirA' -prune \)

这里 find 将完全删除目录 ./dirA 并打印其他所有内容。现在 您要应用过滤器 -name 'bar*' 的顺序非常重要!这之间有很大的区别:

find . -name 'bar*' \! \( -type d -path './dirA' -prune \)

还有这个:

find . \! \( -type d -path './dirA' -prune \) -name 'bar*'

第一个根本没有按预期工作!第二个就好了

备注.

  • 我正在使用 \! 而不是 -not,因为 \! 是 POSIX,-not 是 POSIX 未指定的扩展名.你会争辩说 -path 也不是 POSIX,所以使用 -not 并不重要。就这么个细节,随便你怎么用
  • 您必须使用一些肮脏的技巧来构建您的命令以跳过您的目录,因为您必须将第一个术语与另一个术语分开考虑。通过使用 -false 初始化数组,我不必特别考虑任何术语。
  • 我正在指定 -type d 以确保我正在修剪目录。
  • 由于我的修剪确实适用于目录,因此我不必在我的排除条款中包含通配符。这很有趣:当您如上所述适当地使用 find 时,您的问题似乎与您无法处理的通配符有关。
  • 当然,我给的方法真的也适用于通配符。例如,如果您想要 exclude/prune 所有名为 baz 的子目录位于名为 foo 的子目录中,则由

    给出的 skipdirs 数组
    skipdirs=( "./*/foo/baz" "./*/foo/*/baz" )
    

    会很好用!