I have a function which I use in my shell script and based on a certain condition I'd like to call "exit 0" in that function so that the entire script exits. But for some reason the program still runs after calling exit 0 in the function. Why?
check()
{
printf "Would you like to try again?\n"
read S
if [ "$S" = "y" ]
then
./myprog
else
exit 0
fi
}
And I call it like this:
if test $WRONG -eq 7
then
printf "Sorry\n"
check
fi
What you have works for me:
$ cat test-shell-exit
#!/bin/sh
check()
{
printf "Would you like to try again?\n"
read S
if [ "$S" = "y" ]
then
echo Try again
else
echo Done
exit 0
fi
}
echo Before
check
echo After
$ ./test-shell-exit
Before
Would you like to try again?
y
Try again
After
$ ./test-shell-exit
Before
Would you like to try again?
n
Done
Could you try this test case and update your answer with any differences from it? It appears the problem you're running into is caused by something you haven't mentioned.
Update: Example of using a loop instead of calling your program again:
$ cat test-shell-exit
#!/bin/sh
check()
{
printf "Would you like to try again?\n"
read S
if [ "$S" = "y" ]
then
echo Try again
else
echo Done
exit 0
fi
}
while true; do
echo Before
check
echo After
done
$ ./test-shell-exit
Before
Would you like to try again?
y
Try again
After
Before
Would you like to try again?
y
Try again
After
Before
Would you like to try again?
n
Done
Without any code to go by, I'd guess that your function is invoking a sub-shell, and it's that sub-shell which is exit()ing, and not the parent shell running the main script.
I was expecting to find your code exiting a subshell, but I don't see any reason for that, the code looks OK, and it seems to run as expected for me.
So my guess is that either you are not reading a y when you think you are not, or you are really doing the exit 0 and have just missed noticing it in your test results.
When you say "the program still runs" do you mean "the shell program still runs after calling the check procedure"?
I agree with digitalross that something outside of what you're showing us is causing this behavior. exit 0 WILL exit the current process. so somehow check() is running in a separate child process that is exiting. Do you have code outside of this call that is invoking?
BTW, which OS and shell are you running?
You can force a parent to exit via something like (syntax depends on your shell)
check() || exit $?
Bill
Related
I have the following bash script:
echo one
echo two
cd x
echo three
which fails at the 3rd line as there is no directory named x. However, after running the script, when I do $?, 0 is returned, even though the script has an error. How do I detect whether the script ran successfully or not?
Check the condition of directory existence in the script statements:
[ -d x ] && cd x || { echo "no such directory"; exit 1; }
Or put set -e after shebang line:
#!/bin/bash
set -e
echo one
echo two
cd x
echo three
You should end with an exit statement
echo one
echo two
cd x
exitCode=$?
echo three
exit $exitCode;
Then
./myscript
echo $?
1
I have searched all over with no clear answer to this. Put simply it doesn't appear to be a native feature in bash. So I will give you the hard way.
To make a .sh script with multiple commands and you don't know if any will error but you want to check if at least one has. You would need to put a $? at the end of literally every command and redirect it to a text file. Once it's in the text file you could format it like.
Command1 = 0 Success.
Command2 = 127 Fail.
Or you could just add the numbers to the file run it through some kind of calculator to add everything together and if the output is greater than zero then the command at some point failed. But this won't be overly useful if you want the exact number and there are more than one failure.
UPDATED - This is the best way I could find.
You can put this at the top of your script file to catch any errors and exit if it fails.
set -euo pipefail
Feel free to read the manual pages.
I am currently using something like this:
(
false
true
) && echo "OK" || echo "FAILED";
And it doesn't work. I would like the subshell to exit with an error if something fails (false in this case). Currently it only fails if the last command fails.
It should only exit out of the current subshell and not the whole script.
I am giving this script to people and I don't want them to see all the output but still give them some kind of response if the script was successful or not.
Edit: The commands inside the subshell above are only an example. I would like to run multiple commands inside a subshell without checking the return value after each command. Something like set -e for subshells.
Edit2: I tried adding set -e inside a subshell. Maybe I did something wrong but it didn't change the behavior of my script. It didn't stop execution or exit out of the subshell with a non-0 code.
(
set -e
false
echo "test"
) && echo "OK" || echo "FAILED";
First prints test and then OK. It should print FAILED because of false.
This effect of bash set -e inside a conditional expression like foo || true, is known and considered a problem. I think it is good reason to hate both set -e and shell scripting in general.
http://david.rothlis.net/shell-set-e/
http://fvue.nl/wiki/Bash:_Error_handling
The first link makes the following suggestion. It looks good in small examples, but maybe less clear in the real world.
Do your own error checking by stringing together a series of commands
with “&&” like this:
mkdir abc &&
cd abc &&
do_something_else &&
last_thing ||
{ echo error >&2; exit 1; }
A few important notes: You don’t need a trailing backslash. You don’t
indent the following line. You must stick to 80 chars per line so that
everyone can see the “&&” or “||” at the end. If you need to mix ANDs
and ORs, group with { these braces } which don’t spawn a sub-shell.
To your edited question:
Something like set -e for subshells.
Well, you can just do set -e for the subshell.
( set -e
my
commands
)
You can't implicitly make just your subshells have the errexit option. You can do some trickery using eval, or use a subprocess as a shell (even though a subprocess is not the same as a subshell), like
errexit_shell() {
bash -e
}
but those options are both inadvisable for various reasons, not the least of which being readability. Your best bet in that case would just be to adapt your entire script to use set -e, and your subshells will come along for the ride.
To your original question:
Just capture the status of the part that indicates success or failure:
(
cat teisatrt
status=$?
echo "true"
exit "$status"
) && echo passed || echo failed
(Of course, if all you want to know is if that file is readable, don't cat it, just use test -r.)
As you have it, you are redirecting output from the whole subshell to /dev/null, so you will never see your "true" echo. You should move the redirect inside the subshell to the command you really want it on. In order to exit the subshell when cat fails, you will need to check for failure after cat runs. If you don't, then as you have noted, its return code is wiped out by the following statement. So something like this:
echo "Installing App"
(
cat teisatrt &> /dev/null || exit 1
echo "true"
) && echo "OK" || echo "FAILED";
I want to do the following thing
X=$(some_command)
if [ $? == 0 ]; then
do_something
echo $x
else
do_something_else
fi
Basically, I want to execute some command, store the output to some variable. Meanwhile, I want to branch based on whether the command succeeded or not. The above way works, but looks ugly. Is it a smarter way?
Thanks.
if x=$(some command); then
do_something
echo $x
else
do_something_else
fi
The if command works by running the command and testing whether it was successful, and executing the then or else branch depending on it.
is it possible to assign variable inside if conditional in bash 4? ie. in the function below I want to assign output of executing cmd to output and check whether it is an empty string - both inside test conditional. The function should output
"command returned: bar"
myfunc() {
local cmd="echo bar"
local output=
while [[ -z output=`$cmd` ]];
do
#cmd is failing so far, wait and try again
sleep 5
done
# great success
echo "command returned: $output"
}
why the above?
i prefer to run scripts with 'set -e' - which will cause script to terminate on first non-0 return/exit code that's not in an if/loop conditional.
with that in mind, imagine cmd is an unstable command that may exit with > 1 from time to time, and I want to keep calling it until it succeeds and i get some output.
You can try something like this:
myfunc() {
local cmd="echo bar"
local output=
while ! output=$($cmd) || [[ -z output ]];
do
#cmd is failing so far, wait and try again
sleep 5
done
# great success
echo "command returned: $output"
}
Note that it is strongly recommended to avoid the use of set -e.
I don't think you would be able to do it in your conditional
As yi_H pointed out, the if is equivalent to
if [[ ! -z output=bar ]];
which in turn is basically
if [[ ! -z "output=bar" ]];
So, all you are checking is if the string "output=bar" is empty or not...
So, output=bar could actually be anything like !##!#%=== and it would still do the same thing (that is, the expression isn't evaluated). You might have to assign the variable in a subshell somehow, but I'm not sure that would work.
Since assignment won't work there, you need some workaroudn.
You could temporary do a set +e...
You could use this way ...
$cmd
exit_status=$?
while [[ $exit_status -gt 0 ]];
do
#cmd is failing so far, wait and try again
sleep 5
$cmd
exit_status=$?
done
EDIT: This won't work with 'set -e' or other way around, don't use 'set -e' to begin with.
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