Reputation: 6721
I want to ask if it is possible to pass arguments to a script function by reference:
i.e. to do something that would look like this in C++:
void boo(int &myint) { myint = 5; }
int main() {
int t = 4;
printf("%d\n", t); // t->4
boo(t);
printf("%d\n", t); // t->5
}
So then in BASH I want to do something like:
function boo ()
{
var1=$1 # now var1 is global to the script but using it outside
# this function makes me lose encapsulation
local var2=$1 # so i should use a local variable ... but how to pass it back?
var2='new' # only changes the local copy
#$1='new' this is wrong of course ...
# ${!1}='new' # can i somehow use indirect reference?
}
# call boo
SOME_VAR='old'
echo $SOME_VAR # -> old
boo "$SOME_VAR"
echo $SOME_VAR # -> new
Any thoughts would be appreciated.
Upvotes: 63
Views: 59512
Reputation: 417
Ok, so this question has been waiting for a 'real' solution for some time now, and I am glad to say that we can now accomplish this without using eval at all.
The key to remember is to declare a reference in both the caller as the callee, at least in my example:
#!/bin/bash
# NOTE this does require a bash version >= 4.3
set -o errexit -o nounset -o posix -o pipefail
passedByRef() {
local -n theRef
if [ 0 -lt $# ]; then
theRef=$1
echo -e "${FUNCNAME}:\n\tthe value of my reference is:\n\t\t${theRef}"
# now that we have a reference, we can assign things to it
theRef="some other value"
echo -e "${FUNCNAME}:\n\tvalue of my reference set to:\n\t\t${theRef}"
else
echo "Error: missing argument"
exit 1
fi
}
referenceTester() {
local theVariable="I am a variable"
# note the absence of quoting and escaping etc.
local -n theReference=theVariable
echo -e "${FUNCNAME}:\n\tthe value of my reference is:\n\t\t${theReference}"
passedByRef theReference
echo -e "${FUNCNAME}:\n\tthe value of my reference is now:\n\t\t${theReference},\n\tand the pointed to variable:\n\t\t${theVariable}"
}
Output when run:
referenceTester:
the value of my reference is:
I am a variable
passedByRef:
the value of my reference is:
I am a variable
passedByRef:
value of my reference set to:
some other value
referenceTester:
the value of my reference is now:
some other value,
and the pointed to variable:
some other value
Upvotes: 6
Reputation: 1887
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
, not boo $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 like local -n ref="my_var"
or local -n ref=$(get_var_name)
would work too. declare
can also replace local
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.
Upvotes: 68
Reputation: 2736
This is what works for me on Ubuntu bash shell
#!/bin/sh
iteration=10
increment_count()
{
local i
i=$(($1+1))
eval ${1}=\$i
}
increment_count iteration
echo $iteration #prints 11
increment_count iteration
echo $iteration #prints 12
Upvotes: 1
Reputation: 1709
#!/bin/bash
append_string()
{
if [ -z "${!1}" ]; then
eval "${1}='$2'"
else
eval "${1}='${!1}''${!3}''$2'"
fi
}
PETS=''
SEP='|'
append_string "PETS" "cat" "SEP"
echo "$PETS"
append_string "PETS" "dog" "SEP"
echo "$PETS"
append_string "PETS" "hamster" "SEP"
echo "$PETS"
Output:
cat
cat|dog
cat|dog|hamster
Structure for calling that function is:
append_string name_of_var_to_update string_to_add name_of_var_containing_sep_char
Name of variable is passed to fuction about PETS and SEP while string to append is passed the usual way as value. "${!1}" refers to contents of global PETS variable. In the beginning that variable is empty and contens is added each time we call the function. Separator character can be selected as needed. "eval" starting lines update PETS variable.
Upvotes: 2
Reputation: 6721
I have found a way to do this but I am not sure how correct this is:
Newfun()
{
local var1="$1"
eval $var1=2
# or can do eval $1=2 if no local var
}
var=1
echo var is $var # $var = 1
newfun 'var' # pass the name of the variable…
echo now var is $var # $var = 2
So we pass the variable name as opposed to the value and then use eval ...
Upvotes: 15
Reputation: 8150
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
Upvotes: 25
Reputation: 316
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
Upvotes: 17
Reputation: 10051
Eval should never be used on a string that a user can set because its dangerous. Something like "string; rm -rf ~" will be bad. So generally its best to find solutions where you don't have to worry about it.
However, eval will be needed to set the passed variables, as the comment noted.
$ y=four
$ four=4
$ echo ${!y}
4
$ foo() { x=$1; echo ${!x}; }
$ foo four
4
Upvotes: 2
Reputation: 131690
Bash doesn't have anything like references built into it, so basically the only way you would be able to do what you want is to pass the function the name of the global variable you want it to modify. And even then you'll need an eval
statement:
boo() {
eval ${1}="new"
}
SOME_VAR="old"
echo $SOME_VAR # old
boo "SOME_VAR"
echo $SOME_VAR # new
I don't think you can use indirect references here because Bash automatically accesses the value of the variable whose name is stored in the indirect reference. It doesn't give you the chance to set it.
Upvotes: 14