我搜索过类似问题,但没有找到一个非常适合我的情况。
下面是一个非常简短的脚本,演示了我面临的问题:
#!/bin/bash
includeString="-wholename './public_html/*' -o -wholename './config/*'"
find . ( $includeString ) -type f -mtime -7 -print
基本上,我们需要在文件夹中搜索,但只能在其某些子文件夹中搜索。在我的较长脚本中,includeString是从一个数组中构建的。对于这个演示,我保持简单。
基本上,当我运行脚本时,它什么也找不到。没有错误,但也没有命中。如果我手动运行find命令,它会起作用。如果我删除($includeString),它也可以工作,尽管很明显它并没有将自己限制在我想要的文件夹中。
那么,为什么同样的命令在命令行中有效,而在bash脚本中无效呢?以这种方式传递$includeString会导致它失败,这是怎么回事?
您遇到了shell如何处理变量扩展的问题。在您的脚本中:
includeString="-wholename './public_html/*' -o -wholename './config/*'"
find . ( $includeString ) -type f -mtime -7 -print
这导致find
查找-wholename
与文本字符串'./public_html/*'
匹配的文件。也就是说,包含单引号的文件名。由于路径中没有任何空格,因此这里最简单的解决方案就是去掉单引号:
includeString="-wholename ./public_html/* -o -wholename ./config/*"
find . ( $includeString ) -type f -mtime -7 -print
不幸的是,您可能会被这里的通配符扩展所困扰(shell会在find
看到通配符之前尝试扩展通配符)。
但正如埃坦在评论中指出的那样,这似乎是不必要的复杂;你可以简单地做:
find ./public_html ./config -type f -mtime -7 -print
如果你想存储一个参数列表并在以后进行扩展,正确的格式是数组,而不是字符串:
includeArgs=( -wholename './public_html/*' -o -wholename './config/*' )
find . '(' "${includeArgs[@]}" ')' -type f -mtime -7 -print
BashFAQ#50中详细介绍了这一点。
注意:正如Etan在评论中指出的,这种情况下更好的解决方案可能是重新制定find
命令,但通过变量传递多个参数通常是一种值得探索的技术
tl;dr:
问题不是特定于find
,而是shell如何解析命令行。
-
嵌入变量值中的引号字符被视为文字:它们既不被识别为参数边界分隔符,也不在解析后被删除,因此,不能使用带有嵌入引号的字符串变量通过直接将其作为命令的一部分来传递多个参数。
-
为了稳健地传递存储在变量中的多个参数,
- 在支持它们的shell(
bash
、ksh
、zsh
)中使用数组变量-请参阅下文 - 否则,对于POSIX合规性,请使用
xargs
-请参阅下文
- 在支持它们的shell(
稳健的解决方案:
注意:解决方案假设存在以下脚本,我们称之为echoArgs
,它以诊断形式打印传递给它的参数:
#!/usr/bin/env bash
for arg; do # loop over all arguments
echo "[$arg]" # print each argument enclosed in [] so as to see its boundaries
done
此外,假设将执行以下命令的等效命令:
echoArgs one 'two three' '*' last # note the *literal* '*' - no globbing
带有所有参数,但变量传递的最后一个。
因此,预期结果是:
[one]
[two three]
[*]
[last]
- 使用数组变量(
bash
、ksh
、zsh
):
# Assign the arguments to *individual elements* of *array* args.
# The resulting array looks like this: [0]="one" [1]="two three" [2]="*"
args=( one 'two three' '*' )
# Safely pass these arguments - note the need to *double-quote* the array reference:
echoArgs "${args[@]}" last
- 使用
xargs
-符合POSIX的替代方案:
POSIX实用程序xargs
与shell本身不同,能够识别嵌入字符串中的带引号字符串:
# Store the arguments as *single string* with *embedded quoting*.
args="one 'two three' '*'"
# Let *xargs* parse the embedded quoted strings correctly.
# Note the need to double-quote $args.
echo "$args" | xargs -J {} echoArgs {} last
请注意,{}
是一个自由选择的占位符,允许您控制xargs
提供的参数在生成的命令行中的位置
如果所有xarg
提供的参数都在最后,则根本不需要使用-J
。
为了完整性:eval
也可以用于解析嵌入另一个字符串中的带引号的字符串,但eval
存在安全风险:任意命令最终都可能被执行;考虑到上面讨论的安全解决方案,不需要使用eval
最后,Charles Duffy在一条评论中提到了另一个安全的替代方案,然而,这需要更多的编码:将要调用的命令封装在shell函数中,将变量自变量作为单独的自变量传递给函数,然后操纵函数内的所有自变量数组$@
来补充固定自变量(使用set
),并用CCD_ 26调用该命令
有关shell字符串处理问题的解释:
当将字符串分配给变量时,嵌入的引号字符将成为字符串的部分:
var='one "two three" *'
$var
现在字面上包含one "two three" *
,即以下4-而不是预期的3个单词,每个单词之间用一个空格分隔:one
"two
-"
是单词本身的一部分three"
-"
是单词本身的一部分*
当您使用
$var
未加引号作为参数列表的一部分时,上面分解为4个单词正是外壳最初所做的-一个称为分词的过程请注意,如果对变量引用("$var"
)使用双引号,则整个字符串将始终成为单个参数- 因为
$var
被扩展到其值,也就是所谓的参数扩展之一,外壳不会试图将该值内嵌入的引号识别为标记参数边界-这只适用于字面上指定的引号字符,作为命令行的直接部分(假设这些引号字符本身没有引号) - 类似地,只有这样直接指定的引号字符才会被shell删除,然后将封闭的字符串传递给被调用的命令,这个过程被称为引号删除
- 因为
但是,shell还将路径名扩展(globbing)应用于生成的4个单词,因此任何与文件名匹配的单词都将扩展到匹配的文件名。
简而言之:
$var
值中的引号字符在解析后既不会被识别为参数边界分隔符,也不会被删除此外,$var
值中的单词要进行路径名扩展。这意味着传递多个参数的唯一方法是在变量值内保留未加引号(也保留对该变量的引用),即:
- 无法使用带有嵌入空格或外壳元字符的值
- 总是对值进行路径名扩展