I have a script which quite often fails. It is crucial that the correct exit code is passed on.
This code works as expected:
#!/usr/bin/env bash
function my_function {
echo "Important output"
exit 1
}
function cleanup {
MY_EXIT_CODE=$?
echo "MY_EXIT_CODE: ${MY_EXIT_CODE}"
}
trap cleanup EXIT
my_function
MY_EXIT_CODE is 1 as expected and running echo $? after the script gives me 1 as well (as expected)
However, I need to get the complete output of my_function both to a variable and the console output. In order to do so, as advised in this answer (which seems itself based on this answer) I changed my code into
#!/usr/bin/env bash
function my_function {
echo "Important output"
exit 1
}
function cleanup {
MY_EXIT_CODE=$?
echo "MY_EXIT_CODE: ${MY_EXIT_CODE}"
}
trap cleanup EXIT
exec 5>&1
FF=$(my_function|tee /dev/fd/5)
And now the exit code is wrong. Is it 0 while it should be 1. I know this is somewhat connected to subshell handling but I couldn't figure out how to solve it.
And now the exit code is wrong. Is it 0 while it should be 1
No, your assumption is wrong. Assuming tee succeeded, the exit code should be 0. From the posix shell manual:
If the reserved word ! does not precede the pipeline, the exit status shall be the exit status of the last command specified in the pipeline.
The "last command" in a pipeline is the rightmost command. Because in your case tee exits with 0, the exit status of FF=$(.... | tee) is zero.
how to solve it.
That depends on the behavior you want to achieve. Usually, in bash you may just set -o pipefail to always catch errors.
It seems that simply stating
set -o pipefail
at the beginning of the script file did the trick.
Yes it's subshell and pipe, try like this
FF=$(my_function|tee /dev/fd/5; exit $PIPESTATUS)
Related
I have a script like this:
#!/usr/bin/bash
COMMAND='some_command >> some_log_file 2>&1'
until $COMMAND; do
echo "some_command crashed with exit code $?. Respawning.." >&2
sleep 1
done
(I got the until ... done bit from https://stackoverflow.com/a/697064/88821 , FWIW, but changed it a bit.)
While some_command is being run, the problem is that the output is not going to some_log_file. Instead, it goes to the stdout of the shell from which I ran this wrapper script. How can I get the output to go to some_log_file while keeping the entire command (including redirects) in the variable COMMAND? The idea is to use this script as a more general wrapper script that can be used with other programs. (COMMAND is actually being passed as an argument into the script).
You're passing >> some_log_file 2>&1 as arguments to some_command, rather than honoring them as redirections. This happens because parsing shell syntax (such as redirections) happens before parameter expansions are performed (the point in processing where $foo is replaced with the contents of the relevant variable). That's actually desirable behavior -- it would be impossible to write code in shell handling untrusted data otherwise.
Don't store code in strings. You can include it literally:
until some_command >> some_log_file 2>&1; do
echo "some_command crashed with exit code $?. Respawning.." >&2
sleep 1
done
...or you can store it in a function:
mycode() { some_command >> some_log_file 2>&1; }
until mycode; do
echo "some_command crashed with exit code $?. Respawning.." >&2
sleep 1
done
One way is
#!/usr/bin/bash
COMMAND='some_command >> some_log_file 2>&1'
until bash -c "$COMMAND"; do
echo "some_command crashed with exit code $?. Respawning.." >&2
sleep 1
done
The problem with your approach is that it does not evaluate the variable as bash code, but just as a series of literal strings. It's like how putting "1+1" in a Java string will never cause 2 to be printed without invoking a Java interpreter:
String cmd="1+1";
System.out.println(cmd); // Prints 1+1
System.out.println(1+1); // Prints 2
Consider the following simple bash script:
#!/bin/bash
set -eou pipefail
IFS=$'\n\t'
gen() {
seq 0 3
}
for c in $(gen); do
echo c
done
echo "finish"
It has set -e set, so when something fails, it is supposed to just exit with a non-zero exit code.
It will call gen function and prints the output of seq 0 3, will print finish and exit with code=0.
If I modify gen to fail, say, by invoking seqqq command (which doesn't exist) instead:
$ ./script.sh; echo $?
./script.sh: line 6: seqqq: command not found
finish
0
It prints the error message from the sub-shell, it doesn't fail right away and exit (with a non-zero code) immediately as set -e is supposed to do; it keeps executing and exits with code=0.
What is the explanation behind this? Note, if I just replace with my for loop as this, it fails as expected:
#!/bin/bash
set -eou pipefail
IFS=$'\n\t'
gen() {
seqqq 0 3
}
gen # <-- fails and exits here with code=127
echo "finish"
It appears that set -e is ineffective in the context of command substitution, as in:
for c in $(gen); do
echo c
done
However, set -e does work in the context of straight function invocation, as in:
gen
It is hard or near impossible to write reliable shell scripts with set -e. Use explicit error handling instead. You can read more about this here: BashFAQ/105
Editing this post, original is at bottom beneath the "Thanks!"
command='a.out arg1 arg2 &'
eval ${command}
if [ $? -ne 0 ]; then
printf "Command \'${command}\' failed\n"
exit 1
fi
wait
Here is a test script that demonstrates the problem, which I oversimplified
in the original post. Notice the ampersand in line 2 and the wait command.
These more faithfully represent my script. In case it matters, the ampersand
is sometimes there and sometimes not, its presence is determined by a user-
specified flag that indicates whether or not to background a long arithmetic
calculation. And, also in case it matters, I'm actually backgrounding many
(twelve) processes, i.e., ${command[0..11]}. I want the script to die if any
fail. I use 'wait' to synchronize the successful return of all processes.
Happy (sort of) to use another approach but this almost works.
The ampersand (for backgrounding) seems to cause the problem.
When ${command} omits the ampersand, the script runs as expected:
The executable a.out is not found, a complaint to that effect is issued,
and $? is non-zero so the host script exits. When ${command} includes
the ampersand, the same complaint is issued but $? is zero so the
script continues to execute. I want the script to die immediately when
a.out fails but how do I obtain the non-zero return value from a
backgrounded process?
Thanks!
(original post):
I have a bash script that uses commands of the form
eval ${command}
if [ $? -ne 0 ]; then
printf "Command ${command} failed"
exit 1
fi
where ${command} is a string of words, e.g., "a.out arg1 ... argn".
The problem is that the return code from eval (i.e., $?) is always
zero even when ${command} fails. Removing the "eval" from the above
snippet allows the correct return code ($?) to be returned and thus
halt the script. I need to keep my command string in a variable
(${command}) in order to manipulate it elsewhere, and simply running
${command} without the eval doesn't work well for other reasons. How do I catch the
correct return code when using eval?
Thanks!
Charlie
The ampersand (for backgrounding) seems to cause the problem.
That is correct.
The shell cannot know a command's exit code until the command completes. When you put a command in background, the shell does not wait for completion. Hence, it cannot know the (future) return status of the command in background.
This is documented in man bash:
If a command is terminated by the control operator &, the shell
executes the command in the background in a subshell. The shell does
not wait for the command to finish, and the return status is 0.
In other words, the return code after putting a command in background is always 0 because the shell cannot predict the future return code of a command that has not yet completed.
If you want to find the return status of commands in the background, you need to use the wait command.
Examples
The command false always sets a return status of 1:
$ false ; echo status=$?
status=1
Observe, though, what happens if we background it:
$ false & echo status=$?
[1] 4051
status=0
The status is 0 because the command was put in background and the shell cannot predict its future exit code. If we wait a few moments, we will see:
$
[1]+ Exit 1 false
Here, the shell is notifying us that the brackground task completed and its return status was just as it should be: 1.
In the above, we did not use eval. If we do, nothing changes:
$ eval 'false &' ; echo status=$?
[1] 4094
status=0
$
[1]+ Exit 1 false
If you do want the return status of a backgrounded command, use wait. For example, this shows how to capture the return status of false:
$ false & wait $!; echo status=$?
[1] 4613
[1]+ Exit 1 false
status=1
From the man page on my system:
eval [arg ...] The args are read and concatenated together into a single command. This command is then read and executed by the shell, and its exit status is returned as the value of eval. If there are no args, or only null arguments, eval returns 0.
If your system documentation is 'the same', then, most likely, whatever commands you are running are the problem, i.e. 'a.out' is returning '0' on exit instead of a non-zero value. Add appropriate 'exit return code' to your compiled program.
You might also try using $() which will 'run' your binary instead of 'evaluating' it..., i.e.
STATUS=$(a.out var var var)
As long on only one 'command' is in the stream, then the value of $? is the 'exit code'; otherwise, $? is the return code for the last command in a multi-command 'pipe'...
:)
Dale
If I have the following command
do_stuff -pram somepram
and then typically I do the following to get the status
if [ $OUT -eq 0 ]; then
# do some stuff
else
# do other stuff
fi
However, I need to write the output of my do_stuff command to a file.
So the command looks like
do_stuff -pram somepram 2>&1 | tee someFile
Which means the result of $? will be the exit code on tee and no the command proceeding it.
Is there another way to write to a file that doesn't interfere with this or allows me to obtain the exit code
The command exit status is unrelated to the output it produces. If the command is well behaved, then you can write:
if do_stuff -pram somepram > someFile
then
: OK - it worked
else
: Oops - it failed
fi
The error messages go to standard error still. The standard output goes to the file. You can even check whether the output file is empty on success, treating an empty file as a failure, but if the command is well-behaved, that won't be necessary.
If the command is ill-behaved, then all bets are off. If at all possible, file bugs and get it fixed so that it returns a reliable exit status.
If you want to keep using tee, you can do this at the top of your Bash script:
set -o pipefail
Then the exit status of the pipeline will be non-zero if any of its stages is non-zero.
I'm writing a script in Bash to test some code. However, it seems silly to run the tests if compiling the code fails in the first place, in which case I'll just abort the tests.
Is there a way I can do this without wrapping the entire script inside of a while loop and using breaks? Something like a dun dun dun goto?
Try this statement:
exit 1
Replace 1 with appropriate error codes. See also Exit Codes With Special Meanings.
Use set -e
#!/bin/bash
set -e
/bin/command-that-fails
/bin/command-that-fails2
The script will terminate after the first line that fails (returns nonzero exit code). In this case, command-that-fails2 will not run.
If you were to check the return status of every single command, your script would look like this:
#!/bin/bash
# I'm assuming you're using make
cd /project-dir
make
if [[ $? -ne 0 ]] ; then
exit 1
fi
cd /project-dir2
make
if [[ $? -ne 0 ]] ; then
exit 1
fi
With set -e it would look like:
#!/bin/bash
set -e
cd /project-dir
make
cd /project-dir2
make
Any command that fails will cause the entire script to fail and return an exit status you can check with $?. If your script is very long or you're building a lot of stuff it's going to get pretty ugly if you add return status checks everywhere.
A SysOps guy once taught me the Three-Fingered Claw technique:
yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$#" || die "cannot $*"; }
These functions are *NIX OS and shell flavor-robust. Put them at the beginning of your script (bash or otherwise), try() your statement and code on.
Explanation
(based on flying sheep comment).
yell: print the script name and all arguments to stderr:
$0 is the path to the script ;
$* are all arguments.
>&2 means > redirect stdout to & pipe 2. pipe 1 would be stdout itself.
die does the same as yell, but exits with a non-0 exit status, which means “fail”.
try uses the || (boolean OR), which only evaluates the right side if the left one failed.
$# is all arguments again, but different.
If you will invoke the script with source, you can use return <x> where <x> will be the script exit status (use a non-zero value for error or false). But if you invoke an executable script (i.e., directly with its filename), the return statement will result in a complain (error message "return: can only `return' from a function or sourced script").
If exit <x> is used instead, when the script is invoked with source, it will result in exiting the shell that started the script, but an executable script will just terminate, as expected.
To handle either case in the same script, you can use
return <x> 2> /dev/null || exit <x>
This will handle whichever invocation may be suitable. That is assuming you will use this statement at the script's top level. I would advise against directly exiting the script from within a function.
Note: <x> is supposed to be just a number.
I often include a function called run() to handle errors. Every call I want to make is passed to this function so the entire script exits when a failure is hit. The advantage of this over the set -e solution is that the script doesn't exit silently when a line fails, and can tell you what the problem is. In the following example, the 3rd line is not executed because the script exits at the call to false.
function run() {
cmd_output=$(eval $1)
return_value=$?
if [ $return_value != 0 ]; then
echo "Command $1 failed"
exit -1
else
echo "output: $cmd_output"
echo "Command succeeded."
fi
return $return_value
}
run "date"
run "false"
run "date"
Instead of if construct, you can leverage the short-circuit evaluation:
#!/usr/bin/env bash
echo $[1+1]
echo $[2/0] # division by 0 but execution of script proceeds
echo $[3+1]
(echo $[4/0]) || exit $? # script halted with code 1 returned from `echo`
echo $[5+1]
Note the pair of parentheses which is necessary because of priority of alternation operator. $? is a special variable set to exit code of most recently called command.
I have the same question but cannot ask it because it would be a duplicate.
The accepted answer, using exit, does not work when the script is a bit more complicated. If you use a background process to check for the condition, exit only exits that process, as it runs in a sub-shell. To kill the script, you have to explicitly kill it (at least that is the only way I know).
Here is a little script on how to do it:
#!/bin/bash
boom() {
while true; do sleep 1.2; echo boom; done
}
f() {
echo Hello
N=0
while
((N++ <10))
do
sleep 1
echo $N
# ((N > 5)) && exit 4 # does not work
((N > 5)) && { kill -9 $$; exit 5; } # works
done
}
boom &
f &
while true; do sleep 0.5; echo beep; done
This is a better answer but still incomplete a I really don't know how to get rid of the boom part.
You can close your program by program name on follow way:
for soft exit do
pkill -9 -x programname # Replace "programmname" by your programme
for hard exit do
pkill -15 -x programname # Replace "programmname" by your programme
If you like to know how to evaluate condition for closing a program, you need to customize your question.
#!/bin/bash -x
# exit and report the failure if any command fails
exit_trap () { # ---- (1)
local lc="$BASH_COMMAND" rc=$?
echo "Command [$lc] exited with code [$rc]"
}
trap exit_trap EXIT # ---- (2)
set -e # ---- (3)
Explanation:
This question is also about how to write clean code. Let's divide the above script into multiple parts:
Part - 1:
exit_trap is a function that gets called when any step failed and captures the last executed step using $BASH_COMMAND and captures the return code of that step. This is the function that can be used for any clean-up, similar to shutdownhooks
The command currently being executed or about to be executed, unless the shell is executing a command as the result of a trap, in which case it is the command executing at the time of the trap.
Doc.
Part - 2:
trap [action] [signal]
Register the trap action (here exit_trap function) in case of EXIT signal.
Part - 3:
Exit immediately if a sequence of one or more 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.
Doc.
Part - 4:
You can create a common.sh file and source it in all of your scripts.
source common.sh