Bash: Get message from error that was just trapped - bash

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

Related

Is Exception handling possible in Unix shell script, which includes calling another scripts internally

I have a Scenario where in I need to fetch some data by triggering another external bash file from my shell script. If I end up with any error output from external bash, My shell script should handle and should go through the fall back approach. But I am actually facing issue with that external bash file, Wherein bash returns (exit 1) in failure cases, which causes my script also to exit and never executing fall back approach. Can anyone guide how to handle the exit from external bash and run my fall back approach.
Not sure if this works in sh, but it works in bash.
I made a try / except tool out of this, but it will work here too I believe.
#! /bin/bash
try() {
exec 2> /dev/null
#direct stderr out to /dev/null
#main block
input_function="$1"
#fallback code
catch_function="$3"
#open a sub shell
(
#tell it to exit upon encountering an error
set -e
#main block
"$#"
)
#if exit code of above is > 0, then run fallback code
if [ "$?" != 0 ]; then
$catch_function
else
#success, it ran with no errors
test
fi
#put stderr back into stdout
exec 2> /dev/tty
}
An example of using this would be:
try [function 1] except [function 2]
Function 1 would be main block of code, and 2 would be fallback function/block of code.
Your first function could be:
run() {
/path/to/external/script
}
And your second can be whatever you want to fall back on.
Hope this helps.

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 ?

Bash script does not quit on first "exit" call when calling the problematic function using $(func)

Sorry I cannot give a clear title for what's happening but here is the simplified problem code.
#!/bin/bash
# get the absolute path of .conf directory
get_conf_dir() {
local path=$(some_command) || { echo "please install some_command first."; exit 100; }
echo "$path"
}
# process the configuration
read_conf() {
local conf_path="$(get_conf_dir)/foo.conf"
[ -r "$conf_path" ] || { echo "conf file not found"; exit 200; }
# more code ...
}
read_conf
So basically here what I am trying to do is, reading a simple configuration file in bash script, and I have some trouble in error handling.
The some_command is a command which comes from a 3rd party library (i.e. greadlink from coreutils), required for obtain the path.
When running the code above, I expect it outputs "command not found" because that's where the FIRST error occurs, but actually it always prints "conf file not found".
I am very confused about such behavior, and I think BASH probably intent to handle thing like this but I don't know why. And most importantly, how to fix it?
Any idea would be greatly appreciated.
Do you see your please install some_command first message anywhere? Is it in $conf_path from the local conf_path="$(get_conf_dir)/foo.conf" line? Do you have a $conf_path value of please install some_command first/foo.conf? Which then fails the -r test?
No, you don't. (But feel free to echo the value of $conf_path in that exit 200 block to confirm this fact.) (Also Error messages should, in general, get sent to standard error and not standard output anyway. So they should be echo "..." 2>&1. That way they don't be caught by the normal command substitution at all.)
The reason you don't is because that exit 100 block is never happening.
You can see this with set -x at the top of your script also. Go try it.
See what I mean?
The reason it isn't happening is that the failure return of some_command is being swallowed by the local path=$(some_command) assignment statement.
Try running this command:
f() { local a=$(false); echo "Returned: $?"; }; f
Do you expect to see Returned: 1? You might but you won't see that.
What you will see is Returned: 0.
Now try either of these versions:
f() { a=$(false); echo "Returned: $?"; }; f
f() { local a; a=$(false); echo "Returned: $?"; }; f
Get the output you expected in the first place?
Right. local and export and declare and typeset are statements on their own. They have their own return values. They ignore (and replace) the return value of the commands that execute in their contexts.
The solution to your problem is to split the local path and path=$(some_command) statements.
http://www.shellcheck.net/ catches this (and many other common errors). You should make it your friend.
In addition to the above (if you've managed to follow along this far) even with the changes mentioned so far your exit 100 won't exit the main script since it will only exit the sub-shell spawned by the command substitution in the assignment.
If you want that exit 100 to exit your script then you either need to notice and re-exit with it (check for get_conf_dir failure after the conf_path assignment and exit with the previous exit code) or drop the get_conf_dir function itself and just do that inline in read_conf.

Trap syntax issue in 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.

Conditional actions based on error in shell script

I have a bash script that watch's a directory for uploads. When it sees a xml file in the directory. The script takes the file and calls a java command line program that using xslt transforms the file. I want to mv the file to a different directory (errors) if the script or java command line program throws a error during processing. Then email me the error.
I was going to put
exec 2> mail -s 'Statement Error Processing'
at the top of the script to catch output for stderr. But that doesn't seem very elegant and doesn't move the file in question.
You can trap error conditions in bash (any statement that returns a non-zero condition that isn't handled inline):
trap func ERR
Then in func (a function you define) you can do whatever you need to clean up.
For example:
#!/bin/bash
function foo() {
echo "cleaning up"
}
trap foo ERR
echo "about to execute false"
false
echo "exiting"
Running that will result in:
./foo.sh
about to execute false
cleaning up
exiting
You need two things here. First, determine if the program fails. This is usually known by the return value of the program's main() function. If the program returns anything different to 0, there is an error.
In any case, the second thing you have to do is to capture the standard error output to a file to later mail it. So:
if ! program inputfile 2> errorfile ; then
mv inputfile error_directory
mail -s "error" < errorfile
fi
rm errorfile
You have to check, though, if your program follows this convention to signal an error.

Resources