Ok I'm kind of new to bash scripting [the advanced stuff] and I need a little help. I don't even know exactly how to phrase this so I'll just explain what I am doing and what I need to know about it.
in my script I run a ./configure and I need to be able to catch if there was an error in the configure and react accordingly within the bash script.
the code is:
function dobuild {
echo -e "\e[1;35;40mExecuting Bootstrap and Configure\e[0m"
cd /devel/xbmc
if [ $Debug = "1" ];
then
#either outputs to screen or nulls output
./bootstrap >/dev/null
/usr/bin/auto-apt run ./configure --prefix=/usr --enable-gl --enable-vdpau --enable-crystalhd --enable-rtmp --enable-libbluray >/dev/null
else
./bootstrap
/usr/bin/auto-apt run ./configure --prefix=/usr --enable-gl --enable-vdpau --enable-crystalhd --enable-rtmp --enable-libbluray
fi
}
and say the configure returns an error 1 or 2 how do I trap that and act on it?
TIA
After the execution of every shell command it's return value, a number between 0 and 255, is available in the shell variable ?. You can get the value of this variable by prefixing it with the $ operator.
You have to be a little careful with ?, because it is reset by every command, even a test. For example:
some_command
if (( $? != 0 ))
then
echo "Error detected! $?" >&2
fi
Gives: Error detected! 0 because ? was reset by the test condition. It is probably best to store ? in another variable if you are going to use it later, which includes doing more than one test on it.
To do a numeric test in bash use the (( ... )) numeric test construct:
some_command
result=$?
if (( $result == 0 ))
then
echo "it worked!"
elif (( $result == 1 ))
then
echo "Error 1 detected!" >&2
elif (( $result == 2 ))
then
echo "Error 2 detected!" >&2
else
echo "Some other error was detected: $result" >&2
fi
Alternatively use a case statement.
After the execution of a command, the returned value is stored in the shell variable $?. So you would have to match that with the return values of success and failure
if [ $? == 1 ]
then
#do something
else
#do something else
fi
The other answers about $? are great (though be careful about assuming values other than 0 and not-0 - different commands. or different versions of the same command may fail with different values), but if you just need to act on success or failure immediately, you can simplify things:
if command ; then
# success code here
else
# failure code here
fi
Or if you only want to act on failure, here's a hack for older shells (the colon is a null command but it satisfies the then clause):
if command ; then :
else
# failure code here
fi
But in modern shells like bash this is better:
if ! command ; then # use the ! (not) operator
# failure code here
fi
And, if you only need to do simple things, you can use the "short circuit" operators:
command1 && command2_if_command1_succeeds
command1 || command2_if_command1_fails
Those only work for single commands, stringing more && and || on them doesn't do what you might think in most cases so most people avoid that. However, you can do multiple commands if you group them:
command1 && { command2; command3; command4; }
That can get hard to read so it's best to keep it simple if you use it all:
command1 || { echo "Error, command1 failed!" >&2; exit 1; }
Related
I am trying to detect whenever the following script (random_fail.sh) fails --which happens rarely-- by running it inside a while loop in the second script (catch_error.sh):
#!/usr/bin/env bash
# random_fail.sh
n=$(( RANDOM % 100 ))
if [[ n -eq 42 ]]; then
echo "Something went wrong"
>&2 echo "The error was using magic numbers"
exit 1
fi
echo "Everything went according to plan"
#!/usr/bin/env bash
# catch_error.sh
count=0 # The number of times before failing
error=0 # assuming everything initially ran fine
while [ "$error" != 1 ]; do
# running till non-zero exit
# writing the error code from the radom_fail script into /tmp/error
bash ./random_fail.sh 1>/tmp/msg 2>/tmp/error
# reading from the file, assuming 0 written inside most of the times
error="$(cat /tmp/error)"
echo "$error"
# updating the count
count=$((count + 1))
done
echo "random_fail.sh failed!: $(cat /tmp/msg)"
echo "Error code: $(cat /tmp/error)"
echo "Ran ${count} times, before failing"
I was expecting that the catch_error.sh will read from /tmp/error and come out of the loop once a particular run of random_fail.sh exits with 1.
Instead, the catch script seems to be running forever. I think this is because the error code is not being redirected to the /tmp/error file at all.
Please help.
You aren't catching the error code in the proper/usual manner. Also, no need to prefix the execution with the "bash" command, when it already contains the shebang. Lastly, curious why you don't simply use #!/bin/bash instead of #!/usr/bin/env bash .
Your second script should be modified to look like this:
#!/usr/bin/env bash
# catch_error.sh
count=0 # The number of times before failing
error=0 # assuming everything initially ran fine
while [ "$error" != 1 ]; do
# running till non-zero exit
# writing the error code from the radom_fail script into /tmp/error
./random_fail.sh 1>/tmp/msg 2>/tmp/error
error=$?
echo "$error"
# updating the count
count=$((count + 1))
done
echo "random_fail.sh failed!: $(cat /tmp/msg)"
echo "Error code: ${error}"
echo "Ran ${count} times, before failing"
[ "$error" != 1 ] is true if random_fail.sh prints a lone digit 1 to stderr. As long as this doesn't happen, your script will loop. You could instead test whether there has been written anything to stderr. There are several possibilities to achieve this:
printf '' >/tmp/error
while [[ ! -s /tmp/error ]]
or
error=
while (( $#error == 0 ))
or
error=
while [[ -z $error ]]
/tmp/error will always be either empty or will contain the line "The error was using magic numbers". It will never contain 0 or 1. If you want to know the exit value of the script, just check it directly:
if ./random_fail.sh 1>/tmp/msg 2>/tmp/error; then error=1; else error=0; fi
Or, you can do:
./random_fail.sh 1>/tmp/msg 2>/tmp/error
error=$?
But don't do either of those. Just do:
while ./random_fail.sh; do ...; done
As long as random_fail.sh (please read https://www.talisman.org/~erlkonig/documents/commandname-extensions-considered-harmful/ and stop naming your scripts with a .sh suffix) returns 0, the loop body will be entered. When it returns non-zero, the loop terminates.
This question already has answers here:
Why should there be spaces around '[' and ']' in Bash?
(5 answers)
Closed 7 months ago.
I need to add shell if-else statement to my Makefile, but the if expression always evaluates to false.
For example the next code:
if [1 -eq 1]; then echo "yes"; else echo "no"
prints "no"
The only code that evaluated to true was:
if true; then echo "yes"; else echo "no"
Why all expressions in the code (except for "true") evaluates to false? :(
I would really appreciate any help
** Please note - the statements work correctly when run from Shell
The code snippet from the original Makefile:
SIMULATION_RUN_CMD = rm -rf $(TEST_DIR)/* && mkdir -p $(TEST_DIR) && cd $(TEST_DIR) && (cp -rf $(VIVADO_PROJ)/$(PROJECT)/export_for_sim/$(SIMULATOR)/{*.mem,.mif,design.dat,nocattrs.dat,cpm_data_sim.cdo} $(TEST_DIR) || true) && \
ln -sf $(TEST_DIR)/simulation.log $(RUN_DIR)/simulation.log && \
(timeout $(SIM_TIMEOUT) ${SIM_DIR}/simv +UVM_TESTNAME=$(UVM_TESTNAME) $(SIM_FLAGS) -l $(TEST_DIR)/simulation.log -do $(DO_FILE) ; \
if [1 -eq 1]; then echo "if statement yes " >> $(TEST_DIR)/simulation.log; else echo "if statement no " >> $(TEST_DIR)/simulation.log; fi \
|| true) && \
$(MODEL_POST_SIM_ACIONS)
$(SIMULATION_RUN_TAR):
#echo -e "Make Command: $(SIMULATION_RUN_CMD)" $(PRINT_OUTPUT)
($(SIMULATION_RUN_CMD)) $(PRINT_OUTPUT)
First, you have a syntax error in your command. If you type that exactly into bash you'll get an error:
[[1: command not found
You need spaces after the [[ and before the ]] tokens:
if [[ 1 -eq 1 ]]; then echo "yes"; else echo "no"
Second, the reason it doesn't work when run from make is that make doesn't invoke bash. Make invokes the POSIX standard shell /bin/sh. If you do this you'll see the same behavior you get with make:
$ /bin/sh -c 'if [[ 1 -eq 1 ]]; then echo yes; else echo no; fi'
/bin/sh: 1: [[: not found
no
The [[ operator is a bash-specific feature. If you want to write this using POSIX features you should use:
$ /bin/sh -c 'if [ 1 -eq 1 ]; then echo yes; else echo no; fi'
yes
If you really want make to invoke bash as its shell instead of sh, add this to your makefile:
SHELL := /bin/bash
Of course then your makefile will not work on any system that doesn't have /bin/bash available.
ETA
After seeing the very much more complicated, but still not complete, code you added, I will say the following:
As I said above, you have an error in your script. if [1 -eq 1] is completely illegal. You must have spaces after [ and before ]. Again, if you run this yourself at the shell prompt you will get the same failure. It has nothing to do with make.
Because of this error, the if-statement will ALWAYS fail and so this will ALWAYS run the "else" command and print "no".
You say you don't see any error message. I can't explain that, except that you run this recipe this way:
($(SIMULATION_RUN_CMD)) $(PRINT_OUTPUT)
You don't tell use what the value of the PRINT_OUTPUT variable is, so I can only assume that it throws away stderr into the bit-bucket (or possibly, both stdout and stderr). If you didn't do that, so you could see the output, you'd see the error message being printed. Or maybe that redirects to a log file in which case, you can look there for the message.
One thing I don't get with this operators is when I use two of them in sequence.
What I mean by that:
% true && echo "problem"
problem
% echo $?
0
So far, so good. true returns "error" (exit status 1) and echo "problem" returns 0, so logical AND operation result must be 0.
% true && echo "problem" || echo "exit"
problem
OK, that's a surprise: since true && echo "problem" results in 0, || should also evaluate echo "exit", since after all right-hand operand of || might be true and so the result of this logical OR might be true.
Now:
% true && echo "problem" && echo "exit"
problem
exit
This is also surprising: after all since true && echo "problem" returns zero, the lazy && operator should not evaluate echo "exit" since the result of logical AND must be zero anyway.
Why is the behavior of last two examples opposite to what I intuitively expect?
P.S. This is opposite of Python behavior:
% python
>>> def pp():
... print "problem"
...
>>> def pe():
... print "exit"
>>> True and pp() and pe()
problem
>>> True and pp() or pe()
problem
exit
You're misunderstanding success/failure statuses and also what && and || mean in this context in bash.
An exit status of success is 0, while failure is non-zero so when you say true returns "error" (exit status 1), no it doesn't. Also, nothing in shell "returns" anything. Scripts and functions produce output and have an exit status - using the word "return" leads to confusion over which of those 2 separate things you mean. For example if we define this function:
foo() {
echo "hello"
return 7
}
and then use it to populate a variable:
var=$(foo)
$ echo "$var"
hello
did foo() "return" hello or did foo() "return" 7? The best answer is no, it didn't "return" either - it output hello and exited with status 7.
Although there's a poorly-named "return" keyword there what that REALLY is producing is an exit status for the function, the same as if you had a shell script that was just:
echo "hello"
exit 7
and you can see that if you test it ($? always holds the exit status of the most recently run command):
foo() {
echo "hello"
return 7
}
$ foo
hello
$ echo "$?"
7
I assume the shell creators chose "return" for the function keyword because "exit" already meant "exit from the running process" but IMHO that made things confusing, though I don't have a better suggestion and even if I did that ship has sailed long ago. If you read return 7 as set the exit status to 7 then return from the function without assuming the function is actually "returning" anything then you'd be right.
Note also that the function is outputting "hello" - that's also not a "return" but if you use it as var=$(foo) then var ends up containing hello so then some people incorrectly refer to that as a "return" too since in other languages like C if you wrote var=foo():
char *foo() {
return "hello"
}
var=foo()
then var would contain the argument that was given to return in the function but that is just not the same semantics as shell where, unlike the similar C code above, var=$(foo) sets var to the output from foo(), not the "return" (actually exit status) from foo().
So - the function above doesn't actually "return" anything, it outputs hello and exits with status 7.
So here's what your command line true && echo "problem" || echo "exit" actually does:
true = output nothing and exit with status 0 (success)
echo "problem" - output problem and exit with status 0 (success)
echo "exit" - output exit and exit with status 0 (success)
Now, what do && and || mean? What they really are is shorthand for if statements:
&& foo = if the previously run command exited success then execute foo
|| foo = if the previously run command exited failure then execute foo
So a command line like:
cmdA && cmdB || cmdC
in terms of success/fail status should be read as:
cmdA
ret=$?
if (( ret == 0 )); then
cmdB
ret=$?
fi
if (( ret != 0 )); then
cmdC
ret=$?
fi
( exit "$ret" )
We need the ret temp variable because the if itself has an exit status that'd overwrite $?. So cmdC will get called if cmdA exits with a failure status, but it'll also get called if cmdA succeeded and then cmdB exited with a failure status. At the end of cmdA && cmdB || cmdC the exit status as stored in $? will simply be the exit status of whichever command ran last, it will not, for example, be the product of boolean arithmetic on all of the exit statuses of all the commands that ran as apparently suggested in the question might be the case.
Note also that what that should NOT be read as is what you may intuitively have expected if you thought of && ... || ... as a ternary expression, which trips many people up, especially since that || is typically an error leg whose incorrect placement may escape your code inspectors/testers notice:
cmdA
if (( $? == 0 )); then
cmdB
else
cmdC
fi
Given the above, here's what your command lines actually mean:
true && echo "problem" || echo "exit"
true
ret=$?
if (( ret == 0 )); then
echo "problem"
ret=$?
fi
if (( ret != 0 )); then
echo "exit"
ret=$?
fi
( exit "$ret" )
true && echo "problem" && echo "exit"
true
ret=$?
if (( ret == 0 )); then
echo "problem"
ret=$?
fi
if (( ret == 0 )); then
echo "exit"
ret=$?
fi
( exit "$ret" )
and if what you WANTED to have happen instead of "2" above was actually:
true
if (( $? == 0 )); then
echo "problem"
else
echo "exit"
fi
then you should write that code or similar instead of using &&s and ||s (e.g. as 1 line you could write if true; then echo "problem"; else echo "exit"; fi) so you don't get unexpected output if you reach the echo "problem" leg and it fails for some reason thereby causing you to afterwards fall into the echo "exit" leg (unlikely with just echo but very possible with other commands).
getAnimalFolder() {
local animal=""
if [[ ${ANIMAL} == "lion" ]]; then
animal = "./animals/lion/"
elif [[ ${ANIMAL} == "tiger" ]]; then
animal = "./animals/tiger/"
elif [[ ${ANIMAL} == "cheetah" ]]; then
animal = "./animals/cheetah/"
else
echo "inavalid animal"
exit 1`enter code here`
fi
echo $animal
}
result=$(getAnimalFolder)
cd ../result/age/
If the animal is not lion, tiger or cheetah, the function returns invalid animal and hence gives an error 'No such file or directory', instead I need to do an exit with code = 1. Hence I went for the second option -
if [[ ${ANIMAL} != "lion" && ${ANIMAL} != "tiger" && ${ANIMAL} != "cheetah" ]]; then
echo "Invalid animal"
exit 1
fi
getAnimalFolder() {
local animal=""
if [[ ${ANIMAL} == "lion" ]]; then
animal = "./animals/lion/"
elif [[ ${ANIMAL} == "tiger" ]]; then
animal = "./animals/tiger/"
elif [[ ${ANIMAL} == "cheetah" ]]; then
animal = "./animals/cheetah/"
fi
echo $animal
}
result=$(getAnimalFolder)
cd ../result/age/
This looks like a fix to my problem but if in the future more animals are added, then I need to remember to make changes in 2 places for every new animal added. So is there a better way to do this?
There are a number of problems here; #1 and #3 are the ones that directly address your question.
When a function/command/whatever may need to print both regular output (e.g. the path to an animal directory) and error/status output (e.g. "inavalid animal"), it should send the regular output to standard output (aka stdout aka FD #1, the default), and error/status output to standard error (aka stderr aka FD #2), like this:
echo "Invalid animal" >&2 # This is sent to stderr
Generally, functions should return rather than exiting. If a function does exit, it exits the entire shell, but in this case the function is running in a subshell due to $( ), so it only exits that. Using return avoids this inconsistency.
When a function/command/whatever may fail, you should check its exit status; there are a number of ways to do this:
if result=$(getAnimalFolder); then
: # command succeeded!
else
echo "OMG it failed!" >&2
exit 1
fi
or
result=$(getAnimalFolder)
if [ $? -ne 0 ]; then # $? is the status of the last command
echo "OMG it failed!" >&2
exit 1
fi
or
result=$(getAnimalFolder) || {
echo "OMG it failed!" >&2
exit 1
}
I use the last form a lot, since there are a lot of steps in a script might fail, and having a simple & compact way to include the failure handing code makes the overall script more readable.
In general, functions should take their input as arguments rather than via global variables. So in the function you'd refer to $1 instead of $ANIMAL, and you'd run the function with something like:
result=$(getAnimalFolder "$ANIMAL")
There are also a number of basic syntax errors and bad scripting practices in the script: don't put spaces around the equal sign in assignments; do put double-quotes around variable references; don't use all-caps variable names (to avoid conflicts with the many all-caps variables that have special meanings); do check for errors on cd commands (if they fail, the rest of the script will run in the wrong place); and when comparing a single variable against a bunch of values, use case instead of a bunch of if elseif etc.
shellcheck.net is good at recognizing many of these common mistakes. Strongly recommended.
Here's what I get with all fixes in place:
#!/bin/bash
getAnimalFolder() {
local animalname=$1
local animaldir=""
case "$animalname" in
lion ) animaldir="./animals/lion/" ;;
tiger ) animaldir="./animals/tiger/" ;;
cheetah ) animaldir="./animals/cheetah/" ;;
* )
echo "Invalid animal: $animalname" >&2
return 1 ;;
esac
echo "$animaldir"
}
read -p "Give me an animal: " animalname
result=$(getAnimalFolder "$animalname") || {
exit 1 # Appropriate error message has already been printed
}
cd "../$result/age/" || {
echo "Error changing directory to ../$result/age/ -- aborting" >&2
exit 1
}
Put the animals in an array:
#!/bin/bash
animals=(lion tiger cheetah)
getAnimalFolder() {
local i
for i in "${animals[#]}"; do
if [ "$i" == "${1}" ] ; then
animaldir="./animals/${1}"
return 0
fi
done
exit 1
}
read -rp "Give me an animal: " animalname
getAnimalFolder "${animalname}"
echo "Animaldir=${animaldir}"
EDIT:
I did not use the construction result=$(getAnimalFolder), assuming the OP wants to use the new path once. When needed, the function can be changed into
echo "./animals/${1}"
When the function is called with result=$(getAnimalFolder), OP needs to look at the line
cd ../result/age/
Is resulta fixed path or does he want to use the path from the function:
cd ../${result}/age/
I am running club.sh
inside club.sh script i am running below scripts.
test1.sh
test2.sh
test3.sh
my concern is it should run one by one and if test1 fails it will not run test2.sh and if test2.sh fails it willnot run test3.sh
how can we check? can any one suggest any idea it would be very helpful.
Thanks,
Two approaches -
First, you can examine the exit code of each of your inner scripts (test1.sh, test2.sh, ...) and decide whether to continue accordingly -
$? Will return the exit code of the previous command. It will be 0 (zero) if the script exited without an error. Anything that is not 0 can be considered a failure. So you could so something like this -
./test1.sh # execute script
if [[ $? != 0 ]]; then exit; fi # check return value, exit if not 0
Alternatively, you could use the && bash operator which will only execute subsequent commands if the previous one passed -
./test1.sh && ./test2.sh && test3.sh
Only if test1.sh returns an exit code of 0 (zero) will test2.sh execute and the same goes for test3.sh.
The first approach is good if you need to do some logging or cleanup between executing your scripts, but if you are only concerned that the execution should not continue if there was a failure then the && method would be they way I recommend.
Here is a related post dealing with the meaning behind &&
The returned value of the execution of the first command/script is stored in $? so using this value you can check if your command was successfully executed.
Try this:
bash test1.sh
if [ $? -eq 0 ]; then # if script succeeded
bash test2.sh
else
echo "script failed"
fi
If you want to exit your script whenever a command fails, you just add at the beginning of your script set -e.
#!/bin/bash
set -e
echo hello
ls /root/lalala
echo world
Otherwise, you have two options.
The first one is to use &&. For instance:
echo hello && ls /some_inexistant_directory && echo world
The second one is to check the return value after each command:
#!/bin/bash
echo toto
if [ "$?" != "0" ]; then
exit 1
fi
ls /root
if [ "$?" != "0" ]; then
exit 1
fi
echo world
if [ "$?" != "0" ]; then
exit 1
fi
You just have to put the below at the begging of the script:
#!/bin/bash -e