How to do a try catch in bash scripts with curly braces? - bash

A recommended pattern that mimics try-catch in bash scripts is to use curly braces. This does not work as expected, though. The following script outputs A B 1. Why is that the case and how must the script be modified to output the intended A C 1?
#!/usr/bin/env bash
set -e
{
echo "A"
false
echo "B"
} || echo "C"
{
echo "1"
false
echo "2"
}

There are many issues with the 'set -e' - it does not work well in many error conditions. This has been covered in many posting, just search 'errexit bash'. For example: Bash subshell errexit semantics
At this time, there is no clean solution. However, there are good news. I'm working on a proposed change that will allow the above to work. See discussion in bash-bug archive: https://lists.gnu.org/archive/html/bug-bash/2022-07/index.html
And proposal: https://lists.gnu.org/archive/html/bug-bash/2022-07/msg00006.html
Final proposal for the 'errfail' can be found: https://lists.gnu.org/archive/html/bug-bash/2022-07/msg00055.html
I expect to have the solution submitted for review by the bash dev team this week. Hopefully it will get accepted into next bash release.
It will support new 'errfail' option
set -o errfail
{ echo BEFORE ; false ; echo AFTER ; } || echo "CATCH"
and will output: BEFORE CATCH
If you are looking for more fancy solution consider:
alias try=''
alias catch='||'
try {
echo BEFORE
false
echo AFTER
} catch { echo CATCH ; }

A work-around using 2 separate bash invokations:
bash -c 'set -e; echo "A"; false; echo "B"' || echo 'C'
bash -c 'set -e; echo "1"; false; echo "2"'
Or with intendations:
bash -c '
set -e
echo "A"
false
echo "B"
' || echo 'C'
bash -c '
set -e
echo "1"
false
echo "2"
'
Output:
A
C
1
Code Demo

Just use && and remove this dangerous hardly predictable set -e.
#!/bin/sh
{
echo "A" &&
false &&
echo "B"
} || echo "C"
{
echo "1" &&
false &&
echo "2"
}
Test run it
There is never anything wrong with being absolutely explicit in code.
Don't count on the implementation to handle even part of the logic.
This is what set -e is doing. Handles some logic for you, and it does it very differently from what you'd expect it to be most of the time.
So be always explicit and handle error conditions explicitly.

Related

How to return from sourced bash script automatically on any error?

I have a bash script which is only meant to used be when sourced.
I want to return from it automatically on any error, similar to what set -e does.
However setting set -e doesn't work for me because it will also exit the users shell.
Right now I'm handling returning manually like this command || return 1, for each command.
You can also use command || true or command || return.
If your requirement is something different, please update more precisely.
You can use trap. E.g.:
// foo.sh
function func() {
trap 'if [ $? -ne 0 ]; then echo "Trapped!"; return ; fi' DEBUG
echo 'foo'
find -name "foo" . 2> /dev/null
echo 'bar'
}
func
Two notes. First, the trap needs to be inside the function as shown. It won't work if it's just inside the script.
Two, there is a significant limitation. Even if you set the return to the trap (e.g., return 1), while func exists after the bad find command, $? is still zero, no matter what. I'm not sure if there's a way around that, so if it's important to preserve the exit value of the failed command, this may not work.
E.g., if you had:
func
func_return=$?
echo "return value is: $func_return"
func_return will always be zero. I've played around with trying to get the exit value of the failed command to pass out of the function trap and into the function exit value, but have not found a way to do it.
If you need to preserve the return value, you could update a global variable inside the debug trap.
If I understand well, you can set -e locally in each function.
cat sourced
f1 () {
local -
set -e
[ "$1" -eq "$1" ] 2> /dev/null && echo "$1"
}
cat script.sh
. sourced
param='bad'
ret=$(f1 "$param")
[ $? -eq 0 ] && echo "result = $ret" || \
echo "error in sourced file with param $param"
param=3
ret=$(f1 "$param")
[ $? -eq 0 ] && echo "result = $ret" || \
echo "error in sourced file with param $param"

Test if program exists in Bash Script - abbreviated version

I want to use the abbreviated if then else to determine if ccze exists before using it... and I just cannot get that first part right...
test() {
[ $(hash ccze) 2>/dev/null ] && echo "yes" || echo "no"
}
The above is just test.. what am I doing wrong? It does not matter if ccze exists or not - I'm getting "no"
testcmd () {
command -v "$1" >/dev/null
}
Using it:
if testcmd hello; then
echo 'hello is in the path'
else
echo 'hello is not in the path'
fi
or
testcmd man && echo yes || echo no
or you could put that into a function:
ptestcmd () {
testcmd "$1" && echo 'yes' || echo 'no'
}
This way you'll have one function that does testing and a separate function that does printing dependent on the test result. You may then use the one taht is appropriate for the situation (you may not always want output).
Suggestion #1 - Answer
some_function() {
hash ccze &> /dev/null && echo "yes" || echo "no"
}
Suggestion #2
Rename the function to something else because there is already the command test.
Don't override.
Suggestion #3
Remove the square brackets. Even if, in any case, you want to use square brackets, use double square brackets. Single square brackets are deprecated.
Brain Food
Shellcheck
Check for a command in PATH
Obsolete and Deprecated Bash Syntax
hash
test() #(not a good name)
{
if hash ccze 2>/dev/null; then
echo yes
else
echo no
fi
}
should do it.
command -v ccze >/dev/null is also usable and fairly portable (not POSIX but works with most shells).
Thanks for that but it was the one liner I wanted. I know that version works.
Meanwhile we figured it out...
test() { [ -x "$(command -v cckze)" ] && echo "yes" || echo "no"; }
Thanks anyway.

bash: is there a flag to make a function to return in case of error?

I am using "set -e" to make the script exit in case of an error, but I don't want it to exit if I have an error inside a function, I would like the function to return error instead
For example:
#!/bin/bash
set -e
func() {
echo 1
# code ...
cause_error
echo This should not print
}
func
if [ $? -ne 0 ]; then
echo I want this print
else
echo This should not print either
fi
The output of this script is:
$ /tmp/test.sh
1
But I would like it to be:
1
I want this print
Is this possible? Or do I have to test the exit status of every command executed inside the function?
Explicitly comparing $? is an antipattern, but in addition, getting rid of it will also bypass set -e because it does not fail when the failure happens in a condition.
The proper syntax for what you are trying to do, regardless of set -e, is
if func; then
echo I want this print
else
echo This should not print either
fi
The failure inside the function will cause the function to report an error, like you are saying in the prose description of your requirements, but that also means that the conditional will print This should not print either. If you don't want that, maybe edit your question to clarify this paradoxical requirement.
You can do this:
set -e
function foo() {
echo Foo
cause_error
}
echo Before
set +o errexit; foo; set -o errexit;
echo Will get here
foo
echo Wont get here
The behavior which you are asking is not possible. You want the function to return on error and the script to not exit, even with the set -e option. The requirement you are asking for the bash interpreter is to check exit status for each line and if it is inside a function return non-zero code else exit.
You cannot do the exact requirement which you are asking for. But you can choose to disable the set -e for the execution of the function. The way to disable set -e is to use set +e option or use && : trick
Here is example updated code using && : trick
#!/bin/bash
set -e
func() {
echo 1
# code ...
return 1
echo This should not print
}
func && :
if [ $? -ne 0 ]; then
echo I want this print
else
echo This should not print either
fi
Output:
1
I want this print
Credit: https://stackoverflow.com/a/27793459/2032943

Bash get exit status of command when 'set -e' is active?

I generally have -e set in my Bash scripts, but occasionally I would like to run a command and get the return value.
Without doing the set +e; some-command; res=$?; set -e dance, how can I do that?
From the bash manual:
The shell does not exit if the command that fails is [...] part of any command executed in a && or || list [...].
So, just do:
#!/bin/bash
set -eu
foo() {
# exit code will be 0, 1, or 2
return $(( RANDOM % 3 ))
}
ret=0
foo || ret=$?
echo "foo() exited with: $ret"
Example runs:
$ ./foo.sh
foo() exited with: 1
$ ./foo.sh
foo() exited with: 0
$ ./foo.sh
foo() exited with: 2
This is the canonical way of doing it.
as an alternative
ans=0
some-command || ans=$?
Maybe try running the commands in question in a subshell, like this?
res=$(some-command > /dev/null; echo $?)
Given behavior of shell described at this question it's possible to use following construct:
#!/bin/sh
set -e
{ custom_command; rc=$?; } || :
echo $rc
Another option is to use simple if. It is a bit longer, but fully supported by bash, i.e. that the command can return non-zero value, but the script doesn't exit even with set -e. See it in this simple script:
#! /bin/bash -eu
f () {
return 2
}
if f;then
echo Command succeeded
else
echo Command failed, returned: $?
fi
echo Script still continues.
When we run it, we can see that script still continues after non-zero return code:
$ ./test.sh
Command failed, returned: 2
Script still continues.
Use a wrapper function to execute your commands:
function __e {
set +e
"$#"
__r=$?
set -e
}
__e yourcommand arg1 arg2
And use $__r instead of $?:
if [[ __r -eq 0 ]]; then
echo "success"
else
echo "failed"
fi
Another method to call commands in a pipe, only that you have to quote the pipe. This does a safe eval.
function __p {
set +e
local __A=() __I
for (( __I = 1; __I <= $#; ++__I )); do
if [[ "${!__I}" == '|' ]]; then
__A+=('|')
else
__A+=("\"\$$__I\"")
fi
done
eval "${__A[#]}"
__r=$?
set -e
}
Example:
__p echo abc '|' grep abc
And I actually prefer this syntax:
__p echo abc :: grep abc
Which I could do with
...
if [[ ${!__I} == '::' ]]; then
...

Can I pass an arbitrary block of commands to a bash function?

I am working on a bash script where I need to conditionally execute some things if a particular file exists. This is happening multiple times, so I abstracted the following function:
function conditional-do {
if [ -f $1 ]
then
echo "Doing stuff"
$2
else
echo "File doesn't exist!"
end
}
Now, when I want to execute this, I do something like:
function exec-stuff {
echo "do some command"
echo "do another command"
}
conditional-do /path/to/file exec-stuff
The problem is, I am bothered that I am defining 2 things: the function of a group of commands to execute, and then invoking my first function.
I would like to pass this block of commands (often 2 or more) directly to "conditional-do" in a clean manner, but I have no idea how this is doable (or if it is even possible)... does anyone have any ideas?
Note, I need it to be a readable solution... otherwise I would rather stick with what I have.
This should be readable to most C programmers:
function file_exists {
if ( [ -e $1 ] ) then
echo "Doing stuff"
else
echo "File $1 doesn't exist"
false
fi
}
file_exists filename && (
echo "Do your stuff..."
)
or the one-liner
file_exists filename && echo "Do your stuff..."
Now, if you really want the code to be run from the function, this is how you can do that:
function file_exists {
if ( [ -e $1 ] ) then
echo "Doing stuff"
shift
$*
else
echo "File $1 doesn't exist"
false
fi
}
file_exists filename echo "Do your stuff..."
I don't like that solution though, because you will eventually end up doing escaping of the command string.
EDIT: Changed "eval $*" to $ *. Eval is not required, actually. As is common with bash scripts, it was written when I had had a couple of beers ;-)
One (possibly-hack) solution is to store the separate functions as separate scripts altogether.
The cannonical answer:
[ -f $filename ] && echo "it has worked!"
or you can wrap it up if you really want to:
function file-exists {
[ "$1" ] && [ -f $1 ]
}
file-exists $filename && echo "It has worked"

Resources