如何在 getline 管道中获取命令的退出状态?

Posted

技术标签:

【中文标题】如何在 getline 管道中获取命令的退出状态?【英文标题】:How do I get the exit status of a command in a getline pipeline? 【发布时间】:2014-02-13 07:59:49 【问题描述】:

在 POSIX awk 中,我如何从command 通过command | getline var 处理其输出后获取退出状态(返回码)?如果 command 以非零退出状态退出,我希望我的 awk 脚本为 exit 1

例如,假设我有一个名为 foo.awk 的 awk 脚本,如下所示:

function close_and_get_exit_status(cmd) 
    # magic goes here...

BEGIN 
    cmd = "echo foo; echo bar; echo baz; false"
    while ((cmd | getline line) > 0)
        print "got a line of text: " line
    if (close_and_get_exit_status(cmd) != 0) 
        print "ERROR: command '" cmd "' failed" | "cat >&2"
        exit 1
    
    print "command '" cmd "' was successful"

那么我希望发生以下情况:

$ awk -f foo.awk
got a line of text: foo
got a line of text: bar
got a line of text: baz
ERROR: command 'echo foo; echo bar; echo baz; false' failed
$ echo $?
1

根据POSIX specification for awk,command | getline 输入成功返回 1,文件结束返回 0,错误返回 -1。如果command 以非零退出状态退出,这不是错误,因此不能用于查看command 是否已完成并失败。

同样,close() 不能用于此目的:close() 仅在关闭失败时返回非零值,而不是在相关命令返回非零退出状态时。 (在 gawk 中,close(command) 返回 command 的退出状态。这是我想要的行为,但我认为它违反了 POSIX 规范,并且并非所有 awk 的实现都以这种方式运行。)

awk system() 函数返回命令的退出状态,但据我所知,没有办法将getline 与它一起使用。

【问题讨论】:

+1 表示问题。参考docs.freebsd.org/info/gawk/gawk.info.Getline.html,你可能会想用ERRNO来帮助你获得退出状态。 getline 如果找到记录则返回 1,如果遇到文件末尾则返回 0。如果在获取记录时出现错误,例如文件无法打开,则getline 返回-1。在这种情况下,gawk 将变量 ERRNO 设置为描述所发生错误的字符串。 @BMW:感谢您的评论。不幸的是,POSIX awk 没有ERRNO。此外,即使在 gawk 中,返回非零的命令也不会导致 getline 返回 -1。 用 awk.freeshell.org/AllAboutGetline 代替 awk.info mawk 的工作方式与 gawk 相同。 gawk 的行为取决于:gawk 'BEGIN cmd="exit 1"; cmd | getline; print close(cmd)' 使用 gawk 版本 4.1.4(32 位)获得 256,但版本 5.0.1(64 位)获得 1。 【参考方案1】:

如果您有mktemp 命令,您可以将退出状态存储在临时文件中:

#!/bin/sh
set -e
file=$(mktemp)
finish() 
    rm -f "$file"

trap 'finish' EXIT
trap 'finish; trap - INT; kill -s INT $$' INT
trap 'finish; trap - TERM; kill $$' TERM

awk -v file="$file" 'BEGIN
    o_cmd="echo foo; echo bar; echo baz; false"
    cmd = "("o_cmd "); echo $? >\""file"\""
    print cmd
    while ((cmd | getline) > 0) 
        print "got a line of text: " $0
    
    close(cmd)
    getline ecode <file; close(file)
    print "exit status:", ecode
    if(ecode)exit 1
'

【讨论】:

【参考方案2】:

最简单的做法是在命令执行后从 shell 中回显退出状态,然后使用 getline 读取它。例如

$ cat tst.awk    
BEGIN 
    cmd = "echo foo; echo bar; echo baz; false"

    mod = cmd "; echo \"$?\""
    while ((mod | getline line) > 0) 
        if (numLines++)
            print "got a line of text: " prev
        prev = line
    
    status = line
    close(mod)

    if (status != 0) 
        print "ERROR: command '" cmd "' failed" | "cat >&2"
        exit 1
    
    print "command '" cmd "' was successful"


$ awk -f tst.awk
got a line of text: foo
got a line of text: bar
got a line of text: baz
ERROR: command 'echo foo; echo bar; echo baz; false' failed
$ echo $?
1

如果有人阅读本文并考虑使用 getline,请务必阅读 http://awk.freeshell.org/AllAboutGetline 并首先完全理解这样做的所有注意事项和含义。

【讨论】:

这种方法的唯一缺点是处理延迟了一行。如果cmd 输出文本缓慢且响应性很重要(例如,应立即触发防火墙更改或电子邮件通知的入侵检测系统的不频繁输出),则此延迟可能是一个问题。对于这样的应用程序,我的答案中的解决方案可能更合适。但是这样的应用程序很少见,对于这样的应用程序,我会质疑将 awk 与 getline 一起使用,所以这个答案的简单性使其总体上更好。 我已更新我的答案,只包含最终解决方案。是的,处理延迟了 1 行,我同意这对于任何合理的 awk 应用程序几乎肯定没问题。如果没有,您总是可以在回显时在状态周围粘贴一些古怪的控制字符或其他内容,然后检查它,如果没有找到则处理当前行而不是延迟一行。【参考方案3】:

以下内容非常复杂,但是:

符合 POSIX 标准(大多数情况下 -- fflush() 尚不在 POSIX 标准中,but it will be 已广泛使用) 是通用的(无论命令发出什么样的输出都有效) 不会引入任何处理延迟。此问题的公认答案仅在命令打印 next 行之后才可用。如果命令输出行缓慢且响应性很重要(例如,IDS 系统打印的偶尔事件应触发防火墙更改或电子邮件通知),则此答案可能比接受的答案更合适。

基本方法是在命令完成后回显退出状态/返回值。如果最后一行不为零,则退出 awk 脚本并显示错误。为了防止代码将命令输出的一行文本误认为是退出状态,命令输出的每一行文本都在前面加上一个字母,该字母稍后会被去掉。

function stderr(msg)  print msg | "cat >&2"; 
function error(msg)  stderr("ERROR: " msg); 
function fatal(msg)  error(msg); exit 1; 

# Wrap cmd so that each output line of cmd is prefixed with "d".
# After cmd is done, an additional line of the format "r<ret>" is
# printed where "<ret>" is the integer return code/exit status of the
# command.
function safe_cmd_getline_wrap(cmd) 
    return                                                  \
        "exec 3>&1;"                                        \
        "ret=$("                                            \
        "    exec 4>&1;"                                    \
        "     ( "cmd" ) 4>&-; echo $? >&4;  3>&- |"       \
        "    awk 'print\"d\"$0;fflush()' >&3 4>&-;"       \
        ");"                                                \
        "exec 3>&-;"                                        \
        "echo r$ret;"


# like "cmd | getline line" except:
#   * if getline fails, the awk script exits with an error
#   * if cmd fails (returns non-zero), the awk script exits with an
#     error
#   * safe_cmd_getline_close(cmd) must be used instead of close(cmd)
function safe_cmd_getline(cmd,        wrapped_cmd,ret,type) 
    wrapped_cmd = safe_cmd_getline_wrap(cmd)
    ret = (wrapped_cmd | getline line)
    if (ret == -1) fatal("failed to read line from command: " cmd)
    if (ret == 0) return 0
    type = substr(line, 1, 1)
    line = substr(line, 2)
    if (type == "d") return 1
    if (line != "0") fatal("command '" cmd "' failed")
    return 0

function safe_cmd_getline_close(cmd) 
    if (close(safe_cmd_getline_wrap(cmd))) fatal("failed to close " cmd)

你像这样使用上面的:

cmd = "ls no-such-file"
while (safe_cmd_getline(cmd)) 
    print "got a line of text: " line

safe_cmd_getline_close(cmd)

【讨论】:

您回答了自己的问题? @aks:是的,我问了之后才知道。我的解决方案不是很漂亮,所以我希望能有一个更聪明的答案。回答您自己的问题可能看起来有点奇怪,但实际上是值得鼓励的:***.com/help/self-answer awk 脚本试图解决什么问题?也许可以直接在 bash 中完成? @aks:我想使用 awk 的关联数组。 (在 POSIX shell 中实现您自己的关联数组是可能的,但非常困难。)另外,我想要弄清楚它的挑战。 :) bash 关联数组其实非常好用。看到这个:gist.github.com/aks/8574081【参考方案4】:

不是一个理想的解决方案,但你可以这样做:

"command || echo failure" | getline var; ... if( var == "failure" ) exit;

您必须以这样一种方式选择字符串“失败”以使命令永远不会生成相同的字符串,这存在一些歧义,但也许这是一个适当的解决方法。

【讨论】:

感谢您的建议。我希望有一个更通用的解决方案,但这对于快速而肮脏的脚本来说是可行的。 +1 为简单起见;对文本文件中不太可能遇到的字符串的建议:printf '\\a',然后是测试if(var == "\a")

以上是关于如何在 getline 管道中获取命令的退出状态?的主要内容,如果未能解决你的问题,请参考以下文章

从最后一个管道(stdin)获取退出代码

Bash pipe:获取管道中前一个进程的退出状态

有没有办法获取早期管道 Scala 进程 (#|) 的退出代码?

Linux编程 22 shell编程(输出和输入重定向,管道,数学运算命令,退出脚本状态码)

getline 出错(我相信),在获取管道程序的输入时抛出 std::logic_error

在命令行上完成机器人测试后如何获取退出代码或退出状态