I try to make a function which can interrupt the script execution (due to fatal error):
quit() {
echo -e "[ERROR]" | log
exit 1
}
Call example:
if [ "$#" -eq 1 ]; then
# Do stuff
else
echo -e "function getValue: wrong parameters" | log
quit
fi
Function quit is called (echo in the logfile) but the script keeps going. I've read that exit only terminate the subshell (is that true?) which means that it terminates the quit function but not the entire script.
For now, I prefer not use return code in quit method as it implies a lot of code refactoring.
Is there a way to stop the script from the quit function?
EDIT:
full example of a case where the error appears:
#!/bin/bash
logfile="./testQuit_log"
quit() {
echo "quit" | log
exit 1
}
log() {
read data
echo -e "$data" | tee -a "$logfile"
}
foo() {
if [ "$#" -ne 1 ]; then
echo "foo error" | log
quit
fi
echo "res"
}
rm $logfile
var=`foo p1 p2`
var2=`foo p1`
echo "never echo that!" | log
EDIT2:
it works correctly when I switch these lines:
var=`foo p1 p2`
var2=`foo p1`
with
var= foo p1 p2
var2= foo p1
Any explanation? Is that because of the subshell?
As it has been outlined in the question's comment section, using exit in a subshell will only exit the subshell and it is not easy to work around this limitation. Luckily, exiting from a subshell or even a function in the same shell is not the best idea anyway:
A good pattern to solve the problem of handling an error on a lower level (like a function or subshell) in a language without exceptions is to return the error instead of terminating the program directly from the lower level:
foo() {
if [ "$#" -ne 1 ]; then
echo "foo error" | log
return 1
else
echo "res"
# return 0 is the default
fi
}
This allows control flow to return to the highest level even on error, which is generally considered a good thing (and will incredibly ease debugging complex programs). You can use your function like this:
var=$( foo p1 p2 ) || exit 1
var2=$( foo p1 ) || exit 1
Just to be clear, the || branch is not entered if the assignment fails (it won't), but if the command line inside the command substitution ($( )) returns a non-zero exit code.
Note that $( ) should be used for command substitution instead of backticks, see this related question.
Looking at a debug of the script shows the problem. var=`foo p1 p2` forces execution of foo in a subshell (note: the increase in level from + to ++ at the time of the call below) Execution of the script proceeds in a subshell until exit 1 is reached. exit 1 effectively exits the subshell returning to the primary script.
$ bash -x exitstuck.sh
+ logfile=./testQuit_log
+ rm ./testQuit_log
++ foo p1 p2 # var=`foo p1 p2` enters subshell '+ -> ++'
++ '[' 2 -ne 1 ']'
++ echo 'foo error'
++ log # log() called
++ read data
++ echo -e 'foo error'
++ tee -a ./testQuit_log
++ quit # quit() called
++ echo quit
++ log
++ read data
++ echo -e quit
++ tee -a ./testQuit_log
++ exit 1 # exit 1 exits subshell, note: '++ -> +'
+ var='foo error
quit'
++ foo p1
++ '[' 1 -ne 1 ']'
++ echo res
+ var2=res
+ log
+ read data
+ echo 'never echo that!'
+ echo -e 'never echo that!'
+ tee -a ./testQuit_log
never echo that!
You can use this to your advantage to accomplish what it is you are trying to do. How? When exit 1 exits the subshell, it does so returning the exit code 1. You can test the exit code in your main script and exit as you intend:
var=`foo p1 p2`
if [ $? -eq 1 ]; then
exit
fi
var2=`foo p1`
Running in debug again shows the intended operation:
$ bash -x exitstuck.sh
+ logfile=./testQuit_log
+ rm ./testQuit_log
++ foo p1 p2
++ '[' 2 -ne 1 ']'
++ echo 'foo error'
++ log
++ read data
++ echo -e 'foo error'
++ tee -a ./testQuit_log
++ quit
++ echo quit
++ log
++ read data
++ echo -e quit
++ tee -a ./testQuit_log
++ exit 1
+ var='foo error
quit'
+ '[' 1 -eq 1 ']'
+ exit
Related
I'm trying to establish two-way communication between a Windows box (with a bash cli) and a qnx box (with a ksh cli, though currently I'm using a linux VM with ksh98 to test with) over ssh using scripts.
I can't seem to get the redirects quite right though. Here is my simple setup:
#!/usr/bin/bash
cleanup() {
exec >&$SSH_STDIN- ; rm ssh_stdin
exec <&$SSH_STDOUT-; rm ssh_stdout
echo "Cleaned up."
}
trap 'cleanup' EXIT
mkfifo ssh_stdin ; exec {SSH_STDIN}<>./ssh_stdin
mkfifo ssh_stdout; exec {SSH_STDOUT}<>./ssh_stdout
echo "SSH_STDIN: $SSH_STDIN"
echo "SSH_STDOUT: $SSH_STDOUT"
repeat() { echo "$2"; echo "$2" >&$1; }
fn() {
sleep 5
echo AWAKE! >&2
set +o xtrace
while read -u $SSH_STDOUT a b; do
case "$a" in
Hi!) sleep 1; repeat $SSH_STDOUT "Ho!" ;;
and-away-we-go!) sleep 1; repeat $SSH_STDOUT "quit";;
*) echo "UNRECOGNIZED: $a $b";;
esac
done
}
fn&
ssh user#127.0.0.1 -p 2022 '
. /etc/profile # Used to setup path
repeat() { echo "$2"; echo "$2" >&$1; }
repeat 2 "Hi!"
while read a b; do
case "$a" in
Ho! ) sleep 1; repeat "and-away-we-go!";;
quit ) exit 0;;
* ) echo "UNRECOGINZED: $a $b";;
esac
done
' >&$SSH_STDIN <&$SSH_STDOUT
I feel that I'm close. What am I doing wrong? Maybe this can be done without using named FIFOs?
EDIT
This is the output:
$ ./test-writer.sh
SSH_STDIN: 11
SSH_STDOUT: 12
user#127.0.0.1's password:
Hi!
AWAKE!
./test-writer.sh: line 21: read: read error: 12: Communication error on send
It then hangs and after I press Ctrl-C:
./test-writer.sh: line 6: echo: write error: Communication error on send
Here it is with bash -x
$ bash -x ./test-writer.sh
+ trap cleanup EXIT
+ mkfifo ssh_stdin
+ exec
+ mkfifo ssh_stdout
+ exec
+ echo 'SSH_STDIN: 11'
SSH_STDIN: 11
+ echo 'SSH_STDOUT: 12'
SSH_STDOUT: 12
+ fn
+ ssh user#127.0.0.1 -p 2022 '
. /etc/profile # Used to setup path
repeat() { echo "$2"; echo "$2" >&$1; }
repeat 2 "Hi!"
while read a b; do
case "$a" in
Ho! ) sleep 1; repeat "and-away-we-go!";;
quit ) exit 0;;
* ) echo "UNRECOGINZED: $a $b";;
esac
done
'
+ sleep 5
user#127.0.0.1's password:
Hi!
+ echo 'AWAKE!'
AWAKE!
+ set +o xtrace
./test-writer.sh: line 21: read: read error: 12: Communication error on send
and after it hangs, I press Ctrl-C and get:
+ cleanup
+ exec
+ rm ssh_stdin
+ exec
+ rm ssh_stdout
+ echo 'Cleaned up.'
./test-writer.sh: line 6: echo: write error: Communication error on send
Although I have a solution using coproc, I would like to know why this solution isn't working.
Thanks to a hint by #KamilCuk about using coproc I was able to come up with an answer which is much less unwieldly.
#!/usr/bin/bash
cleanup() {
# Do I even need this?
exec <&${ssh_fd[0]}- >&${ssh_fd[1]}-
echo "Cleaned up."
}
trap 'cleanup' EXIT
coproc ssh_fd {
ssh user#127.0.0.1 -p 2022 '
. /etc/profile # Used to setup path
repeat() { echo "$2"; echo "$2" >&$1; }
repeat 2 "Hi!"
while read a b; do
case "$a" in
Ho! ) sleep 1; repeat 2 "and-away-we-go!";;
quit ) exit 0;;
* ) echo "UNRECOGINZED: .$a. .$b." >&2;;
esac
done
'
}
repeat() { echo "$2"; echo "$2" >&$1; }
fn() {
set +o xtrace
while read -u ${ssh_fd[0]} a b; do
case "$a" in
Hi!) sleep 1; repeat ${ssh_fd[1]} "Ho!" ;;
and-away-we-go!) sleep 1; repeat ${ssh_fd[1]} "quit";;
*) echo "UNRECOGNIZED: $a $b";;
esac
done
}
fn
Output:
$ ./test-writer.sh
user#127.0.0.1's password:
Hi!
Ho!
and-away-we-go!
quit
I didn't actually change anything in the code, just deleted and moved stuff around. I was right! I was almost there! Thx #KamilCuk!
Still, although I have this solution, I would like to know why the original one didn't work. I think it should, and I'll accept those answers if they figure it out.
Actually, still looking for a solution. I'm not sure why the devs fell short of redirecting all of the handles, INCLUDING STDERR! sigh
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 couldn't explain what happens in my scripts, could anyone shed some light please?
I am doing some pretty standard stuff, set errexit, sourcing one script from another, catching errors and eventually bailing out if any.
s1.sh
#!/bin/bash
num=1
if [ $num -eq 1 ]; then
FOO="$(set -o | grep -e "errexit" -e "nounset" | grep off >&2)"
VAR="SOME/TEXT/$(basename "$UNBOUND_VARIABLE")"
RET="$(echo $?)"
#ERR="$UNBOUND_VARIABLE" # this will be trapped and source will exit at this line
BAR="LAST_IS_GOOD"
fi
s2.sh
function source_all
{
local __f
set -exu
for __f in ${#}; do
case "$__f" in
"s1.sh" ) set -o posix; (source "$(pwd)/$__f") || return 1; echo "$$ $?" >&2 ;;
esac
done
set +eux +o posix
}
function main
{
source_all s1.sh || return 1
}
main
output
+ for __f in ${#}
+ case "$__f" in
+ set -o posix
++ pwd
+ source (blah/blah)/s1.sh
++ num=1
++ '[' 1 -eq 1 ']'
+++ set -o
+++ grep -e errexit -e nounset
+++ grep off
++ FOO=
(blah/blah)/s1.sh: line 6: UNBOUND_VARIABLE: unbound variable # should exit
++ VAR=SOME/TEXT/
+++ echo 1
++ RET=1
++ BAR=LAST_IS_GOOD
+ echo '9568 0'
9568 0
+ set +eux +o posix
source --help
Exit Status:
Returns the status of the last command executed in FILENAME; fails if
FILENAME cannot be read.
question is: why source invoked in s2.sh doesn't return 1? Why does it keep processing s1.sh after UNBOUND_VARIABLE?
thanks for your inputs
The UNBOUND VARIABLE error comes because you are using set -u are referencing with $UNBOUND_VARIABLE a variable named _UNBOUND_VARIABLE_ which has not been assigned to, in the statement
VAR="SOME/TEXT/$(basename "$UNBOUND_VARIABLE")"
. The set -e does have an effect, in that the subshell this is executed, i.e.
(source "$(pwd)/$__f")
is aborted. While the subshell due this abort indeed returns with non-zero exit code, but this does not trigger an exit of the parent process, because you have a || return to the right. For the same reason, the command
false || echo x
would not terminate the execution, even though a single
false
would.
I have an infinite while loop that I only want to break out of if the user presses Ctrl - C.
But there are 2 counters inside my while loop whose value I want printed out when I exit the while loop.
OK_COUNT=0
NOK_COUNT=0
while :
do
RESULT=`curl -s http://${IP}${ENDPOINT} --max-time 1`
if [ $RESULT == '{"status":"UP"}' ]
then
(( OK_COUNT+=1 ))
echo "`date` :: ${ENDPOINT} is OK ! Total count is $OK_COUNT "
else
(( NOK_COUNT+=1 ))
echo "`date` :: ${ENDPOINT} is UNREACHABLE ! Total count is $NOK_COUNT"
fi
sleep 0.5
done
echo $OK_COUNT
echo $NOK_COUNT
Now when I press Ctrl + C, I exit out of the while loop and out of the script too. This means the last 2 echo statements dont get to be printed.
Is there a way that if I press Ctrl + C, I only exit out of the while loop but the rest of the script still runs ?
EDIT/SOLUTION ::
After adding trap, It works !
OK_COUNT=0
NOK_COUNT=0
trap printout SIGINT
printout() {
echo $OK_COUNT
echo $NOK_COUNT
exit
}
while :
do
RESULT=`curl -s http://${IP}${ENDPOINT} --max-time 1`
if [ $RESULT == '{"status":"UP"}' ]
then
(( OK_COUNT+=1 ))
echo "`date` :: ${ENDPOINT} is OK ! Total count is $OK_COUNT "
else
(( NOK_COUNT+=1 ))
echo "`date` :: ${ENDPOINT} is UNREACHABLE ! Total count is $NOK_COUNT"
fi
sleep 0.5
done
With the above code, when I exit the code with Ctrl + C, I get.
Wed Oct 18 18:59:13 GMT 2017 :: /cscl_etl/health is OK ! Total count is 471
Wed Oct 18 18:59:13 GMT 2017 :: /cscl_etl/health is OK ! Total count is 472
^C
5
0
#
Trap Statement
Here is one method for making sure the echo statements are run after a Ctrl+C:
trap printout SIGINT
printout() {
echo ""
echo "Finished with count=$count"
exit
}
while :
do
((count++))
sleep 1
done
When run and Ctrl+C is pressed, the output from this script looks like:
$ bash s.sh
^C
Finished with count=2
How it works
The trap statement captures Ctrl+C and executes the function printout. That function can include any statement you like.
Subshell with trap
Alternatively, we can put the loop and the trap statement in a subshell:
$ cat t.sh
(
trap printout SIGINT
printout() {
echo ""
echo "At end of loop: count=$count"
exit
}
while :
do
((count++))
sleep 1
done
)
echo "Finishing script"
when this is run and Ctrl+C is pressed, the output looks like:
$ bash t.sh
^C
At end of loop: count=2
Finishing script
This method allows us to continue with the script after the subshell. Note, though, that any variables set or other environment changes made in the subshell are lost after the subshell exits.
I am new to shell scripting and stuck with a problem. In my shell method if I saw any validation issue then rest of the programm will not execute and will show user a message. Till validation it's done but when I used exit 0 then only it comes out of the validation loop not from full method.
config_wuigm_parameters () {
echo "Starting to config parameters for WUIGM....." | tee -a $log
prepare_wuigm_conf_file
echo "Configing WUIGM parameters....." | tee -a $log
local parafile=`dirname $0`/wuigm.conf
local pname=""
local pvalue=""
create_preference_template
cat ${parafile} |while read -r line;do
pname=`echo $line | egrep -e "^([^#]*)=(.*)" | cut -d '=' -f 1`
if [ -n "$pname" ] ; then
lsearch=`echo $line | grep "[<|>|\"]" `
if [ -n "$lsearch" ] ; then
echo validtion=$lsearch
echo "< or > character present , Replace < with < and > with >"
exit 1;
else
pvalue=`echo $line | egrep -e "^([^#]*)=(.*)" | cut -d '=' -f 2- `
echo "<entry key=\"$pname\" value=\"$pvalue\"/>" >> $prefs
echo "Configured : ${pname} = ${pvalue} " | tee -a $log
fi
fi
done
echo $validtion
echo "</map>" >> $prefs
# Copy the file to the original location
cp -f $prefs /root/.java/.userPrefs/com/ericsson/pgm/xwx
# removing the local temp file
rm -f $prefs
reboot_server
}
Any help would be great
It is because the construction
cat file | while read ...
starts a new (sub)shell.
In the next you can see the difference:
echoline() {
cat "$1" | while read -r line
do
echo ==$line==
exit 1
done
echo "Still here after the exit"
}
echoline $#
and compare with this
echoline() {
while read -r line
do
echo ==$line==
exit 1
done < "$1"
echo "This is not printed after the exit"
}
echoline $#
Using the return doesn't helps too, (because of subshell). The
echoline() {
cat "$1" | while read -r line
do
echo ==$line==
return 1
done
echo "Still here"
}
echoline $#
will still prints the "Still here".
So, if you want exit the script, use the
while read ...
do
...
done < input #this not starts a new subshell
if want exit just the method (return from it) must check the exit startus of the previous command, like:
echoline() {
cat "$1" | while read -r line
do
echo ==$line==
exit 1
done || return 1
echo "In case of exit (or return), this is not printed"
}
echoline $#
echo "After the function call"
Instead of || or you can use the
[ $? != 0 ] && return 1
just after the while.
You use the return instruction to exit a function with a value.
return [n]
Causes a function to exit with the return value specified by n. If n is omitted, the return status is that of the last command executed in the function body. If used outside a function, but during execution of a script by the . (source) command, it causes the shell to stop executing that script and return either n or the exit status of the last command executed within the script as the exit status of the script. If used out‐side a function and not during execution of a script by ., the return status is false. Any command associated with the RETURN trap is executed before execution resumes after the function or script.
If you want to exit a loop, use the break instruction instead:
break [n]
Exit from within a for, while, until, or select loop. If n is specified, break n levels. n must be ≥ 1. If n is greater than the number of enclosing loops, all enclosing loops are exited. The return value is 0 unless n is not greater than or equal to 1.
The exit instruction exits the current shell instead, so the current program as a whole. If you use sub-shells, code written between parenthesis, then only that sub-shell exits.