何时在 shell 变量周围加引号?

306
在shell脚本中,我是否应该在变量周围加上引号?
例如,下面的写法是否正确:
xdg-open $URL
[ $? -eq 2 ]

或者

xdg-open "$URL"
[ "$?" -eq "2" ]

如果是这样的话,为什么呢?

3
参见 http://unix.stackexchange.com/questions/171346 ,忘记在Bash/POSIX shell中引用变量的安全隐患。 - tripleee
1
这个问题有很多重复,其中许多并不涉及变量,因此我将标题改为“值”而不是“变量”。我希望这能帮助更多的人找到这个主题。 - tripleee
1
@codeforester,关于撤销的编辑怎么回事? - tripleee
2
同时,还涉及到Bash中单引号和双引号的区别 - codeforester
6
Bash是一个黑客技巧,最终被广泛使用超出了其设计范围。虽然有更好的方式来完成任务,但并没有所谓的“正确/安全的方式”。我这么说是因为这里有很多参考资料,它们都有不同的观点,对于那些习惯于为特定任务设计的新语言和工具的人来说,这可能会变得非常令人困惑。 - Tegra Detra
显示剩余5条评论
5个回答

245

一般规则:如果字符串可以为空或包含空格(或者任意空白字符)或特殊字符(通配符),则需要加引号。如果不使用带有空格的字符串,则会导致shell将单个参数分解为多个。

$? 不需要引号,因为它是一个数字值。是否需要在 $URL 中加引号取决于您允许在其中输入什么以及是否仍希望在其为空时保留参数

出于习惯,我倾向于始终添加引号,因为这样更安全。


5
请注意,“空格”实际上意味着“任何空白字符”。 - William Pursell
6
如果您不确定变量中可能包含什么内容,最好将其加上引号以确保安全。我倾向于遵循与paxdiablo相同的原则,并养成加上引号的习惯(除非有特定原因不需要)。 - Gordon Davisson
22
无论如何,如果您不知道IFS的值,请引用它。如果IFS=0,那么echo $?可能会非常惊人。 - Charles Duffy
10
根据上下文引用,而不是基于您期望的值进行引用,否则您的错误将更加严重。例如,您确定您的路径中没有空格,因此认为可以编写cp $source1 $source2 $dest,但如果由于某些意外原因未设置dest,第三个参数就会消失,并且它将悄悄地将source1复制到source2上,而不是为空目标提供适当的错误(如果您引用了每个参数,则会出现这种情况)。 - Derek Veit
18
如果说"quote it if..."这个思维过程是错误的——引号不是在需要时添加的,而是在需要时去掉的。除非你需要使用双引号(例如为了让变量扩展)或需要使用没有引号的情况(例如进行通配符和文件名扩展),否则应始终用单引号括起字符串和脚本。 - Ed Morton
显示剩余9条评论

167
简而言之,在您不需要 shell 执行单词拆分和通配符扩展的情况下,请引用所有内容。
单引号保护它们之间的文本,字面上。当您需要确保 shell 完全不触及字符串时,它是正确的工具。通常,当您不需要变量插值时,它是首选的引用机制。
$ echo 'Nothing \t in here $will change'
Nothing \t in here $will change

$ grep -F '@&$*!!' file /dev/null
file:I can't get this @&$*!! quoting right.

当需要变量插值时,双引号是合适的。通过适当的调整,它也是在字符串中需要单引号时的一个很好的解决方法。(在单引号之间转义单引号没有直接的方法,因为单引号内部没有转义机制——如果有的话,它们将不完全引用原样。)

$ echo "There is no place like '$HOME'"
There is no place like '/home/me'

No quotes适用于需要shell执行单词拆分和/或通配符扩展的情况。 单词拆分(又称标记拆分);
 $ words="foo bar baz"
 $ for word in $words; do
 >   echo "$word"
 > done
 foo
 bar
 baz

相比之下:
 $ for word in "$words"; do echo "$word"; done
 foo bar baz

(循环仅运行一次,覆盖单个的引号字符串。)
 $ for word in '$words'; do echo "$word"; done
 $words

(循环仅运行一次,遍历单引号括起来的文字字符串。)
通配符扩展:
$ pattern='file*.txt'
$ ls $pattern
file1.txt      file_other.txt

相比之下:
$ ls "$pattern"
ls: cannot access file*.txt: No such file or directory

(没有名为file*.txt的文件。)

$ ls '$pattern'
ls: cannot access $pattern: No such file or directory

(没有名为$pattern的文件!)

更具体地说,任何包含文件名的内容通常应该加引号(因为文件名可能包含空格和其他shell元字符)。任何包含URL的内容通常应该加引号(因为许多URL包含像?&这样的shell元字符)。任何包含正则表达式的内容通常应该加引号(同上)。任何包含除非单个空格之外的显着空格的内容都需要加引号(否则,shell将把空格混合成实际上是单个空格,并修剪任何前导或尾随空格)。

当您知道变量只能包含不含shell元字符的值时,引用是可选的。因此,未引用的$?基本上是可以的,因为此变量只能包含单个数字。但是,"$?"也是正确的,并且出于一般一致性和正确性的考虑建议使用(尽管这是我的个人建议,而不是广泛认可的政策)。

非变量的值基本上遵循相同的规则,但您也可以转义任何元字符而不是引用它们。例如,一个带有&的URL将被shell解析为后台命令,除非转义或引用元字符:

$ wget http://example.com/q&uack
[1] wget http://example.com/q
-bash: uack: command not found

当然,如果URL在未使用引号的变量中,则也会发生这种情况。对于静态字符串,单引号是最合适的选择,尽管任何形式的引用或转义都可以在此处使用。

wget 'http://example.com/q&uack'  # Single quotes preferred for a static string
wget "http://example.com/q&uack"  # Double quotes work here, too (no $ or ` in the value)
wget http://example.com/q\&uack   # Backslash escape
wget http://example.com/q'&'uack  # Only the metacharacter really needs quoting

最后一个例子还提出了另一个有用的概念,我称之为“平衡引用”。如果您需要混合使用单引号和双引号,可以将它们相邻使用。例如,以下引用字符串
'$HOME '
"isn't"
' where `<3'
"' is."

可以将它们背对背地粘在一起,在分词和引号移除后,形成一个单独的长字符串。
$ echo '$HOME '"isn't"' where `<3'"' is."
$HOME isn't where `<3' is.

这段文字不太易读,但它是一种常见的技术,因此值得了解。

顺便说一下,脚本 通常不应该使用ls进行任何操作。 要展开通配符,只需直接使用它即可。

$ printf '%s\n' $pattern   # not ``ls -1 $pattern''
file1.txt
file_other.txt

$ for file in $pattern; do  # definitely, definitely not ``for file in $(ls $pattern)''
>  printf 'Found file: %s\n' "$file"
> done
Found file: file1.txt
Found file: file_other.txt

在后一个示例中,循环是完全多余的;printf可以很好地处理多个参数。同样,stat也是如此。但是循环通配符匹配是一个常见的问题,经常被错误地执行。
包含要循环遍历的令牌列表或要扩展的通配符的变量不太常见,因此我们有时会缩写为“除非您确切知道自己在做什么,请引用所有内容”。

1
这是我在相关问题中发布的答案的变体。我将其粘贴在这里,因为它简洁明了,并且足够明确,可以成为此特定问题的规范问题。 - tripleee
5
我注意到这是第0项,也是http://mywiki.wooledge.org/BashPitfalls中常见的Bash错误集合中的一个经常出现的主题。该列表中许多单独的项目基本上都与这个问题有关。 - tripleee
令牌拆分在Bash参考中被称为单词拆分。请参阅我的编辑。https://www.gnu.org/software/bash/manual/html_node/Word-Splitting.html - Roland
1
@Roland 谢谢!我改掉了文本以使用官方术语。 - tripleee
空字节应该被提及为即使不应该改变的狗屎,例如:NULL="$(php -r 'echo chr(0);')" + printf "%s" "$NULL" | wc -c 将会打印 0... - hanshenrik
@hanshenrik 我认为这不是一个适合在这里讨论的话题,即使它确实是一个棘手的问题。Shell和各种工具使用C字符串,将空字节保留为字符串终止符。像Perl和Python(以及某些版本的Awk)这样的工具在内部使用更复杂的字符串表示,并且能够容纳完全任意的字符串。 - tripleee

50
以下是有关引号的三个公式:
双引号
在我们需要抑制单词分割和全局匹配时使用。也适用于希望将文字作为字符串而非正则表达式处理的情境。
单引号
在字符串字面值中,我们希望抑制插值和反斜杠的特殊处理。换句话说,使用双引号不合适的情况下使用。
无引号
在我们完全确定不存在单词分割或全局匹配问题时使用,或者我们确实需要单词分割和全局匹配的情况下使用。
示例
双引号
- 带有空格的字面字符串(“StackOverflow rocks!”,“Steve's Apple”) - 变量扩展(“$var”,“${arr[@]}”) - 命令替换(“$(ls)”,“`ls`”) - 包含空格的目录路径或文件名部分的全局匹配(“/ my dir /”*)) - 保护单引号(“single'quote'delimited'string”) - Bash参数扩展(“${filename##*/}”)
单引号
- 具有空格的命令名称和参数 - 需要抑制插值的字面字符串(‘真的要花费$$!’,‘只是一个后跟t的反斜杠:\ t’) - 保护双引号(“关键”) - 需要抑制插值的正则表达式文字 - 对涉及特殊字符的文字使用shell引用($'\n\t') - 在需要保护几个单引号和双引号的字面文字中使用shell引用($'{"table": "users", "where": "first_name"=\'Steve\'}')
原文链接:https://dev59.com/Smw15IYBdhLWcg3wT5_2
  • 普通数字变量周围的符号 ($$, $?, $#等)
  • 在算术环境中像 ((count++)), "${arr[idx]}", "${string:start:length}"
  • [[ ]] 表达式中,它不会被分割和扩展 (这是一种风格问题,意见可能存在很大差异)
  • 我们需要进行单词分割的地方 (for word in $words)
  • 我们需要进行文件名匹配的地方(for txtfile in *.txt; do ...)
  • 我们需要解释 ~$HOME 的地方 (~/"some dir" 但不包括 "~/some dir")

另请参阅:


3
根据这些指南,如果想获取根目录下的文件列表,可以输入 "ls" "/"。需要更加仔细地限定“所有字符串上下文”的意思。 - William Pursell
7
[[ ]]中,引号在=/===~的右侧是有影响的:它决定了是将字符串作为模式/正则表达式解释还是按字面意思解释。 - Benjamin W.
7
概述不错,但应该将 @BenjaminW. 的评论整合进来,同时 ANSI C 引用字符串($'...')应该有自己的章节。 - mklement0
3
@mklement0,确实它们是等价的。这些准则表明你应该总是键入"ls" "/"而不是更常见的ls /,我认为这是准则中的一个严重缺陷。 - William Pursell
5
对于“无引号”,您可以添加变量赋值或case :) - PesaThe
显示剩余4条评论

6

通常我会使用带引号的形式,例如"$var"以确保安全,除非我确定$var不包含空格。

我也会使用$var作为连接行的简单方式:

lines="`cat multi-lines-text-file.txt`"
echo "$lines"                             ## multiple lines
echo $lines                               ## all spaces (including newlines) are zapped

2
最终的评论有些误导性;换行符实际上被替换为空格,而不是简单地删除。 - tripleee
如果_multi-lines-text-file.txt_文件中包含单词*,那么_bash_将用当前目录中所有文件的列表替换它。哈哈。不是笑话。 - bobbogo
对,这只是一种简单的方法,而不是一种确定的方法。 - Bach Lien

-2

正如本页面其他评论和答案所强调的那样,“引用一切,当你发现使用情况需要时再删除引号”是一个更健康的原则。 - tripleee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接