什么时候在一个shell变量周围包装引号?

我是否应该在shell脚本中的变量周围包装引号?

例如,以下是正确的:

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

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

如果有,为什么?

132111 次浏览

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

$?不需要引号,因为它是一个数值。$URL是否需要它取决于你在那里允许什么,以及如果它是空的,你是否仍然需要一个参数。

我倾向于总是引用字符串只是出于习惯,因为这样更安全。

简而言之,在不需要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'

当您特别要求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。但是遍历通配符匹配是一个常见的问题,而且经常做得不正确。)

包含要遍历的令牌列表或要展开的通配符的变量不太常见,所以我们有时缩写为“引用所有东西,除非您确切地知道您在做什么”。

这里有一个关于报价的三点公式:

双引号

在我们想要抑制分词和通配符的上下文中。在我们希望字面量被视为字符串而不是正则表达式的上下文中也是如此。

单引号

在字符串字面量中,我们希望抑制插值和反斜杠的特殊处理。换句话说,使用双引号是不合适的。

没有报价

在上下文中,我们绝对确定没有分词或通配符问题,或者我们要分词和词缀吗


例子

双引号

  • 带空格的文字字符串("StackOverflow rocks!""Steve's Apple")
  • 变量展开("$var""${arr[@]}")
  • 命令替换("$(ls)""`ls`")
  • 目录路径或文件名部分包含空格的glob ("/my dir/"*)
  • 保护单引号("single'quote'delimited'string")
  • Bash参数扩展("${filename##*/}")

单引号

  • 命令名和参数中有空格
  • 需要插值来抑制的文字字符串('Really costs $$!''just a backslash followed by a t: \t')
  • 保护双引号('The "crux"')
  • 正则表达式字面量,需要插值来抑制
  • 对包含特殊字符的字面量使用shell引号($'\n\t')
  • 在需要保护单引号和双引号的地方使用shell引号($'{"table": "users", "where": "first_name"=\'Steve\'}')

没有报价

  • 围绕标准数值变量($$$?$#等)
  • 在算术上下文中,如((count++))"${arr[idx]}""${string:start:length}"
  • [[ ]]表达式中,不存在分词和通词问题(这是一个风格问题,意见可能会有很大差异)
  • 我们需要分词(for word in $words)
  • 我们想要globbing (for txtfile in *.txt; do ...)
  • 其中我们希望~被解释为$HOME (~/"some dir"而不是"~/some dir")

参见:

为了安全起见,我通常使用像"$var"这样的引号,除非我确定$var不包含空格。

我确实使用$var作为连接行的简单方法:

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

当你的编辑器的https://www.shellcheck.net/插件告诉你。