find 命令适用于提示,而不是 bash 脚本 - 通过变量传递多个参数

find command works on prompt, not in bash script - pass multiple arguments by variable

我搜索了具有类似问题的问题,但没有找到一个非常适合我的情况。

下面是一个非常简短的脚本,它演示了我面临的问题:

#!/bin/bash

includeString="-wholename './public_html/*' -o -wholename './config/*'"
find . \( $includeString \) -type f -mtime -7 -print

基本上,我们需要在文件夹内部搜索,但只在其某些子文件夹中搜索。在我较长的脚本中,includeString 是从数组构建的。对于这个演示,我保持简单。

基本上,当我 运行 脚本时,它找不到任何东西。没有错误,但也没有命中。如果我手动 运行 查找命令,它就可以工作。如果我删除 ( $includeString ) 它也可以工作,但显然它不限于我想要的文件夹。

那么,为什么相同的命令在命令行中有效,而在 bash 脚本中却无效?以这种方式传递 $includeString 会导致它失败的原因是什么?

您 运行 遇到了 shell 如何处理变量扩展的问题。在您的脚本中:

includeString="-wholename './public_html/*' -o -wholename './config/*'"
find . \( $includeString \) -type f -mtime -7 -print

这导致 find 查找 -wholename 匹配 文字字符串 './public_html/*' 的文件。即,包含单引号的 filename。由于您的路径中没有任何空格,因此最简单的解决方案是只删除单引号:

includeString="-wholename ./public_html/* -o -wholename ./config/*"
find . \( $includeString \) -type f -mtime -7 -print

不幸的是,您可能会在这里被通配符扩展所困扰(shell 会在 find 看到它们之前尝试扩展通配符)。

但正如 Etan 在他的评论中指出的那样,这似乎不必要地复杂;你可以简单地做:

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中使用数组变量bashksh , zsh) - 见下文。
    • 否则,为了 POSIX 合规,使用 xargs - 见下文。

稳健的解决方案

注意:解决方案假定存在以下 脚本 ,我们称之为 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]
  • 使用数组变量(bashkshzsh):
# 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-compliant 替代方案:

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 提供的参数都是 last,则根本不需要使用 -J

为了完整起见:eval 也可用于解析嵌入另一个字符串中的带引号的字符串,但 eval 存在安全风险:任意命令可能会结束被处决;考虑到上面讨论的安全解决方案,没有必要使用 eval.

最后,Charles Duffy 在评论中提到了另一个安全的替代方案,但是,它需要更多的编码:将要调用的命令封装在 shell 函数中,将可变参数作为单独的参数传递给函数,然后在函数内部操作全参数数组 $@ 以补充固定参数(使用 set),并使用 "$@".


对shell字符串处理问题的解释:

  • 当您将字符串分配给变量时,嵌入的引号字符变为部分字符串

    var='one "two three" *' 
    
  • $var 现在 字面上 包含 one "two three" *,即下面的 4 -而不是预期的 3 个单词,每个单词由 space 分隔:

    • one
    • "two-- " 词本身的一部分!
    • three"-- " 词本身的一部分!
    • *
  • 当您使用 $var unquoted 作为参数列表的一部分时,上述 分解为 4 个词正是shell 最初 - 一个称为 word splitting 的过程。 请注意,如果您要 双引号 变量引用 ("$var"),整个字符串总是会变成 参数。

    • 因为$var扩展到它的值,所以所谓的parameter expansions之一就是shell不会尝试将该值内的嵌入引号识别为标记参数边界 - 这仅适用于字面指定的引号字符,作为直接[=191] =] 命令行的一部分(假设这些引号字符本身没有被引号)。
    • 类似地,在将封闭的字符串传递给被调用的命令之前,shell 只会删除直接指定的引号字符 - 这个过程称为 引号删除.
  • 然而,shell 额外应用路径名扩展(通配)到生成的 4 个词,所以任何恰好匹配的词文件名将扩展为匹配的文件名。

  • 简而言之:$var值中的引号字符既不被识别为参数边界定界符也不被识别它们在解析后删除了 此外,$var 值中的单词受到路径名扩展 的影响。

  • 这意味着传递多个参数的唯一方法是将它们 不加引号 放在变量值中(并且还保留 引用 到那个变量 unquoted),其中:

    • 不适用于嵌入 space 或 shell 元字符的值
    • 总是 使值受到路径名扩展