it-swarm.cn

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

我有两个通过管道连接的进程foobar

$ foo | bar

bar总是退出0;我对foo的退出代码感兴趣。有什么办法吗?

314
Michael Mrozek

如果使用bash,则可以使用PIPESTATUS数组变量来获取管道中每个元素的退出状态。

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

如果使用zsh,则它们的数组称为pipestatus(需要注意!),并且数组索引从1开始:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

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

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

bashzsh中运行上面的命令,您将获得相同的结果;只能设置retval_bashretval_zsh之一。另一个将为空白。这将使函数以return $retval_bash $retval_zsh结尾(请注意缺少引号!)。

277
camh

有3种常见的方法:

管道失败

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

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

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

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

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

单独执行

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

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
253
Patrick

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

情况:

someprog | filter

您需要someprog的退出状态和filter的输出。

这是我的解决方案:

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

此构造的结果是来自filter的stdout作为构造的stdout,而来自someprog的退出状态是该构造的退出状态。


此构造也可用于简单的命令分组{...}而不是子shell (...)。子外壳会带来一些影响,其中包括性能成本,在这里我们不需要。请阅读精美的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

不幸的是,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 $?

输出示例:

filtered line1
filtered line2
filtered line3
42

注意:子进程从父进程继承打开文件描述符。这意味着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

someprog之前的exec 3>&- 4>&-在执行someprog之前关闭文件描述符,因此对于someprog这些文件描述符根本不存在。

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


逐步解释构造:

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

从下至上:

  1. 创建一个子shell,并将文件描述符4重定向到stdout。这意味着在子shell中打印到文件描述符4的所有内容将最终成为整个构造的标准输出。
  2. 创建管道并执行左侧(#part3)和右侧(#part2)的命令。 exit $xs也是管道的最后一个命令,这意味着stdin中的字符串将成为整个构造的退出状态。
  3. 创建一个子shell,并将文件描述符3重定向到stdout。这意味着在此子外壳程序中打印到文件描述符3的所有内容都将以#part2结尾,进而将成为整个构造的退出状态。
  4. 创建管道,并执行左侧(#part5#part6)和右侧(filter >&4)的命令。 filter的输出重定向到文件描述符4。在#part1中,文件描述符4重定向到stdout。这意味着filter的输出是整个构造的标准输出。
  5. #part6的退出状态被打印到文件描述符3。在#part3中,文件描述符3被重定向到#part2。这意味着#part6的退出状态将是整个构造的最终退出状态。
  6. someprog被执行。退出状态以#part5表示。标准输出由#part4中的管道获取,并转发到filterfilter的输出将依次达到stdout,如#part4中所述
58
lesmana

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

#!/bin/bash -o pipefail

以便您的管道返回最后的非零收益。

可能少一些编码

编辑:示例

[[email protected] ~]# false | true
[[email protected] ~]# echo $?
0
[[email protected] ~]# set -o pipefail
[[email protected] ~]# false | true
[[email protected] ~]# echo $?
1
37
Chris

我可能会做的是将退出代码从foo输入到bar。例如,如果我知道foo永远不会产生只包含数字的行,那么我可以添加退出代码:

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

或者,如果我知道foo的输出从不包含仅.的行:

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

如果有某种方法可以使bar在除最后一行以外的所有代码上工作,并将最后一行作为其退出代码传递,则总是可以完成此操作。

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

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

在此之后,$exit_codes通常是foo:X bar:Y,但如果bar在读取所有输入之前退出了或者您很不幸,则可能是bar:Y foo:X。我认为在所有unices上写入最大512字节的管道都是原子的,因此foo:$?bar:$?部分不会混杂在一起,只要标记字符串在507个字节以下即可。

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

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')

当然,还有一个简单的选项 使用临时文件 来存储状态。很简单,但不是生产中简单:

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

从管道开始:

foo | bar | baz

这是仅使用POSIX Shell而不使用临时文件的常规解决方案:

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

$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

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

17
Jander

因此,我想提供一个类似于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.
_

我认为这是最好的从内而外的解释– command1将执行并在stdout(文件描述符1)上打印其常规输出,然后完成后,printf将执行并在其stdout上打印command1的退出代码,但是该stdout重定向到文件描述符3。

当command1运行时,其stdout将通过管道传递给command2(printf的输出从不将其传递给command2,因为我们将其发送到文件描述符3而不是管道读取的1)。然后,我们将command2的输出重定向到文件描述符4,以便它也不会出现在文件描述符1中–因为我们希望稍后释放文件描述符1,因为我们会将文件描述符3的printf输出放回到文件描述符中1 –因为这是命令替换(反引号)将捕获的内容,并且将其放入变量中。

魔术的最后一点是我们首先作为一个单独的命令_exec 4>&1_ –它打开文件描述符4作为外部Shell的stdout的副本。从命令内部的角度来看,命令替换将捕获标准上写的所有内容–但是,由于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>&-
_

请注意,我在示例中使用了复合命令,但是子外壳程序(使用_( )_而不是_{ }_也会起作用,尽管可能效率较低。)

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

我不确定事情多久直接使用文件描述符3和4 –我认为大多数时候程序使用syscall来返回当前未使用的文件描述符,但是有时代码会直接写入文件描述符3猜测(我可以想象一个程序检查文件描述符以查看它是否打开,如果打开则使用它,或者如果没有打开则相应地表现不同)。因此,可能最好记住后者,并在通用情况下使用。

12
mtraceur

如果您安装了 moreutils 软件包,则可以使用mispipe实用程序,它完全可以满足您的要求。

11
Emanuele Aina

lesmana的上述解决方案也可以完成,而无需使用{ .. }来启动嵌套子进程的开销(请记住,这种形式的分组命令始终必须以分号结尾)。像这样:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

我已经用破折号版本0.5.5和bash版本3.2.25和4.2.42检查了此结构,因此,即使某些shell不支持{ .. }分组,它仍然符合POSIX。

7
pkeller

如果您无法使用一种常见的解决方案,则以下内容是@Patrik答案的附加内容。

该答案假定如下:

  • 您有一个不知道$PIPESTATUS也不_set -o pipefail的Shell
  • 您想使用管道进行并行执行,因此没有临时文件。
  • 如果您中断脚本(可能是突然断电),则不希望出现其他混乱情况。
  • 该解决方案应该相对易于遵循并且易于阅读。
  • 您不想引入其他子外壳。
  • 您无法摆弄现有的文件描述符,因此切勿触摸stdin/out/err(但是您可以暂时引入一些新的文件描述符)

其他假设。您可以摆脱所有的麻烦,但是这会使食谱变得太多,因此这里不介绍它:

  • 您只想知道PIPE中的所有命令的退出代码均为0。
  • 您不需要其他边带信息。
  • 您的Shell确实等待所有管道命令返回。

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

想要的:$?不得为0(true),如果管道中的任何命令失败

后:

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

解释:

  • 使用mktemp创建一个临时文件。这通常会立即在/tmp中创建一个文件
  • 然后将该临时文件重定向到FD 9进行写入,并重定向到FD 8进行读取
  • 然后,临时文件将立即删除。但是,它保持打开状态,直到两个FD都不存在为止。
  • 现在开始管道。如果有错误,则每个步骤仅添加到FD 9。
  • wait需要ksh,因为ksh else不会等待所有管道命令完成。但是请注意,如果存在某些后台任务,则会有不良的副作用,因此默认情况下我将其注释掉。如果等待没有影响,可以对其进行注释。
  • 之后,读取文件的内容。如果为空(因为全部有效),则read返回false,因此true表示错误

可以将其用作单个命令的插件替换,仅需要满足以下条件:

  • 未使用的FD 9和8
  • 单个环境变量保存临时文件的名称
  • 而且这个配方可以适应几乎所有允许IO
  • 而且它与平台无关,并且不需要/proc/fd/N

错误:

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

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"

可移植性注意事项:

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

  • 最后一个示例使用printf "%1s" "$?"而不是echo -n "$?",因为这更便于移植。并非每个平台都能正确解释-n

  • printf "$?"也会执行此操作,但是printf "%1s"会遇到一些极端情况,以防您在某些真正损坏的平台上运行脚本。 (阅读:如果您碰巧使用paranoia_mode=extreme编程。)

  • 在支持多个数字的平台上,FD 8和FD 9可能更高。遵循POSIX规范的Shell只需支持单个数字即可。

  • 已使用Debian 8.2进行了测试shbashksh,_ashsash甚至是csh

5
Tino

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

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

编辑:这是继吉尔斯的评论之后的更强版本:

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

Edit2:这是在dubiousjim评论之后的一个稍微轻一些的变体:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
4
jlliagre

采取一些预防措施,这应该起作用:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
3
alex

以下“ if”块仅在“ command”成功执行后才会运行:

if command; then
   # ...
fi

具体地说,您可以运行以下内容:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

哪个会运行haconf -makerw并将其stdout和stderr存储到“ $ haconf_out”。如果从haconf返回的值为true,则将执行'if'块,并且grep将读取“ $ haconf_out”,以使其与“可写群集”相匹配。

注意管道会自动清理。使用重定向时,您必须小心删除“ $ haconf_out”。

不如pipefail优雅,但如果无法实现此功能,则是一个合理的选择。

2
Rany Albeg Wein
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$
1
C.G.

(至少使用bash)与set -e可以使用子shell显式模拟pipefail并在出现管道错误时退出

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

因此,如果foo由于某种原因而失败-程序的其余部分将不会执行,并且脚本会退出并显示相应的错误代码。 (这假定foo打印自己的错误,足以理解失败的原因)

0
noonex