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" )
会很好用!
我正在尝试 运行 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" )
会很好用!