Short version: yes, you can pass an array to a Bash function. You can also manipulate the array contents within the function to pass information back. It is easy and has been supported since Bash version 4.3, I believe.
1 2 3 4 5 6 7 8 9 10 11 | #!/usr/bin/env bash do_stuff() { local -n internal_array=${1} # reads and writes to $internal_array[] are applied to the array passed to the function } declare -A main_array # important, the name of the array is passed as an argument (i.e. without the $), not the array itself. do_stuff main_array |
Long version: I recently watched a talk by some very knowledgeable people about Bash, where they delved deep into its internals and quirks. At one point, they discussed passing information to and from functions without creating subshells. The solution became quite convoluted, and I was surprised because the whole time I was thinking, “just use nameref
“.
Out of curiosity, I searched online, but unfortunately, the internet is full of responses like “doesn’t work,” “Bash can’t do that” and many variations of “just pass all the values of the array to the function as arguments and piece them together again inside the function” (which is a terrible solution since you lose the keys). There are a few posts here and there suggesting local -n
as a solution, but they are rare, and especially on sites like Stack Overflow, they are not the top answers.
In a nutshell what we are going to do is pass a reference to an array to a function (think “pointers” or “symlinks” if that helps).
Relevant parts of the bash man page for declare
and local
:
declare -n
Give each name the
nameref
attribute, making it a name reference to another variable. That other variable is defined by the value of name. All references, assignments, and attribute modifications to name, except for those using or changing the -n attribute itself, are performed on the variable referenced by name’s value. The nameref attribute cannot be applied to array variables.
local
For each argument, a local variable named name is created, and assigned value. The option can be any of the options accepted by
declare
.local
can only be used within a function; it makes the variable name have a visible scope restricted to that function and its children.
So, in summary, we can use local
to define variables with a scope limited to the function they are defined in, and local
accepts all the options that declare
supports.
It seems the “The nameref attribute cannot be applied to array variables.
” part of the declare
definition causes a lot of confusion or deters people from trying to use it for referencing arrays.
What it means is that you can’t do a local -n my_array=()
(i.e. applying the nameref attribute to an array), but local -n my_array
is fine (where my_array is a variable with the nameref attribute which can also point to an array).
Enough theory, let’s get down to practical examples.
We will create a function called do_stuff
that:
- takes the name of an array as argument
$1
- reads the
length
key from the array - add a
random
key to the array with a random number the length of thelength
key previously read
Then we will create an array outside of the function with some keys/values, pass it to the do_stuff
function, and then output the contents
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #!/usr/bin/env bash do_stuff() { local -n internal_array=${1} local length # read values from the array passed to the function length=${internal_array["length"]} # add an entry to the array internal_array["random"]="${RANDOM:0:$length}" } declare -A main_array main_array[name]="one" main_array[length]="5" # important, the name of the array is passed as an argument (i.e. without the $), not the array itself. do_stuff main_array # easier to use declare to print the key/values of the associative array then iterating over all keys and printing the respective values declare -p main_array |
1 | declare -A main_array=([length]="5" [random]="25778" [name]="one" ) |
So this shows us that the do_stuff
function could read from the array (the length value), and write to the array (add the random number key/value), and the changes were applied to the array outside of the function. (where we did the declare -p
). Bonus points for not needing a subshell.
Using this “trick” allows us to pass more complex information to a function, and especially receive more complex information from a function.
There is one caveat: you can’t use the same name for the array inside the function as well as outside. I wouldn’t advise doing this anyway for readability reasons, as the variable’s scope can become confusing. If you try it you get the following output:
1 | local: warning: array: circular name reference |
Oddly I often noticed the following statement on Stack Overflow about nameref
:
“This only works if the array is defined as a global”
Nope, works just as fine passing an array locally scoped to a function to another function this way.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #!/usr/bin/env bash do_stuff() { local -n internal_array=${1} local length # read values from the array passed to the function length=${internal_array["length"]} # add an entry to the array internal_array["random"]="${RANDOM:0:$length}" } main() { local -A main_array main_array[name]="one" main_array[length]="5" # important, the name of the array is passed as an argument (i.e. without the $), not the array itself. do_stuff main_array declare -p main_array } main |
1 | declare -A main_array=([length]="5" [random]="28903" [name]="one" ) |
(I prefer using a main() function like in this example to avoid global variables unless explicitly defined)
So, there you have it: an easy way to pass an array to a function in bash, no weird looping over values. And a better way to receive information from an array than the byte return value and parsing the output of the function.