多年来,我多次遇到变量与字符串文字的比较,其中变量和文字前面有一个字符,例如
if [ "x$A" = "xtrue" ]; then
以便检查是否$A
是"true"
.
我认为这样做是为了实现 shell 兼容性或解决长期错误、不直观的行为等。没有什么明显的想法。
今天我想我想知道原因,但我的研究没有发现任何结果。或者也许这只是我通过频繁接触罕见事件而创造出的东西。
这种做法仍然有用,甚至可能是最好的吗?
答案1
这里要理解的重要一点是,在大多数 shell 中,[
它只是一个由 shell 解析的普通命令,就像任何其他普通命令一样。
然后 shell 使用参数列表调用该[
(也称为)命令,然后将它们解释为条件表达式。test
[
那时,这些只是一个字符串列表,关于哪些字符串是由某种形式的扩展产生的信息会丢失,即使是在那些[
内置的 shell 中(现在都是类似 Bourne 的 shell)。
该[
实用程序过去很难区分哪些参数是运算符,哪些是操作数(运算符所处理的内容)。语法本质上含糊不清,这并没有帮助。例如:
[ -t ]
曾经(并且仍然在某些 shell 中[
)用于测试 stdout 是否是终端。[ x ]
是[ -n x ]
: 测试是否x
是非空字符串的缩写(因此您可以看到与上面的内容有冲突)。- 在某些 shell 中
[
,-a
并且-o
可以都是一元的([ -a file ]
对于可访问的文件(现已替换为[ -e file ]
),[ -o option ]
对于该选项是否已启用?) 和二元运算符 (和和或者)。同样,! -a x
可以是and(nonempty("!"), nonempty("x"))
或not(isaccessible("x"))
。 (
,)
并!
添加更多问题。
在普通编程语言中,如 C 或perl
, 在:
if ($a eq $b) {...}
$a
or的内容不可能$b
被视为运算符,因为条件表达式在它们之前被解析$a
并被$b
扩展。但在 shell 中,在:
[ "$a" = "$b" ]
shell 扩展变量第一的².例如,如果$a
contains(
和$b
contains )
,则命令看到的所有内容都是[
、[
、(
和参数。那么这意味着(在词法上相等)还是(是一个非空字符串)。=
)
]
"(" = ")"
(
)
( -n = )
=
历史实现(test
出现在 70 年代末的 Unix V7 中)过去常常会失败,即使在明确的情况下,仅仅因为它们处理参数的顺序也是如此。
这里是 PDP11 模拟器中的 7 版 Unix:
$ ls -l /bin/[
-rwxr-xr-x 2 bin 2876 Jun 8 1979 /bin/[
$ [ ! = x ]
test: argument expected
$ [ "(" = x ]
test: argument expected
大多数 shell 和[
实现都存在或已经存在这些或其变体的问题. 现版本为bash
4.4:
bash-4.4$ a='(' b=-o c=x
bash-4.4$ [ "$a" = "$b" -o "$a" = "$c" ]
bash: [: `)' expected, found =
POSIX.2(90年代初发布)设计一种算法当以最常见的使用模式(例如仍然未指定)[
传递最多 4 个参数(除了[
和)时,这将使 的行为变得明确和确定。它弃用了、、和,并在没有操作数的情况下删除。确实在2.0中实现了该算法(或者至少尝试过) 。]
[ -f "$a" -o "$b" ]
(
)
-a
-o
-t
bash
bash
因此,在符合 POSIX 标准的[
实现中,无论它们是什么,都可以保证比较和[ "$a" = "$b" ]
的内容是否相等。如果没有,我们会写:$a
$b
-o
[ "$a" = "$b" ] || [ "$a" = "$c" ]
也就是说,调用[
两次,每次使用的参数少于 5 个。
但所有实施都花了相当长的时间[
才变得合规。bash
直到 4.4 才兼容(尽管最后一个问题是[ '(' ! "$var" ')' ]
没有人在现实生活中真正使用它)
Solaris 10 及更早版本的 Solaris shell/bin/sh
不是 POSIX shell,但 Bourne shell 仍然存在问题[ "$a" = "$b" ]
:
$ a='!' b='!'
$ [ "$a" = "$b" ]
test: argument expected
使用[ "x$a" = "x$b" ]
可以解决这个问题,因为没有[
以 开头的运算符x
。另一种选择是使用case
:
case "$a" in
"$b") echo same;;
*) echo different;;
esac
(需要引用 around $b
,而不是 around $a
)。
无论如何,它不是也从来没有涉及过空值。当人们忘记引用变量时,他们会遇到空值问题[
,但这不是问题[
。
$ a= b='-o x'
[ $a = $b ]
默认值$IFS
变为:
[ = -o x ]
=
这是对或x
是否为非空字符串的测试,但没有多少前缀将有帮助[ x$a = x$b ]
,仍然会这样:[ x = x-o x ]
这会导致错误,并且可能会变得更糟,包括 DoS 和带有其他值的任意命令注入,例如bash
:
bash-4.4$ a= b='x -o -v a[`uname>&2`]'
bash-4.4$ [ x$a = x$b ]
Linux
正确的解决方案是总是引用:
[ "$a" = "$b" ] # OK in POSIX compliant [ / shells
[ "x$a" = "x$b" ] # OK in all Bourne-like shells
请注意,也expr
有类似(甚至更糟糕)的问题。
expr
还有一个=
运算符,尽管它用于测试两个操作数在看起来像十进制整数时是否是相等的整数,或者在不是时排序相同。
在许多实现中,expr + = +
、 或expr '(' = ')'
或expr index = index
不进行相等比较。expr "x$a" = "x$b"
可以解决字符串比较的问题,但前缀 anx
可能会影响排序(x
例如在具有以 开头的整理元素的语言环境中),并且显然不能用于数字比较,expr "0$a" = "0$b"
不适用于比较负整数。expr " $a" = " $b"
在某些实现中适用于整数比较,但在其他实现中则不然(对于a=01 b=1
,有些会返回 true,有些会返回 false)。
1ksh93
是一个例外。 In ksh93
,[
可以看作是保留字 in ,它[ -t ]
实际上不同于var=-t; [ "$var" ]
, or""[ -t ]
或cmd='['; "$cmd" -t ]
。这是为了保持向后兼容性,并且在重要的情况下仍然符合 POSIX。仅当它是文字时,才会-t
被视为此处的运算符,并且ksh93
检测到您正在调用该[
命令。
² ksh 添加了一个[[...]]
条件表达式运算符,具有自己的语法解析规则(以及它自己的一些问题)来解决这个问题(在其他一些 shell 中也有发现,但有一些差异)。
zsh
³除外分割+全局不会在参数扩展时调用,但是空去除仍然如此,或者在其他 shell 中全局禁用 split+glob 时set -o noglob; IFS=
答案2
人们经常将前缀归因于空字符串的问题,但这并不是原因。问题非常简单:变量的扩展可能是test
s 运算符之一,突然将二进制相等测试变成了不同的表达式。
大多数平台上该命令的最新实现避免了表达式解析器中的前瞻陷阱,防止解析器将二元运算符的第一个操作数识别为操作数以外的任何内容,只要有足够的标记即可是当然是二元运算符:
%a=-n % /bin/test "$a" = -n ;回声$? 0 % /bin/test "$a" = ;回声$? 0 % /bin/test x"$a" = ;回声$? 测试:=:预期参数 2 %a='(' % /bin/test "$a" = "(" ; echo $? 0 % /bin/test "$a" = ;回声$? 测试:预期结束括号 2 %