Ignoring specific errors in a shell script - bash

I have a small snippet of a shell script which has the potential to throw many errors. I have the script currently set to globally stop on all errors. However i would like for this small sub-section is slightly different.
Here is the snippet:
recover database using backup controlfile until cancel || true;
auto
I'm expecting this to eventually throw a "file not found" error. However i would like to continue executing on this error. For any other error i would like the script to stop.
What would be the best method of achieving this?
Bash Version 3.00.16

In order to cause bash to ignore errors for specific commands you can say:
some-arbitrary-command || true
This would make the script continue. For example, if you have the following script:
$ cat foo
set -e
echo 1
some-arbitrary-command || true
echo 2
Executing it would return:
$ bash foo
1
z: line 3: some-arbitrary-command: command not found
2
In the absence of || true in the command line, it'd have produced:
$ bash foo
1
z: line 3: some-arbitrary-command: command not found
Quote from the manual:
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 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 status is being
inverted with !. A trap on ERR, if set, is executed before the shell
exits.
EDIT: In order to change the behaviour such that in the execution should continue only if executing some-arbitrary-command returned file not found as part of the error, you can say:
[[ $(some-arbitrary-command 2>&1) =~ "file not found" ]]
As an example, execute the following (no file named MissingFile.txt exists):
$ cat foo
#!/bin/bash
set -u
set -e
foo() {
rm MissingFile.txt
}
echo 1
[[ $(foo 2>&1) =~ "No such file" ]]
echo 2
$(foo)
echo 3
This produces the following output:
$ bash foo
1
2
rm: cannot remove `MissingFile.txt': No such file or directory
Note that echo 2 was executed but echo 3 wasn't.

Use:
command || :
: is a bash built-in that always returns success. And, as discussed above, || short-circuits so the RHS is only executed if the LHS fails (returns non-zero).
The above suggestions to use 'true' will also work, but are inefficient as 'true' is an external program.

Related

How to stop bash function on error when called with shell operators && or ||

I use set -e in bash scripts to stop on error. When I want to handle an error I typically use the || operator (i.e. which git || echo "not found")
However, when I use the || operator after a function call set -e seems to be ignored inside the function.
For example:
#!/bin/bash
set -e
test() {
false
echo "This should not be printed!"
}
test || echo "test failed"
echo "Done"
Desired result is:
test failed
Done
but the actual result is
This should not be printed!
Done
When I just call test the script aborts on the false statement and prints nothing
The only (ugly) workaround I could come up with is to replace the call and error handling with:
set +e
(set -e; test)
if [[ $? -ne 0 ]]; then echo "test failed"; fi
set -e
Is there a better way to get the desired behavior ([Edit] preferably without modifying the function)?
(Tested with git-bash on Windows and GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)
If you use set -e you have the be cautionus inside your function just as well as outside.
With set -e the shell basically translates
foo || bar
into
(set +e; foo) || bar
precisely because it needs to see the exit code from the end of the function.
The fix is to be similarly cautious inside the function definition.
# Renamed the function to as to avoid shadowing the test built-in
nst () {
false || return
echo "This should not be printed!"
}
Have a look at the man page of bash. It says about set -e (emphasis by me):
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.
If a compound command or shell function executes in a context where -e is
being ignored, 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. 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.
In the following example, the second call to the function test will terminate the script, as it is the last command in a || list.
#!/bin/bash
set -e
test()
{
false
echo "This should not be printed!"
}
test || echo "test failed"
false || test
echo "Will not be printed"

"set -e" and "test" in shell

In shell scripts set -e is often used to make them more robust by stopping the script when some of the commands executed from the script exits with non-zero exit code. I am confused about "set -e" and "test" commands, the result is contrary to what I want.
#!/bin/bash
set -e
a=10
b=9
#test $a -lt $b && false
#test $a -gt $b && false
echo "111"
The real result is:
if a > b, print nothing
if a < b, print 111.
but i don't think so, i think the result is nothing whatever happened.
If you want to exit when a is greater than b, just write
#!/bin/bash
set -e
a=10
b=9
test $a -le $b
echo "111"
If test fails, set -e will cause the script to exit; if it succeeds, the script continues to echo. However, there are many pitfalls to using set -e; I recommend just doing your own error handling.
#!/bin/bash
abort () { printf '%s\n' >&2; exit 1; }
a=10
b=9
test "$a" -le "$b" || abort "$a is greater than $b"
echo "111"
The way you wrote your script means, it will exit without printing anything in all situation BUT if a and b are equals.
If you try with a = 9 and b = 9, you will see the 111 output.
You can easily all the behaviour, using the -x option of GNU/Bash.
The explanation of the exit, is your use of -e option; you can read this in GNU/Bash manual:
Exit immediately if a pipeline (see Pipelines), which may consist of a single simple command (see Simple Commands), a list (see Lists), or a compound command (see Compound Commands) returns 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 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 status 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), and may cause subshells to exit before executing all the commands in the subshell.
If a compound command or shell function executes in a context where -e is being ignored, 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. 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.
Thanks everyone, i find the answer which is
SHELL Exit immediately if a pipeline (which may consist of a single simple com-mand), 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 !.

"set -e" does not cause a code block to exit if in a conditional

I'm trying to figure out why the bail-on-error behavior -e does not kick in if the failing block is in a conditional chain:
#!/bin/bash
set -e
{ echo "First"
ls blat
echo "(this should not print)"
}
Prints out:
First
ls: cannot access 'blat': No such file or directory
which is correct.
Whereas the following:
#!/bin/bash
set -e
{ echo "First"
ls blat
echo "(this should not print)"
} || echo "Encountered an error"
Prints out:
First
ls: cannot access 'blat': No such file or directory
(this should not print)
I expect Encountered an error to be printed instead of this should not print
Can anybody explain to me the reason for the discrepancy?
From the documentation (emphasis mine):
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 !.
Because ls is part of the {...} compound executed as a non-final part of the || list, the script does not exit when ls has a non-zero exit status.

Evaluating bash "&&" exit codes behaviour

We had a recent experience with bash that even we found a solution, it keeps twisting my mind. How does bash evaluates the && expression in terms of return codes?
Executing this script, that should fail because myrandomcommand does not exist:
#!/bin/bash
set -e
echo "foo"
myrandomcommand
echo "bar"
The result is the expected one:
~ > bash foo.sh
foo
foo.sh: line 6: myrandomcommand: command not found
[exited with 127]
~ > echo $?
127
But changing slightly the code using the && expression:
#!/bin/bash
set -e
echo "foo"
myrandomcommand && ls
echo "bar"
The ls statement is not executed (since the first statement fails and does not evaluate the second statement), but the script behaves very different:
~ > bash foo.sh
foo
foo.sh: line 6: myrandomcommand: command not found
bar # ('bar' is printed now)
~ > echo $?
0
We found out that using the expression between parenthesis (myrandomcommand && ls) it works as expected (like the first example), but I would like to know why.
You can read in the man pages of bash:
-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.

Echoing the last command run in Bash?

I am trying to echo the last command run inside a bash script. I found a way to do it with some history,tail,head,sed which works fine when commands represent a specific line in my script from a parser standpoint. However under some circumstances I don't get the expected output, for instance when the command is inserted inside a case statement:
The script:
#!/bin/bash
set -o history
date
last=$(echo `history |tail -n2 |head -n1` | sed 's/[0-9]* //')
echo "last command is [$last]"
case "1" in
"1")
date
last=$(echo `history |tail -n2 |head -n1` | sed 's/[0-9]* //')
echo "last command is [$last]"
;;
esac
The output:
Tue May 24 12:36:04 CEST 2011
last command is [date]
Tue May 24 12:36:04 CEST 2011
last command is [echo "last command is [$last]"]
[Q] Can someone help me find a way to echo the last run command regardless of how/where this command is called within the bash script?
My answer
Despite the much appreciated contributions from my fellow SO'ers, I opted for writing a run function - which runs all its parameters as a single command and display the command and its error code when it fails - with the following benefits:
-I only need to prepend the commands I want to check with run which keeps them on one line and doesn't affect the conciseness of my script
-Whenever the script fails on one of these commands, the last output line of my script is a message that clearly displays which command fails along with its exit code, which makes debugging easier
Example script:
#!/bin/bash
die() { echo >&2 -e "\nERROR: $#\n"; exit 1; }
run() { "$#"; code=$?; [ $code -ne 0 ] && die "command [$*] failed with error code $code"; }
case "1" in
"1")
run ls /opt
run ls /wrong-dir
;;
esac
The output:
$ ./test.sh
apacheds google iptables
ls: cannot access /wrong-dir: No such file or directory
ERROR: command [ls /wrong-dir] failed with error code 2
I tested various commands with multiple arguments, bash variables as arguments, quoted arguments... and the run function didn't break them. The only issue I found so far is to run an echo which breaks but I do not plan to check my echos anyway.
Bash has built in features to access the last command executed. But that's the last whole command (e.g. the whole case command), not individual simple commands like you originally requested.
!:0 = the name of command executed.
!:1 = the first parameter of the previous command
!:4 = the fourth parameter of the previous command
!:* = all of the parameters of the previous command
!^ = the first parameter of the previous command (same as !:1)
!$ = the final parameter of the previous command
!:-3 = all parameters in range 0-3 (inclusive)
!:2-5 = all parameters in range 2-5 (inclusive)
!! = the previous command line
etc.
So, the simplest answer to the question is, in fact:
echo !!
...alternatively:
echo "Last command run was ["!:0"] with arguments ["!:*"]"
Try it yourself!
echo this is a test
echo !!
In a script, history expansion is turned off by default, you need to enable it with
set -o history -o histexpand
The command history is an interactive feature. Only complete commands are entered in the history. For example, the case construct is entered as a whole, when the shell has finished parsing it. Neither looking up the history with the history built-in (nor printing it through shell expansion (!:p)) does what you seem to want, which is to print invocations of simple commands.
The DEBUG trap lets you execute a command right before any simple command execution. A string version of the command to execute (with words separated by spaces) is available in the BASH_COMMAND variable.
trap 'previous_command=$this_command; this_command=$BASH_COMMAND' DEBUG
…
echo "last command is $previous_command"
Note that previous_command will change every time you run a command, so save it to a variable in order to use it. If you want to know the previous command's return status as well, save both in a single command.
cmd=$previous_command ret=$?
if [ $ret -ne 0 ]; then echo "$cmd failed with error code $ret"; fi
Furthermore, if you only want to abort on a failed commands, use set -e to make your script exit on the first failed command. You can display the last command from the EXIT trap.
set -e
trap 'echo "exit $? due to $previous_command"' EXIT
Note that if you're trying to trace your script to see what it's doing, forget all this and use set -x.
After reading the answer from Gilles, I decided to see if the $BASH_COMMAND var was also available (and the desired value) in an EXIT trap - and it is!
So, the following bash script works as expected:
#!/bin/bash
exit_trap () {
local lc="$BASH_COMMAND" rc=$?
echo "Command [$lc] exited with code [$rc]"
}
trap exit_trap EXIT
set -e
echo "foo"
false 12345
echo "bar"
The output is
foo
Command [false 12345] exited with code [1]
bar is never printed because set -e causes bash to exit the script when a command fails and the false command always fails (by definition). The 12345 passed to false is just there to show that the arguments to the failed command are captured as well (the false command ignores any arguments passed to it)
I was able to achieve this by using set -x in the main script (which makes the script print out every command that is executed) and writing a wrapper script which just shows the last line of output generated by set -x.
This is the main script:
#!/bin/bash
set -x
echo some command here
echo last command
And this is the wrapper script:
#!/bin/sh
./test.sh 2>&1 | grep '^\+' | tail -n 1 | sed -e 's/^\+ //'
Running the wrapper script produces this as output:
echo last command
history | tail -2 | head -1 | cut -c8-
tail -2 returns the last two command lines from history
head -1 returns just first line
cut -c8- returns just command line, removing PID and spaces.
There is a racecondition between the last command ($_) and last error ( $?) variables. If you try to store one of them in an own variable, both encountered new values already because of the set command. Actually, last command hasn't got any value at all in this case.
Here is what i did to store (nearly) both informations in own variables, so my bash script can determine if there was any error AND setting the title with the last run command:
# This construct is needed, because of a racecondition when trying to obtain
# both of last command and error. With this the information of last error is
# implied by the corresponding case while command is retrieved.
if [[ "${?}" == 0 && "${_}" != "" ]] ; then
# Last command MUST be retrieved first.
LASTCOMMAND="${_}" ;
RETURNSTATUS='✓' ;
elif [[ "${?}" == 0 && "${_}" == "" ]] ; then
LASTCOMMAND='unknown' ;
RETURNSTATUS='✓' ;
elif [[ "${?}" != 0 && "${_}" != "" ]] ; then
# Last command MUST be retrieved first.
LASTCOMMAND="${_}" ;
RETURNSTATUS='✗' ;
# Fixme: "$?" not changing state until command executed.
elif [[ "${?}" != 0 && "${_}" == "" ]] ; then
LASTCOMMAND='unknown' ;
RETURNSTATUS='✗' ;
# Fixme: "$?" not changing state until command executed.
fi
This script will retain the information, if an error occured and will obtain the last run command. Because of the racecondition i can not store the actual value. Besides, most commands actually don't even care for error noumbers, they just return something different from '0'. You'll notice that, if you use the errono extention of bash.
It should be possible with something like a "intern" script for bash, like in bash extention, but i'm not familiar with something like that and it wouldn't be compatible as well.
CORRECTION
I didn't think, that it was possible to retrieve both variables at the same time. Although i like the style of the code, i assumed it would be interpreted as two commands. This was wrong, so my answer devides down to:
# Because of a racecondition, both MUST be retrieved at the same time.
declare RETURNSTATUS="${?}" LASTCOMMAND="${_}" ;
if [[ "${RETURNSTATUS}" == 0 ]] ; then
declare RETURNSYMBOL='✓' ;
else
declare RETURNSYMBOL='✗' ;
fi
Although my post might not get any positive rating, i solved my problem myself, finally.
And this seems appropriate regarding the intial post. :)

Resources