Bash control flow using || on function, with set -e - bash

If I put set -e in a Bash script, the script will exit on future errors. I'm confused about how this works with functions. Consider the following, which will only print one to standard out:
set -e # Exit on error
fun(){
echo one
non_existing_command
echo two
}
fun
Clearly, the non_existing_command is an error and so the script exits before the second echo. Usually one can use the or operator || to run another command if and only if the first command fails. That is, I would suspect the following to print out both one and three, but not two:
set -e # Exit on error
fun(){
echo one
non_existing_command
echo two
}
fun || echo three
What I get however is one and two. That is, the || operator prevents the exit (as it should) but it chooses to continue with the function body and disregard the right-hand command.
Any explanation?

It appears to be documented in the set builtin command
If a compound command or shell function executes in a context where -e is being ignored [such as on the left-hand of a ||], none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status.
Emphasis and comment are mine.
Also, if you try to set -e within the function, don't bother: the next sentence:
If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.

Related

How to make exception for a bash script with set -ex

I have a bash script that has set -ex, which means the running script will exit once any command in it hits an error.
My use case is that there's a subcommand in this script for which I want to catch its error, instead of making the running script shutdown.
E.g., here's myscript.sh
#!/bin/bash
set -ex
# sudo code here
error = $( some command )
if [[ -n $error ]] ; then
#do something
fi
Any idea how I can achieve this?
You can override the output of a single command
this_will_fail || true
Or for an entire block of code
set +e
this_will_fail
set -e
Beware, however, that if you decide you don't want to use set -e in the script anymore this won't work.
If you want to handle a particular command's error status yourself, you can use as the condition in an if statement:
if ! some command; then
echo 'An error occurred!' >&2
# handle error here
fi
Since the command is part of a condition, it won't exit on error. Note that other than the ! (which negates it, so the then clause will run if the command fails rather than it succeeds), you just include the command directly in the if statement (no brackets, parentheses, etc).
BTW, in your pseudocode example, you seem to be treating it as an error if the command produces any output; usually that's not what you want, and I'm assuming you actually want to test the exit status to detect errors.

Bash functions ignore errors when called in a boolean expression?

The following bash script prints "This should never run!":
#!/bin/bash
set -e
func() {
echo "Bailing out..."
false # Should bail out because of set -e
echo "This should never run!"
}
func || true
What's going on here?
A simple conclusion to make is that set -e is not propagated inside functions. However, this is not the case. Changing the last line (func || true) to just func causes the function to stop at the false statement as expected.
Another hypothesis is that bash will start executing the function, and on the first error (false) evaluate the rest of the line it was called on (func || true), and if that retuns 0 (true), then it continues executing the function as if nothing happened. However, replacing true with echo "Oops!" disproves that, since no "Oops!" is printed.
Is set -e somehow ignored when executing functions as part of a boolean expression?
From man bash regarding set -e:
Exit immediately if a pipeline (which may consist of a single simple command), a list, or a compound 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 following the if or elif reserved words, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return value is being inverted with !. If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit. A trap on ERR, if set, is executed before the shell exits. This option applies to the shell environment and each subshell environment separately (see COMMAND EXECUTION ENVIRONMENT above), and may cause subshells to exit before executing all the commands in the subshell.

Bash script failing of no reason

I have this bash script (actually a part of https://github.com/ddollar/heroku-buildpack-multi/blob/master/bin/compile with echo I've added myself):
echo "[DEBUG] chmod done"
framework=$($dir/bin/detect $1)
echo "[DEBUG] $framework done"
And I see in the log:
[DEBUG] chmod done
Staging failed: Buildpack compilation step failed
And I do not see the second echo in the logs at all.
I do not know much bash unfortunately. Could anybody explain to me in what case the first echo performs and second do not? I always thought that both echo should always work no matter whether the second line succeeds or not.
It's not visible in your question, but clicking on your link, it says in the third line
set -e
This means to stop processing the script immediately whenever an error occurs. Comment that line, and the script should run through and also print the second echo statement.
Note that I didn't inspect what the script actually does and I cannot tell you if commenting out set -e is actually good advice or not.
From man set:
−e: When this option is on, when any command fails (for any of the reasons listed
in Section 2.8.1, Consequences of Shell Errors or by returning an exit status
greater than zero), the shell immediately shall exit with the following excep‐
tions:
1. 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.
2. 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.
3. 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; how‐
ever, echo two is executed because the exit status of the pipeline (false; echo
one) | cat is zero.

How to use detect failure in command substitution

As you know set -e is very useful to discover any failure when command is executed. However I found it to be delicate to use and I don't know how to use it in the following scenarios:
==============First example================================
set -e
function main() {
local X1="$(exp::notdefined)"
echo "Will reach here : ${LINENO}"
X2="$(exp::notdefined)"
echo "Will not reach here : ${LINENO}"
}
main
==============Second example================================
set -e
function exp::tmp() {
echo "Now '$-' is "$-" : ${LINENO}"
false
return 0
}
function main() {
X1="$(exp::tmp)"
echo "Will reach here : ${LINENO}. '\$X1' : ${X1}"
X2="$(set -e ; exp::tmp)"
echo "Will not reach here : ${LINENO}"
}
main
===============================
The first example shows that, if we use command substitution on a local variable, then it will not fail even if the command substituted is not found. I don't know how to detect these kinds of failures.
The second example shows that, if the bash options (-e) will not propagate unless we call set -e inside the command braces. Is there any better way to do this?
You request immediate exit on pipeline failure with -e, e.g.:
-e Exit immediately if a pipeline (which may consistof a single simple
command), a list, or a compound command (see SHELL GRAMMAR above),
exits with a non-zero status.
The reason the bad command substitution does not cause failure within the function is because local provides its own return status.
local [option] [name[=value] ...]
... The return status is 0 unless local is used outside a function, an
invalid name is supplied, or name is a readonly variable.
The assignment of a failed command substitution does not cause local to return non-zero. Therefore, no immediate-exit is triggered.
As far as checking for a failure of command substitution following local, since the output is assigned to the variable, and the return will not be non-zero in the event of command substitution failure, you would have to validate by checking the variable contents itself for expected values for success/failure.
From the bash manual:
Subshells spawned to execute command substitutions inherit the value of
the -e option from the parent shell. When not in posix mode, bash
clears the -e option in such subshells.
Example 2 behaves differently when bash runs with --posix; however for example 1 I can't find any documentation explaining why local would cause this.

why is bash -e disabled within a function run with not (!) [duplicate]

This question already exists:
“set -e” in a function [duplicate]
Closed 7 years ago.
Note: If you are looking for a workaround because set -e does not work in a function, please go to “set -e” in a function. This question is about why it does not work as expected.
When the following is run on GNU bash, version 4.1.5(1)-release :
set -ex
PS4=' ${FUNCNAME[0]}: $LINENO: '
function f() {
set -e
false
echo $#
}
! f why does it display ? It should stop because of -e
f
It displays
: 11: f why does it display '?' It should stop because of -e
f: 6: set -e
f: 7: false
f: 8: echo why does it display '?' It should stop because of -e
why does it display ? It should stop because of -e
: 12: f
f: 6: set -e
f: 7: false
I expect it to never go past the false, because -e means "exit when a command has a non zero exit status". I am aware that -e has a tricky behavior, as explained in http://mywiki.wooledge.org/BashFAQ/105 but I would like to understand what happens in this specific case. I am using -e and it proved most helpful in many very simple scenarios. This scenario is a little more tricky but if it can be explained I may use -e instead adding || exit 1 after each line.
While trying to solve a similar problem, I read the man page for set -e:
-e errexit
Exit immediately if a simple command exits with a non-zero
status, unless the command that fails is part of an until or
while loop, part of an if statement, part of a && or || list,
or if the command's return status is being inverted using !.
Note the last bit about the return status being inverted. I guess the logical negation of function f must have an effect on the behaviour of set -e within that function.
Here's from the trap section of man bash (my emphasis):
If a sigspec is ERR, the command arg is executed whenever a simple command has a non-zero exit status, subject to the following conditions. The ERR trap is not executed if the failed command is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of a command executed in a && or || list, or if the command's return value is being inverted via !. These are the same conditions obeyed by the errexit option.
So ! command effectively disables the ERR trap inside command. I guess the reasoning is that you might not want a command exiting early if you are "kinda explicitly" checking the exit code after running it with !. Looks like bad design, since it can be used to effectively disable safeguards pretty much anywhere.
I can't precisely answer the why, but I found this snippet here:
On a slightly related note, by default bash takes the error status of the last item in a pipeline, which may not be what you want. For example, false | true will be considered to have succeeded. If you would like this to fail, then you can use set -o pipefail to make it fail.
And by updating your script as such:
function f() {
echo $SHELLOPTS
set -e
set -o pipefail
false
echo "$#"
}
It seems to behave as you expect.
So my best guess for "why" (probably the same as yours by now), is that the ! causes the function to be handled in some sort of "pipe" mode. Again... why ! means 'pipe', I guess I don't really know. Maybe a better bash expert can answer that part for us.

Resources