509

I have two processes foo and bar, connected with a pipe:

$ foo | bar

bar always exits 0; I'm interested in the exit code of foo. Is there any way to get at it?

Lesmana
  • 27,439
Michael Mrozek
  • 93,103
  • 40
  • 240
  • 233
  • 1
    http://stackoverflow.com/questions/1221833/bash-pipe-output-and-capture-exit-status – Ciro Santilli OurBigBook.com Nov 15 '16 at 17:22
  • 2
    This question depends also on the shell. Most answers are about bash or zsh but I came here for Fish. When you google for pipestatus or pipefail and your favourite shell, you likely will find it. – Albert Mar 09 '22 at 08:36

21 Answers21

461

bash and zsh have an array variable that holds the exit status of each element (command) of the last pipeline executed by the shell.

If you are using bash, the array is called PIPESTATUS (case matters!) and the array indicies start at zero:

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

If you are using zsh, the array is called pipestatus (case matters!) and the array indices start at one:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

To combine them within a function in a manner that doesn't lose the values:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

Run the above in bash or zsh and you'll get the same results; only one of retval_bash and retval_zsh will be set. The other will be blank. This would allow a function to end with return $retval_bash $retval_zsh (note the lack of quotes!).

camh
  • 39,069
  • 11
    And pipestatus in zsh. Unfortunately other shells don't have this feature. – Gilles 'SO- stop being evil' Jun 02 '11 at 21:05
  • 11
    Note: Arrays in zsh begin counterintuitively at index 1, so it's echo "$pipestatus[1]" "$pipestatus[2]". – Christoph Wurm Nov 14 '11 at 14:09
  • 7
    You could check the whole pipeline like this: if [ \echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi` – l0b0 Sep 25 '12 at 15:39
  • 1
    There is no $PIPESTATUS in POSIX. Downvoted. – Jan Hudec Dec 15 '14 at 13:50
  • 14
    @JanHudec: Perhaps you should read the first five words of my answer. Also kindly point out where the question requested a POSIX-only answer. – camh Dec 16 '14 at 08:27
  • 2
    @camh: I did. I don't care. The question is not tagged bash, so the answer is incomplete. – Jan Hudec Dec 16 '14 at 08:50
  • 22
    @JanHudec: Nor was it tagged POSIX. Why do you assume the answer must be POSIX? It was not specified so I provided a qualified answer. There is nothing incorrect about my answer, plus there are multiple answers to address other cases. – camh Dec 17 '14 at 09:15
  • 2
    @camh: In that case correct answer should work all the way back to Bourne Shell! – Jan Hudec Dec 17 '14 at 19:05
  • I edited this answer to include the zsh equivalent as well as a version that will work in either zsh or bash. Thanks, this is far preferable to the POSIX file descriptor hack, (at least for my rc files). – Adam Katz Jan 15 '15 at 02:20
  • You can capture all the status values in one go with an array assignment - false | true; r=("${PIPESTATUS[@]}") and then echo "> ${r[0]} - ${r[1]} <" – Chris Davies Sep 07 '18 at 19:06
  • Note that the false | true example may mislead; ${PIPESTATUS[0]} is the result of the first command (false) of which the output is 1. This can be proven by; false | echo $? (outputs 1). IOW, If you're scripting, and have a command with pipes, use ${PIPESTATUS[0]} if you want the result of the very first command, ${PIPESTATUS[1]} for the result of the second command (i.e. after the first '|' pipe) and so on. – Roel Van de Paar Jul 03 '19 at 01:12
  • Apparently it is not reliable. In this example I run false in a pipeline, and I still get 0 as result. – Hi-Angel Apr 21 '21 at 14:39
  • @Gilles'SO-stopbeingevil' fish shell also has this feature (exactly the same as zsh). – Sergey Podobry Feb 14 '24 at 17:29
373

There are 3 common ways of doing this:

Pipefail

The first way is to set the pipefail option (ksh, zsh or bash). This is the simplest and what it does is basically set the exit status $? to the exit code of the last program to exit non-zero (or zero if all exited successfully).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$PIPESTATUS

Bash also has an array variable called $PIPESTATUS ($pipestatus in zsh) which contains the exit status of all the programs in the last pipeline.

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

You can use the 3rd command example to get the specific value in the pipeline that you need.

Separate executions

This is the most unwieldy of the solutions. Run each command separately and capture the status

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
phemmer
  • 71,831
  • 3
    For reference there are several other techniques discussed in this SO question: http://stackoverflow.com/questions/1221833/bash-tee-output-and-capture-exit-status – slm Apr 21 '13 at 14:00
  • 1
    @Patrick the pipestatus solution is works on bash , just more quastion in case I use ksh script you think we can find something similar to pipestatus ? , ( meenwhile I see the pipestatus not supported by ksh ) – yael Apr 21 '13 at 14:32
  • 2
    @yael I don't use ksh, but from a brief glance at it's manpage, it doesn't support $PIPESTATUS or anything similar. It does support the pipefail option though. – phemmer Apr 21 '13 at 15:30
  • 1
    I decided to go with pipefail as it allows me to get the status of the failed command here: LOG=$(failed_command | successful_command) – vmrob Feb 08 '14 at 09:20
  • Note that with the third solution, you won't see the command output until the end, which is a problem e.g. if the command hangs. The first two solutions don't have this problem. – ntc2 Jan 19 '16 at 19:49
  • 1
    i've done some experiments with PIPESTATUS -- I found a couple of hiccups that makes it a bit difficult for my use-case. It appears that you can't use it successfully with a quoted command, viz.: count='ls *201701*jpg | wc -l ; status=${PIPESTATUS[0]}'; doesn't set status. Secondly, it seems that once I get status from PIPESTATUS[0], then that clears PIPESTATUS / $?so you can get just One status from a pipe ... Unless, you parse a string like: statuses="0=${PIPESTATUS[0]}, 1=${PIPESTATUS[1]}, 2=${PIPESTATUS[2]}, 3=${PIPESTATUS[3]}". Is that more-or-less accurate? More checking? – will Nov 22 '17 at 12:22
  • The pipefail option works with busybox "ash", as an additional bonus ! – J.P. Tosoni Oct 05 '21 at 12:15
  • Good answer, but the separate executions option isn't the same: it needs more memory (to store intermediate output) and the pipeline is sequential, not parallel. – zakmck Mar 31 '23 at 12:04
75

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

the result of this construct is stdout from filter as stdout of the construct and exit status from someprog as exit status of the construct.


this construct also works with simple command grouping {...} instead of subshells (...). subshells have some implications, among others a performance cost, which we do not need here. read the fine bash manual for more details: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

Unfortunately the bash grammar requires spaces and semicolons for the curly braces so that the construct becomes much more spacious.

For the rest of this text I will use the subshell variant.


Example someprog and filter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

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

echo $?

Example output:

filtered line1
filtered line2
filtered line3
42

Note: the child process inherits the open file descriptors from the parent. That means someprog will inherit open file descriptor 3 and 4. If someprog writes to file descriptor 3 then that will become the exit status. The real exit status will be ignored because read only reads once.

If you worry that your someprog might write to file descriptor 3 or 4 then it is best to close the file descriptors before calling someprog.

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

The exec 3>&- 4>&- before someprog closes the file descriptor before executing someprog so for someprog those file descriptors simply do not exist.

It can also be written like this: someprog 3>&- 4>&-


Step by step explanation of the construct:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

From bottom up:

  1. A subshell is created with file descriptor 4 redirected to stdout. This means that whatever is printed to file descriptor 4 in the subshell will end up as the stdout of the entire construct.
  2. A pipe is created and the commands on the left (#part3) and right (#part2) are executed. exit $xs is also the last command of the pipe and that means the string from stdin will be the exit status of the entire construct.
  3. A subshell is created with file descriptor 3 redirected to stdout. This means that whatever is printed to file descriptor 3 in this subshell will end up in #part2 and in turn will be the exit status of the entire construct.
  4. A pipe is created and the commands on the left (#part5 and #part6) and right (filter >&4) are executed. The output of filter is redirected to file descriptor 4. In #part1 the file descriptor 4 was redirected to stdout. This means that the output of filter is the stdout of the entire construct.
  5. Exit status from #part6 is printed to file descriptor 3. In #part3 file descriptor 3 was redirected to #part2. This means that the exit status from #part6 will be the final exit status for the entire construct.
  6. someprog is executed. The exit status is taken in #part5. The stdout is taken by the pipe in #part4 and forwarded to filter. The output from filter will in turn reach stdout as explained in #part4
Lesmana
  • 27,439
  • Ten years later. But, golly gosh, I'm gobsmacked. This is a great answer! You could even imagine extending this to give you pipefail characteristics. Well done @Lesmana! – Paul Wagland Nov 09 '23 at 22:17
40

While not exactly what you asked, you could use

#!/bin/bash -o pipefail

so that your pipes return the last non zero return.

might be a bit less coding

Edit: Example

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
Chris
  • 549
  • 10
    set -o pipefail inside the script should be more robust, e.g. in case someone executes the script via bash foo.sh. – maxschlepzig Jun 03 '11 at 09:17
  • How does that work? do you have a example? – Johan Jun 08 '11 at 18:40
  • This works perfectly for me; I agree that it's more robust than the accepted solution insofar as you don't have to worry about indexes and making sure that you check the exit status on all your commands. Rather, you can simply run the pipe through bash -o pipefail -c "false | true" and if any piped command fails, echo $? will show it. – tobias.mcnulty Apr 24 '12 at 14:55
  • 3
    Note that -o pipefail is not in POSIX. – scy Jan 25 '13 at 15:15
  • 2
    This does not work in my BASH 3.2.25(1)-release. At the top of /tmp/ff I have #!/bin/bash -o pipefail. Error is: /bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name – Felipe Alvarez Mar 24 '14 at 06:01
  • The question is not tagged bash. – Jan Hudec Dec 15 '14 at 13:50
  • 3
    @FelipeAlvarez: Some environments (including linux) don't parse spaces on #! lines beyond the first one, and so this becomes /bin/bash -o pipefail /tmp/ff, instead of the necessary /bin/bash -o pipefail /tmp/ff -- getopt (or similar) parsing using the optarg, which is the next item in ARGV, as the argument to -o, so it fails. If you were to make a wrapper (say, bash-pf that just did exec /bin/bash -o pipefail "$@", and put that on the #! line, that would work. See also: https://en.wikipedia.org/wiki/Shebang_%28Unix%29 – lindes Dec 05 '15 at 00:09
21

What I do when possible is to feed the exit code from foo into bar. For example, if I know that foo never produces a line with just digits, then I can just tack on the exit code:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

Or if I know that the output from foo never contains a line with just .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

This can always be done if there's some way of getting bar to work on all but the last line, and pass on the last line as its exit code.

If bar is a complex pipeline whose output you don't need, you can bypass part of it by printing the exit code on a different file descriptor.

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

After this $exit_codes is usually foo:X bar:Y, but it could be bar:Y foo:X if bar quits before reading all of its input or if you're unlucky. I think writes to pipes of up to 512 bytes are atomic on all unices, so the foo:$? and bar:$? parts won't be intermixed as long as the tag strings are under 507 bytes.

If you need to capture the output from bar, it gets difficult. You can combine the techniques above by arranging for the output of bar never to contain a line that looks like an exit code indication, but it does get fiddly.

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

And, of course, there's the simple option of using a temporary file to store the status. Simple, but not that simple in production:

  • If there are multiple scripts running concurrently, or if the same script uses this method in several places, you need to make sure they use different temporary file names.
  • Creating a temporary file securely in a shared directory is hard. Often, /tmp is the only place where a script is sure to be able to write files. Use mktemp, which is not POSIX but available on all serious unices nowadays.
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")
  • 1
    When using the temporary file approach I prefer to add a trap for EXIT that removes all temporary files so that no garbage will be left even if the script dies – miracle173 Oct 16 '13 at 20:16
  • Why use a temporary file when you can pass the command to a function, run it there and capture the exit code in a global variable and use it later in the original place? – ManSamVampire Aug 02 '22 at 14:04
  • @ManSamVampire How do you capture the exit code from a command that's piped into something else? Ksh, bash and zsh have set -o pipefail, bash has $PIPESTATUS, zsh has $pipestatus, but dash has nothing like this. – Gilles 'SO- stop being evil' Aug 02 '22 at 15:39
  • @Gilles'SO-stopbeingevil' replace the command with a function call (let's call it capture()) that will run the passed command (eval "$@"), and store the exit code in a variable (_exc=$?), which can be accessed after the entire pipe has executed. I am using ash btw, it doesn't have pipestatus either. – ManSamVampire Aug 02 '22 at 17:39
  • @ManSamVampire What you describe doesn't work. The piped command is executed in a subshell. You can't pass variables out of a subshell. – Gilles 'SO- stop being evil' Aug 02 '22 at 17:51
19

Starting from the pipeline:

foo | bar | baz

Here is a general solution using only POSIX shell and no temporary files:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statuses contains the status codes of any failed processes, in random order, with indexes to tell which command emitted each status.

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

Note the quotes around $error_statuses in my tests; without them grep can't differentiate because the newlines get coerced to spaces.

Lesmana
  • 27,439
Jander
  • 16,682
15

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 command1'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.

mtraceur
  • 1,166
  • 9
  • 14
  • Looks interesting, but I can't quite figure out what you expect this command to do, and my computer can't, either; I get -bash: 3: Bad file descriptor. – G-Man Says 'Reinstate Monica' Jun 05 '15 at 07:03
  • @G-Man Right, I keep forgetting bash has no idea what it's doing when it comes to file descriptors, unlike the shells I typically use (the ash that comes with busybox). I'll let you know when I think of a workaround that makes bash happy. In the meantime if you've got a debian box handy you can try it in dash, or if you've got busybox handy you can try it with the busybox ash/sh. – mtraceur Jun 05 '15 at 12:09
  • @G-Man As to what I expect the command to do, and what it does do in other shells, is redirect stdout from command1 so it doesn't get caught by the command substitution, but once outside the command substitution, it goes drops fd3 back to stdout so it's piped as expected to command2. When command1 exits, the printf fires and prints its exit status, which is captured into the variable by the command substitution. Very detailed breakdown here: http://stackoverflow.com/questions/985876/tee-and-exit-status/30659751#30659751 Also, that comment of yours read as if it was meant to be kinda insulting? – mtraceur Jun 05 '15 at 12:17
  • Where shall I begin?   (1) I’m sorry if you felt insulted.   “Looks interesting” was meant earnestly; it would be great if something as compact as that worked as well as you expected it to.  Beyond that, I was saying, simply, that I didn’t understand what your solution was supposed to be doing.  I’ve been working/playing with Unix for a long time (since before Linux existed), and, if I don’t understand something, that’s a red flag that, maybe, other people won’t understand it either, and that it needs more explanation (IMNSHO).  … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 06 '15 at 03:14
  • (Cont’d) …  Since you “… like to think … that [you] understand just about everything more than the average person”, maybe you should remember that the objective of Stack Exchange is not to be a command-writing service, churning out thousands of one-off solutions to trivially distinct questions; but rather to teach people how to solve their own problems.  And, to that end, maybe you need to explain stuff well enough that an “average person” can understand it.  Look at lesmana’s answer for an example of an excellent explanation.  … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 06 '15 at 03:15
  • (Cont’d) …  Your explanation in your Stack Overflow answer isn’t bad, either; it would have been nice if you had at least referenced that in your answer here.  (But, answers should be self-sufficient; you really should have included an explanation here.  And I keep forgetting to say, explanations of answers belong *in* the answers, not in the comments.)  (1½) Also, just for clarity, you might want to change \…`` to $(…) — see this, this, and this.  … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 06 '15 at 03:16
  • (Cont’d) …  (2) I hope one of our POSIX experts stumbles across this, and tells us whether your command can be expected to work in a POSIX-compliant shell.  (2½) Has it occurred to you that lesmana’s answer *is* the simplest and most light-weight of the bash-compatible Bourne-shell solutions?  (3) OK, I tried your command in dash.  I get the output of command1 | command2, but $exitstatus is not set.  … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 06 '15 at 03:17
  • (Cont’d) …  Same deal with your answer over on Stack Overflow.  (3⅓) Having read lesmana’s excellent explanation, and stared at your answer for over an hour, I’m beginning to understand.  (It’s still your job to write an explanation for the benefit of the average people.)  It seems like it can’t work, because the exitstatus=… part is on the left side of a pipe, so it’s executed in a subshell.  Things like variable=value | command and cd some_directory | command can’t affect the main (parent) shell process, not even in dash.  … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 06 '15 at 03:18
  • (Cont’d) …  (3⅔) Good news!  I partially solved your problem with file descriptor manglement.  In bash, (exec 3>&1; exitstatus=\command1 1>&3; printf $?`) | command2and, semi-equivalently,{ exec 3>&1; exitstatus=`command1 1>&3; printf $?`;} | command2give the expected output ofcommand1 | command2.  Of course,$exitstatus` still is not set (in the parent shell, after the pipeline completes). – G-Man Says 'Reinstate Monica' Jun 06 '15 at 03:23
  • @G-Man I appreciate your comments and feedback - I wasn't sure if it was an insult or not, hence my question. I have edited my reply with a new (actually, to me, old, but more on that in a followup comment) answer. I will probably edit the other more in-depth answer of mine, and in the process refine both of these until they both explain what's happening better. I gave the POSIX spec a read and I couldn't really tell - command substitutions must be in subshell environments in POSIX, but they have a unique/different usecase so I could see why a shell implementation might not associate (cont'd) – mtraceur Jun 10 '15 at 23:36
  • (con't #1) ..those file descriptors - after all, a shell could reasonably expect a command substitution to be fully done before executing whatever statement it finds it in, so redirection inside the containing statement might not get parsed. So far I've only see this work in BusyBox's ash implementation, which I admit surprises me. As for backticks vs the better, newer way of command substitution, I'm well acquainted with those arguments, but I am a portability zealot. If only someone would obliterate Solaris 10 /bin/sh and below from this world, I could use so many better habits in (cont'd) – mtraceur Jun 10 '15 at 23:41
  • (redone) (cont'd #2) ..shell scripting. Anyway, the solution I edited in was one I had found about a month or month and a half ago... I think what happened when I posted my answers is that I got so excited about having just that evening found what I thought was a leaner variant, that I failed to properly test for compatibility with other shells. I do think that even my older, two-file-descriptor using solution is a little better in a couple of respects than lesmana's because it requires no additional function and still preserves command2's exit status in $? like a normal pipe would. – mtraceur Jun 10 '15 at 23:52
  • (1) Good job (and, no, that is not an insult  :-)   ⁠  It works in bash, and the explanation is clear.  My one quibble is that you should probably do the exec 4>&- at the end, even if you’re not concerned about command1 and command2.  The solution you just posted leaves file descriptor 4 open, potentially affecting the behavior off all subsequent commands.  (2) If you want to propagate command1’s exit status in $?, you can modify your answer to (exec 4>&1; exit \{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`)`.  … (Cont’d) – G-Man Says 'Reinstate Monica' Jun 11 '15 at 02:02
  • 1
    (Cont’d) …  (3) Criticizing lesmana’s answer for requiring an additional function seems like a bit of a red herring; it can trivially be rewritten ((((command1; echo $? >&3) | command2 >&4) 3>&1) | (read e; exit "$e")) 4>&1. – G-Man Says 'Reinstate Monica' Jun 11 '15 at 02:04
14

If you have the moreutils package installed you can use the mispipe utility which does exactly what you asked.

Emanuele Aina
  • 241
  • 2
  • 4
10

lesmana's solution above can also be done without the overhead of starting nested subprocesses by using { .. } instead (remembering that this form of grouped commands always has to finish with semicolons). Something like this:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

I've checked this construct with dash version 0.5.5 and bash versions 3.2.25 and 4.2.42, so even if some shells don't support { .. } grouping, it is still POSIX compliant.

pkeller
  • 201
  • 1
    This works really well with most shells I've tried it with, including NetBSD sh, pdksh, mksh, dash, bash. However I can't get it to work with AT&T Ksh (93s+, 93u+) or zsh (4.3.9, 5.2), even with set -o pipefail in ksh or any number of sprinkled wait commands in either. I think it may, in part at least, be a parsing issue for ksh, as if I stick to using subshells then it works fine, but even with an if to choose the subshell variant for ksh but leave the compound commands for others, it fails. – Greg A. Woods Aug 03 '17 at 00:42
5

This is portable, i.e. works with any POSIX compliant shell, doesn't require the current directory to be writable and allows multiple scripts using the same trick to run simultaneously.

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

Edit: here is a stronger version following Gilles' comments:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2: and here is a slightly lighter variant following dubiousjim comment:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
jlliagre
  • 61,204
  • 3
    This doesn't work for several reasons. 1. The temporary file may be read before it's written. 2. Creating a temporary file in a shared directory with a predictable name is insecure (trivial DoS, symlink race). 3. If the same script uses this trick several times, it'll always use the same file name. To solve 1, read the file after the pipeline has completed. To solve 2 and 3, use a temporary file with a randomly-generated name or in a private directory. – Gilles 'SO- stop being evil' Jun 08 '11 at 23:00
  • +1 Well the ${PIPESTATUS[0]} is easier but the basic idea here do work if one know about the problems that Gilles mentions. – Johan Jun 09 '11 at 06:36
  • You can save a few subshells: (s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s)). @Johan: I agree it's easier with Bash but in some contexts, knowing how to avoid Bash is worth it. – dubiousjim Aug 29 '12 at 22:25
5

Following is meant as an addon to the answer of @Patrik, in case you are not able to use one of the common solutions.

This answer assumes following:

  • You have a shell which does not know of $PIPESTATUS nor set -o pipefail
  • You want to use a pipe for parallel execution, so no temporary files.
  • You do not want to have additional clutter around if you interrupt the script, possibly by a sudden power outage.
  • This solution should be relatively easy to follow and clean to read.
  • You do not want to introduce additional subshells.
  • You cannot fiddle with the existing file descriptors, so stdin/out/err must not be touched (however you can introduce some new one temporarily)

Additional assumptions. You can get rid of all, but this clobbers the recipe too much, so it is not covered here:

  • All you want to know is that all commands in the PIPE have exit code 0.
  • You do not need additional side band information.
  • Your shell does wait for all pipe commands to return.

Before: foo | bar | baz, however this only returns the exit code of the last command (baz)

Wanted: $? must not be 0 (true), if any of the commands in the pipe failed

After:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

Explained:

  • A tempfile is created with mktemp. This usually immediately creates a file in /tmp
  • This tempfile is then redirected to FD 9 for write and FD 8 for read
  • Then the tempfile is immediately deleted. It stays open, though, until both FDs go out of existence.
  • Now the pipe is started. Each step adds to FD 9 only, if there was an error.
  • The wait is needed for ksh, because ksh else does not wait for all pipe commands to finish. However please note that there are unwanted sideffects if some background tasks are present, so I commented it out by default. If the wait does not hurt, you can comment it in.
  • Afterwards the file's contents are read. If it is empty (because all worked) read returns false, so true indicates an error

This can be used as a plugin replacement for a single command and only needs following:

  • Unused FDs 9 and 8
  • A single environment variable to hold the name of the tempfile
  • And this recipe can be adapted to fairly any shell out there which allows IO redirection
  • Also it is fairly platform agnostic and does not need things like /proc/fd/N

BUGs:

This script has a bug in case /tmp runs out of space. If you need protection against this artificial case, too, you can do it as follows, however this has the disadvantage, that the number of 0 in 000 depends on the number of commands in the pipe, so it is slightly more complicated:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

Portablility notes:

  • ksh and similar shells which only wait for the last pipe command need the wait uncommented

  • The last example uses printf "%1s" "$?" instead of echo -n "$?" because this is more portable. Not every platform interprets -n correctly.

  • printf "$?" would do it as well, however printf "%1s" catches some corner cases in case you run the script on some really broken platform. (Read: if you happen to program in paranoia_mode=extreme.)

  • FD 8 and FD 9 can be higher on platforms which support multiple digits. AFAIR a POSIX conformant shell does only need to support single digits.

  • Was tested with Debian 8.2 sh, bash, ksh, ash, sash and even csh

Tino
  • 1,167
3

With a bit of precaution, this should work:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
crayor
  • 3
alex
  • 7,223
  • How about to cleanup like jlliagre? Don't you leave a file behind called foo-status? – Johan Jun 08 '11 at 18:39
  • @Johan: If you prefer my suggestion, don't hesitate to vote it up ;) In addition not to leaving a file, it has the advantage of allowing multiple processes to run this simultaneously and the current directory need not to be writable. – jlliagre Jun 08 '11 at 20:40
2

The following 'if' block will run only if 'command' succeeded:

if command; then
   # ...
fi

Specifically speaking, you can run something like this:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

Which will run haconf -makerw and store its stdout and stderr to "$haconf_out". If the returned value from haconf is true, then the 'if' block will be executed and grep will read "$haconf_out", trying to match it against "Cluster already writable".

Notice that pipes automatically clean themselves up; with the redirection you'll have to be carefull to remove "$haconf_out" when done.

Not as elegant as pipefail, but a legitimate alternative if this functionality is not within reach.

1
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$
C.G.
  • 11
1

For everyone using bash. I think that by far cleanest solution to both get output value and each exit status in pipe is this.

  1. A one-time preparation. Enable lastpipe shell option. It allows to get value from the last command in pipe without using subshell. If you're in an interactive shell, also disable job control: set +m (The latter isn't needed for scripts - job control is disabled there by default.)

    shopt -s lastpipe
    set +m
    
  2. read the value into your variable, and use PIPESTATUS naturally. E.g.

    grep -E "\S" "file.txt" | sort | uniq | read -d '' RES
    # 'read' exit status 1 means all input was read till EOF, we're OK with that
    if (( PIPESTATUS[0] > 1 || PIPESTATUS[1] > 0 || PIPESTATUS[2] > 0 || PIPESTATUS[3] > 1 )); then
      echo "ERROR"
    else
      echo "$RES"
    fi
    

The above reads all non-empty lines from "file.txt", sorts them and removes duplicates. In this example we have custom processing of exit codes: grep exit status 1 means no lines were selected, we're OK with that; read exit status 1 is expected for saving multi-line output (at least, I don't know another clean way to do it). Clean means without cluttering the code just to avoid the exit status of 1 of read.

NOTE: the read -d '' option is used to read all the input into our variable (by disabling delimiter read stops at, which by default is a newline symbol). This is how we save multi-line output. If your pipe has just one line then you can use plain read RES (and likely can change expected exit status from read to 0 and not 1 as above).

Denis P
  • 111
0

(With bash at least) combined with set -e one can use subshell to explicitly emulate pipefail and exit on pipe error

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

So if foo fails for some reason - the rest of program will not be executed and script exits with corresponding error code. (This assumes that foo prints its own error, which is sufficient to understand the reason of failure)

noonex
  • 111
0

The following answer is similar to some other ones (especially [0] and [1] ) here, with the difference:

  • It also captures (in a variable) the standard output of the final program in the pipeline.

It also:

  • tries to be POSIX compatible
  • retrieves the exit status of the commands in the pipeline
  • lets standard error (file descriptor 2) pass through

Extensions and limitations:

  • I guess it should be extensible to have more than one command in the pipeline.
  • But it can return only one of the exit statuses (or a combined one).

As you'll see from my choice of commands (one that reads binary data and base64), I've used this to read and store binary data (including 0x0) in a Shell variable (of course in encoded form only). If that needs to be processed later it would need to be decoded again (e.g. printf '%s' "${stdout_command2}" | base64 -d | someting_that_can_handle_binary_data).

This is the construct:

stdout_command2="$(
                    exec 4>&1
                    exitstatus_command1="$(
                                            {
                                                { read_binary 3>&- ; printf $? >&3 ; } 4>&-   | \
                                                base64 -w 0 >&4 3>&- 4>&- ;
                                            } 3>&1
                                        )" 4>&1
                    exitstatus_command2=$?  # this must come directly after the above command substitution
                    exec 4>&-
                if [ &quot;${exitstatus_command1}&quot; -ne 0 ]; then
                    exit 11
                fi
                if [ &quot;${exitstatus_command2}&quot; -ne 0 ]; then
                    exit 22
                fi
            )&quot;

overall_exitstatus=$? # this would be e.g. 11 or 22 if either of the two commands failed

This seems to work in dash, bash and busybox’s sh respectively ash.

The exec 4>&1 (and subsequent closure with exec 4>&-) is needed in order to be POSIX-ly correct:
For a simple command with no command word (which there is none in the exitstatus_command1=… line, which is just an assignment), POSIX leaves the order of variable assignments and redirections open (see 2.9.1 Simple Commands: “In the preceding list, the order of steps 3 and 4 may be reversed if no command name results from step 2…”).
For example dash and busybox’s sh respectively ash seem to perform the redirection before the assignment (which thus works without the exec), while bash performs it afterwards (which thus fails without the exec).

I also close file descriptors 3 and 4 where these are not needed:

  • 3, with 3>&- for read_binary
  • 4, with 4>&- for the whole { read_binary 3>&- ; printf $? >&3 ; }
  • 3 and 4, with 3>&- 4>&- for base64 -w 0 but only after 1 has been redirected to 4 with >&4 (so order matters)

You can check which file descriptors are seen by the commands, by wrapping them in extra shell scripts like this:

#!/bin/sh
find /proc/$$/fd -type l | sort -V |  sed 's/^/before /' >&2
base64 -w 0
e=$?
find /proc/$$/fd -type l | sort -V |  sed 's/^/after /' >&2
exit $e

I for some reasons (which I didn’t quite understand) it did not work by wrapping them in shell functions executed in the same shell as the construct above. In that case I never so all the extra file descriptors.

Last but not least, e.g. read_binary (or base64) could very well be functions (executed in the same shell), like so:

read_binary()
{
    if [ -z "${foo}" ]
        some_more_complext_read"${path}"
        if [ $? -ne 0 ]; then
            return 1
            #exit 1 # exit wouldn't work here
        fi
    else
        cat -- "${path}"
        if [ $? -ne 0 ]; then
            return 1
            #exit 2 # exit wouldn't work here
        fi
    fi
}

But beware that one cannot use exit in such function (with the above construct), as that would not just leave the function, but the whole subshell from the respective command subsitution ($(…)).

And exit in either read_binary or base64 (if that was a function) might cause the printf $? >&3 to never happen and thus that exit status would get lost.

calestyo
  • 147
0

If you aren't interested in making use of the actual value in the script, per se, but instead dealing with the errors on each element in a pipeline, you can do something like:

    if ! command1
    then
        error_handler1
    fi | \
    if ! command2
    then
        error_handler2
    fi | \
    if ! command3
    then
        error_handler3
    fi | \
...
0

This is what "mispipe" (included in moreutils https://joeyh.name/code/moreutils/ ) was written for.

mispipe "prog1 | prog2 | prog3" "prog4 | prog 5"

will run like

prog1 | prog2 | prog3 | prog4 | prog5

except that it will return the exit status at the end of prog3, and will completely ignore the exit status of the later programs in the pipe.

This is ideal for certain logging situations where you don't care whether the logger programs at the end of the pipe (prog4 | prog5) fail, but you do care very much whether the main programs in the pipe (prog1 | prog2 | prog3) fail.

Internally, the mispipe program is very straightforwardly written in C.

It uses "system()" to execute its two arguments (after linking the standard output and input together properly with pipes and making sure signals will be handled right). So it will execute the subcommands with whatever the standard system shell is, which should be fine if you're writing in portable Bourne shell.

0

As yet another approach, you can use fifo inode files and explicit job control. They make the script much more verbose, but allow for finer control over the execution. Fifo inodes have an advantage over straight files, because they don't consume disk space. This can matter if you're running in a limited environment or have a script working with large files.

It does imply, though, that your execution environment must support fifo inodes and your shell must support basic job control.

Rather than, say:

generate-large-data | sort | uniq

You can use:

#!/bin/sh

Put the fifo files in a secure, temporary directory.

fifos="$( mktemp -d )" || exit 1

And clean up when complete.

trap 'rm -r "${fifos}"' EXIT

Create the fifo queues

mkfifo "${fifos}/gen.fifo" || exit 1 mkfifo "${fifos}/sort.fifo" || exit 1

Pipe the generate-large-data output into the gen fifo file

generate-large-data > "${fifos}/gen.fifo" &

and capture the job id.

gen_job=%%

The sort operation reads from the generated data fifo and outputs to its own.

sort < "${fifos}/gen.fifo" > "${fifos}/sort.fifo" &

and capture the job id.

sort_job=%%

Uniq, as the final operation, runs as a usual command, not as a job.

uniq < "${fifos}/sort.fifo" uniq_code=$?

Now, the script can capture the exit code for each job in the pipeline.

"wait" waits for the job to end, and exits with the job's exit code.

wait ${gen_job} gen_code=$?

wait ${sort_job} sort_code=$?

And the script can access each exit code.

echo "generate-large-data exit code: ${gen_code}" echo "sort exit code: ${sort_code}" echo "uniq exit code: ${uniq_code}"

Unlike some other approaches, this gives your script complete access to all exit codes in every step of the operation.

Groboclown
  • 41
  • 2
-1

EDIT: This answer is wrong, but interesting, so I'll leave it for future reference.


Adding a ! to the command inverts the return code.

http://tldp.org/LDP/abs/html/exit-status.html

# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command     # bash: bogus_command: command not found
echo $?                # 127

! ls | bogus_command   # bash: bogus_command: command not found
echo $?                # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #
Falmarri
  • 13,047