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

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.

Related

Bash exit status not caught despite set -e and/or trap being active

Can someone explain the bash/set -e behaviour on the code snippet below ?
#!/bin/bash
# Comment if you want to test the trap only
set -e -o pipefail -u -E
# Comment if you want to test the set -e only
trap "echo ERROR CAUGHT; exit 12" ERR
function reproduce() {
# Trigger arithmetic error on purpose
a=$((1109962735 - hello=12272 + 1))
}
reproduce
# The script is expected to trigger the trap and/or activate the set -e. In both cases it should stop and exit here on error.
status_code=$?
echo "STATUS ${status_code}"
if [[ "${status_code}" != "0" ]];then
echo "FIXME: why was status code not caught by set -e ?"
echo "Executing false to prove set -e is still active"
false
# If the following is not executed then it proves -e is still active
echo "set -e not active !!!!!"
exit 2
fi
Here is what is obtained when executing it:
$ bash reproduce.sh
reproduce.sh: line 8: 1109962735 - hello=12272 + 1: attempted assignment to non-variable (error token is "=12272 + 1")
STATUS 1
FIXME: why was status code it not caught by set -e ?
Executing false to prove set -e is still active
ERROR CAUGHT
Checking the exit code
$ echo $?
1
Bash version
bash --version
GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu)
Reproduced as well with
GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
Additional notes, related to comments (thanks to all suggestions anyway):
Commenting trap does not change the weird behaviour observed
Removing set -e to only keep the trap does trigger the trap either
Let's simplify it; minimum amount of code required for
reproducing the issue you're dealing with is
set -e
: $((+)) # arithmetic expansion error
echo survived
According to the standard, this should never print survived, it says a POSIX shell running non-interactively shall immediately exit upon an expansion error. But seemingly Bash doesn't think so. Although this difference isn't explicitly documented in the man page, in description of POSIX mode it says
Non-interactive shells exit if a syntax error in an arithmetic
expansion results in an invalid expression.
We can say this means in its default operating mode, a non-interactive Bash session doesn't exit upon such error, but as you realized, it doesn't trigger errexit mechanism, or ERR trap either. Instead, it assigns a non-zero value to $? an moves on.
To overcome this and get the expected behavior, you should define reproduce as follows
function reproduce() (
# Trigger arithmetic error on purpose
a=$((1109962735 - hello=12272 + 1))
)
This way the expansion error will take place in a subshell and cause it to exit with a non-zero status, thus, errexit and trap will be able to catch it.
Upon dash-o's request, here is a version that sets a for the current execution environment when the expression is valid
function reproduce() {
if : $((expression)); then
a=$((expression))
fi
}
On surface, it looks as if bash will not trigger the trap on various SYNTAX errors. Only when it a command (external, built-in) is executed (and return non-zero), the ERR trap will be trigered.
From the man page:
If a sigspec is ERR, the command arg is executed whenever a pipeline
(which may consist of a single simple command), a list, or a compound
command returns a non-zero exit status, subject to the following
conditions ...
The ERR trap only applies to PIPELINE. If bash identifies a syntax error, it aborts before executing the pipeline, therefore NO trap. Even though he documentation for '-e' specify the same condition (if a pipeline ... exit with non-zero status), the observed behavior is different.
If you try other expansions - e.g. command expansion- trap is triggered, as there is pipeline execution:
a=$(bad-commands)
a=$([)
If use try various syntax errors in arithmetic expansion, trap not triggered - there was no pipeline.
a=$((2+))
a=$((2 #))
Also, other bash Syntax error do not trigger the trap: (), [[ ]].
I could not find a solution that does not require extensive changes to the source script. May be file a bug/feature request with the bash team ?

Using Set -e , how to make exception for one command

Using set -e in the shell script can let the process
exit immediately if one of the commands exits with a non-zero status
However, if I want to exclude one command, so that if that specific command exits with non-zero value, the process continues.
First rule, don't use set -e. Second rule, see rule 1. The implementation of set -e has strange behavior in edge cases, and has changed through various versions of shells. It is safest not to use it. However, you can suppress it simply by making your command part of a compound command. That is, instead of
cmd_which_may_fail
you can simply do:
cmd_which_may_fail || true
Now, if cmd_which_may_fail does fail, the script will not exit.
Note that is is sometimes tempting (and you will often see code that does this) to do something like cmd_which_may_fail || echo 'command failed!' >&2, but you really should not do that. Instead, allow cmd_which_may_fail to emit a reasonable error message (most common commands do, and if you have a command that doesn't emit good error messages you might want to reconsider using it at all), since that process will have context and can write a useful error message. Command failed is generally considered a useless error message.
I usually do
command ||:
the return status of : is zero. If command returns nonzero status, then : is executed, which returns zero status. If command returns a zero status, then : is not executed. In both cases - the return status is zero, ie. success.
I've seen:
command |:
Well... it's one character shorter, but binding stdout to : process seems like a waste of cpu time for me. The return status of the pipe is the return status of the last executed command, and it's :. But this can fail when set -o pipefail, so I wouldn't recommend it.
You can run a command in a subshell:
( set +e; command; )
this allows for more flexibility, but you need to add ( before the command and is far more typing.
( set +e; command; ) do not work
( set +e; command; true ) is the solution
Example:
#!/usr/bin/bash
set -e
( set +e ; killall DUMMY_PROCESS_ABC )
echo happy end
This will work:
#!/usr/bin/bash
set -e
echo $( set +e ; killall DUMMY_PROCESS_ABC )
echo happy end
this also work:
#!/usr/bin/bash
set -e
(set +e ; killall DUMMY_PROCESS_ABC ; true)
echo happy end

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

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.

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.

Bash conditional execution

I took this code from a script I found online :
[ $# = 0 ] && usage
If there are no parameters at the command line, then call the usage method (which print the help info).
The thing I don't understand is why does the script exits after calling usage? Shouldn't it simply continue to other code?
There are multiple ways this can happen:
The usage method has an exit command in it
The usage method has a return 1 command (or other non-zero value) and the script is invoked with -e flag, for example #!/bin/sh -e shebang
The usage method has an operation that fails and the script is invoked with -e flag
Maybe there are more ways that I don't recall now.
Personally, I always use exit 1 as the last command in a usage method, so the behavior seems all natural to me.
It would carry on unless 'usage' executes 'exit' command

Resources