Passing arguments by reference

BashShellScriptingPass by-Reference

Bash Problem Overview


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.

Bash Solutions


Solution 1 - Bash

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.

Solution 2 - Bash

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

Solution 3 - Bash

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

Solution 4 - Bash

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 ...

Solution 5 - Bash

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.

Solution 6 - Bash

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

Solution 7 - Bash

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

Solution 8 - Bash

#!/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.

Solution 9 - Bash

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRomanMView Question on Stackoverflow
Solution 1 - BashSergey ShevchenkoView Answer on Stackoverflow
Solution 2 - BashAndreas SpindlerView Answer on Stackoverflow
Solution 3 - BashFreddy VultoView Answer on Stackoverflow
Solution 4 - BashRomanMView Answer on Stackoverflow
Solution 5 - BashDavid ZView Answer on Stackoverflow
Solution 6 - BashAethalidesView Answer on Stackoverflow
Solution 7 - BashIan KellingView Answer on Stackoverflow
Solution 8 - BashajaaskelView Answer on Stackoverflow
Solution 9 - BashenthusiasticgeekView Answer on Stackoverflow