I was under impression that exit would terminate the current bash script no matter what, and had the following error handler at the top of my script:
function err {
printf "\e[31m$1\e[0m\n" 1>&2
exit 1
}
It worked like a charm for most cases until this line:
item=$(myfunc $1)
Normally, that line works fine, with the STDOUT of myfunc dumped into $item, as intended. The problem arises when myfunc throws an error, via the err function seen above. The $() ends up swallowing the non-zero return and guarding the exit from exiting the script itself. If I understand correctly, the problem is that $() actually spawns a new subshell (just like the deprecated backticks), but I know of no other way to capture the output of a function into a variable in bash that allows the exit to work.
I tried using set -e as well, and had no luck with that either. Can someone suggest how to build my error handler so that it exits the script even in these cases?
You can test the result of the assignment:
if item=$(myfunc "$1")
then : Function worked
else : Function failed
fi
This tests the exit status of the command run in the sub-shell that the $(…) uses.
Without actually using functions, you can experiment with:
$ if item=$(echo Hi; exit 1); then echo "$item - OK"; else echo "$item - OH"; fi
Hi - OH
$ if item=$(echo Hi; exit 0); then echo "$item - OK"; else echo "$item - OH"; fi
Hi - OK
$
Or, if functions are deemed crucial, then:
$ err() { exit $1; }
$ myfunc() { echo Mine; err $1; }
$ if item=$(myfunc 1); then echo "$item - OK"; else echo "$item - OH"; fi
Mine - OH
$ if item=$(myfunc 0); then echo "$item - OK"; else echo "$item - OH"; fi
Mine - OK
$
Tested using Bash 3.2.57 on Mac OS X 10.11 El Capitan.
for what it's worth you also can't use bash short circuit evaluation to test the exit status of the function. It tests the assignment to the variable instead.
eg.
while
[[ $(myfunc "$1") ]] && echo success || echo failure
or
[[ myfunc "$1" ]] && echo success || echo failure
are fairly common and might do what's expected,
[[ item=$(myfunc "$1") ]] && echo success || echo failure
Always returns success even if myfunc returns non zero. You have to use if then else to test the exit status.
Related
In Bash environment, I have a command, and I want to detect if it fails.
However it is not failing gracefully:
# ./program
do stuff1
do stuff2
error!
do stuff3
# echo $?
0
When it runs without errors (successful run), it returns with 0. When it runs into an error, it can either
return with 1, easily detectable
return with 0, but during run it prints some error messages
I want to use this program in a script with these goals:
I need the output to be printing to stdout normally (not at once after it finished!)
I need to catch the output's return value by $? or similar
I need to grep for "error" string in the output and set a variable in case of presence
Then I can evaluate by checking the return value and the "error" output.
However, if I add tee, it will ruin the return value.
I have tried $PIPESTATUS[0] and $PIPESTATUS[1], but it doesn't seem to work:
program | tee >(grep -i error)
Even if there is no error, $PIPESTATUS[1] always returns 0 (true), because the tee command was successful.
So what is the way to do this in bash?
#!/usr/bin/env bash
case $BASH_VERSION in
''|[0-3].*|4.[012].*) echo "ERROR: bash 4.3+ required" >2; exit 1;;
esac
exec {stdout_fd}>&1
if "$#" | tee "/dev/fd/$stdout_fd" | grep -i error >/dev/null; then
echo "Errors occurred (detected on stdout)" >&2
elif (( ${PIPESTATUS[0]} )); then
echo "Errors detected (via exit status)" >&2
else
echo "No errors occurred" >&2
fi
Tested as follows:
$ myfunc() { echo "This is an ERROR"; return 0; }; export -f myfunc
$ ./test-err myfunc
This is an ERROR
Errors occurred (detected on stdout)
$ myfunc() { echo "Everything is not so fine"; return 1; }; export -f myfunc
$ ./test-err myfunc
Everything is not so fine
Errors detected (via exit status)
$ myfunc() { echo "Everything is fine"; }; export -f myfunc
$ ./test-err myfunc
Everything is fine
No errors occurred
I have a bash script that runs a series of python scripts. It always runs all of them, but exits with a failure code if any script exited with a failure code. At least that's what I hope it does. Here it is ...
#!/bin/bash
res=0
for f in scripts/*.py
do
python "$f";
res=$(( $res | $? ))
done
exit $res
I'd like to run this as a bash command in the terminal, but i can't work out how to replace exit so that the command fails like a failed script, rather than exits the terminal. How do I do that?
Replace your last line exit $res with
$(exit ${res})
This exits the spawned subshell with the exit value of ${res} and because it is the last statement, this is also the exit value of your script.
Bash doesn't have a concept of anonymous functions (e.g. Go) which you can defined inline and get the return value, you need to do it explicitly. Wrap the whole code in a function say f()
f() {
local res=0
for f in scripts/*.py
do
python "$f";
res=$(( $res | $? ))
done
return $res
}
and use the exit code in command line.
if ! f; then
printf '%s\n' "one more python scripts failed"
fi
Is it true, that the value of the error code doesn't matter. Then I have another solution:
#!/bin/bash
total=0
errcount=0
for f in scripts/*.py
do
let total++
python "$f" || let errcount++
done
if ((errcount))
then
printf '%u of %u scripts failed\n' $errcount $total >&2
exit 1
fi
exit 0
#!/bin/bash
for f in scripts/*.py
do
python "$f" && echo "1" >> stack || echo "0" >> stack
done
[ $(grep -o stack) -eq 0 ] && rm -v ./stack && exit 1
I am rather stoned at the moment, so I apologise if I am misinterpreting, but I believe that this will do what you need. Every time the python script returns an error code of 0, (which means it works) a 1 is echoed into a stack file. At the end of the loop, the stack is checked for the presence of a single 0, and if it finds one, exits with an error code of 1, which is for general errors.
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
...
I'm writing a bash script and I'd like it to crash on the first error. However, I can't get it to do this in a specific circumstance I simplified below:
#!/bin/bash
set -Exu
bad_command() {
false
#exit 1
echo "NO!!"
}
(set -o pipefail; bad_command | cat ; echo "${PIPESTATUS[#]}; $?") || false
echo "NOO!!"
The expected behaviour would be a crash of the bad_command subshell, propagated to a crash of the () subshell, propagated to a crash of the outter shell. But none of those crash, and both NOs get printed(!?)
If I uncomment the exit 1 statement, then the NO is no longer printed, but NOO still is(!?)
I tried using set -e expicitly inside each of the 3 shells (first line in function, first statement after (, but there's no change.
Note: I need to execute the pipe inside the () subshell, because this is a simplification of a more elaborate script. Without the () subshell, everything works as expected, no NOs whatsoever with either false or exit 1.
This seems to be a bash or even POSIX bug: https://groups.google.com/forum/?fromgroups=#!topic/gnu.bash.bug/NCK_0GmIv2M
After hitting the same problem, I have found a workaround. Actually 3 depending on what you want to achieve.
First a small rewrite of the OP example code since handling the exit code requires some extra work down the line:
#! /bin/bash
set -eEu
bad_command_extra() {
return 42
}
bad_command() {
bad_command_extra
echo "NO!!"
}
if bad_command; then echo "NOO!!"; else echo "errexit worked: $?"; fi
If it's only needed to have the errexit work, following is sufficient in calling bad_command. The trick is to launch bad_command in the background:
(bad_command) &
bc_pid=$!
if wait $bc_pid; then echo "NOO!!"; else echo "errexit worked: $?"; fi
If you want to work with the output as well (similar to abc=$(bad_command)), capture it in a temporary file as usual:
tmp_out=$(mktemp)
tmp_err=$(mktemp)
(bad_command >$tmp_out 2>$tmp_err) &
bc_pid=$!
if wait $bc_pid; then echo "NOO!!"; else echo "errexit worked: $?"; fi
cat $tmp_out $tmp_err
rm -f $tmp_out $tmp_err
Finally, I found out in my testings that the wait command returned either 0 or 1 but not the actual exit code of bad_command (bash 4.3.42). This requires some more work:
tmp_out=$(mktemp)
tmp_err=$(mktemp)
tmp_exit=$(mktemp)
echo 0 > $tmp_exit
(
get_exit () {
echo $? > $tmp_exit
}
trap get_exit ERR
bad_command >$tmp_out 2>$tmp_err
) &
bc_pid=$!
bc_exit=$(cat $tmp_exit)
if wait $bc_pid
then echo "NOO!!"
else echo "errexit worked: $bc_exit"
fi
cat $tmp_out $tmp_err
rm -f $tmp_out $tmp_err $tmp_exit
For some strange reason, putting the if on one line as before got me exit code 0 in this case !
In perl, you can exit with an error msg with die "some msg". Is there an equivalent single command in bash? Right now, I'm achieving this using commands: echo "some msg" && exit 1
You can roll your own easily enough:
die() { echo "$*" 1>&2 ; exit 1; }
...
die "Kaboom"
Here's what I'm using. It's too small to put in a library so I must have typed it hundreds of times ...
warn () {
echo "$0:" "$#" >&2
}
die () {
rc=$1
shift
warn "$#"
exit $rc
}
Usage: die 127 "Syntax error"
This is a very close function to perl's "die" (but with function name):
function die
{
local message=$1
[ -z "$message" ] && message="Died"
echo "$message at ${BASH_SOURCE[1]}:${FUNCNAME[1]} line ${BASH_LINENO[0]}." >&2
exit 1
}
And bash way of dying if built-in function is failed (with function name)
function die
{
local message=$1
[ -z "$message" ] && message="Died"
echo "${BASH_SOURCE[1]}: line ${BASH_LINENO[0]}: ${FUNCNAME[1]}: $message." >&2
exit 1
}
So, Bash is keeping all needed info in several environment variables:
LINENO - current executed line number
FUNCNAME - call stack of functions, first element (index 0) is current function, second (index 1) is function that called current function
BASH_LINENO - call stack of line numbers, where corresponding FUNCNAME was called
BASH_SOURCE - array of source file, where corresponfing FUNCNAME is stored
Yep, that's pretty much how you do it.
You might use a semicolon or newline instead of &&, since you want to exit whether or not echo succeeds (though I'm not sure what would make it fail).
Programming in a shell means using lots of little commands (some built-in commands, some tiny programs) that do one thing well and connecting them with file redirection, exit code logic and other glue.
It may seem weird if you're used to languages where everything is done using functions or methods, but you get used to it.
# echo pass params and print them to a log file
wlog(){
# check terminal if exists echo
test -t 1 && echo "`date +%Y.%m.%d-%H:%M:%S` [$$] $*"
# check LogFile and
test -z $LogFile || {
echo "`date +%Y.%m.%d-%H:%M:%S` [$$] $*" >> $LogFile
} #eof test
}
# eof function wlog
# exit with passed status and message
Exit(){
ExitStatus=0
case $1 in
[0-9]) ExitStatus="$1"; shift 1;;
esac
Msg="$*"
test "$ExitStatus" = "0" || Msg=" ERROR: $Msg : $#"
wlog " $Msg"
exit $ExitStatus
}
#eof function Exit