60

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.

melpomene
  • 84,125
  • 8
  • 85
  • 148
RomanM
  • 6,363
  • 9
  • 33
  • 41

9 Answers9

60

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.

melpomene
  • 84,125
  • 8
  • 85
  • 148
Sergey Shevchenko
  • 1,750
  • 14
  • 13
  • It would be useful to note which version of bash this was introduced in as I have an old version and do not have this feature. – stephenmm Sep 19 '18 at 23:10
  • 1
    The Interwebz are telling me this feature was introduced in Bash 4.3-alpha: http://wiki.bash-hackers.org/commands/builtin/declare. – Sergey Shevchenko Sep 21 '18 at 00:15
  • 4
    Important catch is that neither the local name, nor any other local variable in the function, may coincide with the argument, because bash has no way of indicating you actually mean the variable from calling scope. – Jan Hudec Oct 31 '19 at 10:24
  • @Jan Hudec, sorry, your statement is contradictory. Did you mean that _any_ local name or local variable may collide with the referenced variable name? In that case, I believe, declaring all local references at the very beginning of the function boo effectively solves all issues. – Kyselejsyreček Aug 28 '23 at 07:07
  • @Kyselejsyreček my quick test suggests it does not help. That is, if the function has `local -n ref=$1; local some_var; …` and you call it with `some_var`, the reference will reference the local `some_var`, not the outer `some_var`, breaking the function and not doing what you expected (bash 5.1.16 here). – Jan Hudec Aug 28 '23 at 14:02
  • @JanHudec, alright. This behaviour is apparently already fixed in Bash 5.2.15 from Ubuntu 23.04. – Kyselejsyreček Aug 28 '23 at 16:18
  • @Kyselejsyreček … which is way newer than that comment. – Jan Hudec Aug 29 '23 at 08:53
25

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
Andreas Spindler
  • 7,568
  • 4
  • 43
  • 34
17

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
fvue
  • 316
  • 2
  • 5
15

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

jwfearn
  • 28,781
  • 28
  • 95
  • 122
RomanM
  • 6,363
  • 9
  • 33
  • 41
14

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.

David Z
  • 128,184
  • 27
  • 255
  • 279
  • thats what i figured, but being new to bash it seemed weird that to pass the variable as if "by reference" you have to pass the quoted variable name ... – RomanM Feb 12 '09 at 08:01
6

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
Aethalides
  • 367
  • 5
  • 12
2
#!/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.

ajaaskel
  • 1,639
  • 12
  • 12
2

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
Ian Kelling
  • 9,643
  • 9
  • 35
  • 39
  • 1
    but can you use it to set the variable, i.e. ${!x}=5 – RomanM Feb 13 '09 at 08:21
  • Indirection can be almost as bad as eval when it comes to user input because bash processes expansions indirectly. `f() { local x=${!1}; }; f 'a[$(echo hi >&2)]'`. Just have to make sure the input is never handled as arithmetic, which in Bash can be harder than most people realize. – ormaaj Apr 23 '12 at 08:58
1

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
enthusiasticgeek
  • 2,640
  • 46
  • 53