find 命令的参数扩展
Parameter expansion for find command
考虑代码(变量 $i
在那里是因为它在一个循环中,向模式添加了几个条件,例如 *.a
和 *.b
,...但是说明这个问题只有一个通配符模式就足够了):
#!/bin/bash
i="a"
PATTERN="-name bar -or -name *.$i"
find . \( $PATTERN \)
如果 运行 位于包含文件 bar
和 foo.a
的文件夹上,它会工作,输出:
./foo.a
./bar
但是如果你现在在文件夹中添加一个新文件,即zoo.a
,那么它就不再起作用了:
find: paths must precede expression: zoo.a
大概是因为 *.$i
中的通配符被 shell 扩展为 foo.a zoo.a
,这导致无效的 find
命令模式。因此,一种修复尝试是在通配符模式周围加上引号。除非它不起作用:
带单引号 -- PATTERN="-name bar -or -name '*.$i'"
find
命令仅输出 bar
。转义单引号 (\'
) 会产生相同的结果。
同上双引号:PATTERN="-name bar -or -name \"*.$i\""
-- 仅返回 bar
。
在find
命令中,如果把$PATTERN
换成"$PATTERN"
,就会报错(单引号同样报错,只是单引号左右通配符模式):
查找:未知谓词-name bar -or -name "*.a"'
当然,将$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扩展,允许变量扩展,完美。
要真正深入研究细节,您需要在这里做两件事:
- 转义或引用
*
以防止 glob 扩展。
- 将
$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中我们可以看到操作顺序:
- 大括号展开
- 波浪号扩展、参数和变量扩展、算术扩展、命令替换和进程替换
- 分词
- 路径名(又名 glob)扩展
- 引用删除
让我们运行手动完成这些操作,看看find . \( $PATTERN \)
是如何解析的。最终结果将是一个字符串列表,因此我将使用类似于 JSON 的语法来显示每个阶段。我们将从包含单个字符串的列表开始:
['find . \( $PATTERN \)']
作为一个准备步骤,整个命令行都要进行分词。
['find', '.', '\(', '$PATTERN', '\)']
大括号扩展 -- 无变化。
变量展开
['find', '.', '\(', '-name bar -or -name "*.a"', '\)']
$PATTERN
被替换。目前它都是一个字符串,空格和所有。
分词
['find', '.', '\(', '-name', 'bar', '-or', '-name', '"*.a"', '\)']
shell扫描未在双引号内发生的变量展开结果进行分词。 $PATTERN
没有被引用,所以它被扩展了。现在它是一堆单独的单词。到目前为止一切顺利。
全局扩展
['find', '.', '\(', '-name', 'bar', '-or', '-name', '"*.a"', '\)']
Bash 扫描 glob 的分词结果。不是整个命令行,只是标记 -name
、bar
、-or
、-name
和 "*.a"
.
好像什么都没发生是吧?没那么快!人不可貌相。 Bash 实际执行了 glob 扩展。碰巧 glob 不匹配任何东西。但它可以...†
引用删除
['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
将被扩展、拆分和组合。它没有被双引号包围。它 包含 双引号并不重要。当那些双引号出现时为时已晚。分词已经完成,所以通配也完成了。
考虑代码(变量 $i
在那里是因为它在一个循环中,向模式添加了几个条件,例如 *.a
和 *.b
,...但是说明这个问题只有一个通配符模式就足够了):
#!/bin/bash
i="a"
PATTERN="-name bar -or -name *.$i"
find . \( $PATTERN \)
如果 运行 位于包含文件 bar
和 foo.a
的文件夹上,它会工作,输出:
./foo.a
./bar
但是如果你现在在文件夹中添加一个新文件,即zoo.a
,那么它就不再起作用了:
find: paths must precede expression: zoo.a
大概是因为 *.$i
中的通配符被 shell 扩展为 foo.a zoo.a
,这导致无效的 find
命令模式。因此,一种修复尝试是在通配符模式周围加上引号。除非它不起作用:
带单引号 --
PATTERN="-name bar -or -name '*.$i'"
find
命令仅输出bar
。转义单引号 (\'
) 会产生相同的结果。同上双引号:
PATTERN="-name bar -or -name \"*.$i\""
-- 仅返回bar
。在
find
命令中,如果把$PATTERN
换成"$PATTERN"
,就会报错(单引号同样报错,只是单引号左右通配符模式):查找:未知谓词
-name bar -or -name "*.a"'
当然,将$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扩展,允许变量扩展,完美。
要真正深入研究细节,您需要在这里做两件事:
- 转义或引用
*
以防止 glob 扩展。 - 将
$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 gotbar
). 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 thefind
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中我们可以看到操作顺序:
- 大括号展开
- 波浪号扩展、参数和变量扩展、算术扩展、命令替换和进程替换
- 分词
- 路径名(又名 glob)扩展
- 引用删除
让我们运行手动完成这些操作,看看find . \( $PATTERN \)
是如何解析的。最终结果将是一个字符串列表,因此我将使用类似于 JSON 的语法来显示每个阶段。我们将从包含单个字符串的列表开始:
['find . \( $PATTERN \)']
作为一个准备步骤,整个命令行都要进行分词。
['find', '.', '\(', '$PATTERN', '\)']
大括号扩展 -- 无变化。
变量展开
['find', '.', '\(', '-name bar -or -name "*.a"', '\)']
$PATTERN
被替换。目前它都是一个字符串,空格和所有。分词
['find', '.', '\(', '-name', 'bar', '-or', '-name', '"*.a"', '\)']
shell扫描未在双引号内发生的变量展开结果进行分词。
$PATTERN
没有被引用,所以它被扩展了。现在它是一堆单独的单词。到目前为止一切顺利。全局扩展
['find', '.', '\(', '-name', 'bar', '-or', '-name', '"*.a"', '\)']
Bash 扫描 glob 的分词结果。不是整个命令行,只是标记
-name
、bar
、-or
、-name
和"*.a"
.好像什么都没发生是吧?没那么快!人不可貌相。 Bash 实际执行了 glob 扩展。碰巧 glob 不匹配任何东西。但它可以...†
引用删除
['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
将被扩展、拆分和组合。它没有被双引号包围。它 包含 双引号并不重要。当那些双引号出现时为时已晚。分词已经完成,所以通配也完成了。