为什么带引号的变量中的选项会失败,但不带引号的情况下会起作用?


18

我了解到我应该在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

这个作品:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

这个不会(注意$ wget_options周围的双引号):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • 这是什么原因呢?

  • 第一行是好的版本;还是我应该怀疑某个地方存在隐藏的错误导致此行为?

  • 通常,在哪里可以找到好的文档来了解bash及其引用的工作原理?在编写此脚本期间,我觉得我开始在反复试验的基础上工作,而不是理解规则。


3
您的问题在这里得到回答:mywiki.wooledge.org/BashFAQ/050
glenn jackman '17

3
前往规则源:bash手册。请密切注意第3.5节“ Shell扩展”,尤其是单词拆分和文件名扩展-这两个因素是您使用引号来控制的。
格伦·杰克曼(Glenn jackman)


4
我认为这有助于理解命令行参数在较低级别上的工作方式。执行程序时,它将接收参数作为字符列表的列表(足够接近)。每个内部列表就是我们所谓的“参数”。大多数程序取决于arg之间的逻辑分隔。在这里,您会看到wget不知道是什么--mirror --no-host-directories意思(作为一个参数),但是当它分成两个参数时,它将对其进行处理。很少有程序将空格和引号放入参数向量后才对其进行特殊处理。问题在于bash,以及其他炮弹,应该是>
HTNW

2
>被人类使用。手动定义参数之间的边界会很烦人,因此shell在空格上拆分以将一行(一个字符列表)变成一个参数向量(一个字符列表)。变量扩展是第一个扩展bash所做的事情,因此您可以想象这$a完全等同于直接编写其内容。现在问题显而易见了:a="-a -b"; cmd "$a"扩展为cmd "-a -b",但cmd可能不知道这意味着什么。cmd $a扩展为cmd -a -b,可能起作用。
HTNW

Answers:


28

基本上,您应该使用双引号括起变量扩展名,以防止它们受到单词拆分(和文件名生成)的影响。但是,在您的示例中,

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

分词正是您想要的

使用"$wget_options"(带引号),wget不知道如何处理单个参数--mirror --no-host-directories并抱怨

wget: unknown option -- mirror --no-host-directories

为了wget查看这两个选项--mirror--no-host-directories并且必须分开进行分词。

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

具有良好答案的相关问题:为什么我的shell脚本在空白或其他特殊字符上会阻塞?


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

在某些情况下,您不需要引用变量扩展。但是无论如何继续使用双引号会更容易,因为区别不大。一种这样的情况是

variable=$other_variable

另一个是

case $variable in
    ...) ... ;;
esac

2
在使用split + glob运算符之前,可能需要确保其中$IFS包含正确的值。在这里,您需要在空间上进行分割,并且文本碰巧不包含任何制表符或换行符,因此默认值是$IFS可以的,但是如果要在$IFS可能已被修改的上下文中调用的函数中使用该代码,是,你要设置$IFS事前(也可能是事后恢复,或使用本地范围的,如果代码的其余部分假定未修改$IFS
斯特凡Chazelas

32

最可靠的编码方法是使用数组:

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

这是正确的答案。参考
l0b0

2
这是一个很好的答案,所以我投票赞成,但是Kusalanda的帮助我更多地了解了为什么我的代码是错误的,我只能接受一个。
z32a7ul

直到rsync列表上的某人向我展示了此构造,我才陷入麻烦的世界。如果某些元素可能为空字符串,则特别有用。这使空字符串消失。如果您的命令扩展为,某些命令会像cprsync会做意外的事情rsync '' rest of parameters。这对于有条件地逐步构建命令然后仅在一个位置运行一次命令非常有用。

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[@]}" 

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

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

有关更多信息,请参见为什么我的shell脚本在空白或其他特殊字符上感到窒息?


对于plain sh,子shell将保留位置参数:(set -- ...; exec wget "$@" ...)
约翰·库格曼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.