通知管道右侧左侧故障?

25

我喜欢在我的Shell脚本中使用生成器类似的模式来连接函数,就像这样:

parse_commands /da/cmd/file | process_commands
然而,这种模式的基本问题在于,如果parse_command遇到错误,我发现唯一通知process_command失败的方法是通过显式告诉它(例如echo“FILE_NOT_FOUND”)。这意味着,parse_command中每个可能出错的操作都必须被圈起来。

是否有办法让process_command检测到左侧以非零退出代码退出了呢?


一如既往,第二次谷歌搜索找到了这个链接:http://www.unix.com/302268337-post4.html - Bittrance
1
https://dev59.com/34_ea4cB1Zd3GeqPM1RO#32699218 是这个问题的一个更清晰的版本,可以说它有更好的答案(该问题询问如何通知管道右侧的命令失败,但被接受的答案只解决了如何在父shell中检测到该情况)。 - Charles Duffy
9个回答

29

在你的bash脚本顶部使用set -o pipefail,这样当管道的左侧失败(exit status != 0)时,右侧不会执行。


9
"set -o pipefail"不能阻止右侧命令的执行,因此它无法回答最初的问题,即“如何通知右侧...”。基本上,这会更改管道的退出状态:默认情况下,管道的退出状态是右侧的退出状态,使用此选项后,它将变为管道命令的最大值。 - mcoolive
3
但这正是我在寻找的 :-) 所以它帮助了我。谢谢 - mcoolive
1
@dtracers 是的,目的正是让它看起来像整个命令失败了,如果至少有一个阶段失败。我仍然想知道shopt选项和set选项之间的区别。哦,好吧。 - Tobia

14
管道进程会在第一个进程结束后继续执行吗?或者问题是你无法知道第一个进程是否失败了?
如果是后者,你可以查看PIPESTATUS变量(实际上是一个BASH数组)。这将给出第一个命令的退出代码。
parse_commands /da/cmd/file | process_commands
temp=("${PIPESTATUS[@]}")
if [ ${temp[0]} -ne 0 ]
then
    echo 'parse_commands failed'
elif [ ${temp[1]} -ne 0 ]
then
    echo 'parse_commands worked, but process_commands failed'
fi
否则,你将不得不使用协程。

2
这确实提供了信息,但它不能在process_command内完成,因此可读性会受到影响。 - Bittrance

5

与逻辑运算符(&&)不同,管道运算符(|)通过同时生成两个进程来工作,因此第一个进程可以将其输出管道传递给第二个进程,而无需缓冲中间数据。这允许使用很少的内存或磁盘使用量处理大量数据。

因此,在第一个进程完成之前,第二个进程无法获得第一个进程的退出状态。


不一定正确。例如,参见https://dev59.com/34_ea4cB1Zd3GeqPM1RO#32699218。 - Charles Duffy
1
@Charles 嗯,我仍然认为只有在第一个进程退出后才能获得其退出状态。但确实根据各种答案(包括你的),使用FIFO或PIPESTATUS似乎是OP问题的通用解决方案,可以以适当的方式访问退出状态。 - jjmontes

4
您可以尝试使用一个FIFO进行一些解决方案:

mkfifo /tmp/a
cat /tmp/a | process_commands &

parse_cmd /da/cmd/file > /tmp/a || (echo "error"; # kill process_commands)

是的,我认为这可能是最好的选择,而不必编写一个C程序来完成大致相同的事情,只需使用常规管道而不是命名管道。我想知道zsh coproc工具是否有助于解决这个问题,但我对它并不了解。 - andrewdski

2
"I don't have enough reputation to comment, but the accepted answer was missing a closing } on line 5. After fixing this, the code will throw a -ne: unary operator expected error, which points to a problem: PIPESTATUS is overwritten by the conditional following the if command, so the return value of process_commands will never be checked! This is because [ ${PIPESTATUS[0]} -ne 0 ] is equivalent to test ${PIPESTATUS[0]} -ne 0, which changes $PIPESTATUS just like any other command. For example:"
return0 () { return 0;}
return3 () { return 3;}

return0 | return3
echo "PIPESTATUS: ${PIPESTATUS[@]}"

这将按预期返回PIPESTATUS: 0 3。但是如果我们引入条件语句会怎样呢?
return0 | return3
if [ ${PIPESTATUS[0]} -ne 0 ]; then
    echo "1st command error: ${PIPESTATUS[0]}"
elif [ ${PIPESTATUS[1]} -ne 0 ]; then
    echo "2nd command error: ${PIPESTATUS[1]}"
else
    echo "PIPESTATUS: ${PIPESTATUS[@]}"
    echo "Both return codes = 0."
fi

我们遇到了[: -ne: unary operator expected错误,以及这个:
PIPESTATUS: 2
Both return codes = 0.

为了解决这个问题,$PIPESTATUS 应该被存储在一个不同的数组变量中,像这样:
return0 | return3
TEMP=("${PIPESTATUS[@]}")
echo "TEMP: ${TEMP[@]}"
if [ ${TEMP[0]} -ne 0 ]; then
    echo "1st command error: ${TEMP[0]}"
elif [ ${TEMP[1]} -ne 0 ]; then
    echo "2nd command error: ${TEMP[1]}"
else
    echo "TEMP: ${TEMP[@]}"
    echo "All return codes = 0."
fi

这段文本的意思是:“打印出以下内容:”。
TEMP: 0 3
2nd command error: 3

按照预期的意图。
编辑:我已经修复了被接受的答案,但是我会留下这个解释供后人参考。

0

您可以在显式子shell中运行parse_commands /da/cmd/file,并通过管道将此子shell的退出状态echoprocess_commands,后者也在显式子shell中运行以处理包含在/dev/stdin中的管道数据。

远非优雅,但似乎能完成工作 :)

一个简单的例子:

(
( ls -l ~/.bashrcxyz; echo $? ) | 
( 
piped="$(</dev/stdin)"; 
[[ "$(tail -n 1 <<<"$piped")" -eq 0 ]] && printf '%s\n' "$piped" | sed '$d' || exit 77 
); 
echo $?
)

0
如果你有 command1 && command2,那么只有在第一个命令成功执行时才会执行第二个命令 - 否则布尔短路就会发生。使用这种方式的一种方法是构建一个第一个命令(你的 parse_commands...)将其转储到临时文件中,然后让第二个命令从该文件中获取。
编辑:通过巧妙地使用 ;,你可以整理临时文件,例如。
(command1 && command2) ; rm temporaryfile

0

在Bash 4.0中有一种方法可以实现此功能,该版本添加了coproc内置命令(从ash借鉴的coprocess机制)。这个coprocess机制是从ksh借鉴来的,因为ksh使用不同的语法。我在我的系统上唯一可以访问支持coprocesses的shell是ksh。这里是用ksh编写的解决方案:

parse_commands  /da/cmd/file |&
parser=$!

process_commands <&p &
processor=$!

if wait $parser
then
    wait $processor
    exit $?
else
    kill $processor
    exit 1
fi

这个想法是在后台启动parse_commands,并使用管道将其连接到主shell。pid保存在parser中。然后使用<&p启动process_commands,以parse_commands的输出作为其输入。这也被放置在后台,并将其pid保存在processor中。

通过一个管道将这两者都放在后台,我们的主shell可以自由地等待解析器终止。如果它没有错误地终止,我们等待处理器完成并退出其返回代码。如果它带有错误地终止,我们杀死处理器并以非零状态退出。

将其转换为使用bash 4.0 / ash coproc内置应该相当简单,但我没有好的文档,也没有测试的方法。


你如何确保 process_commands 在接收到信号之前不会因为遇到 EOF(当 FIFO 的写端关闭时)而退出? - Charles Duffy

0

这个怎么样:

parse_commands /da/cmd/file > >(process_commands)

不在这里。使用 false > >(/bin/echo foo) 进行测试。 - xebeche

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