为什么带引号的变量中的选项失败,但在不带引号的情况下有效?

z32*_*7ul 18 shell bash options quoting variable

我读到我应该在 bash 中引用变量,例如“$foo”而不是 $foo。但是,在编写脚本时,我发现了一个案例,它没有引号但不能使用引号:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line
Run Code Online (Sandbox Code Playgroud)

这个有效:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"
Run Code Online (Sandbox Code Playgroud)

这个没有(注意 $wget_options 周围的双引号):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
Run Code Online (Sandbox Code Playgroud)
  • 这是什么原因?

  • 第一行是不是好版本;还是我应该怀疑某处存在导致这种行为的隐藏错误?

  • 一般来说,我在哪里可以找到好的文档来了解 bash 及其引用的工作原理?在编写这个脚本的过程中,我觉得我开始在试错的基础上工作,而不是理解规则。

gle*_*man 33

最健壮的编码方式是使用数组:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"
Run Code Online (Sandbox Code Playgroud)

  • 这是一个很好的答案,所以我赞成它,但 Kusalanda 帮助我更多地理解为什么我的代码是错误的,我只能接受一个。 (2认同)

Kus*_*nda 28

基本上,您应该双引号变量扩展以保护它们免于分词(和文件名生成)。但是,在您的示例中,

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"
Run Code Online (Sandbox Code Playgroud)

分词正是您想要的

With "$wget_options"(quoted),wget不知道如何处理单个参数--mirror --no-host-directories并抱怨

wget: unknown option -- mirror --no-host-directories
Run Code Online (Sandbox Code Playgroud)

对于wget看到两个选项--mirror,并--no-host-directories作为单独的,词的拆分有发生。

有更强大的方法可以做到这一点。如果您正在使用bash或任何其他使用数组的 shell bash,请参阅glenn jackman 的回答Gilles 的回答还描述了一种用于更简单外壳的替代解决方案,例如标准/bin/sh. 两者本质上都将每个选项存储为数组中的单独元素。

相关问题有很好的答案:为什么我的 shell 脚本会因空格或其他特殊字符而阻塞?


双引号变量扩展是一个很好的经验法则。这样做。然后注意极少数情况下您不应该这样做。这些将通过诊断消息(例如上述错误消息)呈现给您。

还有一些情况不需要引用变量扩展。但无论如何继续使用双引号更容易,因为它没有太大区别。一种这样的情况是

variable=$other_variable
Run Code Online (Sandbox Code Playgroud)

另一个是

case $variable in
    ...) ... ;;
esac
Run Code Online (Sandbox Code Playgroud)

  • 在使用 split+glob 运算符之前,可能需要确保 `$IFS` 包含正确的值。在这里,您需要在空间上进行拆分,并且文本恰好不包含任何制表符或换行符,因此`$IFS` 的默认值就可以了,但是如果该代码要用于可能在上下文中调用的函数中`$IFS` 可能已被修改,您需要事先设置 `$IFS`(如果其余代码假定为未修改的 `$IFS`,则可能在事后恢复它或使用本地作用域) (2认同)

Gil*_*il' 17

您正在尝试将字符串列表存储在字符串变量中。它不适合。无论您如何访问变量,都会出现问题。

wget_options='--mirror --no-host-directories'将变量设置为wget_options包含空格的字符串。在这一点上,没有办法知道空格应该是选项的一部分,还是选项之间的分隔符。

当您使用带引号的替换访问变量时,变量wget "$wget_options"的值将用作字符串。这意味着它作为单个参数传递给wget,因此它是一个选项。这在您的情况下会中断,因为您打算将其表示为多个选项。

当您使用不带引号的替换时wget $wget_options,字符串变量的值会经历一个昵称为“split+glob”的扩展过程:

  1. 获取变量的值并将其拆分为以空格分隔的部分(假设您尚未修改该$IFS变量)。这会产生一个中间字符串列表。
  2. 对于中间列表的每个元素,如果它是匹配一个或多个文件的通配符模式,则将该元素替换为匹配文件的列表。

这恰好适用于您的示例,因为拆分过程将空格转换为分隔符,但通常不起作用,因为选项可能包含空格和通配符。

在 ksh、bash、yash 和 zsh 中,您可以使用数组变量。shell 术语中的数组是字符串列表,因此不会丢失信息。要创建数组变量,请在为变量赋值时在数组元素周围加上括号。要访问数组的所有元素,请使用- 这是 的泛化,它从数组的元素形成一个列表。请注意,这里也需要双引号,否则每个元素都会经历 split+glob。"${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" …
Run Code Online (Sandbox Code Playgroud)

在普通 sh 中,没有数组变量。如果您不介意丢失位置参数,您可以使用它们来存储一个字符串列表。

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" …
Run Code Online (Sandbox Code Playgroud)

有关更多信息,请参阅为什么我的 shell 脚本会因空格或其他特殊字符而阻塞?