Pipe output and capture exit status in Bash

后端 未结 15 1094
盖世英雄少女心
盖世英雄少女心 2020-11-22 08:07

I want to execute a long running command in Bash, and both capture its exit status, and tee its output.

So I do this:

command | tee out.txt
ST=$?


        
相关标签:
15条回答
  • 2020-11-22 08:24

    This solution works without using bash specific features or temporary files. Bonus: in the end the exit status is actually an exit status and not some string in a file.

    Situation:

    someprog | filter
    

    you want the exit status from someprog and the output from filter.

    Here is my solution:

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

    See my answer for the same question on unix.stackexchange.com for a detailed explanation and an alternative without subshells and some caveats.

    0 讨论(0)
  • 2020-11-22 08:27

    So I wanted to contribute an answer like lesmana's, but I think mine is perhaps a little simpler and slightly more advantageous pure-Bourne-shell solution:

    # 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.
    

    I think this is best explained from the inside out - command1 will execute and print its regular output on stdout (file descriptor 1), then once it's done, printf will execute and print icommand1's exit code on its stdout, but that stdout is redirected to file descriptor 3.

    While command1 is running, its stdout is being piped to command2 (printf's output never makes it to command2 because we send it to file descriptor 3 instead of 1, which is what the pipe reads). Then we redirect command2's output to file descriptor 4, so that it also stays out of file descriptor 1 - because we want file descriptor 1 free for a little bit later, because we will bring the printf output on file descriptor 3 back down into file descriptor 1 - because that's what the command substitution (the backticks), will capture and that's what will get placed into the variable.

    The final bit of magic is that first exec 4>&1 we did as a separate command - it opens file descriptor 4 as a copy of the external shell's stdout. Command substitution will capture whatever is written on standard out from the perspective of the commands inside it - but since command2's output is going to file descriptor 4 as far as the command substitution is concerned, the command substitution doesn't capture it - however once it gets "out" of the command substitution it is effectively still going to the script's overall file descriptor 1.

    (The exec 4>&1 has to be a separate command because many common shells don't like it when you try to write to a file descriptor inside a command substitution, that is opened in the "external" command that is using the substitution. So this is the simplest portable way to do it.)

    You can look at it in a less technical and more playful way, as if the outputs of the commands are leapfrogging each other: command1 pipes to command2, then the printf's output jumps over command 2 so that command2 doesn't catch it, and then command 2's output jumps over and out of the command substitution just as printf lands just in time to get captured by the substitution so that it ends up in the variable, and command2's output goes on its merry way being written to the standard output, just as in a normal pipe.

    Also, as I understand it, $? will still contain the return code of the second command in the pipe, because variable assignments, command substitutions, and compound commands are all effectively transparent to the return code of the command inside them, so the return status of command2 should get propagated out - this, and not having to define an additional function, is why I think this might be a somewhat better solution than the one proposed by lesmana.

    Per the caveats lesmana mentions, it's possible that command1 will at some point end up using file descriptors 3 or 4, so to be more robust, you would do:

    exec 4>&1
    exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
    exec 4>&-
    

    Note that I use compound commands in my example, but subshells (using ( ) instead of { } will also work, though may perhaps be less efficient.)

    Commands inherit file descriptors from the process that launches them, so the entire second line will inherit file descriptor four, and the compound command followed by 3>&1 will inherit the file descriptor three. So the 4>&- makes sure that the inner compound command will not inherit file descriptor four, and the 3>&- will not inherit file descriptor three, so command1 gets a 'cleaner', more standard environment. You could also move the inner 4>&- next to the 3>&-, but I figure why not just limit its scope as much as possible.

    I'm not sure how often things use file descriptor three and four directly - I think most of the time programs use syscalls that return not-used-at-the-moment file descriptors, but sometimes code writes to file descriptor 3 directly, I guess (I could imagine a program checking a file descriptor to see if it's open, and using it if it is, or behaving differently accordingly if it's not). So the latter is probably best to keep in mind and use for general-purpose cases.

    0 讨论(0)
  • 2020-11-22 08:29

    using bash's set -o pipefail is helpful

    pipefail: the return value of a pipeline is the status of the last command to exit with a non-zero status, or zero if no command exited with a non-zero status

    0 讨论(0)
  • 2020-11-22 08:33

    There's an array that gives you the exit status of each command in a pipe.

    $ cat x| sed 's///'
    cat: x: No such file or directory
    $ echo $?
    0
    $ cat x| sed 's///'
    cat: x: No such file or directory
    $ echo ${PIPESTATUS[*]}
    1 0
    $ touch x
    $ cat x| sed 's'
    sed: 1: "s": substitute pattern can not be delimited by newline or backslash
    $ echo ${PIPESTATUS[*]}
    0 1
    
    0 讨论(0)
  • 2020-11-22 08:33

    Outside of bash, you can do:

    bash -o pipefail  -c "command1 | tee output"
    

    This is useful for example in ninja scripts where the shell is expected to be /bin/sh.

    0 讨论(0)
  • 2020-11-22 08:33
    (command | tee out.txt; exit ${PIPESTATUS[0]})
    

    Unlike @cODAR's answer this returns the original exit code of the first command and not only 0 for success and 127 for failure. But as @Chaoran pointed out you can just call ${PIPESTATUS[0]}. It is important however that all is put into brackets.

    0 讨论(0)
提交回复
热议问题