保留 $? 的空操作 shell 命令

No-op shell command that preserves $?

仅使用 POSIX shell 的功能,是否存在不执行任何操作 并且不更改 $?[ 的值的“简单命令” =51=] 人们通常将 : 描述为 shell 的无操作命令,但这总是将 $? 设置为零,所以这不是我想要的。

生成 shell 脚本的程序需要它。在几个地方它需要发出一个 if-then-else 块

if CONDITION
then
    TRUE-COMMANDS
else
    FALSE-COMMANDS
fi

但是,由于我现在不想解释的更复杂的情况,它无法可靠地判断 TRUE-COMMANDSFALSE-COMMANDS 是否为空。空的 then 或 else 子句将是 shell 语法错误。

: 可以放在 then 子句的开头,以处理 TRUE-COMMANDS 为空的情况,但这对 FALSE-COMMANDS 不起作用,因为它破坏了 $? 并且 FALSE-COMMANDS 可能想要查看条件设置的值 $?。同样的道理,:也不能放在之后 FALSE-COMMANDS——if-then-else后面的代码可能要看$?来自 if-then-else 中的最后一个操作。

如果你能避免的话加分:

  • 分叉:(exit $?) 完成了工作,但生成的脚本中有太多这样的条件块,它会产生可测量的减速。

  • 功能:给定 nop () { return $? } 然后 nop 完成工作,但由于 更多 并发症,我宁愿不得到进入,为所有需要它的地方尽早定义 nop 是不切实际的。

在符合 POSIX 的 shell 中当然不可能编写不涉及 $? 的命令。通过阅读 IEEE Std 1003.1-2017 vol. 3 第 2 章“Shell 命令语言”,我们了解到:

  • §2.8.2 说“[e]每个命令都有退出状态”(所以没有命令没有退出状态)。
  • §2.5.2 ‘特殊参数’说 $? ‘[e]x 扩展到最近管道的十进制退出状态(参见第 2.9.2 节)’。
  • §2.9.2 “管道”表示管道是一个或多个命令的序列,可选地(作为一个整体)前面有 !,命令由 | 连接。关于退出状态,它说

    If the pipeline does not begin with the ! reserved word, the exit status shall be the exit status of the last command specified in the pipeline. Otherwise, the exit status shall be the logical NOT of the exit status of the last command.

  • §2.9 'Shell Commands' 定义了一个 'command' 并表示 '[u] 除非另有说明,否则命令的退出状态应为该命令执行的最后一个简单命令的退出状态' . “命令”可以是以下任何一项:
    • 一个简单的命令(§2.9.1),它只是一个外部程序,一个shell内置程序或一个变量赋值(没有命令名)。前者当然会 return 执行命令的退出状态。关于后者,规范说:

      If there is no command name, but the command contained a command substitution, the command shall complete with the exit status of the last command substitution performed. Otherwise, the command shall complete with a zero exit status.

    • 上面§2.9.2描述的管道。
    • 一个复合列表(§2.9.3),它是一个或多个{一个或多个{一个或多个管道的序列(见上文)的序列,由&&连接或||},每个由 ;& 终止(最后的 ; 可选)},由换行符连接。 A compound-list returns 同步执行的最后一个管道的退出状态;异步执行的管道(由 & 终止的管道)将退出状态设置为零。 None 个序列可能为空,这保证至少会执行一个管道。
    • 复合命令 (§2.9.4),它是:
      • 子shell 或大括号复合列表 (§2.9.4.1),其中 return 是基础复合列表的退出状态(见上文)。
      • 条件构造(case,§2.9.4.3 或 if,§2.9.4.4)或循环(for,§2.9.4.2;while, §2.9.4.5; until, §2.9.4.6)。如果未执行此构造的主体,则它 return 退出状态为零。
    • 函数定义 (§2.9.5),return如果定义被接受,退出状态为零,否则为非零状态。
  • 最后,§2.9.4.4“if 条件构造”将 if 构造的条件和主体定义为复合列表,如上述 §2.9.3 所述。这意味着 if 构造的主体将始终包含至少一个会破坏 $? 的命令。该规范甚至没有为“实现定义的行为”留下任何回旋余地。

以上所有意味着编写保留 $? 的无操作命令的唯一方法是读出 $? 中的值,然后 return 将其返回。 POSIX shell 中有三个结构能够做到这一点,其中两个已经在问题正文中提到:来自子 shell 的 exit 或函数调用.

第三个是.:@Jens的回答中提到的shell采购声明。通过获取仅包含 return "$?" 命令的脚本,可以保留 $? 的值。但是,这需要您安排在某个已知位置找到合适的脚本,我认为这与确保在文件中足够早地定义无操作函数(如果不是实际上更多)一样不方便。

如果稍微改变严格的 POSIX 要求,即使这样也可以克服:

. /dev/stdin <<EOF
return "$?"
EOF

/dev/stdin 不是 POSIX 功能,但可以广泛使用;它在 IEEE Std 1003.1-2017 vol. 中明确列出。 1 §2.1.1 作为扩展。上面的代码片段已经过测试,可以在 bash 5.0.8、dash 0.5.11 和 busybox sh 1.30.1 on Linux 中工作(适当设置 /dev 等等)。

最简单的方法是使用简单的赋值。不要使用 :,而是使用 _rc=$?.

if condition; then
   [ list-true ]     # optional 
   _rc=$?
else
   [ list-false ]    # optional
   _rc=$?
fi
( exit $_rc )        # optional
list-post-if

使用这个变量 _rc,你已经存储了最后执行的命令的退出状态,无论这是 condition 还是 list-truelist-false 中的最后一个命令.

  • 支持此方法的论点是赋值的低开销。
  • 反对的论点是至少需要重写 list-post-if 以使用 _rc 而不是 $?
  • 如果后者不可能,或者太乏味,您可能会考虑在条件语句之后添加一个 (exit $_rc)。然而,这需要一个子shell,但它只是一个

不要让任何人告诉你只有函数和子 shell。

你能创建或分发另一个小文件吗?如果是这样,您可以创建一个文件

return $?

然后将其作为保持退出状态的“空”命令获取:

$ echo 'return $?' > keepstatus
$ ls foobar
ls: fooobar: No such file or directory
$ . ./keepstatus
$ echo $?
2
$ sleep 100
^C
$ . ./keepstatus
$ echo $?
130

不分叉,不使用函数,没有额外的变量,保持状态并保持原样。

我什至还有第四种方法,当我牺牲分叉和假设的奖励点时,因为你在 autoconf m4 领域,找到和使用主机编译器是一件轻而易举的事。

cat > keepstatus.c <<EOF
#include <stdlib.h>
int main(int argc, char **argv) {
    return argv[1] ? atoi(argv[1]) : 0;
}
EOF
$CC -o keepstatus keepstatus.c

然后使用/path/to/keepstatus $?.