This works in both bash
(since release 4.3) and ksh93
. To "bashify" it, replace all typeset
with local
in the functions, and the typeset
in the global scope with declare
(while keeping all the options!). I honestly don't know why Bash has so many different names for things that are just variations of typeset
.
function stack_push
{
typeset -n _stack="$1"
typeset element="$2"
_stack+=("$element")
}
function stack_pop
{
typeset -n _stack="$1"
typeset -n _retvar="$2"
_retvar="${_stack[-1]}"
unset _stack[-1]
}
typeset -a stack=()
stack_push stack "hello"
stack_push stack "world"
stack_pop stack value
printf '%s ' "$value"
stack_pop stack value
printf '%s\n' "$value"
Using a nameref in the function, you avoid eval
(I've never had to use eval
anywhere in any script!). By providing the stack_pop
function with a place to store the popped value, you avoid the subshell. By avoiding the subshell, the stack_pop
function can modify the value of the stack
variable in the outer scope.
The underscores in the local variables in the function is to avoid having a nameref that has the same name as the variable that it references (Bash doesn't like it, ksh
doesn't mind, see this question).
In ksh
you could write the stack_pop
function like
function stack_pop
{
typeset -n _stack="$1"
printf '%s' "${_stack[-1]}"
unset _stack[-1]
}
And then call it with
printf '%s %s\n' "${ stack_pop stack }" "${ stack_pop stack }"
(${ ... }
is the same as $( ... )
but does not create a subshell)
But I'm not a big fan of this. IMHO, stack_pop
should not have to send the data to stdout, and I should not have to call it with ${ ... }
to get the data. I could possibly be more ok with my original stack_pop
, and then add a stack_pop_print
that does the above, if needed.
For Bash, you could go with the stack_pop
in the beginning of my post, and then have a stack_top_print
that just prints the top element of the stack to stdout, without removing it (which it can't because it would most likely be running in a $( ... )
subshell).
This is all perfectly understandable if we step through slowly.
Some more logging is required,
so run bash with the -x
parameter,
which will echo commands just before bash executes them,
prefixed by +
.
First run
$ bash -x sandbox.sh; echo $?
+ set -eu -o pipefail -E
++ func1
++ echo FUNC1
++ exit 1
+ var=FUNC1
1
-e
says this shell will exit immediately a command returns non-zero.
Crucially though, you run func1
in a subshell (using $(
)
).
The trace above shows this fact by using two +
s as the prefix (++
).
- The subshell spits out
FUNC1
on stdout, and then exits with return code 1.
- Note:
-e
is off inside this subshell. The reason the subshell quit was due to the exit
command, not -e
. You can't really tell this due to the way func1
is written.
- Back in the first shell, we assign
FUNC1
to the variable var. However, the exit code of this assignment command is the exit code of the last command substitution. Bash sees this failure (i.e., non-zero exit code), and quits.
To quote the manual's SIMPLE COMMAND EXPANSION section:
If one of the expansions contained a command substitution, the exit status of the command is the exit status of the last command substitution performed.
Second run
Exactly the same explanation as the first run.
We note again that the -e
is not in effect inside the subshell.
This time however,
there is a material difference — we get a clearer view of what is happening.
- The exit code of
func2
is the exit code of its last command
- That
echo
always succeeds.
func2
always succeeds
- The assignment always succeeds.
-e
has no effect.
shopt -s inherit_errexit
?
This will turn on -e
in subshells.
It is however a difficult bedfellow.
It does not guarantee we assert when a command fails.
Consider this:
set -e
shopt -s inherit_errexit
f() { echo a; (exit 22); echo b; }
echo "f says [$(f)] $?"
echo byee
This time the command substitution is part of an echo
, rather than an assignment, and we get
+ set -e
+ shopt -s inherit_errexit
++ f
++ echo a
++ exit 22
+ echo 'f says [a] 22'
f says [a] 22
+ echo byee
byee
- The subshell sees a command that fails with exit code 22. Since
-e
is in effect, the shell exits with code 22 (echo b
does not execute).
- Back in the first shell,
echo
gets a
as the output of f
, and 22
as the exit code of the subshell
- Thing is, unlike an assignment, the exit code of the
echo
is zero.
Version
$ bash --version
GNU bash, version 5.0.17(1)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Best Answer
Subshells
There is no way for a subshell to kill the whole script. Under the effect of
set -u
, a subshell has no way to tell its parent “hey, I want you to treat this as a fatal error”. This wouldn't even make sense ifset -u
was in effect in the subshell but not in its parent. All the subshell can do is return a nonzero exit code. It's up to you to handle that exit code in the parent, either manually or throughset -e
.Subprocesses
What you've done here is not a subshell: you're launching another program. That other program happens to be bash, which is also the interpreter that's executing the present script, but it's a coincidence. There is absolutely no reason why a setting in a program invoked by bash would cause bash itself to exit.
This is no different than if you'd written
perl -e 'use strict; print $barf'
— Perl would die and return a nonzero status but that doesn't cause the bash script to exit (except underset -e
). Useset -e
if you want to exit when a subprocess returns a nonzero return code.