如何确定变量赋值的返回状态?

Wil*_*ard 14 bash command-substitution variable exit-status

我在脚本中看到过这样的结构:

if somevar="$(somecommand 2>/dev/null)"; then
...
fi
Run Code Online (Sandbox Code Playgroud)

这是在某处记录的吗?变量的返回状态是如何确定的,它与命令替换有什么关系?(例如,我会得到相同的结果if echo "$(somecommand 2>/dev/null)"; then吗?)

G-M*_*ca' 18

它在 Open Group Base Specifications 的第 2.9.1 节简单命令中有记录(针对 POSIX)。那里有一堵文字墙;我将你的注意力引向最后一段:

如果有命令名称,则应按照命令搜索和执行中的描述继续执行。如果没有命令名称,但命令包含命令替换,则该命令应以上次执行的命令替换的退出状态完成。否则,该命令将以零退出状态完成。

所以,例如,

   Command                                         Exit Status
$ FOO=BAR                                   0 (but see also the note from icarus, below)
$ FOO=$(bar)                                Exit status from "bar"
$ FOO=$(bar)$(quux)                         Exit status from "quux"
$ FOO=$(bar) baz                            Exit status from "baz"
$ foo $(bar)                                Exit status from "foo"
Run Code Online (Sandbox Code Playgroud)

这也是 bash 的工作方式。但另请参阅最后的“不那么简单”部分。

phk,在他的问题中,分配就像具有退出状态的命令,除非有命令替换?, 建议

……似乎赋值本身算作一个命令……退出值为零,但它适用于赋值的右侧(例如,命令替换调用……)

这不是一种可怕的看待方式。?用于确定一个简单的命令的返回状态的粗方案(一个不含有;&|&&||α)为:

  • 从左到右扫描该行,直到到达末尾或命令字(通常是程序名称)。

  • 如果您看到变量赋值,则该行的返回状态可能只是 0。

  • 如果您看到命令替换——即$(…)——从该命令中获取退出状态。

  • 如果到达实际命令(不在命令替换中),请从该命令获取退出状态。

  • 该行的返回状态是您遇到的最后一个数字。
    命令替换作为命令的参数,例如,foo?$(bar)不计算在内;你从foo. 用phk 的符号来解释,这里的行为是

      temporary_variable  = EXECUTE( "bar" )
      overall_exit_status = EXECUTE( "foo", temporary_variable )
    
    Run Code Online (Sandbox Code Playgroud)

但这有点过于简单化了。总体退货状态来自

A=$( cmd 1 ) B=$( cmd 2 ) C=$( cmd 3 ) D=$( cmd 4 ) E=mc 2
是从 的退出状态。在之后出现的分配分配不会整体退出状态设置为0。cmd4E=D=

icarus回答 phk 的问题时提出了一个重要观点:变量可以设置为只读。POSIX 标准第 2.9.1 节的倒数第三段说,

如果任何变量赋值尝试为在当前 shell 环境中设置了readonly属性的变量赋值(无论是否在该环境中进行赋值),都会发生变量赋值错误。有关这些错误的后果,请参阅Shell 错误的后果。

所以如果你说

A=$(cmd1)  B=$(cmd2)  C=$(cmd3)  D=$(cmd4)  E=mc2

返回状态为 1。字符串GarfieldFelix和/ 或Tigger 是否替换为命令替换并不重要- 但请参阅下面的注释。

第 2.8.1 节 Shell 错误的后果有另一串文本和一个表格,并以

在表中显示的所有交互式 shell 要求不退出的情况下,shell 不应对发生错误的命令执行任何进一步处理。

一些细节是有道理的;有些没有:

  • A=分配有时中止命令行,因为这最后一句,似乎说明。在上面的例子中,C设置为Garfield,但T没有设置(当然,也没有设置 A)。
  • 同样, 执行 但不执行。但是,在我的 bash 版本(包括 4.1.X 和 4.3.X)中,它确实执行. (顺便说一句,这进一步弹劾了 phk 的解释,即赋值的退出值适用于赋值的右侧之前。)C=$(cmd1) A=$(cmd2) T=$(cmd3)cmd1cmd3
    cmd2

但这里有一个惊喜:

在我的 bash 版本中,

只读 A
C=某物A=某物T=某物 cmd 0

确实执行。特别是,cmd0

C=$( cmd 1 ) A=$( cmd 2 ) T=$( cmd 3 )    cmd 0
执行 and ,但不执行。(请注意,这与它在没有命令时的行为相反。)并且它在 的环境中设置(以及)。我想知道这是否是 bash 中的错误。cmd1cmd3cmd2TCcmd0


没那么简单:

这个答案的第一段是指“简单命令”。  规范说,

“简单命令”是一系列可选的变量赋值和重定向,以任何顺序,可选地后跟单词和重定向,由控制运算符终止。

这些语句类似于我的第一个示例块中的语句:

readonly A
C=Garfield A=Felix T=Tigger
Run Code Online (Sandbox Code Playgroud)

前三个包括变量赋值,后三个包括命令替换。

但是一些变量赋值并不是那么简单。  bash(1)说,

赋值语句也可能出现作为参数传递给aliasdeclaretypesetexportreadonly,和local内置命令(声明命令)。

对于exportPOSIX 规范说,

退出状态

    0
      所有名称操作数均已成功导出。
    >0
      至少无法导出一个名称,或者-p指定了选项并发生错误。

POSIX 不支持local,但bash(1)说,

local不在函数内时使用是错误的。返回状态为 0,除非local在函数外使用、提供了无效名称name是只读变量。

在两行之间阅读,我们可以看到声明命令如下

readonly A
C=something A=something T=something cmd0

C=$(cmd1)   A=$(cmd2)   T=$(cmd3)   cmd0

更像是

$ FOO=BAR
$ FOO=$(bar)
$ FOO=$(bar) baz
$ foo $(bar)
Run Code Online (Sandbox Code Playgroud)

只要他们忽略退出状态bar ,并给您基于主命令的退出状态(exportlocal,或foo)。所以我们有像

export FOO=$(bar)
Run Code Online (Sandbox Code Playgroud)

我们可以用

local FOO=$(bar)
Run Code Online (Sandbox Code Playgroud)

foo $(bar)
Run Code Online (Sandbox Code Playgroud)

幸运的是,ShellCheck捕获到错误并引发SC2155,它建议

   Command                                           Exit Status
$ FOO=$(bar)                                    Exit status from "bar"
                                                  (unless FOO is readonly)
$ export FOO=$(bar)                             0 (unless FOO is readonly,
                                                  or other error from “export”)
$ local FOO=$(bar)                              0 (unless FOO is readonly,
                                                  statement is not in a function,
                                                  or other error from “local”)
Run Code Online (Sandbox Code Playgroud)

应该改为

$ export FRIDAY=$(date -d tomorrow)
$ echo "FRIDAY   = $FRIDAY, status = $?"
FRIDAY   = Fri, May 04, 2018  8:58:30 PM, status = 0
$ export SATURDAY=$(date -d "day after tomorrow")
date: invalid date ‘day after tomorrow’
$ echo "SATURDAY = $SATURDAY, status = $?"
SATURDAY = , status = 0
Run Code Online (Sandbox Code Playgroud)

myfunc() {
    local x=$(echo "Foo"; true);  echo "x = $x -> $?"
    local y=$(echo "Bar"; false); echo "y = $y -> $?"
    echo -n "BUT! "
    local z; z=$(echo "Baz"; false); echo "z = $z -> $?"
}

$ myfunc
x = Foo -> 0
y = Bar -> 0
BUT! z = Baz -> 1
Run Code Online (Sandbox Code Playgroud)

应该改为

export foo="$(mycmd)"
Run Code Online (Sandbox Code Playgroud)

信用和参考

我想到了连接命令替换的想法 — $(bar)$(quux)— 从 Gilles 的回答 如何以与 pipefail 类似的方式让 bash 在反引号失败时退出?,其中包含许多与此问题相关的信息。