When you execute a pipeline, each pipe-separated element is executed in its own process. Variable assignments only take effect in their own process. Under ksh and zsh, the last element of the pipeline is executed in the original shell; under other shells such as bash, each pipeline element is executed in its own subshell and the original shell just waits for them all to end.
$ bash -c 'GROUPSTATUS=foo; echo GROUPSTATUS is $GROUPSTATUS'
GROUPSTATUS is foo
$ bash -c 'GROUPSTATUS=foo | :; echo GROUPSTATUS is $GROUPSTATUS'
GROUPSTATUS is
In your case, since you only care about all the commands succeeding, you can make the status code flow up.
{ tar -cf - my_folder 2>&1 1>&3 | grep -v "Removing leading" 1>&2;
! ((PIPESTATUS[0])); } 3>&1 |
gzip --rsyncable > my_file.tar.gz;
if ((PIPESTATUS[0] || PIPESTATUS[1])); then rm my_file.tar.gz; fi
If you want to get more than 8 bits of information out of the left side of a pipe, you can write to yet another file descriptor. Here's a proof-of-principle example:
{ { tar …; echo $? >&4; } | …; } | { gzip …; echo $? >&4; } \
4>&1 | ! grep -vxc '0'
Once you get data on standard output, you can feed it into a shell variable using command substitution, i.e. $(…)
. Command substitution reads from the command's standard output, so if you also meant to print things to script's standard output, they need to temporarily go through another file descriptor. The following snippet uses fd 3 for things that eventually go to the script's stdout and fd 4 for things that are captured into $statuses
.
statuses=$({ { tar -v … >&3; echo tar $? >&4; } | …; } |
{ gzip …; echo gzip $? >&4; } 4>&1) 3>&1
If you need to capture the output from different commands into different variables, I think there is no direct way even in “advanced” shells such as bash, ksh or zsh. Here are some workarounds:
- Use temporary files.
- Use a single output stream, with e.g. a prefix on each line to indicate its origin, and filter at the top level.
- Use a more advanced language such as Perl or Python.
Best Answer
You're asking for a lot with a general answer, but it should be possible to do it relatively easily to some extent.
However,
bash
has at least two general mechanisms that can help solve that. You can set the variable PROMPT_COMMAND and this will be executed after anything you run in your prompt:We can use this to save the last entered command and then reread it, so we can store it in memory for further use (history expansion does not work in this case):
Now comes the logic part, which is completely up to you. Let's use just very simplistic logic to prevent the example in the question. The thing to remember is, that bash functions still work in there, so you don't have to cram everything on a single line, in a single variable.
Of course, this approach is useful to prevent PEBKAC (especially if you add
sleep 3
at the end), but it can't break the next command on its own.If that's really what you want, trap the
DEBUG
signal (eg.trap 'echo "I am not deterministic, haha"' DEBUG
), since it runs beforehand. Be careful with combining these two approaches, since the output/action will be doubled:To make the trap break a command, you'll have to enable
extdebug
(shopt -s extdebug
). You also don't need to save and reread history constantly, but can inspect$BASH_COMMAND
to get the command about to be run. Then you just need to make sure the logic checker return 1 when it detects something bad and 0 otherwise.