Why does "false && true" not exit with set -e in Bash? [duplicate] - bash

This question already has an answer here:
Why doesn't set -e cause a failure with `false || false && true`? [duplicate]
(1 answer)
Closed 6 years ago.
Why is the third case returning success with exit code 0?
case 1 ~$ bash -c 'set -e; false || true; echo success'; echo $?
success
0
case 2 ~$ bash -c 'set -e; true || false; echo success'; echo $?
success
0
case 3 ~$ bash -c 'set -e; false && true; echo success'; echo $?
success
0
case 4 ~$ bash -c 'set -e; true && false; echo success'; echo $?
1
case 5 ~$ bash -c 'set -e; false || false; echo success'; echo $?
1
case 6 ~$ bash -c 'set -e; false && false; echo success'; echo $?
success
0

The bash documentation for set -e says:
The shell does not exit if the command that fails is [...] part of any command executed in a && or || list except the command following the final && or ||, [...]
The command list in question is false && true. The failing command is false, which is not the last command in the list, so the shell does not exit. The 0 you're seeing is the exit status of echo success.

set -e is a bit subtle.
From the reference:
-e
When this option is on, when any command fails (for any of the reasons listed in Consequences of Shell Errors or by returning an exit
status greater than zero), the shell immediately shall exit, as if by
executing the exit special built-in utility with no arguments, with
the following exceptions:
The failure of any individual command in a multi-command pipeline shall not cause the shell to exit. Only the failure of the
pipeline itself shall be considered.
The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word, a pipeline
beginning with the ! reserved word, or any command of an AND-OR list
other than the last.
If the exit status of a compound command other than a subshell command was the result of a failure while -e was being ignored, then
-e shall not apply to this command.
This requirement applies to the shell environment and each subshell environment separately. For example, in:
set -e; (false; echo one) | cat; echo two
the false command causes the subshell to exit without executing echo one; however, echo two is executed because the exit status of the
pipeline (false; echo one) | cat is zero.
Since false && true is part of an AND or OR list and false isn't the last, the shell doesn't exit immediately.
So echo success is executed, and it's return code is 0.
By the way, have you noticed case 6?

true && false; echo success returns the return code from echo which is 0.
bash -c 'false && true'; echo $?
yields 1 as expected. Pheew!
Aside: bash -c 'set -e; false || false; echo success'; echo $? yields return code 1 because second command does not execute echo (stops at false because errors stop the current command), note that success is not printed.

Related

How to perform action on non-zero return code of any command in bash [duplicate]

I want to know whether any commands in a bash script exited with a non-zero status.
I want something similar to set -e functionality, except that I don't want it to exit when a command exits with a non-zero status. I want it to run the whole script, and then I want to know that either:
a) all commands exited with exit status 0
-or-
b) one or more commands exited with a non-zero status
e.g., given the following:
#!/bin/bash
command1 # exits with status 1
command2 # exits with status 0
command3 # exits with status 0
I want all three commands to run. After running the script, I want an indication that at least one of the commands exited with a non-zero status.
Set a trap on ERR:
#!/bin/bash
err=0
trap 'err=1' ERR
command1
command2
command3
test $err = 0 # Return non-zero if any command failed
You might even throw in a little introspection to get data about where the error occurred:
#!/bin/bash
for i in 1 2 3; do
eval "command$i() { echo command$i; test $i != 2; }"
done
err=0
report() {
err=1
printf '%s' "error at line ${BASH_LINENO[0]}, in call to "
sed -n ${BASH_LINENO[0]}p $0
} >&2
trap report ERR
command1
command2
command3
exit $err
You could try to do something with a trap for the DEBUG pseudosignal, such as
trap '(( $? && ++errcount ))' DEBUG
The DEBUG trap is executed
before every simple command, for command, case command, select command, every arithmetic for command, and before the first command executes in a shell function
(quote from manual).
So if you add this trap and as the last command something to print the error count, you get the proper value:
#!/usr/bin/env bash
trap '(( $? && ++errcount ))' DEBUG
true
false
true
echo "Errors: $errcount"
returns Errors: 1 and
#!/usr/bin/env bash
trap '(( $? && ++errcount ))' DEBUG
true
false
true
false
echo "Errors: $errcount"
prints Errors: 2. Beware that that last statement is actually required to account for the second false because the trap is executed before the commands, so the exit status for the second false is only checked when the trap for the echo line is executed.
I am not sure if there is a ready-made solution for your requirement. I would write a function like this:
function run_cmd_with_check() {
"$#"
[[ $? -ne 0 ]] && ((non_zero++))
}
Then, use the function to run all the commands that need tracking:
run_cmd_with_check command1
run_cmd_with_check command2
run_cmd_with_check command3
printf "$non_zero commands exited with non-zero exit code\n"
If required, the function can be enhanced to store all failed commands in an array which can be printed out at the end.
You may want to take a look at this post for more info: Error handling in Bash
You have the magic variable $? available in bash which tells the exit code of last command:
#!/bin/bash
command1 # exits with status 1
C1_output=$? # will be 1
command2 # exits with status 0
C2_output=$? # will be 0
command3 # exits with status 0
C3_output=$? # will be 0
For each command you could do this:
if ! Command1 ; then an_error=1; fi
And repeat this for all commands
At the end an_error will be 1 if any of them failed.
If you want a count of failures set an_error to 0 at the beginning and do $((an_error++)). Instead of an_error=1
You could place your list of commands into an array and then loop over the commands. Any that return an error code your keep the results for later viewing.
declare -A results
commands=("your" "commands")
for cmd in "${commands[#]}"; do
out=$($cmd 2>&1)
[[ $? -eq 0 ]] || results[$cmd]="$out"
done
Then to see any non zero exit codes:
for cmd in "${!results[#]}"; do echo "$cmd = ${results[$cmd]}"; done
If the length of results is 0, there were no errors on your list of commands.
This requires Bash 4+ (for the associative array)
You can use the DEBUG trap like:
trap 'code+=$?' DEBUG
code=0
# run commands here normally
exit $code

Why does eval exit subshell mid-&& with set -e?

Why does bash do what I'd expect here with a compound command in a subshell:
$ bash -x -c 'set -e; (false && true; echo hi); echo here'
+ set -e
+ false
+ echo hi
hi
+ echo here
here
But NOT do what I'd expect here:
$ bash -x -c 'set -e; (eval "false && true"; echo hi); echo here'
+ set -e
+ eval 'false && true'
++ false
Basically, the difference is between 'eval'-uating a compound command and just executing a compound command. When the shell executes a compound command, non-terminal commands in the compound command that fail do not cause the entire compound command to fail, they simply terminate the command. But when eval runs the compound command and any non-terminal sub-command terminates the command with an error, eval terminates the command with an error.
I guess I need to format my eval statement like this:
eval "false && true" || :
so that the eval command doesn't exit my subshell with an error, because this works as I'd expect it to:
$ bash -x -c 'set -e; (eval "false && true" || :; echo hi); echo here'
+ set -e
+ false
+ echo hi
hi
+ echo here
here
The problem I have with this is that I've written a function:
function execute() {
local command="$1"
local remote="$2"
if [ ! -z "$remote" ]; then
$SSH $remote "$command" || :
else
eval "$command" || :
fi
}
I'm using set -e in my script. The same problem occurs with ssh in this function - if the last command in the ssh script is a compound command that terminates early, the entire command terminates with an error. I want commands like this to behave as if they were executing locally - early terminating compound commands should not cause ssh or eval to return 1, failing the entire command. If I tack || : on the end of my eval statement or my ssh statement, then all such commands will succeed, even if they shouldn't because the last command in the eval'd or ssh'd command failed.
Any ideas would be much appreciated.
I should also mention that set -e is terribly error-prone; see http://mywiki.wooledge.org/BashFAQ/105 for a bunch of examples. So the best solution might be to dispense with it, and write your own logic to detect errors and abort.
That out of the way . . .
The problem here is that eval "false && true" is a single command, and evaluates to false (nonzero), so set -e aborts after that command runs.
If you were instead to run eval "false && true; true", you would not see this behavior, because then eval evaluates to true (zero). (Note that, although eval does implement the set -e behavior, it obeys the rule that false && true is non-aborting.)
This is not actually specific to eval, by the way. A subshell would give the same result, for the same reason:
$ bash -x -c 'set -e; (false && true); echo here'
+ set -e
+ false
The simplest fix for your problem is probably just to run an extra true if the end is reached:
$SSH $remote "set -e; $command; true"
eval "$command; true"
eval counts as its own command with its own exit code.
Since eval "false && true" returns an exit code of 1, it triggers set -e.

Why does set -e; true && false && true not exit?

According to this accepted answer using the set -e builtin should suffice for a bash script to exit on the first error. Yet, the following script:
#!/usr/bin/env bash
set -e
echo "a"
echo "b"
echo "about to fail" && /bin/false && echo "foo"
echo "c"
echo "d"
prints:
$ ./foo.sh
a
b
about to fail
c
d
removing the echo "foo" does stop the script; but why?
To simplify EtanReisner's detailed answer, set -e only exits on an 'uncaught' error. In your case:
echo "about to fail" && /bin/false && echo "foo"
The failing code, /bin/false, is followed by && which tests its exit code. Since && tests the exit code, the assumption is that the programmer knew what he was doing and anticipated that this command might fail. Ergo, the script does not exit.
By contrast, consider:
echo "about to fail" && /bin/false
The program does not test or branch on the exit code of /bin/false. So, when /bin/false fails, set -e will cause the script to exit.
Alternative that exits when /bin/false fails
Consider:
set -e
echo "about to fail" && /bin/false ; echo "foo"
This version will exit if /bin/false fails. As in the case where && was used, the final statement echo "foo" would therefore only be executed if /bin/false were to succeed.
Because that answer is not sufficiently specific enough.
It should say (bolded text is my addition):
# Any subsequent simple commands which fail will cause the shell script to exit immediately
Since the man page reads thusly:
-e Exit immediately if a simple command (see SHELL GRAMMAR
above) exits with a non-zero status. The shell does not
exit if the command that fails is part of the command
list immediately following a while or until keyword,
part of the test in an if statement, part of a && or ││
list, or if the command’s return value is being inverted
via !. A trap on ERR, if set, is executed before the
shell exits.
And SHELL GRAMMAR expands thusly:
SHELL GRAMMAR
Simple Commands
A simple command is a sequence of optional variable assignments fol-
lowed by blank-separated words and redirections, and terminated by a
control operator. The first word specifies the command to be executed,
and is passed as argument zero. The remaining words are passed as
arguments to the invoked command.
The return value of a simple command is its exit status, or 128+n if
the command is terminated by signal n.
I came across set -e for Bash scripts but had problems understanding what happens regarding the evaluation of the command following the last && or || in a && or || list. I know of the following quote from http://man7.org/linux/man-pages/man1/bash.1.html about set -e:
The shell does not exit if the command that fails is (...) part of
any command executed in a && or || list except the command
following the final && or || (...)
To test this, I wrote a small Bash script:
#!/bin/bash
bash -c "set -e ; true ; echo -n A"
bash -c "set -e ; false ; echo -n B"
bash -c "set -e ; true && true ; echo -n C"
bash -c "set -e ; true && false ; echo -n D"
bash -c "set -e ; false && true ; echo -n E"
bash -c "set -e ; false && false ; echo -n F"
bash -c "set -e ; true || true ; echo -n G"
bash -c "set -e ; true || false ; echo -n H"
bash -c "set -e ; false || true ; echo -n I"
bash -c "set -e ; false || false ; echo -n J"
echo ""
It prints:
ACEFGHI
About A:
true does not have a non-zero status. Therefore, the shell doesn't exit.
About B:
false does have a non-zero status and is not part of a && or || list. Therefore, the shell exits.
About C:
This is a && or || list. We will have to look at the command following the last && or ||. The command is true which does not have a non-zero status. So it doesn't matter if the command is evaluated or not - the shell doesn't exit either way.
About D:
This is a && or || list. We will have to look at the command following the last && or ||. This time, the command is false which does have a non-zero status. So we have to check if that false is being evaluated - which is indeed the case since && is following the true. Therefore, the shell exits.
About E:
Same reasoning as with C: true is the command following the last && or ||. Therefore, the shell doesn't exit.
About F:
Similar to D: This is a && or || list. We will have to look at the command following the last && or ||. Again the command is false which does have a non-zero status. But this time it doesn't matter, because the first command is false as well. Since it's a && list, the second false won't be evaluated. Therefore, the shell doesn't exit.
About G:
Same reasoning as with C or E: true is the command following the last && or ||. Therefore, the shell doesn't exit.
About H:
This is a && or || list. We will have to look at the command following the last && or ||. This command is false which does have a non-zero status, but it won't be evaluated since || is preceded by true. Therefore, the shell doesn't exit.
About I:
Same reasoning as with C, E or G: true is the command following the last && or ||. Therefore, the shell doesn't exit.
About J:
This is a && or || list. We will have to look at the command following the last && or ||. This command is false which does have a non-zero status. Since || is preceded by false the second false will be evaluated. Therefore, the shell does exit.
You should be able to apply these test cases to your case: true && false && true. Since the command following the last && or || is true which doesn't have a non-zero status, it doesn't matter what precedes && or ||, the shell won't exit either way.

Bash get exit status of command when 'set -e' is active?

I generally have -e set in my Bash scripts, but occasionally I would like to run a command and get the return value.
Without doing the set +e; some-command; res=$?; set -e dance, how can I do that?
From the bash manual:
The shell does not exit if the command that fails is [...] part of any command executed in a && or || list [...].
So, just do:
#!/bin/bash
set -eu
foo() {
# exit code will be 0, 1, or 2
return $(( RANDOM % 3 ))
}
ret=0
foo || ret=$?
echo "foo() exited with: $ret"
Example runs:
$ ./foo.sh
foo() exited with: 1
$ ./foo.sh
foo() exited with: 0
$ ./foo.sh
foo() exited with: 2
This is the canonical way of doing it.
as an alternative
ans=0
some-command || ans=$?
Maybe try running the commands in question in a subshell, like this?
res=$(some-command > /dev/null; echo $?)
Given behavior of shell described at this question it's possible to use following construct:
#!/bin/sh
set -e
{ custom_command; rc=$?; } || :
echo $rc
Another option is to use simple if. It is a bit longer, but fully supported by bash, i.e. that the command can return non-zero value, but the script doesn't exit even with set -e. See it in this simple script:
#! /bin/bash -eu
f () {
return 2
}
if f;then
echo Command succeeded
else
echo Command failed, returned: $?
fi
echo Script still continues.
When we run it, we can see that script still continues after non-zero return code:
$ ./test.sh
Command failed, returned: 2
Script still continues.
Use a wrapper function to execute your commands:
function __e {
set +e
"$#"
__r=$?
set -e
}
__e yourcommand arg1 arg2
And use $__r instead of $?:
if [[ __r -eq 0 ]]; then
echo "success"
else
echo "failed"
fi
Another method to call commands in a pipe, only that you have to quote the pipe. This does a safe eval.
function __p {
set +e
local __A=() __I
for (( __I = 1; __I <= $#; ++__I )); do
if [[ "${!__I}" == '|' ]]; then
__A+=('|')
else
__A+=("\"\$$__I\"")
fi
done
eval "${__A[#]}"
__r=$?
set -e
}
Example:
__p echo abc '|' grep abc
And I actually prefer this syntax:
__p echo abc :: grep abc
Which I could do with
...
if [[ ${!__I} == '::' ]]; then
...

Exit Shell Script Based on Process Exit Code [duplicate]

This question already has answers here:
Aborting a shell script if any command returns a non-zero value
(10 answers)
Closed 1 year ago.
I have a shell script that executes a number of commands. How do I make the shell script exit if any of the commands exit with a non-zero exit code?
After each command, the exit code can be found in the $? variable so you would have something like:
ls -al file.ext
rc=$?; if [[ $rc != 0 ]]; then exit $rc; fi
You need to be careful of piped commands since the $? only gives you the return code of the last element in the pipe so, in the code:
ls -al file.ext | sed 's/^/xx: /"
will not return an error code if the file doesn't exist (since the sed part of the pipeline actually works, returning 0).
The bash shell actually provides an array which can assist in that case, that being PIPESTATUS. This array has one element for each of the pipeline components, that you can access individually like ${PIPESTATUS[0]}:
pax> false | true ; echo ${PIPESTATUS[0]}
1
Note that this is getting you the result of the false command, not the entire pipeline. You can also get the entire list to process as you see fit:
pax> false | true | false; echo ${PIPESTATUS[*]}
1 0 1
If you wanted to get the largest error code from a pipeline, you could use something like:
true | true | false | true | false
rcs=${PIPESTATUS[*]}; rc=0; for i in ${rcs}; do rc=$(($i > $rc ? $i : $rc)); done
echo $rc
This goes through each of the PIPESTATUS elements in turn, storing it in rc if it was greater than the previous rc value.
If you want to work with $?, you'll need to check it after each command, since $? is updated after each command exits. This means that if you execute a pipeline, you'll only get the exit code of the last process in the pipeline.
Another approach is to do this:
set -e
set -o pipefail
If you put this at the top of the shell script, it looks like Bash will take care of this for you. As a previous poster noted, "set -e" will cause Bash to exit with an error on any simple command. "set -o pipefail" will cause Bash to exit with an error on any command in a pipeline as well.
See here or here for a little more discussion on this problem. Here is the Bash manual section on the set builtin.
"set -e" is probably the easiest way to do this. Just put that before any commands in your program.
If you just call exit in Bash without any parameters, it will return the exit code of the last command. Combined with OR, Bash should only invoke exit, if the previous command fails. But I haven't tested this.
command1 || exit;
command2 || exit;
Bash will also store the exit code of the last command in the variable $?.
[ $? -eq 0 ] || exit $?; # Exit for nonzero return code
http://cfaj.freeshell.org/shell/cus-faq-2.html#11
How do I get the exit code of cmd1 in cmd1|cmd2
First, note that cmd1 exit code could be non-zero and still don't mean an error. This happens for instance in
cmd | head -1
You might observe a 141 (or 269 with ksh93) exit status of cmd1, but it's because cmd was interrupted by a SIGPIPE signal when head -1 terminated after having read one line.
To know the exit status of the elements of a pipeline
cmd1 | cmd2 | cmd3
a. with Z shell (zsh):
The exit codes are provided in the pipestatus special array.
cmd1 exit code is in $pipestatus[1], cmd3 exit code in
$pipestatus[3], so that $? is always the same as
$pipestatus[-1].
b. with Bash:
The exit codes are provided in the PIPESTATUS special array.
cmd1 exit code is in ${PIPESTATUS[0]}, cmd3 exit code in
${PIPESTATUS[2]}, so that $? is always the same as
${PIPESTATUS: -1}.
...
For more details see Z shell.
For Bash:
# This will trap any errors or commands with non-zero exit status
# by calling function catch_errors()
trap catch_errors ERR;
#
# ... the rest of the script goes here
#
function catch_errors() {
# Do whatever on errors
#
#
echo "script aborted, because of errors";
exit 0;
}
In Bash this is easy. Just tie them together with &&:
command1 && command2 && command3
You can also use the nested if construct:
if command1
then
if command2
then
do_something
else
exit
fi
else
exit
fi
#
#------------------------------------------------------------------------------
# purpose: to run a command, log cmd output, exit on error
# usage:
# set -e; do_run_cmd_or_exit "$cmd" ; set +e
#------------------------------------------------------------------------------
do_run_cmd_or_exit(){
cmd="$#" ;
do_log "DEBUG running cmd or exit: \"$cmd\""
msg=$($cmd 2>&1)
export exit_code=$?
# If occurred during the execution, exit with error
error_msg="Failed to run the command:
\"$cmd\" with the output:
\"$msg\" !!!"
if [ $exit_code -ne 0 ] ; then
do_log "ERROR $msg"
do_log "FATAL $msg"
do_exit "$exit_code" "$error_msg"
else
# If no errors occurred, just log the message
do_log "DEBUG : cmdoutput : \"$msg\""
fi
}

Resources