Below is a simple script that should use exit 1 when run normally, and return 1 when source-executed. For some reason, however, it fails to recognize source-execution on every 2nd attempt after having executed normally. It runs fine when using source-execution only, and strange enough, the problem doesn't happen when I use exit instead of exit 1. I tried various Bash versions and Linux distributions, but the problem remains. Maybe something fundamental.
#!/bin/bash
# testme.sh
$(return >/dev/null 2>&1)
if [[ $? -ne 0 ]]; then
echo status = $?
echo Source-execution required.
exit 1
else
if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then
iam=${BASH_SOURCE##*/}
else
echo Bash 4 or later required.
return
fi
fi
echo done ${iam}
But here is what happens. When I source-execute testme.sh for the 2nd time it does logoff. It runs fine, however, when I change exit 1 to exit.
# . testme.sh
done testme.sh
# testme.sh
status = 0
Source-execution required.
# . testme.sh
status = 0
Source-execution required.
Connection to 192.168.2.102 closed.
The following works:
# . testme.sh
done testme.sh
# . testme.sh
done testme.sh
# . testme.sh
done testme.sh
# testme.sh
status = 0
Source-execution required.
# testme.sh
status = 0
Source-execution required.
# testme.sh
status = 0
Source-execution required.
And when I change exit 1 to exit, it works too:
# . testme.sh
done testme.sh
# testme.sh
status = 0
Source-execution required.
# . testme.sh
done testme.sh
Any ideas? Thanks!
I made a few changes. Adding the following:
$(return >/dev/null 2>&1)
status=$?
echo status=$status
echo SHLVL=$SHLVL
if [[ $status -ne 0 ]]; then
Yes, the $? was returning the value of the if clause. I tried so many things and got confused. Anyway, now we see that every 2nd sourcing after there was a normal execution with (exit 1) screws up the status of the return command. Again if I change "exit 1" to "exit" it works fine. Btw, $(return... or (return didn't make any difference.
# . testme.sh
status=0
SHLVL=1
done testme.sh
# testme.sh
status=1
SHLVL=2
Source-execution required.
# . testme.sh
status=1
SHLVL=1
Source-execution required.
Connection to 192.168.2.102 closed.
CodePudding user response:
The problem exhibited is due to how return
works, and when sourcing a script that runs exit
- will exit the current shell. Mixing both makes for confusing results, so I will demonstrate. To make the demonstration easier to follow, the shell's prompt is set to display bash's pid (export PS1="\s-\v [pid:$BASHPID] $ "
).
exit behavior
exit
terminates the current "context"; Running a script will spawn a child process for it to run in, so exit
will terminate the context it is running in - the child process. When sourcing a script, its contents are executed in the current context (current shell), so exit
will actually terminate the sourcing shell:
bash-5.1 [pid:1] $ cat ex.sh
#!/bin/bash
exit 7
bash-5.1 [pid:1] $ ./ex.sh
bash-5.1 [pid:1] $ echo $?
7
bash-5.1 [pid:1] $ bash
bash-5.1 [pid:11] $ # <--- we are now in a subprocess shell
bash-5.1 [pid:11] $ ./ex.sh
bash-5.1 [pid:11] $ echo $?
7
bash-5.1 [pid:11] $ . ./ex.sh # <--- sourcing
bash-5.1 [pid:1] $ echo $? # <--- we are back to parent shell, exit ended subshell pid:11
7
bash-5.1 [pid:1] $
return behavior
These are the relevant pieces from the man page, I emphasized the most important factor:
return [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.
In this case, the "last command executed" relates to testme.sh
(if no other command was executed in the meantime, including echo $?
), and "within the script" relates to the current bash shell if the script is sourced. I slightly modified the original script to a. store the value of $?
, and b. exit
with a distinguishable exitcode (42):
#!/bin/bash
# testme.sh
$(return >/dev/null 2>&1)
return_returncode=$?
if [[ $return_returncode -ne 0 ]]; then
echo status = $return_returncode
echo Source-execution required.
exit 42
else
if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then
iam=${BASH_SOURCE##*/}
else
echo Bash 4 or later required.
return
fi
fi
echo done ${iam}
Now to the fun part - running things. As long as the latest exitcode is 0, sourcing the script works well. Running the script also "works", as it is executed in a subshell so does not carry the latest exitcode to it:
bash-5.1 [pid:1] $ bash
bash-5.1 [pid:9] $ . ./testme.sh
done testme.sh
bash-5.1 [pid:9] $ echo $?
0
bash-5.1 [pid:9] $ ./testme.sh
status = 2
Source-execution required.
bash-5.1 [pid:9] $ echo $?
42
bash-5.1 [pid:9] $
The problem happens only when sourcing the script immediately after running it. Notice the change in the echo status =
line:
bash-5.1 [pid:1] $ bash
bash-5.1 [pid:9] $ ./testme.sh
status = 2
Source-execution required.
bash-5.1 [pid:9] $ . ./testme.sh
status = 42
Source-execution required.
bash-5.1 [pid:1] $ echo $? # <--- running exit from a sourced script has terminated its process, back to pid:1
42
bash-5.1 [pid:1] $
Proposed fix
By clearing the last exitcode, the behavior will no longer depend on the last command. For minimal side-effects, I suggest using true
:
#!/bin/bash
# testme.sh
true # clear previous exitcode
$(return >/dev/null 2>&1)
if [[ $? -ne 0 ]]; then
echo status = $?
echo Source-execution required.
exit 1
else
if [[ ${BASH_VERSINFO:-0} -ge 4 ]]; then
iam=${BASH_SOURCE##*/}
else
echo Bash 4 or later required.
return
fi
fi
echo done ${iam}
EDIT: another approach would be to explicitly set the return value n
to 0 when testing for return
's functionality, like so:
#!/bin/bash
# testme.sh
$(return 0 >/dev/null 2>&1)
if [[ $? -ne 0 ]]; then
...
As @DennisWilliamson kindly commented, and considering the purpose of the statement, this (well, his) approach is better, as it is less "fragile" and will hold true even if additional statements are added just before checking return
's functionality.
Keeping the change as minimal as possible to achieve the desired result, the above code does not include other modifications or potential improvements (like storing the $?
in a separate variable for later use). Now the results are consistent:
bash-5.1 [pid:1] $ . ./testme.sh
done testme.sh
bash-5.1 [pid:1] $ . ./testme.sh
done testme.sh
bash-5.1 [pid:1] $ ./testme.sh
status = 0
Source-execution required.
bash-5.1 [pid:1] $ ./testme.sh
status = 0
Source-execution required.
bash-5.1 [pid:1] $ . ./testme.sh
done testme.sh
bash-5.1 [pid:1] $ . ./testme.sh
done testme.sh
bash-5.1 [pid:1] $
Additional thought;
It may seem curious that while the exitcode was set to 42, executing the script echoes status = 2
. The man page entry for return
also states:
If used outside a function and not during execution of a script by ., the return status is false.
Hypothesis: false
is passed literally, and in this implementation - eventually to exit
. This is an invalid value for exit
, and invalid values will cause exit
to return exitcode 2:
bash-5.1 [pid:1] $ bash
bash-5.1 [pid:9] $ exit 72
exit
bash-5.1 [pid:1] $ echo $?
72
bash-5.1 [pid:1] $ bash
bash-5.1 [pid:12] $ exit false
exit
bash: exit: false: numeric argument required
bash-5.1 [pid:1] $ echo $?
2
bash-5.1 [pid:1] $ bash
bash-5.1 [pid:15] $ false
bash-5.1 [pid:15] $ echo $?
1
bash-5.1 [pid:15] $ exit $(false)
exit
bash-5.1 [pid:1] $ echo $?
1
bash-5.1 [pid:1] $ bash
bash-5.1 [pid:19] $ exit foo
exit
bash: exit: foo: numeric argument required
bash-5.1 [pid:1] $ echo $?
2
bash-5.1 [pid:1] $
Proving this hypothesis (that what actually happens is literally exit false
) is out-of-scope for the question, so while it might not be the actual case, it is reasonable enough to explain how status = 2
might be echoed.