Passing arguments by reference
It's 2018, and this question deserves an update. At least in Bash, as of Bash 4.3-alpha, you can use namerefs to pass function arguments by reference:
function boo()
{
local -n ref=$1
ref='new'
}
SOME_VAR='old'
echo $SOME_VAR # -> old
boo SOME_VAR
echo $SOME_VAR # -> new
The critical pieces here are:
Passing the variable's name to boo, not its value:
boo SOME_VAR
, notboo $SOME_VAR
.Inside the function, using
local -n ref=$1
to declare a nameref to the variable named by$1
, meaning it's not a reference to$1
itself, but rather to a variable whose name$1
holds, i.e.SOME_VAR
in our case. The value on the right-hand side should just be a string naming an existing variable: it doesn't matter how you get the string, so things likelocal -n ref="my_var"
orlocal -n ref=$(get_var_name)
would work too.declare
can also replacelocal
in contexts that allow/require that. See chapter on Shell Parameters in Bash Reference Manual for more information.
The advantage of this approach is (arguably) better readability and, most importantly, avoiding eval
, whose security pitfalls are many and well-documented.
Use a helper function upvar
:
# Assign variable one scope above the caller.
# Usage: local "$1" && upvar $1 value [value ...]
# Param: $1 Variable name to assign value to
# Param: $* Value(s) to assign. If multiple values, an array is
# assigned, otherwise a single value is assigned.
# NOTE: For assigning multiple variables, use 'upvars'. Do NOT
# use multiple 'upvar' calls, since one 'upvar' call might
# reassign a variable to be used by another 'upvar' call.
# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
upvar() {
if unset -v "$1"; then # Unset & validate varname
if (( $# == 2 )); then
eval $1=\"\$2\" # Return single value
else
eval $1=\(\"\${@:2}\"\) # Return array
fi
fi
}
And use it like this from within Newfun()
:
local "$1" && upvar $1 new
For returning multiple variables, use another helper function upvars
. This allows passing multiple variables within one call, thus avoiding possible conflicts if one upvar
call changes a variable used in another subsequent upvar
call.
See: http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference for helper function upvars
and more information.
The problem with:
eval $1=new
is that it's not safe if $1
happens to contain a command:
set -- 'ls /;true'
eval $1=new # Oops
It would be better to use printf -v
:
printf -v "$1" %s new
But printf -v
cannot assign arrays.
Moreover, both eval
and printf
won't work if the variable happens to be declared local
:
g() { local b; eval $1=bar; } # WRONG
g b # Conflicts with `local b'
echo $b # b is empty unexpected
The conflict stays there even if local b
is unset
:
g() { local b; unset b; eval $1=bar; } # WRONG
g b # Still conflicts with `local b'
echo $b # b is empty unexpected
From the Bash man-page (Parameter Expansion):
If the first character of parameter is an exclamation point (!), a level of variable indirection is introduced. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion.
Therefore a reference is the variable's name. Here is a swap
function using
variable indirection that does not require a temporary variable:
function swap()
{ #
# @param VARNAME1 VARNAME2
#
eval "$1=${!2} $2=${!1}"
}
$ a=1 b=2
$ swap a b
$ echo $a $b
2 1