在Bash中,变量赋值到命令替换的退出代码

72

我不确定在执行变量赋值时,简单地使用命令替换会返回什么错误代码:

a=$(false); echo $?

它输出1,这让我想到变量赋值不会在最后一个错误代码上进行清除或生成新的错误代码。但是当我尝试了以下内容时:

false; a=""; echo $?

显然,这就是a =“”的返回值,覆盖了false返回的1,因此它输出0

我想知道为什么会发生这种情况,变量分配是否有任何与其他正常命令不同的特殊性?还是仅因为a = $(false)被认为是单个命令,并且只有命令替换部分有意义?

-- 更新 --

感谢大家的回答和评论,我得到了这一点:“当您使用命令替换分配变量时,退出状态是该命令的状态。”(来自@Barmar),这个解释非常清晰易懂,但对于程序员来说并不够准确,我想看到来自TLDP或GNU man页面等权威来源的这一点的参考资料,请帮助我找出来,再次感谢!


3
TLDP不是权威来源——ABS因展示Bash中的不良实践而被公认为臭名昭著,就像W3Schools在JavaScript领域一样。 - Charles Duffy
1
相关:如何确定变量赋值的返回状态? - G-Man Says 'Reinstate Monica'
5个回答

77

在执行命令 $(command) 时,允许命令的输出替换自身

当你说:

a=$(false)             # false fails; the output of false is stored in the variable a

命令false生成的输出存储在变量a中。此外,退出代码与该命令生成的代码相同。help false会显示:

false: false
    Return an unsuccessful result.
    
    Exit Status:
    Always fails.

另一方面,说:

$ false                # Exit code: 1
$ a=""                 # Exit code: 0
$ echo $?              # Prints 0

导致分配给a的退出代码被返回,即0


编辑:

引用自手册

如果扩展中包含命令替换,则该命令的退出状态是执行的最后一个命令替换的退出状态。

引用自BASHFAQ/002

如何将命令的返回值和/或输出存储在变量中?

...

output=$(command)

status=$?

对于command的退出状态,对output的赋值没有影响,它仍然在$?中。

这不是特定于bash的。引用自POSIX.1-2017的“Shell&Utilities”卷的第2.9.1节“简单命令”的结尾:

如果没有命令名称,但命令包含命令替换,则该命令将以执行的最后一个命令替换的退出状态完成。

91
请记住,这并不适用于“本地”变量(例如,“local output=$(command)”),因为“local”本身是一个命令,其退出代码将覆盖分配函数的退出代码。 - sevko
1
@sevko,这个区别非常重要,感谢你指出来! - qodeninja
31
关于 @sevko 和 @qodeninja 关于local的评论,如果变量不是 readonly,则可以在声明后进行初始化以避免此问题: local my_var; my_var=$(command); local status=$? - Adrian Günter
8
我的天啊,这浪费了我太多时间。谢谢 @sevko,否则我可能永远也想不出来。 - justin.m.chase
3
将“local”声明与命令替换结合使用的陷阱在[Bash Pitfall#27](https://mywiki.wooledge.org/BashPitfalls#local_var.3D.24.28cmd.29)中有所描述。它还会影响“declare”和“export”。 - Robin A. Meade
shellcheck 可以捕获和解释 localexport 的陷阱。shellcheck 可以挽救生命,一定要使用它。 - MarcH

22
注意,当与 local 结合使用时,情况并非如此,例如 local variable="$(command)"。即使 command 失败,该形式也会成功退出。
以这个 Bash 脚本为例:
#!/bin/bash

function funWithLocalAndAssignmentTogether() {
    local output="$(echo "Doing some stuff.";exit 1)"
    local exitCode=$?
    echo "output: $output"
    echo "exitCode: $exitCode"
}

function funWithLocalAndAssignmentSeparate() {
    local output
    output="$(echo "Doing some stuff.";exit 1)"
    local exitCode=$?
    echo "output: $output"
    echo "exitCode: $exitCode"
}

funWithLocalAndAssignmentTogether
funWithLocalAndAssignmentSeparate

这是此操作的输出结果:

nick.parry@nparry-laptop1:~$ ./tmp.sh 
output: Doing some stuff.
exitCode: 0
output: Doing some stuff.
exitCode: 1

这是因为local实际上是一个内置命令,而像local variable="$(command)"这样的命令在调用local之前会先替换command的输出。因此,您会得到来自local的退出状态。


3
原因在于 local (同样适用于 export)是一个 Bash 内置函数,在第一个例子中的调用方式导致成功,因此返回了一个 0 的退出码。 - metatoaster
希望你不介意,我已经编辑了这个答案以澄清情况;只有当你将local与赋值结合在一个语句中时才会出现问题。(有趣的是:因为local是一个内置命令,而不是特殊语法,所以你甚至可以写类似于local "$(echo foo=bar)"的东西,它会像local foo=bar一样运行,创建一个名为foo的本地变量并将其初始化为bar。相比之下,命令"$(echo foo=bar)"会给你bash: foo=bar: command not found,因为Bash在那个点上不期望有赋值。) - ruakh
哈!这正是让我困扰的细节! - Geoff

5

我昨天(2018年8月29日)遇到了同样的问题。

除了Nick P.的答案和@sevko在被接受的答案中提到的local,全局范围中的declare也有相同的行为。

这是我的Bash代码:

#!/bin/bash

func1()
{
    ls file_not_existed
    local local_ret1=$?
    echo "local_ret1=$local_ret1"

    local local_var2=$(ls file_not_existed)
    local local_ret2=$?
    echo "local_ret2=$local_ret2"

    local local_var3
    local_var3=$(ls file_not_existed)
    local local_ret3=$?
    echo "local_ret3=$local_ret3"
}

func1

ls file_not_existed
global_ret1=$?
echo "global_ret1=$global_ret1"

declare global_var2=$(ls file_not_existed)
global_ret2=$?
echo "global_ret2=$global_ret2"

declare global_var3
global_var3=$(ls file_not_existed)
global_ret3=$?
echo "global_ret3=$global_ret3"

输出:
$ ./declare_local_command_substitution.sh 2>/dev/null 
local_ret1=2
local_ret2=0
local_ret3=2
global_ret1=2
global_ret2=0
global_ret3=2

请注意上面输出中local_ret2global_ret2的值。退出码被localdeclare覆盖。
我的Bash版本:
$ echo $BASH_VERSION 
4.4.19(1)-release

4
(不是对原问题的回答,但太长了不能在评论中)请注意,export A=$(false); echo $?输出0!显然,devnull的回答中引用的规则不再适用。为了给引用添加一点上下文(强调是我的):

3.7.1简单命令扩展

...

如果扩展后还有剩余的命令名称,则执行如下所述。否则,该命令退出。如果扩展中包含命令替换,则该命令的退出状态为最后执行的命令替换的退出状态。如果没有命令替换,则该命令以零状态退出。

3.7.2命令搜索和执行[ - 这是"以下"的情况]

如果我理解得正确,手册将var=foo描述为var=foo命令...语法的特殊情况(非常令人困惑!)。“最后一个命令替换的退出状态”规则仅适用于无命令的情况。

虽然认为export var=foo是“修改后的赋值语法”很诱人,但它不是 - export是一个内置命令(只是偶然接受类似于赋值的参数)。

=>如果要导出变量并捕获命令替换状态,请分为2个阶段执行:

A=$(false)
# ... check $?
export A

这种方法在set -e模式下也有效——如果命令替换返回非0,则立即退出。

0

正如其他人所说,命令替换的退出代码是被替换命令的退出代码,因此

FOO=$(false)
echo $?
---
1

然而,出乎意料的是,在开头添加export会产生不同的结果:

export FOO=$(false)
echo $?
---
0

这是因为,虽然替换命令false失败了,但export命令成功了,这就是语句返回的退出代码。

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