How to use call-by-reference on an argument in a bash function

14,012

Solution 1

The Bash FAQ has a whole entry about calling by reference / indirection.

In the simple case, a better alternative to the eval suggested by other answers, that makes the quoting much easier.

func() {  # set the caller's simple non-array variable
    local retvar=$1
    printf -v "$retvar"  '%s ' "${@:2}"  # concat all the remaining args
}

Bash-completion (the code that runs when you hit tab) has switched over to printf -v instead of eval for its internal functions, because it's more readable and probably faster.

For returning arrays, the Bash FAQ suggests using read -a to read into sequential array indices of an array variable:

# Bash
aref=realarray
IFS=' ' read -d '' -ra "$aref" <<<'words go into array elements'

Bash 4.3 introduced a feature that makes call-by-reference massively more convenient. Bash 4.3 is still new-ish (2014).

func () { # return an array in a var named by the caller
    typeset -n ref1=$1   # ref1 is a nameref variable.
    shift   # remove the var name from the positional parameters
    echo "${!ref1} = $ref1"  # prints the name and contents of the real variable
    ref1=( "foo" "bar" "$@" )  # sets the caller's variable.
}

Note that the wording of the bash man page is slightly confusing. It says the -n attribute can't be applied to array variables. This means you can't have an array of references, but you can have a reference to an array.

Solution 2

You cannot change the variable (or array in this case) inside the function because you pass only its content - function doesn't know which variable has been passed.

As a workaround you can pass the name of the variable and inside the functionevaluate it to get the content.

#!/bin/bash 

function delim_to_array() {
  local list=$1
  local delim=$2
  local oifs=$IFS;

  IFS="$delim"
  temp_array=($(eval echo '"${'"$list"'}"'))
  IFS=$oifs;

  eval "$list=("${temp_array[@]}")"            
}                                             

animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","
printf "NAME: %s\n" "${animal_list[@]}"

people_list="alvin|baron|caleb|doug"
delim_to_array "people_list" "|"
printf "NAME: %s\n" "${people_list[@]}"

Pay close attention to the quotes in the lines where eval is used. Part of the expression needs to be in single quotes, other part in double quotes. Additionally I've replaced the for loop to the simpler printf command in the final printing.

Output:

NAME: anaconda
NAME: bison
NAME: cougar
NAME: dingo
NAME: alvin
NAME: baron
NAME: caleb
NAME: doug
Share:
14,012

Related videos on Youtube

mackdoyle
Author by

mackdoyle

Updated on September 18, 2022

Comments

  • mackdoyle
    mackdoyle over 1 year

    I am trying to pass a "var name" to a function, have the function transform the value the variable with such "var name" contains and then be able to reference the transformed object by its original "var name".

    For example, let's say I have a function that converts a delimited list into an array and I have a delimited list named 'animal_list'. I want to convert that list to an array by passing the list name into the function and then reference, the now array, as 'animal_list'.

    Code Example:

    function delim_to_array() {
      local list=$1
      local delim=$2
      local oifs=$IFS;
    
      IFS="$delim";
      temp_array=($list);
      IFS=$oifs;
    
      # Now I have the list converted to an array but it's 
      # named temp_array. I want to reference it by its 
      # original name.
    }
    
    # ----------------------------------------------------
    
    animal_list="anaconda, bison, cougar, dingo"
    delim_to_array ${animal_list} ","
    
    # After this point I want to be able to deal with animal_name as an array.
    for animal in "${animal_list[@]}"; do 
      echo "NAME: $animal"
    done
    
    # And reuse this in several places to converted lists to arrays
    people_list="alvin|baron|caleb|doug"
    delim_to_array ${people_list} "|"
    
    # Now I want to treat animal_name as an array
    for person in "${people_list[@]}"; do 
      echo "NAME: $person"
    done
    
    • Admin
      Admin over 8 years
      So you want call-by-reference in bash, where you can pass a variable name for the function to put the result, instead of printing it to stdout like the usual calling convention.
    • Admin
      Admin over 8 years
      exactly, yes, thanks. Call by Reference is the feature I should have referenced in my question
  • Peter Cordes
    Peter Cordes over 8 years
    Are you sure you need an eval to IFS-split a string into an array? And if you do, couldn't you combine that eval with the one for doing indirection? (Also note that you can avoid this messy quoting with read -a, see my answer.) Also, local IFS=$2 would avoid the save/restore.
  • Peter Cordes
    Peter Cordes over 8 years
    @BinaryZebra: Yeah, typeset -n lets you leave out the eval, and just use any normal way of setting array elements, which makes it a LOT easier to see that you got the quoting right, and aren't interpreting data as code. (e.g. if there was a $(rm -rf /*) (or accidental non-malicious shell meta-characters) in something that you accidentally let the shell eval). I didn't want to go into details about splitting in my answer; your answer covered that nicely, and I upvoted it for that. Like you said, here be dragons :P. The part of this question I found interesting was the indirection.