21

Because shells other than ksh do not support pass-by-reference, how can multiple arrays be passed into a function in bash without using global variables, and in a way which allows any legal variable content to be included as an array element (no reserved sigils)?

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441

3 Answers3

41

Since bash 4.3

As of 2016, modern bash supports pass-by-reference (a.k.a nameref attribute) as:

demo_multiple_arrays() {
  local -n _array_one=$1
  local -n _array_two=$2
  printf '1: %q\n' "${_array_one[@]}"
  printf '2: %q\n' "${_array_two[@]}"
}

array_one=( "one argument" "another argument" )
array_two=( "array two part one" "array two part two" )

demo_multiple_arrays array_one array_two

See also declare -n in the man page.


Before bash 4.3

This can be done safely by using a calling convention which puts number-of-arguments before each array, as such:

demo_multiple_arrays() {
  declare -i num_args array_num;
  declare -a curr_args;
  while (( $# )) ; do
    curr_args=( )
    num_args=$1; shift
    while (( num_args-- > 0 )) ; do
      curr_args+=( "$1" ); shift
    done
    printf "$((++array_num)): %q\n" "${curr_args[@]}"
  done
}

This can then be called as follows:

array_one=( "one argument" "another argument" )
array_two=( "array two part one" "array two part two" )
demo_multiple_arrays \
  "${#array_one[@]}" "${array_one[@]}" \
  "${#array_two[@]}" "${array_two[@]}"
Dave Jarvis
  • 30,436
  • 41
  • 178
  • 315
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Note: Array's names can't be the same inside and outside of the functions. Also can't be the same between functions. – Anastasios Andronidis Nov 13 '20 at 11:12
  • @AnastasiosAndronidis, the inside/outside problem is why I used the underscore-prefixed names here, to reduce collision potential. – Charles Duffy Nov 13 '20 at 15:18
  • @AnastasiosAndronidis, ...however, if "can't be the same between functions" is a problem for you except where those names are the specific ones being passed between functions, that implies that you need to be declaring things `local` more aggressively. – Charles Duffy Nov 13 '20 at 15:19
  • 1
    @AnastasiosAndronidis, btw, for an example of how I manage this in production code, see the naming convention in https://github.com/charles-dyfis-net/declarative.bash/blob/master/declarative.bash -- using a prefix to namespace all variables used by the library. – Charles Duffy Nov 13 '20 at 15:20
5

I found a concise way to pass multiple arrays to a function. It isn't a perfect solution (because it doesn't work if any of the elements in your array contains a space), but I thought it might be helpful to share for anyone still using a Bash version older than 4.3 like me.

Declaring function:

myFunction()
{
  IFS=' ' read -ra array1 <<< "${1}"
  IFS=' ' read -ra array2 <<< "${2}"
  
  echo "first array (length=${#array1[@]}): ${array1[@]}"
  echo "second array (length=${#array2[@]}): ${array2[@]}"
}

Passing two distinct arrays to function (as space-separated strings):

declare -a firstArray=(1 2 3 4 5)
declare -a secondArray=(one two three)

myFunction "${firstArray[*]}" "${secondArray[*]}"

Output:

first array (length=5): 1 2 3 4 5
second array (length=3): one two three

Although this solution isn't perfect, it made it easier for me to pass additional non-array arguments to my function. (I found that using nested while-loops to sort out multiple arrays in addition to other non-array arguments got messy really fast.)

yellowfellow
  • 51
  • 1
  • 1
  • If your data were this simple, there would be no reason to use a real array in the first place. The _whole point_ of arrays is that they preserve argument boundaries accurately. – Charles Duffy Jul 22 '22 at 21:38
1

Can also be done using eval:

declare -a a=( "aa bb" 123 '$ $ $' )
declare -a b=( "bb cc" 456 '###' )

printf "\n%s\n" 'a before sub:'
printf "'%s'\n" "${a[@]}"
printf "\n%s\n" 'b after sub:'
printf "'%s'\n" "${b[@]}"


sub ()
{
  eval a0=\${$1[0]}                     # get value a[0]
  eval b1=\${$2[1]}                     # get value b[1]
  echo "a[0] = '$a0'"
  echo "b[1] = '$b1'"

  eval $1[0]='a---a'                    # set value a[0]
  eval $2[1]=999                        # set value b[1]

} # ----------  end of function sub  ----------

sub a b     # call function sub

printf "\n%s\n" 'a after sub:'
printf "'%s'\n" "${a[@]}"
printf "\n%s\n" 'b after sub:'
printf "'%s'\n" "${b[@]}"

The output:

a before sub:
'aa bb'
'123'
'$ $ $'

b after sub:
'bb cc'
'456'
'###'
a[0] = 'aa bb'
b[1] = '456'

a after sub:
'a---a'
'123'
'$ $ $'

b after sub:
'bb cc'
'999'
'###'
Fritz G. Mehner
  • 16,550
  • 2
  • 34
  • 41
  • 1
    To be safe, this should be using `printf %q` to quote rather than assuming that `printf "'%s'"` will be safe. See http://mywiki.wooledge.org/BashFAQ/048 for discussion of security issues around eval. – Charles Duffy Jun 08 '12 at 20:00
  • As a concrete example, consider `declare -a a=( $'\'$(touch /tmp/i-could-have-been-rm-rf-~)\'' )` -- you don't want the `printf` to make it `''$(touch /tmp/i-could-have-been-rm-rf-~)''`, such that the quotes inside the data are cancelling out the ones added by the `printf`. – Charles Duffy Dec 01 '17 at 15:49