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

时间:2021-11-06 01:08:55

I've searched around questions with similar issues but haven't found one that quite fits my situation.

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

Below is a very brief script that demonstrates the problem I'm facing:

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

#!/bin/bash

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

Basically, we need to search inside a folder, but only in certain of its subfolders. In my longer script, includeString gets built from an array. For this demo, I kept things simple.

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

Basically, when I run the script, it doesn't find anything. No errors, but also no hits. If I manually run the find command, it works. If I remove ( $includeString ) it also works, though obviously it doesn't limit itself to the folders I want.

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

So why would the same command work from the command line but not from the bash script? What is it about passing in $includeString that way that causes it to fail?

那么为什么同一命令可以从命令行运行而不是从bash脚本运行?传递$ includeString的原因是什么导致它失败?

3 个解决方案

#1


You're running into an issue with how the shell handles variable expansion. In your script:

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

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

This results in find looking for files where -wholename matches the literal string './public_html/*'. That is, a filename that contains single quotes. Since you don't have any whitespace in your paths, the easiest solution here would be to just drop the single quotes:

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

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

Unfortunately, you'll probably get bitten by wildcard expansion here (the shell will attempt to expand the wildcards before find sees them).

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

But as Etan pointed out in his comment, this appears to be needlessly complex; you can simply do:

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

find ./public_html ./config -type f -mtime -7 -print

#2


If you want to store a list of arguments and expand it later, the correct form to do that with is an array, not a string:

如果要存储参数列表并在以后展开它,那么使用的正确表单是数组,而不是字符串:

includeArgs=( -wholename './public_html/*' -o -wholename './config/*' )
find . '(' "${includeArgs[@]}" ')' -type f -mtime -7 -print

This is covered in detail in BashFAQ #50.

BashFAQ#50详细介绍了这一点。

#3


Note: As Etan points out in a comment, the better solution in this case may be to reformulate the find command, but passing multiple arguments via variable(s) is a technique worth exploring in general.

注意:正如Etan在评论中指出的那样,在这种情况下更好的解决方案可能是重新配置find命令,但是通过变量传递多个参数是一种值得探索的技术。

tl;dr:

The problem is not specific to find, but to how the shell parses command lines.

问题并非特定于查找,而是shell如何解析命令行。

  • Quote characters embedded in variable values are treated as literals: They are neither recognized as argument-boundary delimiters nor are they removed after parsing, so you cannot use a string variable with embedded quoting to pass multiple arguments simply by directly using it as part of a command.

    嵌入在变量值中的引号字符被视为文字:它们既不被识别为参数边界分隔符,也不会在解析后被删除,因此您不能使用带嵌入式引号的字符串变量来简单地通过直接使用它作为一部分来传递多个参数命令。

  • To robustly pass multiple arguments stored in a variable,

    要稳健地传递存储在变量中的多个参数,

    • use array variables in shells that support them (bash, ksh, zsh) - see below.
    • 在支持它们的shell中使用数组变量(bash,ksh,zsh) - 见下文。

    • otherwise, for POSIX compliance, use xargs - see below.
    • 否则,对于POSIX合规性,请使用xargs - 见下文。


Robust solutions:

Note: The solutions assume presence of the following script, let's call it echoArgs, which prints the arguments passed to it in diagnostic form:

注意:解决方案假设存在以下脚本,我们称之为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

Further, assume that the equivalent of the following command is to be executed:

此外,假设要执行以下命令的等效操作:

echoArgs one 'two three' '*' last  # note the *literal* '*' - no globbing

with all arguments but the last passed by variable.

所有参数但最后一个由变量传递。

Thus, the expected outcome is:

因此,预期结果是:

[one]
[two three]
[*]
[last]
  • Using an array variable (bash, ksh, zsh):
  • 使用数组变量(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
  • Using xargs - a POSIX-compliant alternative:
  • 使用xargs - 符合POSIX的替代方案:

POSIX utility xargs, unlike the shell itself, is capable of recognized quoted strings embedded in a string:

与shell本身不同,POSIX实用程序xargs能够识别嵌入在字符串中的引用字符串:

# 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

Note that {} is a freely chosen placeholder that allows you to control where in the resulting command line the arguments provided by xargs go.
If all xarg-provided arguments go last, there is no need to use -J at all.

请注意,{}是一个*选择的占位符,允许您控制xargs提供的参数在结果命令行中的位置。如果所有xarg提供的参数都是最后一个,则根本不需要使用-J。

For the sake of completeness: eval can also be used to parse quoted strings embedded in another string, but eval is a security risk: arbitrary commands could end up getting executed; given the safe solutions discussed above, there is no need to use eval.

为了完整起见:eval也可以用来解析嵌入在另一个字符串中的引用字符串,但eval是一个安全风险:任意命令最终可能会被执行;鉴于上面讨论的安全解决方案,不需要使用eval。

Finally, Charles Duffy mentions another safe alternative in a comment, which, however, requires more coding: encapsulate the command to invoke in a shell function, pass the variable arguments as separate arguments to the function, then manipulate the all-arguments array $@ inside the function to supplement the fixed arguments (using set), and invoke the command with "$@".

最后,Charles Duffy在注释中提到了另一个安全的替代方案,然而,这需要更多编码:封装命令以在shell函数中调用,将变量参数作为单独的参数传递给函数,然后操作all-arguments数组$ @在函数内部补充固定参数(使用set),并使用“$ @”调用命令。


Explanation of the shell's string-handling issues involved:

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

  • When you assign a string to a variable, embedded quote characters become part of the string:

    将字符串分配给变量时,嵌入的引号字符将成为字符串的一部分:

    var='one "two three" *' 
    
  • $var now literally contains one "two three" *, i.e., the following 4 - instead of the intended 3 - words, separated by a space each:

    $ var现在字面上包含一个“两个三”*,即以下4个 - 而不是预期的3个字,每个字隔开一个空格:

    • one
    • "two-- " is part of the word itself!
    • “两个 - ”是这个词本身的一部分!

    • three"-- " is part of the word itself!
    • 三个“ - ”是这个词本身的一部分!

    • *
  • When you use $var unquoted as part of an argument list, the above breakdown into 4 words is exactly what the shell does initially - a process called word splitting. Note that if you were to double-quote the variable reference ("$var"), the entire string would always become a single argument.

    当你使用$ var unquoted作为参数列表的一部分时,上面分解成4个单词正是shell最初所做的 - 一个称为单词拆分的过程。请注意,如果要对变量引用(“$ var”)进行双引号,则整个字符串将始终成为单个参数。

    • Because $var is expanded to its value, one of the so-called parameter expansions, the shell does NOT attempt to recognize embedded quotes inside that value as marking argument boundaries - this only works with quote characters specified literally, as a direct part of the command line (assuming these quote characters aren't themselves quoted).
    • 因为$ var被扩展为它的值,即所谓的参数扩展之一,所以shell不会尝试将该值中的嵌入引号识别为标记参数边界 - 这仅适用于字面指定的引号字符,作为其直接部分命令行(假设这些引号字符本身不引用)。

    • Similarly, only such directly specified quote characters are removed by the shell before passing the enclosed string to the command being invoked - a process called quote removal.
    • 类似地,在将附带的字符串传递给被调用的命令之前,shell仅删除了这样的直接指定的引号字符 - 一个名为quote removal的进程。

  • However, the shell additionally applies pathname expansion (globbing) to the resulting 4 words, so any of the words that happen to match filenames will expand to the matching filenames.

    但是,shell还会将路径名扩展(globbing)应用于生成的4个单词,因此恰好匹配文件名的任何单词都将扩展为匹配的文件名。

  • In short: the quote characters in $var's value are neither recognized as argument-boundary delimiters nor are they removed after parsing. Additionally, the words in $var's value are subject to pathname expansion.

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

  • This means that the only way to pass multiple arguments is to leave them unquoted inside the variable value (and also leave the reference to that variable unquoted), which:

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

    • won't work with values with embedded spaces or shell metacharacters
    • 不适用于具有嵌入空格或shell元字符的值

    • invariably subjects the values to pathname expansion
    • 总是将值赋予路径名扩展

#1


You're running into an issue with how the shell handles variable expansion. In your script:

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

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

This results in find looking for files where -wholename matches the literal string './public_html/*'. That is, a filename that contains single quotes. Since you don't have any whitespace in your paths, the easiest solution here would be to just drop the single quotes:

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

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

Unfortunately, you'll probably get bitten by wildcard expansion here (the shell will attempt to expand the wildcards before find sees them).

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

But as Etan pointed out in his comment, this appears to be needlessly complex; you can simply do:

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

find ./public_html ./config -type f -mtime -7 -print

#2


If you want to store a list of arguments and expand it later, the correct form to do that with is an array, not a string:

如果要存储参数列表并在以后展开它,那么使用的正确表单是数组,而不是字符串:

includeArgs=( -wholename './public_html/*' -o -wholename './config/*' )
find . '(' "${includeArgs[@]}" ')' -type f -mtime -7 -print

This is covered in detail in BashFAQ #50.

BashFAQ#50详细介绍了这一点。

#3


Note: As Etan points out in a comment, the better solution in this case may be to reformulate the find command, but passing multiple arguments via variable(s) is a technique worth exploring in general.

注意:正如Etan在评论中指出的那样,在这种情况下更好的解决方案可能是重新配置find命令,但是通过变量传递多个参数是一种值得探索的技术。

tl;dr:

The problem is not specific to find, but to how the shell parses command lines.

问题并非特定于查找,而是shell如何解析命令行。

  • Quote characters embedded in variable values are treated as literals: They are neither recognized as argument-boundary delimiters nor are they removed after parsing, so you cannot use a string variable with embedded quoting to pass multiple arguments simply by directly using it as part of a command.

    嵌入在变量值中的引号字符被视为文字:它们既不被识别为参数边界分隔符,也不会在解析后被删除,因此您不能使用带嵌入式引号的字符串变量来简单地通过直接使用它作为一部分来传递多个参数命令。

  • To robustly pass multiple arguments stored in a variable,

    要稳健地传递存储在变量中的多个参数,

    • use array variables in shells that support them (bash, ksh, zsh) - see below.
    • 在支持它们的shell中使用数组变量(bash,ksh,zsh) - 见下文。

    • otherwise, for POSIX compliance, use xargs - see below.
    • 否则,对于POSIX合规性,请使用xargs - 见下文。


Robust solutions:

Note: The solutions assume presence of the following script, let's call it echoArgs, which prints the arguments passed to it in diagnostic form:

注意:解决方案假设存在以下脚本,我们称之为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

Further, assume that the equivalent of the following command is to be executed:

此外,假设要执行以下命令的等效操作:

echoArgs one 'two three' '*' last  # note the *literal* '*' - no globbing

with all arguments but the last passed by variable.

所有参数但最后一个由变量传递。

Thus, the expected outcome is:

因此,预期结果是:

[one]
[two three]
[*]
[last]
  • Using an array variable (bash, ksh, zsh):
  • 使用数组变量(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
  • Using xargs - a POSIX-compliant alternative:
  • 使用xargs - 符合POSIX的替代方案:

POSIX utility xargs, unlike the shell itself, is capable of recognized quoted strings embedded in a string:

与shell本身不同,POSIX实用程序xargs能够识别嵌入在字符串中的引用字符串:

# 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

Note that {} is a freely chosen placeholder that allows you to control where in the resulting command line the arguments provided by xargs go.
If all xarg-provided arguments go last, there is no need to use -J at all.

请注意,{}是一个*选择的占位符,允许您控制xargs提供的参数在结果命令行中的位置。如果所有xarg提供的参数都是最后一个,则根本不需要使用-J。

For the sake of completeness: eval can also be used to parse quoted strings embedded in another string, but eval is a security risk: arbitrary commands could end up getting executed; given the safe solutions discussed above, there is no need to use eval.

为了完整起见:eval也可以用来解析嵌入在另一个字符串中的引用字符串,但eval是一个安全风险:任意命令最终可能会被执行;鉴于上面讨论的安全解决方案,不需要使用eval。

Finally, Charles Duffy mentions another safe alternative in a comment, which, however, requires more coding: encapsulate the command to invoke in a shell function, pass the variable arguments as separate arguments to the function, then manipulate the all-arguments array $@ inside the function to supplement the fixed arguments (using set), and invoke the command with "$@".

最后,Charles Duffy在注释中提到了另一个安全的替代方案,然而,这需要更多编码:封装命令以在shell函数中调用,将变量参数作为单独的参数传递给函数,然后操作all-arguments数组$ @在函数内部补充固定参数(使用set),并使用“$ @”调用命令。


Explanation of the shell's string-handling issues involved:

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

  • When you assign a string to a variable, embedded quote characters become part of the string:

    将字符串分配给变量时,嵌入的引号字符将成为字符串的一部分:

    var='one "two three" *' 
    
  • $var now literally contains one "two three" *, i.e., the following 4 - instead of the intended 3 - words, separated by a space each:

    $ var现在字面上包含一个“两个三”*,即以下4个 - 而不是预期的3个字,每个字隔开一个空格:

    • one
    • "two-- " is part of the word itself!
    • “两个 - ”是这个词本身的一部分!

    • three"-- " is part of the word itself!
    • 三个“ - ”是这个词本身的一部分!

    • *
  • When you use $var unquoted as part of an argument list, the above breakdown into 4 words is exactly what the shell does initially - a process called word splitting. Note that if you were to double-quote the variable reference ("$var"), the entire string would always become a single argument.

    当你使用$ var unquoted作为参数列表的一部分时,上面分解成4个单词正是shell最初所做的 - 一个称为单词拆分的过程。请注意,如果要对变量引用(“$ var”)进行双引号,则整个字符串将始终成为单个参数。

    • Because $var is expanded to its value, one of the so-called parameter expansions, the shell does NOT attempt to recognize embedded quotes inside that value as marking argument boundaries - this only works with quote characters specified literally, as a direct part of the command line (assuming these quote characters aren't themselves quoted).
    • 因为$ var被扩展为它的值,即所谓的参数扩展之一,所以shell不会尝试将该值中的嵌入引号识别为标记参数边界 - 这仅适用于字面指定的引号字符,作为其直接部分命令行(假设这些引号字符本身不引用)。

    • Similarly, only such directly specified quote characters are removed by the shell before passing the enclosed string to the command being invoked - a process called quote removal.
    • 类似地,在将附带的字符串传递给被调用的命令之前,shell仅删除了这样的直接指定的引号字符 - 一个名为quote removal的进程。

  • However, the shell additionally applies pathname expansion (globbing) to the resulting 4 words, so any of the words that happen to match filenames will expand to the matching filenames.

    但是,shell还会将路径名扩展(globbing)应用于生成的4个单词,因此恰好匹配文件名的任何单词都将扩展为匹配的文件名。

  • In short: the quote characters in $var's value are neither recognized as argument-boundary delimiters nor are they removed after parsing. Additionally, the words in $var's value are subject to pathname expansion.

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

  • This means that the only way to pass multiple arguments is to leave them unquoted inside the variable value (and also leave the reference to that variable unquoted), which:

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

    • won't work with values with embedded spaces or shell metacharacters
    • 不适用于具有嵌入空格或shell元字符的值

    • invariably subjects the values to pathname expansion
    • 总是将值赋予路径名扩展