获取通过管道传输到另一个进程的退出状态

Mic*_*zek 392 shell pipe exit

我有两个进程foobar,用管道连接:

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

bar总是退出 0;我对foo. 有什么办法可以得到吗?

cam*_*amh 351

bashzsh有一个数组变量,用于保存 shell 执行的最后一个管道的每个元素(命令)的退出状态。

如果您使用bash,则调用数组PIPESTATUS(大小写很重要!)并且数组索引从零开始:

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0
Run Code Online (Sandbox Code Playgroud)

如果您使用zsh,则调用数组pipestatus(大小写很重要!)并且数组索引从 1 开始:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0
Run Code Online (Sandbox Code Playgroud)

以不丢失值的方式将它们组合在一个函数中:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0
Run Code Online (Sandbox Code Playgroud)

bashor 中运行上面的代码zsh,你会得到相同的结果;只有其中之一retval_bashretval_zsh将被设置。另一个将是空白的。这将允许函数以return $retval_bash $retval_zsh(注意缺少引号!)结尾。

  • @JanHudec:它也没有标记为 POSIX。为什么你认为答案必须是 POSIX?它没有指定,所以我提供了一个合格的答案。我的答案没有任何不正确,另外还有多个答案可以解决其他情况。 (16认同)
  • 和 zsh 中的`pipestatus`。不幸的是,其他外壳没有此功能。 (10认同)
  • @JanHudec:也许您应该阅读我回答的前五个字。还请指出问题要求仅 POSIX 答案的地方。 (10认同)
  • 注意:zsh 中的数组从索引 1 开始违反直觉,所以它是 `echo "$pipestatus[1]" "$pipestatus[2]"`。 (9认同)
  • 你可以像这样检查整个管道:`if [ \`echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc\` -ne 0 ]; 然后回显失败;菲` (6认同)
  • @camh:我做到了。我不在乎。该问题未标记为 bash,因此答案不完整。 (2认同)
  • @camh:在那种情况下,正确的答案应该一直适用于 Bourne Shell! (2认同)

phe*_*mer 301

有3种常见的方法来做到这一点:

管道故障

第一种方法是设置pipefail选项(ksh,zshbash)。这是最简单的,它所做的基本上是将退出状态设置为$?最后一个程序的退出代码以退出非零(如果全部成功退出,则为零)。

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1
Run Code Online (Sandbox Code Playgroud)

$管道状态

Bash 还有一个名为$PIPESTATUS( $pipestatusin zsh)的数组变量,它包含最后一个管道中所有程序的退出状态。

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1
Run Code Online (Sandbox Code Playgroud)

您可以使用第三个命令示例来获取您需要的管道中的特定值。

分开处决

这是最笨拙的解决方案。分别运行每个命令并捕获状态

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
Run Code Online (Sandbox Code Playgroud)

  • 该死!我只是要发布有关 PIPESTATUS 的帖子。 (2认同)
  • 作为参考,这个问题中讨论了其他几种技术:http://stackoverflow.com/questions/1221833/bash-tee-output-and-capture-exit-status (2认同)
  • @yael 我不使用 `ksh`,但从它的联机帮助页上看一眼,它不支持 `$PIPESTATUS` 或任何类似的东西。不过它确实支持`pipefail` 选项。 (2认同)

les*_*ana 63

此解决方案无需使用 bash 特定功能或临时文件即可工作。奖励:最终退出状态实际上是退出状态,而不是文件中的某个字符串。

情况:

someprog | filter
Run Code Online (Sandbox Code Playgroud)

你想要退出状态someprog和输出filter

这是我的解决方案:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
Run Code Online (Sandbox Code Playgroud)

这个构造的结果是filter作为构造的标准输出的标准输出和作为构造的退出状态的someprog退出状态。


此构造也适用于简单的命令分组{...}而不是 subshel​​ls (...)。subshel​​ls 有一些影响,其中包括性能成本,我们在这里不需要。阅读精美的 bash 手册以获取更多详细信息:https : //www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1
Run Code Online (Sandbox Code Playgroud)

不幸的是,bash 语法需要花括号的空格和分号,以便结构变得更加宽敞。

对于本文的其余部分,我将使用 subshel​​l 变体。


示例someprogfilter

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?
Run Code Online (Sandbox Code Playgroud)

示例输出:

filtered line1
filtered line2
filtered line3
42
Run Code Online (Sandbox Code Playgroud)

注意:子进程从父进程继承打开的文件描述符。这意味着someprog将继承打开的文件描述符 3 和 4。如果someprog写入文件描述符 3,则将成为退出状态。真正的退出状态将被忽略,因为read只读取一次。

如果您担心someprog可能会写入文件描述符 3 或 4,那么最好在调用someprog.

(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
Run Code Online (Sandbox Code Playgroud)

exec 3>&- 4>&-之前someprog执行前关闭文件描述符someprog那么someprog这些文件描述符根本不存在。

也可以这样写: someprog 3>&- 4>&-


构造的逐步解释:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1
Run Code Online (Sandbox Code Playgroud)

自下而上:

  1. 使用重定向到标准输出的文件描述符 4 创建子shell。这意味着在子 shell 中打印到文件描述符 4 的任何内容都将作为整个构造的标准输出结束。
  2. 创建管道并执行左侧 ( #part3) 和右侧 ( #part2) 的命令。exit $xs也是管道的最后一个命令,这意味着来自 stdin 的字符串将是整个构造的退出状态。
  3. 使用重定向到标准输出的文件描述符 3 创建子shell。这意味着在这个子 shell 中打印到文件描述符 3 的任何内容都将最终进入#part2,反过来将成为整个构造的退出状态。
  4. 创建一个管道并执行左侧 (#part5#part6) 和右侧 ( filter >&4) 的命令。的输出filter被重定向到文件描述符 4。在#part1文件描述符 4 中被重定向到 stdout。这意味着 的输出filter是整个构造的标准输出。
  5. 退出状态从#part6打印到文件描述符 3。在#part3文件描述符 3 中被重定向到#part2. 这意味着退出状态 from#part6将是整个构造的最终退出状态。
  6. someprog被执行。退出状态为#part5。标准输出由管道接收#part4并转发到filter. 的输出filter将依次到达 stdout,如中所述#part4

  • `(exec 3>&- 4>&-; someprog)` 简化为 `someprog 3>&- 4>&-`。 (5认同)

Chr*_*ris 39

虽然不完全符合您的要求,但您可以使用

#!/bin/bash -o pipefail
Run Code Online (Sandbox Code Playgroud)

以便您的管道返回最后一个非零返回。

可能会少一点编码

编辑:示例

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
Run Code Online (Sandbox Code Playgroud)

  • 脚本中的`set -o pipefail` 应该更加健壮,例如,如果有人通过`bash foo.sh` 执行脚本。 (9认同)
  • 请注意,`-o pipefail` 不在 POSIX 中。 (2认同)
  • 这在我的 BASH 3.2.25(1) 版本中不起作用。在 /tmp/ff 的顶部,我有 `#!/bin/bash -o pipefail`。错误是:`/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name` (2认同)
  • @FelipeAlvarez:某些环境(包括 linux)不会解析第一个行之外的 `#!` 行上的空格,因此这变成了 `/bin/bash` `-o pipefail` `/tmp/ff`,而不是必要的 `/bin/bash` `-o` `pipefail` `/tmp/ff` -- `getopt`(或类似的)使用 `optarg`(它是 `ARGV` 中的下一项)作为参数进行解析`-o`,所以它失败了。如果你要制作一个包装器(比如,`bash-pf` 只是做了`exec /bin/bash -o pipefail "$@"`,然后把它放在`#!` 行上,那就行了。另见: https://en.wikipedia.org/wiki/Shebang_%28Unix%29 (2认同)

Gil*_*il' 22

什么可以当我这样做是从饲料退出代码foobar。例如,如果我知道foo永远不会产生只有数字的行,那么我可以添加退出代码:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'
Run Code Online (Sandbox Code Playgroud)

或者,如果我知道从foonever的输出只包含一行.

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'
Run Code Online (Sandbox Code Playgroud)

如果有某种方法bar可以处理除最后一行之外的所有内容,并将最后一行作为其退出代码传递,则始终可以完成此操作。

如果bar是一个复杂的管道,您不需要其输出,您可以通过在不同的文件描述符上打印退出代码来绕过它的一部分。

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)
Run Code Online (Sandbox Code Playgroud)

在这之后$exit_codes通常是foo:X bar:Y,但它可能是bar:Y foo:X,如果bar退出所有阅读它的输入之前,或者如果你运气不好。我认为写入最多 512 字节的管道在所有 unices 上都是原子的,因此只要标签字符串低于 507 字节,就不会混合foo:$?bar:$?部分。

如果您需要从 捕获输出bar,则变得困难。您可以通过安排barnever的输出包含看起来像退出代码指示的行来组合上述技术,但它确实变得繁琐。

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')
Run Code Online (Sandbox Code Playgroud)

当然,还有使用临时文件来存储状态的简单选项。简单,但在生产中并不那么简单:

  • 如果有多个脚本同时运行,或者同一个脚本在多个地方使用这种方法,你需要确保它们使用不同的临时文件名。
  • 在共享目录中安全地创建临时文件很困难。通常,/tmp是脚本确定能够写入文件的唯一地方。使用mktemp,这不是 POSIX,但现在可以在所有严肃的 unice 上使用。
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")
Run Code Online (Sandbox Code Playgroud)


Jan*_*der 17

从管道开始:

foo | bar | baz
Run Code Online (Sandbox Code Playgroud)

这是仅使用 POSIX shell 而没有临时文件的通用解决方案:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-
Run Code Online (Sandbox Code Playgroud)

$error_statuses 包含任何失败进程的状态代码,以随机顺序排列,并带有指示哪个命令发出每个状态的索引。

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null
Run Code Online (Sandbox Code Playgroud)

注意$error_statuses我的测试中的引号;没有它们grep就无法区分,因为换行符被强制为空格。


小智 12

如果您安装了moreutils包,您可以使用mispipe实用程序,它完全符合您的要求。


mtr*_*eur 12

所以我想贡献一个像 lesmana 的答案,但我认为我的可能更简单一点,而且更有利的纯 Bourne-shell 解决方案:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.
Run Code Online (Sandbox Code Playgroud)

我认为最好从内向外解释——command1 将在标准输出(文件描述符 1)上执行并打印其常规输出,然后一旦完成,printf 将在其标准输出上执行并打印 command1 的退出代码,但该标准输出被重定向到3. 文件描述符

当 command1 正在运行时,它的 stdout 正在通过管道传输到 command2(printf 的输出永远不会将其发送到 command2,因为我们将它发送到文件描述符 3 而不是 1,这是管道读取的内容)。然后我们将 command2 的输出重定向到文件描述符 4,这样它也不会出现在文件描述符 1 之外——因为我们希望文件描述符 1 稍后空闲,因为我们会将文件描述符 3 上的 printf 输出带回文件描述符1 - 因为这是命令替换(反引号)将捕获的内容,这就是将放入变量的内容。

最后一点神奇的是,首先exec 4>&1我们作为一个单独的命令执行——它打开文件描述符 4 作为外部 shell 标准输出的副本。命令替换将从其中的命令的角度捕获标准输出上写入的任何内容 - 但是,由于 command2 的输出将发送到文件描述符 4 就命令替换而言,命令替换不会捕获它 - 但是,一旦它从命令替换中“退出”,它实际上仍然会转到脚本的整体文件描述符 1。

exec 4>&1必须是一个单独的命令,因为当您尝试在命令替换中写入文件描述符时,许多常见的 shell 不喜欢它,该命令在使用替换的“外部”命令中打开。所以这是最简单的便携式方法。)

你可以用一种不太技术、更有趣的方式来看待它,好像命令的输出是相互跳跃的:command1 管道到 command2,然后 printf 的输出跳过命令 2,这样 command2 就不会捕捉到它,然后命令 2 的输出跳过命令替换,就像 printf 及时着陆以被替换捕获以便它结束在变量中一样,并且命令 2 的输出继续以愉快的方式写入标准输出,就像在普通管道中。

另外,据我了解,$?管道中仍然会包含第二条命令的返回码,因为变量赋值、命令替换和复合命令对于其中的命令返回码都是有效透明的,所以返回状态为command2 应该被传播出去——这就是为什么我认为这可能是比 lesmana 提出的更好的解决方案,而不必定义额外的函数。

根据 lesmana 提到的警告,command1 可能会在某个时候最终使用文件描述符 3 或 4,因此为了更健壮,您可以这样做:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-
Run Code Online (Sandbox Code Playgroud)

请注意,我在我的示例中使用了复合命令,但是子 shell(使用( )而不是{ }也可以工作,但可能效率较低。)

命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符 4,后面的复合命令3>&1将继承文件描述符 3。因此4>&-确保内部复合命令不会继承文件描述符 4,并且3>&-不会继承文件描述符 3,因此 command1 得到一个“更干净”、更标准的环境。您也可以将内部移动到4>&-旁边3>&-,但我想为什么不尽可能限制其范围。

我不确定直接使用文件描述符 3 和 4 的频率 - 我认为大多数时候程序使用返回未使用的文件描述符的系统调用,但有时代码直接写入文件描述符 3,我猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果没有则相应地表现不同)。所以后者可能最好记住并用于通用情况。


小智 7

上面 lesmana 的解决方案也可以通过使用{ .. }代替来完成,而无需启动嵌套子进程的开销(记住这种形式的分组命令总是必须以分号结束)。像这样的东西:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1
Run Code Online (Sandbox Code Playgroud)

我已经使用 dash 版本 0.5.5 和 bash 版本 3.2.25 和 4.2.42 检查了这个构造,所以即使某些 shell 不支持{ .. }分组,它仍然符合 POSIX。


jll*_*gre 5

这是可移植的,即可以与任何 POSIX 兼容的 shell 一起使用,不需要当前目录可写,并且允许使用相同技巧的多个脚本同时运行。

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))
Run Code Online (Sandbox Code Playgroud)

编辑:这是根据吉尔斯的评论的更强版本:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))
Run Code Online (Sandbox Code Playgroud)

Edit2:这是一个稍轻的变体,遵循 dubiousjim 评论:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
Run Code Online (Sandbox Code Playgroud)

  • 由于多种原因,这不起作用。1. 临时文件可以在写入之前被读取。2. 在共享目录中创建具有可预测名称的临时文件是不安全的(微不足道的 DoS、符号链接竞争)。3. 如果同一个脚本多次使用此技巧,它将始终使用相同的文件名。要解决 1,请在管道完成后读取文件。要解决问题 2 和 3,请使用名称随机生成的临时文件或位于私有目录中。 (3认同)

Tin*_*ino 5

以下是@Patrik 答案的补充,以防您无法使用其中一种常见解决方案。

这个答案假设如下:

  • 你有一个不知道$PIPESTATUS也不知道的壳set -o pipefail
  • 您想使用管道进行并行执行,因此没有临时文件。
  • 如果您中断脚本(可能是突然断电),您不希望周围有额外的混乱。
  • 这个解决方案应该相对容易理解并且易于阅读。
  • 您不想引入额外的子shell。
  • 您无法摆弄现有的文件描述符,因此不得触及 stdin/out/err (但您可以临时引入一些新的)

额外的假设。你可以去掉所有的,但这对食谱的破坏太大了,所以这里不涉及:

  • 您只想知道 PIPE 中的所有命令都具有退出代码 0。
  • 您不需要额外的边带信息。
  • 您的 shell 会等待所有管道命令返回。

之前:foo | bar | baz,但是这仅返回最后一个命令的退出代码 ( baz)

通缉:$?不能是0(真),如果管道中的任何命令失败

后:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0
Run Code Online (Sandbox Code Playgroud)

解释:

  • 临时文件是用mktemp. 这通常会立即在/tmp
  • 然后将此临时文件重定向到 FD 9 进行写入和 FD 8 进行读取
  • 然后立即删除临时文件。但是,它会保持打开状态,直到两个 FD 都不存在为止。
  • 现在管道已启动。如果出现错误,每个步骤只会添加到 FD 9。
  • wait是需要ksh的,因为ksh别人不会等待所有管道命令完成。但是请注意,如果存在一些后台任务,则会产生不需要的副作用,因此我默认将其注释掉。如果等待没有受到伤害,您可以评论它。
  • 然后读取文件的内容。如果它是空的(因为所有工作)read返回false,所以true表示一个错误

这可以用作单个命令的插件替换,只需要以下内容:

  • 未使用的 FD 9 和 8
  • 保存临时文件名称的单个环境变量
  • 这个秘籍可以适用于任何允许 IO 重定向的 shell
  • 此外,它与平台无关,不需要像 /proc/fd/N

错误:

此脚本有一个错误,以防/tmp空间不足。如果您也需要针对这种人为情况进行保护,您可以按如下方式进行,但是这样做有一个缺点,即0in000的数量取决于管道中的命令数量,因此稍微复杂一些:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"
Run Code Online (Sandbox Code Playgroud)

便携性注意事项:

  • ksh和只等待最后一个管道命令的类似 shell 需要未wait注释的

  • 最后一个例子使用了printf "%1s" "$?"而不是echo -n "$?"因为它更便携。并非每个平台都能-n正确解释。

  • printf "$?"也会这样做,但是printf "%1s"如果您在某个真正损坏的平台上运行脚本,则会捕获一些极端情况。(阅读:如果您碰巧在paranoia_mode=extreme.)

  • FD 8 和 FD 9 在支持多位数的平台上可以更高。AFAIR 符合 POSIX 的 shell 只需要支持个位数。

  • 已用 Debian 8.2 shbashkshashsash甚至csh


归档时间:

查看次数:

198044 次

最近记录:

5 年,6 月 前