Trap syntax issue in bash - bash

I intend to use trap to execute some clean up code in case of a failure. I have the following code, but it seems to be have some syntactical issues.
#!/bin/bash
set -e
function handle_error {
umount /mnt/chroot
losetup -d $LOOP_DEV1 $LOOP_DEV2
}
trap "{ echo \"$BASH_COMMAND failed with status code $?\"; handle_error; }" ERR
Does any one see an issue with the way the trap has been written. In case of an error the trap does get executed fine but it also throws another unwanted error message below.
/root/myscript.sh: line 60: } ERR with status code 0: command not found
##line 60 is that line of code that exited with a non zero status
How do I write it correctly to avoid the error message? Also what if I had to send arguments $LOOP_DEV1 and $LOOP_DEV2 from the main script to the trap and then to the handle_error function? Right now they are exported as environment variables in the main script. I did some search for trap examples but I couldn't get something similar.
EDIT
I changed the shebang from /bin/sh to /bin/bash. As /bin/sh was already symlinked to bash I did not expect unicorns nor did I see any.

That trap call is creating an interesting recursion, because $BASH_COMMAND (and $?) are being expanded when the trap command executes. However, $BASH_COMMAND at that point is the trap command itself, textually including $BASH_COMMAND (and some quotes and semicolons). Actually figuring out what the command to be executed when the trap fires is an interesting study, but it's not necessary to fix the problem, which you can do like this:
trap '{ echo "$BASH_COMMAND failed with status code $?"; handle_error; }' ERR
Note that replacing " with ' not only avoids immediate parameter expansion, it also avoids have to escape the inner "s.

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: Get message from error that was just trapped

Bash allows you to trap signals. Is there a way to actually get the message that printed immediately before (the cause of) a particular signal? It's ERR in particular I'm interested in. I'm aware that not all signals are associated with a message. Just wondering if bash itself sets a variable or something when it raises an error.
Sample code:
#!/bin/bash
# Running bash 5.0
handler () {
echo "Handling the error"
exit 0
}
trap handler ERR
notacommand
The code above will print:
./example.sh: line 11: notacommand: command not found
Is there a way to see this message inside the handler?
Edit: I know I could save all output to a file and then read the tail of that file when an error occurs. That seems problematic to me, as it's possible that the last message written to the file is something other than the error (especially if any subprocesses are started with & in the script). I was hoping that maybe bash sets a var or something, in the same way it sets $1, $?, $RANDOM, and others.
Redirect bash's stderr to a file:
#!/bin/bash
# Running bash 5.0
log="error.txt"
exec 2>"$log"
handler () {
echo "Handling this error: $(< "$log")"
exit 0
}
trap "handler" ERR
trap "rm $log" EXIT
notacommand
Output:
Handling this error: ./example.sh: line 15: notacommand: command not found

How to detect "Cannot fork" errors in bash?

For currently unknown reasons, one of our bash-scripts produces "Cannot fork" errors when running a simple line like:
myvar=`mycmd || echo "error"; exit 2`
Obviously the problem is that no new process can be created (forked) so that command fails.
However bash just ignores the error and continues in the script which caused unexpected problems.
As you can see, we already check for errors in the command itself, but the "Cannot fork" error appears before the command is even run.
Is there a way to catch that error and stop bash from execution?
There are actually several problems with this error check, that'll prevent it from properly handing any error, not just "Cannot fork" errors.
The first problem is that || has higher precedence than ;, so mycmd || echo "error"; exit 2 will run echo "error" only if mycmd fails, but it'll run exit 2 unconditionally, whether or not mycmd succeeds or fails.
In order to fix this, you should group the error handling commands with { }. Something like: mycmd || { echo "error"; exit 2; }. Note that a ; or line break before } is required, or the } will be treated as an argument to exit.
(BTW, I sometimes see this shorthanded as mycmd || echo "error" && exit 2. Don't do this. If the echo fails for some weird reason, it won't run the exit.)
Also all of this, the echo and the exit, is run in the subshell created by the backticks (or would be, if that subshell had forked successfully). That means the error message will get saved in myvar rather than printed (error messages should generally be sent to standard error, e.g. echo "error" >&2); and more importantly it'll be the subshell that exits, not the shell that's running the script. The main script will note that the subshell exited with an error... and blithely keep running. (Well, unless you have -e set, but that's a whole other ball of potential bugs.)
The solution to that is to put the || stuff outside the backticks (or `$( ), since that's generally preferred over backticks). That way it happens in the main shell, that's what prints the error, that's what exits if there's an error, etc. This should also solve the "Cannot fork" problem, although I haven't tested it.
So, with all these corrections, it should look something like this:
myvar=$(mycmd) || {
echo "error" >&2
exit 2
}
Oh, and as Charles Duffy pointed out in a comment, if you use something like local myvar=$(mycmd) or export myvar=$(mycmd), the local/export/whatever command will override the exit status from mycmd. If you need to do that, set the variable's properties separately from its value:
local myvar
myvar=$(mycmd) || {
...

Make 'trap ERR' working inside bash functions with 'return' (or in any subshells)

I'm trying to use trap ERR in my scripts. But:
function hmmm() {
trap 'exit 10' ERR
echo 12>/SOME/NONEXISTING/FILE
# some commands that must not be done if previous has failed
echo "THAT MUST NOT BE PRINTED" >&2
return 5
}
echo ok1
a=$(hmmm) || status="$?"
echo "function returns: $status"
Prints
ok1
test2.sh: line 3: /SOME/NONEXISTING/FILE: No such file or directory
THAT MUST NOT BE PRINTED
function returns: 5
The same behavior with any combination of set -e, set -E, trap on top level etc. I always need to handle return code of function - so, as I undertand, I can't use trap ERR in my scripts at all - I willn't ever working. Am I right, or there is working method to enable trap ERR inside functions, subshells and sourced libraries and keep constructions like
a=$( ...somecode ... ) || result="$?"
working? Or, that more important, in example above make bash exit on error inside function ALWAYS, not depending on calling method.
Added:
In fact, I want to know is there any working way to BE SURE that errors are trapped inside functions, subshells and sourced code. Because my functions and libraries can be used by other peoples - so, I can't control how this functions are called, and, to be honest, even if I can - I will never use some behavior inside function, that can be accidentally and silently changed from outside.
trap isn't executed since it's a part of ||.
Change:
a=$(hmmm) || status="$?"
to:
a=$(hmmm)
status="$?"
From bash manual:
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 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 using !. These are the same conditions obeyed by the errexit
(-e) option.

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 ?

Resources