![但如果……呢?](https://linux22.com/image/60016/%E4%BD%86%E5%A6%82%E6%9E%9C%E2%80%A6%E2%80%A6%E5%91%A2%EF%BC%9F.png)
如果您关注 unix.stackexchange.com 一段时间,那么您现在应该知道,在echo $var
Bourne/POSIX shell(zsh 是例外)中的列表上下文中(如 )中保留未加引号的变量具有非常特殊的含义,并且除非你有充分的理由,否则不应该这样做。
此处的许多问答对此进行了详细讨论(示例:为什么我的 shell 脚本会因为空格或其他特殊字符而卡住?,什么时候需要双引号?,shell 变量的扩展以及 glob 和 split 对它的影响,带引号与不带引号的字符串扩展)
自 70 年代末 Bourne shell 首次发布以来就是这种情况,并且 Korn shell(其中之一)没有改变过David Korn 最大的遗憾(问题#7))或者bash
主要复制 Korn shell,这就是 POSIX/Unix 指定的方式。
现在,我们仍然可以在这里看到许多答案,甚至偶尔会看到公开发布的未引用变量的 shell 代码。你可能认为人们现在已经学会了。
根据我的经验,主要有 3 类人省略引用变量:
初学者。这些都是可以原谅的,因为不可否认这是一种完全不直观的语法。我们在这个网站上的作用就是教育他们。
健忘的人。
反复锤打仍不服气的人,认为当然,Bourne shell 作者并不想让我们引用所有变量。
如果我们揭露与此类行为相关的风险,也许我们可以说服他们。
如果您忘记引用变量,可能发生的最糟糕的事情是什么?是不是真的那坏的?
我们在这里谈论什么样的漏洞?
在什么情况下可能会出现问题?
答案1
前言
首先,我想说这不是解决问题的正确方法。这有点像说“你不应该杀人,否则你会进监狱”。
同样,您不要引用变量,因为否则会引入安全漏洞。您引用变量是因为不这样做是错误的(但如果对监狱的恐惧可以有所帮助,为什么不呢)。
给那些刚刚跳上火车的人一个小总结。
在大多数 shell 中,不加引号的变量扩展(尽管(以及本答案的其余部分)也适用于命令替换 (`...`
或$(...)
) 和算术扩展 ($((...))
或$[...]
))具有非常特殊的含义。描述它的最好方式是,它就像调用某种隐式的分割+全局运算符 1。
cmd $var
用另一种语言会写成这样:
cmd(glob(split($var)))
$var
首先根据涉及特殊$IFS
参数(分裂部分),然后该拆分产生的每个单词都被视为图案它被扩展为与其匹配的文件列表(全局部分)。
例如, if $var
contains*.txt,/var/*.xml
和$IFS
contains ,
,cmd
将使用多个参数进行调用,第一个参数是 当前目录中的文件,下cmd
一个参数是.txt
xml
/var
如果您只想cmd
使用两个文字参数cmd
和进行调用*.txt,/var/*.xml
,您可以编写:
cmd "$var"
这将是您其他更熟悉的语言:
cmd($var)
我们所说的shell 中的漏洞?
毕竟,从一开始就知道 shell 脚本不应该在安全敏感的上下文中使用。当然,好吧,将变量不加引号是一个错误,但这不会造成那么大的伤害,不是吗?
好吧,尽管事实上有人会告诉你 shell 脚本永远不应该用于 Web CGI,或者值得庆幸的是,现在大多数系统都不允许 setuid/setgid shell 脚本,但 shellshock(可远程利用的 bash 错误使得2014 年 9 月的头条新闻)揭示了 shell 仍然在它们可能不应该的地方广泛使用:在 CGI 中、在 DHCP 客户端挂钩脚本中、在 sudoers 命令中、调用经过(如果不作为) setuid 命令...
有时不知不觉中。例如,system('cmd $PATH_INFO')
在php
// CGI 脚本中perl
,python
确实会调用 shell 来解释该命令行(更不用说它cmd
本身可能是一个 shell 脚本,并且其作者可能从未期望它会从 CGI 中调用)。
当存在特权升级的路径时,即当某人(我们称他为攻击者)能够做一些他不应该做的事情。
总是意味着攻击者提供数据,该数据由特权用户/进程处理,该用户/进程无意中做了一些不应该做的事情,在大多数情况下是因为错误。
基本上,当你的有缺陷的代码在以下控制下处理数据时,你就会遇到问题攻击者。
现在,并不总是很明显数据可能来自,并且通常很难判断您的代码是否会处理不受信任的数据。
就变量而言,对于 CGI 脚本来说,很明显,数据是 CGI GET/POST 参数以及 cookie、路径、主机...等参数。
对于 setuid 脚本(当被另一个用户调用时作为一个用户运行),它是参数或环境变量。
另一个非常常见的向量是文件名。如果您从目录中获取文件列表,则文件可能已被植入那里攻击者。
在这方面,即使在交互式 shell 的提示下,您也可能容易受到攻击(例如在处理文件时/tmp
)~/tmp
。
即使 a 也~/.bashrc
可能容易受到攻击(例如,当调用它来运行 类似服务器部署中的某些变量在客户端控制下bash
时会解释它)。ssh
ForcedCommand
git
现在,脚本可能不会被直接调用来处理不受信任的数据,但它可能被另一个命令调用。或者你的错误代码可能会被复制粘贴到脚本中(三年后由你或你的同事复制粘贴)。一个特别容易被忽略的地方是批判的位于问答网站的答案中,因为您永远不知道代码的副本可能最终会出现在哪里。
言归正传;有多糟糕?
迄今为止,不加引号的变量(或命令替换)是与 shell 代码相关的安全漏洞的第一大来源。部分原因是这些错误通常会转化为漏洞,但也因为经常会看到未加引号的变量。
实际上,在寻找 shell 代码中的漏洞时,首先要做的就是寻找未加引号的变量。它很容易发现,通常是一个很好的候选者,通常很容易追踪到攻击者控制的数据。
未加引号的变量可以通过多种方式变成漏洞。我在这里只给出一些常见的趋势。
信息披露
大多数人会遇到与未加引号的变量相关的错误,因为分裂(例如,现在文件名称中包含空格是很常见的,并且空格是 IFS 的默认值)。很多人都会忽略 全局部分。这全局部分至少与 分裂部分。
在未经消毒的外部输入方式上完成通配攻击者可以让你读取任意目录的内容。
在:
echo You entered: $unsanitised_external_input
如果$unsanitised_external_input
包含/*
,则意味着攻击者可以看到 的内容/
。没什么大不了。它变得更有趣,尽管/home/*
它为您提供了机器上的用户名列表,/tmp/*
以 /home/*/.forward
提示其他危险行为,/etc/rc*/*
启用服务......无需单独命名它们。值/* /*/* /*/*/*...
将仅列出整个文件系统。
拒绝服务漏洞。
前面的例子有点过分了,我们得到了 DoS。
实际上,列表上下文中任何未加引号的变量以及未经净化的输入都是至少DoS 漏洞。
即使是专业的 shell 脚本编写者也通常会忘记引用以下内容:
#! /bin/sh -
: ${QUERYSTRING=$1}
:
是无操作命令。可能会出现什么问题?
这意味着如果 未设置则分配$1
给它。这也是使 CGI 脚本可从命令行调用的快速方法。$QUERYSTRING
$QUERYSTRING
不过,这$QUERYSTRING
仍然是扩展的,因为它没有被引用,分割+全局调用运算符。
现在,有些 glob 的扩展成本特别高。这/*/*/*/*
已经够糟糕的了,因为它意味着列出最多 4 级的目录。除了磁盘和 CPU 活动之外,这还意味着存储数万个文件路径(这里在最小的服务器虚拟机上有 40k,其中有 10k 个目录)。
现在/*/*/*/*/../../../../*/*/*/*
意味着 40k x 10k,
/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*
足以让最强大的机器屈服。
亲自尝试一下(但要做好计算机崩溃或挂起的准备):
a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'
当然,如果代码是:
echo $QUERYSTRING > /some/file
然后就可以把磁盘填满了。
只需进行谷歌搜索即可外壳程序或者bash CGI或者克什克吉,您会发现一些页面向您展示如何在 shell 中编写 CGI。请注意,处理参数的一半很容易受到攻击。
甚至大卫·科恩自己的一张 容易受到攻击(查看 cookie 处理)。
最多任意代码执行漏洞
任意代码执行是最严重的漏洞类型,因为如果攻击者可以运行任何命令,他可以做的事情没有限制。
一般就是这样的分裂导致这些的部分。这种拆分会导致在只需要一个参数时将多个参数传递给命令。虽然其中第一个将在预期的上下文中使用,但其他的将在不同的上下文中使用,因此可能会有不同的解释。最好举个例子:
awk -v foo=$external_input '$2 == foo'
这里的目的是将 shell 变量的内容分配
$external_input
给该foo
awk
变量。
现在:
$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux
拆分后的第二个字$external_input
不会分配给foo
而是被视为awk
代码(此处执行任意命令:uname
)。
对于可以执行其他命令(awk
、env
、sed
(GNU one)、perl
... find
)的命令来说,尤其是对于 GNU 变体(在参数后接受选项)来说,这尤其是一个问题。有时,您不会怀疑命令能够执行其他命令,例如ksh
,bash
或zsh
's[
或printf
...
for file in *; do
[ -f $file ] || continue
something-that-would-be-dangerous-if-$file-were-a-directory
done
如果我们创建一个名为 的目录x -o yes
,则测试结果为正,因为它是我们正在评估的完全不同的条件表达式。
更糟糕的是,如果我们创建一个名为 的文件x -a a[0$(uname>&2)] -gt 1
,至少包含所有 ksh 实现(其中包括sh
大多数商业 Unices 和一些 BSD),该文件会执行,uname
因为这些 shell 对命令的数字比较运算符执行算术评估[
。
$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux
与bash
类似x -a -v a[0$(uname>&2)]
.
当然,如果他们不能任意处决的话,攻击者可能会满足于较小的损害(这可能有助于任意执行)。任何可以写入文件或更改权限、所有权或具有任何主要或副作用的命令都可能被利用。
可以使用文件名完成各种各样的事情。
$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done
最终你会变得..
可写(使用 GNU 递归
chmod
)。
在公共可写区域自动处理文件的脚本需要/tmp
非常小心地编写。
关于什么[ $# -gt 1 ]
这是我觉得令人恼火的事情。有些人不厌其烦地想知道某个特定的扩展是否可能有问题,以决定是否可以省略引号。
就好像说一样。嘿,看起来$#
不能受 split+glob 操作符的约束,让我们让 shell 来 split+glob 吧。或者嘿,让我们写不正确的代码吧,因为 bug 不太可能被击中。
现在这可能性有多大?好的,$#
(或$!
,$?
或任何算术替换)可能只包含数字(或-
某些²),因此全局部分已经出来了。为了分裂不过,我们只需要$IFS
包含数字(或-
)即可。
对于某些 shell,$IFS
可能会从环境继承,但如果环境不安全,无论如何游戏都结束了。
现在如果你写一个像这样的函数:
my_function() {
[ $# -eq 2 ] || return
...
}
这意味着函数的行为取决于调用它的上下文。或者换句话说,$IFS
成为它的输入之一。严格来说,当你为你的函数编写API文档时,它应该是这样的:
# my_function
# inputs:
# $1: source directory
# $2: destination directory
# $IFS: used to split $#, expected not to contain digits...
调用函数的代码需要确保$IFS
不包含数字。所有这些都是因为您不想输入这两个双引号字符。
现在,要使该[ $# -eq 2 ]
错误成为漏洞,您需要以某种方式$IFS
使 的值受到控制攻击者。可以想象,这通常不会发生,除非攻击者设法利用另一个错误。
但这并非闻所未闻。一种常见的情况是人们忘记在算术表达式中使用数据之前对其进行清理。上面我们已经看到,它可以允许在某些 shell 中执行任意代码,但在所有 shell 中,它都允许 攻击者给任何变量一个整数值。
例如:
n=$(($1 + 1))
if [ $# -gt 2 ]; then
echo >&2 "Too many arguments"
exit 1
fi
对于$1
with value (IFS=-1234567890)
,该算术评估具有设置 IFS 的副作用,并且下一个[
命令失败,这意味着检查参数太多被绕过。
当分割+全局运算符没有被调用?
还有另一种情况,变量和其他扩展需要引号:当它用作模式时。
[[ $a = $b ]] # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac
不测试$a
和是否$b
相同(除了zsh
),但 if$a
与 中的模式匹配$b
。$b
如果要作为字符串进行比较,则需要引用(如果不将其视为模式,则应在"${a#$b}"
or"${a%$b}"
或"${a##*$b*}"
where中引用相同的内容)。$b
这意味着,在与 不同的[[ $a = $b ]]
情况下(例如当is和is时)可能返回 true,或者当它们相同时(例如当两者和are时)可能返回 false。$a
$b
$a
anything
$b
*
$a
$b
[a]
这会造成安全漏洞吗?是的,就像任何错误一样。这里,攻击者可以改变脚本的逻辑代码流和/或打破脚本所做的假设。例如,使用如下代码:
if [[ $1 = $2 ]]; then
echo >&2 '$1 and $2 cannot be the same or damage will incur'
exit 1
fi
攻击者可以通过通过绕过检查'[a]' '[a]'
。
现在,如果该模式匹配和分割+全局运算符 apply,不加引号的变量有什么危险?
我必须承认我确实写过:
a=$b
case $a in...
在那里,引用并没有什么坏处,但并不是绝对必要的。
然而,在这些情况下(例如在问答答案中)省略引号的一个副作用是它可能会向初学者发送错误的信息:不引用变量可能没问题。
例如,他们可能开始认为如果a=$b
可以,那么也可以(这它不在很多贝壳中因为它位于export a=$b
export
命令的参数中,所以在列表上下文中) 或.env a=$b
但有一些地方不接受报价。主要的一个是在许多 shell 中的 Korn 风格算术表达式中,例如不得引用的echo "$(( $1 + 1 ))" "${array[$1 + 1]}" "${var:$1 + 1}"
地方$1
(在列表上下文中——简单命令的参数——但整体扩展仍然需要引用)。
在这些内部,shell 理解一种完全受 C 启发的独立语言。ksh
例如,在 AT&T 中$(( 'd' - 'a' ))
,它会像在 C 中一样扩展到 3,但与在 C 中不同$(( d - a ))
。双引号在 ksh93 中被忽略,但在许多其他 shell 中会导致语法错误。在 C 中,"d" - "a"
将返回指向 C 字符串的指针之间的差异。在 shell 中做同样的事情是没有意义的。
关于什么zsh
?
zsh
确实解决了大部分设计上的尴尬。在zsh
(至少当不处于 sh/ksh 模拟模式时),如果你想要分裂, 或者通配, 或者模式匹配,您必须显式请求它:$=var
拆分、$~var
全局或将变量的内容视为模式。
然而,拆分(但不是通配符)仍然在未加引号的命令替换时隐式完成(如 中所示echo $(cmd)
)。
此外,不引用变量有时会带来不必要的副作用清空移除。该zsh
行为类似于在其他 shell 中通过完全禁用通配符(使用set -f
)和拆分(使用IFS=''
)来实现的效果。还在:
cmd $var
将没有分割+全局,但如果$var
为空,则cmd
不会接收任何参数,而不是接收一个空参数。
这可能会导致错误(就像显而易见的那样[ -n $var ]
)。这可能会破坏脚本的期望和假设并导致漏洞。
由于空变量可能导致参数只是已删除,这意味着下一个参数可能会在错误的上下文中被解释。
举个例子,
printf '[%d] <%s>\n' 1 $attacker_supplied1 2 $attacker_supplied2
如果$attacker_supplied1
为空,则将$attacker_supplied2
被解释为算术表达式 (for %d
) 而不是字符串 (for %s
) 并且算术表达式中使用的任何未经处理的数据都是类似 Korn 的 shell(例如 zsh)中的命令注入漏洞。
$ attacker_supplied1='x y' attacker_supplied2='*'
$ printf '[%d] <%s>\n' 1 $attacker_supplied1 2 $attacker_supplied2
[1] <x y>
[2] <*>
很好,但是:
$ attacker_supplied1='' attacker_supplied2='psvar[$(uname>&2)0]'
$ printf '[%d] <%s>\n' 1 $attacker_supplied1 2 $attacker_supplied2
Linux
[1] <2>
[0] <>
这uname
任意命令被运行。
另请注意,虽然默认情况下不会在替换时进行通配符,但由于 zsh 中的通配符比其他 shell 中的通配符强大得多,这意味着如果您同时zsh
启用该选项,它们可能会造成更大的损害,或者不禁用并无意中留下一些变量未加引号。globsubst
extendedglob
bareglobqual
例如,甚至:
set -o globsubst
echo $attacker_controlled
将是任意命令执行漏洞,因为命令可以作为 glob 扩展的一部分执行,例如使用e
评估 glob 限定符:
$ set -o globsubst
$ attacker_controlled='.(e[uname])'
$ echo $attacker_controlled
Linux
.
emulate sh # or ksh
echo $attacker_controlled
不会导致 ACE 漏洞(尽管它仍然是像 sh 中的 DoS 漏洞),因为bareglobqual
在 sh/ksh 模拟中被禁用。globsubst
当想要解释 sh/ksh 代码时,没有充分的理由启用这些 sh/ksh 模拟之外的功能。
那当你做需要分割+全局操作员?
是的,这通常是当您确实想让变量不加引号时。但接下来你需要确保调整你的分裂和全局使用前请正确操作。如果你只想要分裂部分而不是全局部分(大多数情况下都是这种情况),那么您确实需要禁用通配符(set -o noglob
/ set -f
)并修复$IFS
。否则,您也会导致漏洞(如上面提到的 David Korn 的 CGI 示例)。
结论
简而言之,在 shell 中不加引号的变量(或命令替换或算术扩展)确实可能非常危险,尤其是在错误的上下文中执行时,并且很难知道哪些是错误的上下文。
这就是它被认为的原因之一不好的做法。
感谢您到目前为止的阅读。如果它超出了你的想象,别担心。人们不能指望每个人都能理解以他们编写代码的方式编写代码的所有含义。这就是为什么我们有 良好做法建议,因此可以遵循它们而不必理解原因。
(如果这还不明显,请避免在 shell 中编写安全敏感代码)。
和请在本网站的答案中引用您的变量!
¹在ksh93
和pdksh
和导数中,大括号扩展除非禁用通配符(对于ksh93
ksh93u+ 版本,即使该braceexpand
选项被禁用),也会执行。
² 在ksh93
and中yash
,算术展开式还可以包括1,2
, 1e+66
, inf
,等内容nan
。还有更多 in zsh
,包括#
which 是带有 的 glob 运算符extendedglob
,但即使在模拟zsh
中,也不会在算术扩展时执行 split+globsh
答案2
但如果……呢?
但是,如果我的脚本在使用变量之前将其设置为已知值会怎样?特别是,如果它将一个变量设置为两个或多个可能值之一(但它总是将其设置为已知的值),并且所有值都不包含空格或全局字符?不加引号使用不安全吗在这种情况下?
如果可能的值之一是空字符串,并且我依赖于“空值删除”怎么办?即,如果变量包含空字符串,我不想在命令中获取空字符串;我什么都不想得到。例如,如果某些_条件 然后 忽略大小写=“-i” 别的 忽略大小写=“” 菲 # 注意上面命令中的引号是不是严格需要。 grep $忽略大小写 其他_grep_args我不能说;如果是空字符串,则会失败。
grep "$ignorecase" other_grep_args
$ignorecase
回复:
正如另一个答案中所讨论的,如果IFS
包含 a-
或 an ,这仍然会失败i
。如果您确保IFS
变量中不包含任何字符(并且您确定变量不包含任何全局字符),那么这可能是安全的。
但有一种更安全的方法(尽管它有点难看而且很不直观):使用${ignorecase:+"$ignorecase"}
.从POSIX Shell 命令语言规范, 在下面 2.6.2 参数扩展,
${parameter:+[word]}
使用替代价值。 如果
parameter
未设置或为 null,应替换为 null;否则,扩展word
(或者一个空字符串,如果word
被省略)应被替换。
这里的技巧是,我们ignorecase
使用parameter
并"$ignorecase"
作为word
。所以${ignorecase:+"$ignorecase"}
意味着
如果
$ignorecase
未设置或 null(即空)、null(即不加引号)没有什么)须予取代;否则,"$ignorecase"
应替换的扩展。
这让我们到达了我们想要去的地方:如果变量设置为空字符串,它将被“删除”(整个复杂的表达式将计算为没有什么— 甚至不是空字符串),如果变量有非空值,我们就会得到该值,并用引号引起来。
但如果……呢?
但是,如果我有一个想要/需要拆分为单词的变量怎么办? (这与第一种情况类似;我的脚本已设置变量,并且我确信它不包含任何全局字符。但它可能包含空格,并且我希望它在空格处拆分为单独的参数PS我仍然想要删除空的。)
例如,如果某些_条件 然后 标准=“-类型f” 别的 标准=“” 菲 如果其他一些条件 然后 标准=“$criteria -mtime +42” 菲 查找“$start_directory”$criteria 其他_寻找_args
回复:
您可能认为这是使用
不! 抵制住想在eval
.eval
这里使用的诱惑。
同样,如果您确保IFS
变量中不包含任何字符(除了空格,您希望尊重这些字符),并且您确定变量不包含任何全局字符,那么上面的内容可能是安全的。
但是,如果您使用 bash(或 ksh、zsh 或 yash),有一种更安全的方法:使用数组:
如果某些_条件 然后 criteria=(-type f) # 你可以说 `criteria=("-type" "f")`,但这确实没有必要。 # 但不要说 `criteria=("-type f")` 或 `criteria="(-type f)"`。 别的 标准=() #不要在此命令中使用任何引号! 菲 如果其他一些条件 然后 criteria+=(-mtime +42) # 注意:不是`=`,而是`+=`,添加(追加)到数组。 菲 查找“$start_directory”“${criteria[@]}” 其他_寻找_args
从重击(1),
可以使用 引用数组的任何元素。 … 如果
${name[subscript]}
subscript
是@
或者*
,这个词扩展到所有成员name
。仅当单词出现在双引号内时,这些下标才会有所不同。如果该词被双引号括起来,... 展开每个元素${name[@]}
name
到一个单独的词。
So"${criteria[@]}"
扩展到(在上面的示例中)数组的零个、两个或四个元素criteria
,每个元素都被引用。特别是,如果两者都不是状况 s 为 true,criteria
数组没有内容(由criteria=()
语句设置),并且"${criteria[@]}"
计算结果为没有什么
(甚至不是不方便的空字符串)。
当您处理多个单词时,这会变得特别有趣和复杂,其中一些是动态(用户)输入,您事先不知道,并且可能包含空格或其他特殊字符。考虑:
printf "输入要查找的文件名:" 读取文件名 if [ "$fname" != "" ] 然后 标准+=(-名称“$fname”) 菲
注意$fname
是引用的每个使用时间。即使用户输入类似foo bar
或 之类的内容,这也有效foo*
。
"${criteria[@]}"
计算结果为-name "foo bar"
或-name "foo*"
。 (请记住,数组的每个元素都被引用。)
数组并不适用于所有 POSIX shell;数组是 ksh / bash / zsh / yash-ism。除了……还有一所有 shell 支持的数组:参数列表,又名"$@"
.如果您完成了调用的参数列表(例如,您已将所有“位置参数”(参数)复制到变量中,或以其他方式处理它们),则可以将 arg 列表用作数组:
如果某些_条件 然后 set -- -type f # 你可以说`set -- "-type" "f"`,但这确实没有必要。 别的 放 - 菲 如果其他一些条件 然后 设置--“$@”-mtime +42 菲 # 同样:set -- "$@" -name "$fname" 找到“$start_directory”“$@” 其他_寻找_args
该"$@"
构造(历史上是最先出现的)具有相同的语义- 它将每个参数(即参数列表的每个元素)扩展为一个单独的单词,就像您键入了 一样。"${name[@]}"
"$1" "$2" "$3" …
摘自POSIX Shell 命令语言规范, 在下面2.5.2 特殊参数,
@
扩展到位置参数,从 1 开始,最初为设置的每个位置参数生成一个字段。 …,初始字段应保留为单独的字段,…。如果没有位置参数,则展开
@
应生成零场,即使@
在双引号内; ……
全文有些晦涩;关键点是它指定"$@"
当没有位置参数时应生成零字段。历史记录:"$@"
1979 年首次在 Bourne shell(bash 的前身)中引入时,它有一个错误,"$@"
在没有位置参数时被单个空字符串替换;看${1+"$@"}
shell 脚本中的含义是什么?它与 有何不同"$@"
?,
传统的 Bourne Shell 系列,
...是什么${1+"$@"}
意思?哪里有必要?, 和"$@"
相对${1+"$@"}
。
数组也有助于解决第一种情况:
如果某些_条件 然后 ignorecase=(-i) # 你可以说 `ignorecase=("-i")`,但这确实没有必要。 别的 忽略大小写=()#不要在此命令中使用任何引号! 菲 grep "${ignorecase[@]}" 其他_grep_args
____________________
PS(csh)
这应该是不言而喻的,但是,为了新来的人的利益:csh、tcsh 等不是 Bourne/POSIX shell。他们是完全不同的一家人。一匹不同颜色的马。完全是另一场球赛。不同品种的猫。异类。而且,最特别的是,还有一种不同的蠕虫。
本页所述的一些内容适用于 csh;例如:最好引用所有变量,除非您有充分的理由不这样做,并且您确定自己知道自己在做什么。但是,在 csh 中,每个变量都是一个数组 - 碰巧几乎每个变量都是一个仅包含一个元素的数组,并且其行为与 Bourne/POSIX shell 中的普通 shell 变量非常相似。而且语法非常不同(我的意思是非常)。所以我们在这里不再谈论 csh 系列 shell。
答案3
我对 Stéphane 的回答表示怀疑,但可能会滥用$#
:
$ set `seq 101`
$ IFS=0
$ printf '%s\n' $#
1
1
$ printf '%s\n' "$#"
101
或$?:
$ IFS=0
$ awk 'BEGIN {exit 101}'
$ status=$?
$ printf '%s\n' $status
1
1
$ printf '%s\n' "$status"
101
这些都是人为的例子,但潜力确实存在。
答案4
如果我不在每个脚本中引用我的每个变量,无论是我自己的还是在 SO 的答案中,我都会否认我在道德上腐败或技术上无能的观念。
流行的答案可以归结为最后一行:
请避免在 shell 中编写安全敏感代码
该帖子假定有攻击者。我建议 99% 的 shell 脚本不会面临任何攻击,也永远不会。它们的存在/usr/local/bin
是$HOME
为了用户的方便。它们控制某种初始化根据定义不受任意输入的影响。
你~/.profile
没有面对攻击者。用于解析日志文件或准备数据库加载的 30 行 awk 脚本可能也不会。对无处不在且始终编写 shell 脚本就好像它们面临攻击一样的通用建议使得绝大多数脚本比它们需要的更加复杂,并且只会鼓励这样的观念:它们可以是安全的。
您的脚本面临攻击者吗?好的,所以请小心,也许考虑一下在这种情况下运行具有 root 权限的 shell 脚本是否是一个好主意。
编辑
始终引用建议断言引用 shell 变量始终可以防止错误。事实并非如此。到盲目地当输入不应包含空格或通配符引起语义错误时,防止拆分和扩展。
name=$(ls foo*.cfg)
...
if [ ! -f $name ]
then
touch $name
fi
如果$name
意外地包含多个文件名,则上面的代码将中断预期二元运算符。如果用神奇的防错引号“正确”编写,它会默默地错误地成功,创建一个新的伪造文件。毫无疑问,它稍后会以某种难以理解的方式出错,这将更加难以调试。
通过不引用$name
,脚本会隐式断言内容将包含一个不嵌入空格的名称。这是一个有用的断言。它不会触及多个名字,也不会创造出愚蠢的名字。如果它正在运行(应该是这种情况),它将停止-e
。否则至少它会产生一条消息来提醒您哪里出了问题。