首页 > 解决方案 > find 命令的参数扩展

问题描述

考虑代码(变量$i存在是因为它在一个循环中,向模式添加了几个条件,例如*.aand *.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

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

当然,替换$PATTERN'$PATTERN'不起作用......(不会发生任何扩展)。

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

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

这可以正常工作:

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

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

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) 以这种方式使用数组应该扩展«到所有单独引用的元素»。如何使用变量执行此操作(参见我的第一次尝试)?让它发挥作用后,我回去尝试再次使用一个变量,用黑斜线单引号或\\',但没有任何效果(我刚得到bar)。我必须做些什么来模拟“手动”,使用数组时完成的引用?

预先感谢您的帮助。

标签: arraysbashfindvariable-expansion

解决方案


必读:

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

您需要使用某种形式的引用来防止 shell 对*. 变量未在单引号中展开,因此'*.$i'不起作用。它确实抑制了全局扩展,但它也阻止了变量扩展。"*.$i"抑制全局扩展但允许变量扩展,这是完美的。

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

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

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

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

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

最终结果是,以下任何一项都可以工作:

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

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

b) 以这种方式使用数组应该扩展«到所有单独引用的元素»。如何使用变量执行此操作(参见我的第一次尝试)?让它发挥作用后,我回去尝试再次使用一个变量,用黑斜线单引号或\\',但没有任何效果(我刚得到bar)。我必须做些什么来模拟“手动”,使用数组时完成的引用?

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

您能否更详细地解释一下,为什么...PATTERN="-name bar -or -name \"*.$i\""不起作用?当find实际运行命令时,带引号的双引号应该展开$i而不是 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 是如何进行命令行扩展的。在“扩展”部分下的Bash 手册页中,我们可以看到操作顺序:

  1. 大括号扩展
  2. 波浪号扩展、参数和变量扩展、算术扩展、命令替换和进程替换
  3. 分词
  4. 路径名(AKA 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。不是整个命令行,只是标记-name, bar, -or, -name, 和"*.a".

    好像什么都没发生,是吗?没那么快!人不可貌相。Bash 实际上执行了全局扩展。只是碰巧 glob 不匹配任何东西。但它可以... †</sup>

  5. 报价移除

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

    反斜杠消失了。但是双引号仍然存在

    在前面的扩展之后,所有未引用的字符\, ', 和" 不是由上述扩展之一产生的都将被删除。

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

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

TL;DR —变量内的引号与变量的引号的解析方式不同。


†</sup> 前四个标记没有特殊字符。但最后一个"*.a",,,。该星号是通配符。如果您仔细阅读手册页的“路径名扩展”部分,您会发现没有提及忽略引号。双引号保护星号。

不挂断!什么?我认为引号会抑制全局扩展!

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

$ 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 被扩展并匹配(诚然做作的)文件名!

为什么引号没有帮助?嗯,解释很微妙,也很棘手。手册页说:

分词后... bash 扫描每个单词中的字符*,?[.

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

如果您写ls "foo*"引号,请防止foo*被拆分和通配。但是,如果您编写,ls $var则将$var被扩展、拆分和全局化。它没有被双引号包围。它包含双引号并不重要。当这些双引号出现时,为时已晚。已经执行了分词,因此也完成了通配。


推荐阅读