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

12
在POSIX awk中,如果我通过command | getline var处理输出后,要如何获取command的退出状态码(返回码)呢?我希望我的awk脚本在command以非零的退出状态码退出时也能执行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规范的awk部分command | getline返回1表示成功输入,0表示文件末尾,-1表示错误。如果command以非零的退出状态退出,则不算作错误,因此无法用于检查command是否完成且失败。

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

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


1
参考http://docs.freebsd.org/info/gawk/gawk.info.Getline.html,您可以考虑使用 ERRNO 来帮助您获取退出状态。如果 getline 找到一条记录,则返回 1;如果遇到文件结束,则返回 0。如果在获取记录时出现某些错误(例如无法打开文件),则 getline 返回 -1。在这种情况下,gawk将变量 ERRNO 设置为描述所发生错误的字符串。 - BMW
@BMW:感谢您的评论。不幸的是,POSIX awk没有ERRNO。而且,即使在gawk中,返回非零的命令也不会导致getline返回-1。 - Richard Hansen
请将http://awk.freeshell.org/AllAboutGetline替换为awk.info。 - Ed Morton
mawk的工作方式与gawk相同。 - jarno
gawk的行为取决于:在gawk版本4.1.4(32位)中,通过gawk 'BEGIN{ cmd="exit 1"; cmd | getline; print close(cmd)}'我得到256,但在版本5.0.1(64位)中得到1。 - jarno
这个更改是针对4.2版本进行的参考 - jarno
4个回答

4

最简单的方法就是在命令执行后直接回显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,并完全理解使用getline可能存在的注意事项和影响。


这种方法唯一的缺点是处理会被一行数据的延迟所拖慢。如果 cmd 输出文本速度较慢且响应性很重要(例如入侵检测系统的不频繁输出需要立即触发防火墙更改或电子邮件通知),这种延迟可能会成为问题。对于那样的应用程序,我回答中提供的解决方案可能更加合适。但这样的应用程序很少见,我会质疑在这种应用程序中使用带有 getline 的 awk,因此这个答案的简单性使它整体上更好。 - Richard Hansen
我已经更新了我的答案,只包含最终解决方案。是的,处理被延迟了一行,我同意对于任何合理的awk应用程序来说,这几乎肯定是可以接受的。如果不行,您可以始终在状态回显时添加一些奇怪的控制字符字符串之类的东西,然后检查是否找到它,如果没有找到,则处理当前行而不是将其延迟一行。 - Ed Morton

2

虽然不是理想的解决方案,但你可以这么做:

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

这里有些含糊不清的地方,你需要以某种方式选择字符串“failure”,使得命令永远不可能生成相同的字符串,但也许这是一个足够好的解决方法。


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

1
以下内容非常复杂,但是:
  • 符合POSIX标准(大部分 -- fflush()目前还不在POSIX标准中,但会被加入并且广泛可用)
  • 通用性强(无论命令输出什么类型的内容都能正常工作)
  • 不会引入任何处理延迟。对于这个问题的接受答案只有在命令打印出下一行后才会使该行可用。如果命令缓慢输出行并且响应速度很重要(例如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
1
@aks:是的,我在问完之后就想出来了。我的解决方案不太好看,所以我希望能有更聪明的答案出现。回答自己的问题可能听起来有点奇怪,但实际上这是受到鼓励的:https://stackoverflow.com/help/self-answer - Richard Hansen
awk脚本试图解决什么问题?也许可以直接在bash中完成吗? - aks
@aks: 我想使用awk的关联数组。(在POSIX shell中实现自己的关联数组是可能的,但非常困难。)此外,我也想挑战自己去理解它。 :) - Richard Hansen
Bash关联数组实际上非常容易使用。请参见此链接:https://gist.github.com/aks/8574081 - aks
@aks:我要开发的平台上没有Bash。如果我选择非POSIX语言,我可能会选择Python。 - Richard Hansen

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

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