find 命令的参数扩展

Parameter expansion for find command

考虑代码(变量 $i 在那里是因为它在一个循环中,向模式添加了几个条件,例如 *.a*.b,...但是说明这个问题只有一个通配符模式就足够了):

#!/bin/bash

i="a"
PATTERN="-name bar -or -name *.$i"
find . \( $PATTERN \)

如果 运行 位于包含文件 barfoo.a 的文件夹上,它会工作,输出:

./foo.a
./bar

但是如果你现在在文件夹中添加一个新文件,即zoo.a,那么它就不再起作用了:

find: paths must precede expression: zoo.a

大概是因为 *.$i 中的通配符被 shell 扩展为 foo.a zoo.a,这导致无效的 find 命令模式。因此,一种修复尝试是在通配符模式周围加上引号。除非它不起作用:

当然,将$PATTERN替换为'$PATTERN'也不行...(不会发生任何扩展)。

我让它工作的唯一方法是使用... eval!

FINDSTR="find . \( $PATTERN \)"
eval $FINDSTR

这正常工作:

./zoo.a
./foo.a
./bar

经过大量谷歌搜索,我看到它多次提到要做这种事情,应该使用arrays。但这不起作用:

i="a"
PATTERN=( -name bar -or -name '*.$i' )
find . \( "${PATTERN[@]}" \)

# result: ./bar

find 行中,数组必须用双引号括起来,因为我们 想要 它被扩展。但是通配符表达式周围的单引号不起作用,而且根本没有引号:

i="a"
PATTERN=( -name bar -or -name *.$i )
find . \( "${PATTERN[@]}" \)

# result: find: paths must precede expression: zoo.a

但双引号确实有效!!

i="a"
PATTERN=( -name bar -or -name "*.$i" )
find . \( "${PATTERN[@]}" \)

# result:
# ./zoo.a
# ./foo.a
# ./bar

所以我想我的问题实际上是两个问题:

a) 在最后一个使用数组的示例中,为什么 *.$i 周围需要双引号?

b) 以这种方式使用数组应该会扩展 «to all elements individually quoted»。如何使用变量来做到这一点(参见我的第一次尝试)?使其运行后,我返回并尝试再次使用变量,使用黑斜杠单引号或 \',但没有任何效果(我刚得到 bar)。我需要做些什么来模拟 "by hand" 使用数组时的引用?

预先感谢您的帮助。

必读:

a) in this last example using arrays, why are double quotes required around the *.$i?

您需要使用某种形式的引号来防止 shell 在 * 上执行 glob 扩展。变量没有用单引号展开,所以 '*.$i' 不起作用。它确实抑制了 glob 扩展,但它也阻止了变量扩展。 "*.$i"禁止glob扩展,允许变量扩展,完美。

要真正深入研究细节,您需要在这里做两件事:

  1. 转义或引用 * 以防止 glob 扩展。
  2. $i视为变量扩展,但引用它以防止分词和glob扩展。

任何形式的引用都适用于第 1 项:\*"*"'*'$'*' 都是可以接受的方式,以确保将其视为文字星号。

对于第 2 项,双引号是唯一的答案。裸露的 $i 会受到单词拆分和通配符的影响——如果您有 i='foo bar'i='foo*',空格和通配符会导致问题。 $i'$i' 都按字面意思对待美元符号,所以他们出局了。

"$i" 是唯一正确的引用。这就是为什么常见的 shell 建议是 总是双引号变量扩展 .

最终结果是,以下任何一种都可行:

"*.$i"
\*."$i"
'*'."$i"
"*"."$i"
'*.'"$i"

显然,第一个是最简单的。

b) using an array in this way is supposed to expand «to all elements individually quoted». How would do this with a variable (cf my first attempt)? After getting this to function, I went back and tried using a variable again, with blackslashed single quotes, or \', but nothing worked (I just got bar). What would I have to do to emulate "by hand" as it were, the quoting done when using arrays?

你必须用 eval 拼凑一些东西,但这很危险。从根本上说,数组比简单的字符串变量更强大。没有引号和反斜杠的神奇组合可以让您做数组可以做的事情。数组是完成这项工作的正确工具。

Could you explain in a little more detail, why ... PATTERN="-name bar -or -name \"*.$i\"" does not work? The quoted double quotes should, when the find command is actually ran, expand the $i but not the glob.

当然可以。假设我们写:

i=a
PATTERN="-name bar -or -name \"*.$i\""
find . \( $PATTERN \)

前两行运行之后,$PATTERN的值是多少?让我们检查一下:

$ i=a
$ PATTERN="-name bar -or -name \"*.$i\""
$ printf '%s\n' "$PATTERN"
-name bar -or -name "*.a"

您会注意到 $i 已被替换为 a,并且反斜杠已被删除。

现在让我们看看find命令是如何解析的。在最后一行 $PATTERN 没有被引用,因为我们希望所有的词都被分开,对吧?如果你写一个裸变量名 Bash 最终会执行一个隐含的 split+glob 操作。它执行分词和全局扩展。这到底是什么意思?

我们来看看Bash是如何进行命令行扩展的。在"Expansion"段下的Bash man page中我们可以看到操作顺序:

  1. 大括号展开
  2. 波浪号扩展、参数和变量扩展、算术扩展、命令替换和进程替换
  3. 分词
  4. 路径名(又名 glob)扩展
  5. 引用删除

让我们运行手动完成这些操作,看看find . \( $PATTERN \)是如何解析的。最终结果将是一个字符串列表,因此我将使用类似于 JSON 的语法来显示每个阶段。我们将从包含单个字符串的列表开始:

['find . \( $PATTERN \)']

作为一个准备步骤,整个命令行都要进行分词。

['find', '.', '\(', '$PATTERN', '\)']
  1. 大括号扩展 -- 无变化。

  2. 变量展开

    ['find', '.', '\(', '-name bar -or -name "*.a"', '\)']
    

    $PATTERN 被替换。目前它都是一个字符串,空格和所有。

  3. 分词

    ['find', '.', '\(', '-name', 'bar', '-or', '-name', '"*.a"', '\)']
    

    shell扫描未在双引号内发生的变量展开结果进行分词。 $PATTERN 没有被引用,所以它被扩展了。现在它是一堆单独的单词。到目前为止一切顺利。

  4. 全局扩展

    ['find', '.', '\(', '-name', 'bar', '-or', '-name', '"*.a"', '\)']
    

    Bash 扫描 glob 的分词结果。不是整个命令行,只是标记 -namebar-or-name"*.a".

    好像什么都没发生是吧?没那么快!人不可貌相。 Bash 实际执行了 glob 扩展。碰巧 glob 不匹配任何东西。但它可以...

  5. 引用删除

    ['find', '.', '(', '-name', 'bar', '-or', '-name', '"*.a"', ')']
    

    反斜杠不见了。但是双引号仍然存在.

    After the preceding expansions, all unquoted occurrences of the characters \, ', and " that did not result from one of the above expansions are removed.

这就是最终结果。双引号仍然存在,因此不是搜索名为 *.a 的文件,而是搜索名称中带有文字双引号字符的名为 "*.a" 的文件。该搜索注定会失败。

添加一对转义引号 \" 根本没有达到我们想要的效果。引号并没有像预期的那样消失并中断了搜索。不仅如此,他们也没有像他们应该的那样抑制 globbing。

TL;DR — 引用 inside 变量的解析方式与 outside 变量的解析方式不同。


前四个标记没有特殊字符。但最后一个 "*.a" 确实如此。那个星号是一个通配符。如果您仔细阅读手册页的 "pathname expansion" 部分,您会发现没有提到忽略引号。双引号保护星号。

等一下!什么?我认为引号会抑制 glob 扩展!

他们这样做——通常如此。如果您手写引号,它们确实会阻止 glob 扩展。但是如果你把它们放在一个不带引号的变量中,它们就不会。

$ touch 'foobar' '"foobar"'
$ ls
foobar   "foobar"
$ ls foo*
foobar
$ ls "foo*"
ls: foo*: No such file or directory
$ var="\"foo*\""
$ echo "$var"
"foo*"
$ ls $var
"foobar"

仔细阅读。如果我们创建一个名为 "foobar" 的文件——也就是说,它的文件名中有文字双引号——然后 ls $var 打印 "foobar"。 glob 被扩展并匹配(公认的人为的)文件名!

为什么引用没有帮助?好吧,这个解释很微妙,也很棘手。手册页说:

After word splitting ... bash scans each word for the characters *, ?, and [.

任何时候 Bash 执行单词拆分 它也会扩展 globs。还记得我说过不带引号的变量受隐含的 split+glob 运算符的影响吗?这就是我的意思。拆分和通配齐头并进。

如果你写 ls "foo*" 引号可以防止 foo* 受到拆分和通配。但是,如果您编写 ls $var,那么 $var 将被扩展、拆分和组合。它没有被双引号包围。它 包含 双引号并不重要。当那些双引号出现时为时已晚。分词已经完成,所以通配也完成了。