On , I learnt ...

About Bash’s $PIPESTATUS variable

Problem: you have a pipeline like so:

$cmd1 | $cmd2 | $cmd3

and you want to detect if $cmd1 exits with a exit code greater than 1.

I had this problem when piping mypy output into grep and then wc to count the number of “untyped def” errors:

mypy $MYPY_ARGS | grep -F "[no-untyped-def]" | wc -l

If the mypy command is misconfigured, or terminated by the out-of-memory killer, it will exit with a code greater than 1 — that’s the case we want to detect and handle. We’re not interested when mypy exits with a 0 (no errors) or 1 (some errors) exit code.

Default behaviour ❌

Bash’s default behaviour is for the pipeline to return the exit code of the last command (wc -l), which would be 0 even if mypy errored.

PIPEFAIL option ❌

If we set the PIPEFAIL option (with set -o pipefail), then the pipeline’s return value is set to the rightmost non-zero exit code from the pipeline. This won’t work for us as grep will use an exit code of 1 if it doesn’t find any matches and so the pipeline’s return value will be 1 if mypy errors, which is indistinguishable from the happy path of mypy finding some type errors.

$PIPESTATUS array ✅

Instead we can use Bash’s $PIPESTATUS internal variable. This is an array that stores the exit code of each command within the last pipeline. E.g.

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

We can use this to detect if the mypy command has returned an exit code greater than 1:

> mypy $MYPY_ARGS | grep -F "[no-untyped-def]" | wc -l;
> [ "${PIPESTATUS[0]}" -le 1 ]

Now $? will be 1 if mypy exited with a code greater than 1 and 0.