查找命令的参数扩展

时间:2018-12-07 10:07:04

标签: arrays bash find variable-expansion

考虑一下代码(变量$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="-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

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

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)。我该怎么做才能像“手工”一样模仿使用数组时的引用?

预先感谢您的帮助。

1 个答案:

答案 0 :(得分:2)

必读:

  

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

您需要使用某种形式的引号来防止Shell对*执行glob扩展。变量不会用单引号引起来,因此'*.$i'不起作用。它确实抑制了球体膨胀,但也停止了变量膨胀。 "*.$i"禁止全局扩展,但允许变量扩展,这是完美的。

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

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

对于项目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而不是全局名称。

好的。假设我们写:

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 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扩展。碰巧,这个问题与任何事物都不匹配。但这可能...

  5. 删除行情

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

    反斜杠不见了。但是双引号还在那里

      

    在前面的扩展之后,不是上述扩展之一引起的字符\'" 的所有未加引号的出现删除。

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

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

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


前四个标记没有特殊字符。但是最后一个"*.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每次执行单词拆分操作它还会扩展单词范围。还记得我怎么说无引号的变量受隐式 split + glob 运算符的约束吗?这就是我的意思。分裂和球体并存。

如果您写ls "foo*",则引号可防止foo*分裂和浮现。但是,如果您编写ls $var,则$var会被展开,拆分和扩展。它没有被双引号引起来。 包含双引号并不重要。等到双引号出现时,为时已晚。单词拆分已经完成,因此遍历也完成了。