Pregunta

In POSIX awk, how do I get the exit status (return code) from command after processing its output via command | getline var? I want my awk script to exit 1 if command exited with a non-zero exit status.

For example, suppose I had an awk script named foo.awk that looks like this:

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

then I want the following to happen:

$ 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

According to the POSIX specification for awk, command | getline returns 1 for successful input, zero for end-of-file, and -1 for an error. It's not an error if command exits with a non-zero exit status, so this can't be used to see if command is done and has failed.

Similarly, close() can't be used for this purpose: close() returns non-zero only if the close fails, not if the associated command returns a non-zero exit status. (In gawk, close(command) returns the exit status of command. This is the behavior I'd like, but I think it violates the POSIX spec and not all implementations of awk behave this way.)

The awk system() function returns the exit status of the command, but as far as I can tell there's no way to use getline with it.

¿Fue útil?

Solución

The simplest thing to do is just echo the exit status from shell after the command executes and then read that with getline. e.g.

$ 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

In case anyone's reading this and considering using getline, make sure you read http://awk.freeshell.org/AllAboutGetline and FULLY understand all the caveats and implications of doing so first.

Otros consejos

Not an ideal solution, but you can do:

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

There is some ambiguity in that you have to select the string "failure" in such a way that command can never generate the same string, but perhaps this is an adequate workaround.

The following is horrifically complicated, but it:

  • is POSIX conformant (mostly -- fflush() isn't yet in the POSIX standard, but it will be and it's widely available)
  • is general (it works no matter what kind of output is emitted by the command)
  • does not introduce any processing delay. The accepted answer to this question makes a line available only after the next line has been printed by the command. If the command slowly outputs lines and responsiveness is important (e.g., occasional events printed by an IDS system that should trigger a firewall change or email notification), this answer might be more appropriate than the accepted answer.

The basic approach is to echo the exit status/return value after the command completes. If this last line is non-zero, exit the awk script with an error. To prevent the code from mistaking a line of text output by the command for the exit status, each line of text output by the command is prepended with a letter that is later stripped off.

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

You use the above like this:

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

If you have mktemp command, you could store the exit status in a temporary file:

#!/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
}'
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top