Bash测试运算符[[...-eq...]]中是Bug还是特性?

3
有人能解释一下以下两者之间的区别吗:
VAR=1xyz && [[ $VAR -eq $VAR ]] 2>/dev/null && echo "Yes, VAR = $VAR is an integer" || echo "No, VAR = $VAR is NOT an integer"
No, VAR = 1xyz is NOT an integer

并且:

VAR=xyz1 && [[ $VAR -eq $VAR ]] 2>/dev/null && echo "Yes, VAR = $VAR is an integer" || echo "No, VAR = $VAR is NOT an integer"
Yes, VAR = xyz1 is an integer

这是Bash中的一个bug还是特性?

如果我使用[ ... ]代替[[ ... ]],我会得到预期结果,即在两种情况下$VAR都不是整数。


请您详细说明一下,唯一的区别在于变量内容是"1xyz"还是"xyz1",但是[[ ... -eq ... ]]的存在状态在两种情况下是不同的!我原本期望两种情况下都不是整数... - user9751447
哦!我看错了,我的错! - N.K
[ ... ] 不关心整数。 - axiac
@user9751447,如果某个变量不是整数但变量名,则会将其视为可能包含整数的变量名称。而一个空变量实际上包含整数0。也就是说,[[ $foo -eq $foo ]]不是检查foo是否为整数的安全方法。请参阅如何在bash中检查变量是否为数字?以了解一些可行的做法。 - Charles Duffy
1个回答

4
为了理解这里发生了什么,您需要明确两件事情:
1. 条件语句的确切含义
在大多数语言中,有某种值可以被解释为真或假。这可能是布尔数据类型、整数(其中0为假,其他所有值都为真)或实现逐个类型的“真值”概念。
但是,Posix shell没有“真值”和“假值”。它们拥有的是可能成功或失败的语句。 “成功”和“失败”意味着什么主要取决于命令本身来确定,但bash本身将把某些行为分类为失败。例如,如果shell无法确定命令名称指的是什么,它将认为该命令已失败:
$ undefined_command && echo Yes || echo No
undefined_command: command not found
No

另外,如果一个命令因为信号(如分段错误)而终止,那么shell会将其视为失败:

$ ./segfault && echo Yes || echo No
Segmentation fault (core dumped)
No

但是许多命令即使错误不致命,也会发出失败信号(通过将它们的状态设置为非零值来实现)。例如,ls 如果其中任何一个文件名参数不存在(即使其他参数存在),它也会返回失败:

$ ls no_file exists && echo Yes || echo No
ls: cannot access 'no_file': No such file or directory
-rw-rw-r-- 1 rici rici 0 May  7 13:13 exists
No

如图所示,通常(虽然不总是)会在stderr中打印出一个错误消息,这个消息可以给出一些关于失败原因的提示。如果你想让自己感到困惑,通常可以抑制掉这个错误消息:

$ undefined_command 2>/dev/null && echo Yes || echo No
No
$ ls no_file exists 2>/dev/null && echo Yes || echo No
-rw-rw-r-- 1 rici rici 0 May  7 13:13 exists
No

这正是您在原始问题中所做的。如果我们不隐藏错误消息,问题就更加明显:

$ VAR=1xyz && [[ $VAR -eq $VAR ]] && echo Yes || echo No
bash: [[: 1xyz: value too great for base (error token is "1xyz")
No
$ VAR=xyz1 && [[ $VAR -eq $VAR ]] && echo Yes || echo No
Yes

换句话说,试图将字符串1xyz作为数字使用(因为-eq数值相等),会产生错误,这被视为失败。然而,字符串xyz1是一个有效的数字值。我们将在接下来的部分中看到为什么会这样。
但在此之前,我们需要注意,[[...]]是一个命令(尽管它是Bash的扩展),而不是一些例外规则,即shell没有布尔值。像任何其他命令一样,[[可以成功或失败;其文档表明,如果将其参数评估为“true”,则它将成功。虽然在Bash中[[是一个内置命令--必须如此,因为它需要不同的参数解析规则--它仍然是一个命令,并且像[一样自己评估其参数。
2. 算术求值的特异性
算术求值发生在$((...))的展开中(在任何Posix shell中),以及在许多其他数字上下文中(在扩展Posix标准的Bash和其他shell中),包括算术条件((...))以及[[...]]$[[...]]中的数字比较运算符的参数。在Bash中,算术求值还用于声明为算术(使用declare -i)和数组下标的变量赋值(不是关联数组)。
对于这个问题而言,算术求值最重要的特性是一个参数可以是shell变量的名称(仅名称,没有$)。在这种情况下,如果可能的话,该变量的值将转换为整数,并用作参数。虽然Posix标准不要求,但几乎所有的shell都会认为未定义的变量或值为空的变量具有数值0。但是,如果变量具有无法转换为数字的非空值,则会产生错误。
这与变量名前面带有$的情况略有不同。如果变量名前面有$,则在计算算术表达式之前,将像正常情况下一样进行普通参数替换。因此,在问题的第二个示例中,
VAR=xyz1 && [[ $VAR -eq $VAR ]] && echo Yes || echo No

参数扩展的结果将会是:
[[ xyz1 -eq xyz1 ]]

由于xyz1(很可能)未定义,因此它将被视为将0与0进行比较,这是正确的(因此命令将成功)。如果xyz1被定义为数字字符串,则会产生相同的结果,但如果其值无法转换为整数,则不会产生相同的结果:

$ VAR=xyz1 && xyz1=42 && [[ $VAR -eq $VAR ]] && echo Yes || echo No
Yes
$ VAR=xyz1 && xyz1=42z && [[ $VAR -eq $VAR ]] && echo Yes || echo No
bash: [[: 42z: value too great for base (error token is "42z")
No

Bash的数字计算规则实际上相当复杂(如果应用于不受信任的输入就不安全)。我不会详细介绍所有细节,但基本上bash将在算术计算中使用变量名称作为参数的“值”上执行算术计算。实际上,这允许递归替换变量名称,但它也允许您将变量的值设置为更复杂的内容:
$ x=y+7
$ y=35
$ echo $((x))
42

1
做得好,Rici。在你和Charles之间,我知道我们会找到问题的根源。 - David C. Rankin

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