在bash中扩展星号

时间:2015-02-24 15:12:45

标签: bash parsing escaping glob

我正在尝试运行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非常相似,但是,针对该问题发布的解决方案似乎存在我在上面列出的问题。

2 个答案:

答案 0 :(得分:5)

安全的方法是明确构建数组:

#!/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时,您的问题似乎与您无法处理的通配符完全消失。
  • 当然,我给出的方法也适用于通配符。例如,如果要在名为baz的子目录中排除/修剪名为foo的所有子目录,则由

    指定的skipdirs数组
    skipdirs=( "./*/foo/baz" "./*/foo/*/baz" )
    

    工作正常!

答案 1 :(得分:4)

这里的问题是,您在"%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

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

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