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.
First of all, while they are functionally equivalent,
$(…)
is widely considered to be clearer than `…`
—
see this, this, and this. Secondly,
you don’t need to use $?
to check whether a command succeeded or failed.
My attention was recently drawn to Section 2.9.1, Simple Commands
of The Open Group Base Specifications for Shell & Utilities (Issue 7):
A "simple command" is a sequence of optional variable assignments and redirections, in any sequence, optionally followed by words and redirections, terminated by a control operator.
When a given simple command is required to be executed …
⋮ (blah, blah, blah …)
If there is a command name, execution shall continue as described
in Command Search and Execution. If there is no command name,
but the command contained a command substitution, the command shall complete
with the exit status of the last command substitution performed. …
For example,
the exit status of the command
ls -ld "module_$(uname).c"
is the exit status from the ls
, but
the exit status of the command
myfile="module_$(uname).c"
is the exit status from the uname
.
So ferada’s answer can be streamlined a bit:
if output=$(/etc/grub.d/30_os-prober) && [ -z "$output" ]
# i.e., if 30_os-prober successfully produced no output
then
install_linux_only
else
install_dual_boot
fi
Note that it is good practice to use all-upper-case names
only for environment variables
(or variables to be visible throughout the script).
Variables of limited scope are usually named in lower case
(or, if you prefer, camelCase).
Best Answer
Your command,
will be interpreted by the shell as "run the command
-ci
with the argumentstext
andfile.sh
, and set the variablecheck
to the valuegrep
in its environment".The shell stores the exit value of most recently executed command in the variable
?
. You can assign its value to one of your own variables like this:If you want to act on this value, you may either use your
check
variable:or you could skip using a separate variable and having to inspect
$?
all together:(note the
-q
, it instructsgrep
to not output anything and to exit as soon as something matches; we aren't really interested in what matches here)Or, if you just want to "do things" when the pattern is not found:
Saving
$?
into another variable is only ever needed if you need to use it later, when the value in$?
has been overwritten, as inIn the above code snippet,
$?
will be overwritten by the result of the[ "$err" -ne 0 ] && [ ! -d "$dir" ]
test. Saving it here is really only necessary if we need to display it and use it withexit
.