174

Struggling for a while passing an array as argument but it's not working anyway. I've tried like below:

#! /bin/bash

function copyFiles{
   arr="$1"
   for i in "${arr[@]}";
      do
          echo "$i"
      done

}

array=("one" "two" "three")

copyFiles $array

An answer with explanation would be nice.

Edit: Basically, i will eventually call the function from another script file. Plz explain the constraints if possible.

1

11 Answers 11

237
  • Expanding an array without an index only gives the first element, use

    copyFiles "${array[@]}"
    

    instead of

    copyFiles $array
    
  • Use a she-bang

    #!/bin/bash
    
  • Use the correct function syntax

    Valid variants are

    function copyFiles {…}
    function copyFiles(){…}
    function copyFiles() {…}
    

    instead of

    function copyFiles{…}
    
  • Use the right syntax to get the array parameter

    arr=("$@")
    

    instead of

    arr="$1"
    

Therefore

#!/bin/bash
function copyFiles() {
   arr=("$@")
   for i in "${arr[@]}";
      do
          echo "$i"
      done

}

array=("one 1" "two 2" "three 3")

copyFiles "${array[@]}"

Output is (my script has the name foo)

$ ./foo   
one 1
two 2
three 3
7
  • thanks, but isn't function copyFiles{…} a correct syntax? Though i am a new bie i run some program successfully with the syntax. Commented Sep 15, 2015 at 11:11
  • 2
    Valid variants are copyFiles {…} and copyFiles(){…} and copyFiles() {…}, but not copyFiles{…}. Note the space in the variant without ()
    – A.B.
    Commented Sep 15, 2015 at 11:14
  • 1
    among the variants, there's copyFiles() { ... } without a function keyword. Added benefit : also valid in POSIX (unix.stackexchange.com/questions/73750/…)
    – YoungFrog
    Commented Mar 28, 2020 at 11:04
  • 1
    In this particular case you can do for i; do ...; done to iterate over $@. Also, this wasn't asked, but if you need to pass a couple of arrays, you probably want to pass by reference.
    – x-yuri
    Commented Jan 12, 2021 at 19:18
  • Shebang is spelled without the dash. Commented Aug 30, 2021 at 7:11
113

If you want to pass one or more arguments AND an array, I propose this change to the script of @A.B.
Array should be the last argument and only one array can be passed

#!/bin/bash
function copyFiles() {
   local msg="$1"   # Save first argument in a variable
   shift            # Shift all arguments to the left (original $1 gets lost)
   local arr=("$@") # Rebuild the array with rest of arguments
   for i in "${arr[@]}";
      do
          echo "$msg $i"
      done
}

array=("one" "two" "three")

copyFiles "Copying" "${array[@]}"

Output:

$ ./foo   
Copying one
Copying two
Copying three
6
  • 9
    +1 for learning about an array needing to be at the end and that only one should be sent Commented Sep 21, 2018 at 11:03
  • 9
    It's also useful to use shift's argument sometimes, so if you had 6 arguments before the array, you can use shift 6. Commented Jul 4, 2019 at 17:49
  • You convert "the rest of arguments" into arr. Is it possible to have an array parameter in the middle? Or even several arrays parameters? function copyAndMove() { msg1=$1 ; arr1=...?... ; msg2=? ; arr2=...?... ; msg3=? ; ... }. Like I would define it in python: def copyAndMove(msg1="foo", cpFiles=[], msg2="bar", mvFiles=[], msg3="baz"): .... Never mind, I found stackoverflow.com/a/4017175/472245
    – towi
    Commented Oct 25, 2019 at 7:07
  • Explained really well. and didn't know about shift. Thanks.
    – Sannu
    Commented May 8, 2020 at 22:59
  • 5
    You can also achieve same behavior without using shift by doing this: local msg="$1"; local -a arr=( "${@:2}" ) Commented May 28, 2020 at 1:32
47

You could also pass the array as a reference. i.e.:

#!/bin/bash

function copyFiles {
   local -n arr=$1

   for i in "${arr[@]}"
   do
      echo "$i"
   done
}

array=("one" "two" "three")

copyFiles array

but note that any modifications to arr will be made to array.

7
  • 5
    Though, it wasn't exactly what i want, but it still nice to know how pass by reference work in bash. +1 :) Commented Sep 15, 2015 at 12:27
  • 13
    Requires bash 4.3+
    – dtmland
    Commented May 4, 2019 at 18:47
  • 1
    Bash is not Haskell. You've got to compromise. On a different note, it makes sense to do [ "$1" = arr ] || local -n arr=$1 in place of just local -n arr=$1. That way the argument and the parameter names can coincide. As far as I can tell, the scope in bash is dynamic.
    – x-yuri
    Commented Jan 12, 2021 at 19:13
  • 1
    @Sapphire_Brick why would an array named n be a circular reference? If you changed the name array to n and called the function e.g. copyFiles n then inside the function the local variable arr is created effectively as an alias or synonym of n. local -n arr also means a locally (function) scoped variable so even if you called that n as well it wouldn't be circular. The fact that changing the arr inside the function also changes the value of array outside the function is not circular either, it's because both names ultimately point to values in the same address space.
    – Davos
    Commented Jun 10, 2021 at 13:28
  • 1
    This is quite helpful. I wonder, though, about the statement in man bash-builtins (for the declare -n option, which local incorporates): "The -n attribute cannot be applied to array variables." I don't know enough yet to know what to make of this statement in light of this answer. Can someone comment?
    – ebsf
    Commented Jun 3, 2022 at 18:06
13

There are couple of problems. Here is the working form :

#!/bin/bash
function copyFiles {
   arr=( "$@" )
   for i in "${arr[@]}";
      do
          echo "$i"
      done

}

array=("one" "two" "three")
copyFiles "${array[@]}"
  • There need to be at least a space between function declaration and {

  • You can not use $array, as array is an array not a variable. If you want to get all the values of an array use "${array[@]}"

  • In you main function declaration you need arr="$@" as "${array[@]}" will expand to the indexed values separated by spaces, if you use $1 you would get only the first value. To get all the values use arr="${arr[@]}".

5
  • 1
    You need arr=("$@")
    – A.B.
    Commented Sep 15, 2015 at 9:38
  • To see the difference, add a break below echo "$i". In your version you will still see all the elements. However, it should be three lines.
    – A.B.
    Commented Sep 15, 2015 at 9:43
  • @heemayl: small typo -- The { in your array of the second bullet went missing ... "${array[@]}" ...
    – Cbhihe
    Commented Sep 15, 2015 at 17:36
  • At the very end of the last bullet point is there also a missing {, or is this intentional arr="$arr[@]}" ?
    – Davos
    Commented Jun 10, 2021 at 14:15
  • 1
    @Davos Typo. Fixed.
    – heemayl
    Commented Jun 10, 2021 at 15:47
4

Here follows a slightly larger example. For explanation, see the comments in the code.

#!/bin/bash -u
# ==============================================================================
# Description
# -----------
# Show the content of an array by displaying each element separated by a
# vertical bar (|).
#
# Arg Description
# --- -----------
# 1   The array
# ==============================================================================
show_array()
{
    declare -a arr=("${@}")
    declare -i len=${#arr[@]}
    # Show passed array
    for ((n = 0; n < len; n++))
    do
        echo -en "|${arr[$n]}"
    done
    echo "|"
}

# ==============================================================================
# Description
# -----------
# This function takes two arrays as arguments together with their sizes and a
# name of an array which should be created and returned from this function.
#
# Arg Description
# --- -----------
# 1   Length of first array
# 2   First array
# 3   Length of second array
# 4   Second array
# 5   Name of returned array
# ==============================================================================
array_demo()
{
    declare -a argv=("${@}")                           # All arguments in one big array
    declare -i len_1=${argv[0]}                        # Length of first array passad
    declare -a arr_1=("${argv[@]:1:$len_1}")           # First array
    declare -i len_2=${argv[(len_1 + 1)]}              # Length of second array passad
    declare -a arr_2=("${argv[@]:(len_1 + 2):$len_2}") # Second array
    declare -i totlen=${#argv[@]}                      # Length of argv array (len_1+len_2+2)
    declare __ret_array_name=${argv[(totlen - 1)]}     # Name of array to be returned

    # Show passed arrays
    echo -en "Array 1: "; show_array "${arr_1[@]}"
    echo -en "Array 2: "; show_array "${arr_2[@]}"

    # Create array to be returned with given name (by concatenating passed arrays in opposite order)
    eval ${__ret_array_name}='("${arr_2[@]}" "${arr_1[@]}")'
}

########################
##### Demo program #####
########################
declare -a array_1=(Only 1 word @ the time)                                       # 6 elements
declare -a array_2=("Space separated words," sometimes using "string paretheses") # 4 elements
declare -a my_out # Will contain output from array_demo()

# A: Length of array_1
# B: First array, not necessary with string parentheses here
# C: Length of array_2
# D: Second array, necessary with string parentheses here
# E: Name of array that should be returned from function.
#          A              B             C              D               E
array_demo ${#array_1[@]} ${array_1[@]} ${#array_2[@]} "${array_2[@]}" my_out

# Show that array_demo really returned specified array in my_out:
echo -en "Returns: "; show_array "${my_out[@]}"
2

As ugly as it is, here is a workaround that works as long as you aren't passing an array explicitly, but a variable corresponding to an array:

function passarray()
{
    eval array_internally=("$(echo '${'$1'[@]}')")
    # access array now via array_internally
    echo "${array_internally[@]}"
    #...
}

array=(0 1 2 3 4 5)
passarray array # echo's (0 1 2 3 4 5) as expected

I'm sure someone can come up with a cleaner implementation of the idea, but I've found this to be a better solution than passing an array as "{array[@]"} and then accessing it internally using array_inside=("$@"). This becomes complicated when there are other positional/getopts parameters. In these cases, I've had to first determine and then remove the parameters not associated with the array using some combination of shift and array element removal.

A purist perspective likely views this approach as a violation of the language, but pragmatically speaking, this approach has saved me a whole lot of grief. On a related topic, I also use eval to assign an internally constructed array to a variable named according to a parameter target_varname I pass to the function:

eval $target_varname=$"(${array_inside[@]})"
4
  • 1
    That is ugly and unncessary. If you want to pass the array by name, make array_internally an alias of it: declare -n array_internally=$1. And the rest of it about "becomes complicated" and "determine and then remove ..." applies to irrespective of how you pass the array, so I don't see what's the point of that. And evaling an array potentially containing special characters is just waiting for grief to happen at a later date.
    – muru
    Commented Jul 12, 2018 at 4:04
  • Just posted a proposition : pass one or multiple strings and make them an array inside the function.
    – tisc0
    Commented Feb 7, 2020 at 9:28
  • @muru, what do you make of the statement in man bash-builtins (for the declare -n option, which local incorporates): "The -n attribute cannot be applied to array variables." I don't know enough yet to understand how this fits, except to know that absent other input such as your suggestion, the statement would, in my ignorance, discourage me from using the -n option in this situation.
    – ebsf
    Commented Jun 3, 2022 at 18:13
  • @ebsf I believe what it means is that you can't have something like declare -an foo=(...) where a variable is both an array and a nameref. Having an array foo and then doing declare -n bar=foo has worked fine for years, e.g., another answer from 7 years ago on this post, or this answer from me on U&L from 5 years ago
    – muru
    Commented Jun 4, 2022 at 2:38
1

The best way is to pass as position arguments. Nothing else. You may pass as string, but this way may cause some troubles. Example:

array=(one two three four five)

function show_passed_array(){
  echo $@
}

or

function show_passed_array(){
  while $# -gt 0;do
    echo $1;shift
  done
}

    show_passed_array ${array[@]}

output:

  one two three four five

You mean if array value has space symbols you must quote elements first before pass for accessing value by index in function use $1 $2 $3 ... position parameters. Where index 0 -> 1, 1 -> 2,... To iterate access it is best to use always $1 and after Shift. Nothing additional is needed. You may pass arguments without any array like this:

show_passed_array one two three four five

bash media automatically builds an array from passed arguments that passed them to function and then you have position arguments. Furthermore when you write ${array[2]} you really write consequent argument one two three four and passed them to the function. So those calls are equivalent.

1

I propose to avoid any limitation about "passing array(s) args" to a function by... passing strings, and make them array INSIDE the function :

#!/bin/bash

false_array1='1 2 3 4 5'
false_array2='my super fake array to deceive my function'
my_normal_string='John'

function i_love_arrays(){
  local myNumbers=("$1")
  local mySentence=("$2")
  local myName="$3"
  echo "My favorite numbers are, for sure: "
  for number in ${myNumbers[@]}
  do
    echo $number
  done
  echo "Let's make an ugly split of a sentence: "
  for word in ${mySentence[@]}
  do
    echo $word
  done
  echo "Yeah, I know, glad to meet you too. I'm ${myName}."
}

i_love_arrays "${false_array1}" "${false_array2}" "${my_normal_string}"
1

Using references (easiest way; best and only way for passing associative arrays):

print()
{
    [ "$1" = "arr" ] || { declare -n arr; arr="$1"; }
    # The name of reference mustn't match the input coming to the function.
    # If it does, use the name directly as array instead of reference.
    
    echo "${arr[@]}"
}

print arr

For normal (non-associative) arrays:

Here is a function that copies the passed array in new (local) array for usage. Can be time-taking for long arrays.

copy_fn()
{
    eval local newarr=\(\${$1[@]}\)
    echo ${newarr[@]}
}

Further, like in C++ you can only pass an array pointer, but in python, as a shared variable which you can modify as well.

Here is the shared variables approach.. useful for long arrays.

modify_arr()
{
    eval local ptr=$1        # works as if ptr is a pointer to actual "array-name" (arr)  # the original array is, thus, still same, not a copy
    eval $ptr+=(1)           # modify the array
    echo ${ptr[@]}           # will print name of the array (arr)
    eval echo \$\{$ptr[@]\}  # will print the actual array (arr)
}

modify_arr a
echo ${a[@]}

-Himanshu

2
  • What I like about your answer is the [ "$1" = arr ] || local -n arr=$1 part. The rest... I don't think you put enough effort for it to be readable. Also, your examples lack quoting. And I doubt the rest of the ideas are practical.
    – x-yuri
    Commented Jan 12, 2021 at 18:54
  • Thanks for feedback. I'll consider improvements in my further answers. I put too much unnecessary effort in explaining everything I guess. Commented Jan 25, 2021 at 9:29
0

@SBF

Your idea sparked some thinking on a situation I needed where two arrays were to be compared.

You can pass multiple arrays using your technique with a minor modification. See below

Thank you!

(works in bash 3.3 on Debian)

#!/bin/bash
#
function TakeTwoArrays() {
    local Var1="$1"
    local Var2="$2"
    local NumArray1="$3"
    local NumArray2="$4"
    shift 4
    local -a AllArray=("$@")
    local -a Array1="${AllArray[@]:0:${NumArray1}}"
    local -a Array2="${AllArray[@]:${NumArray1}}"

    echo "Var1 is ${Var1}"
    echo "Var2 is ${Var2}"
    echo "Array1 has ${NumArray1} entries:"
    printf '%s\n' "${Array1[@]}"
    echo "Array2 has ${NumArray2} entries:"
    printf '%s\n' "${Array2[@]}"
}
FirstArg="So Far"
SecondArg="So Good"
Array1Arg=("But" "the" "real" "question" "is")
NumArray1Arg="${#Array1Arg[@]}"
Array2Arg=("What" "happens" "with" "the" "second" "set" "of" "elements")
NumArray2Arg="${#Array2Arg[@]}"
TakeTwoArrays "${FirstArg}" "${SecondArg}" "${NumArray1Arg}" "${NumArray2Arg}" "${Array1Arg[@]}" "${Array2Arg[@]}"
0

No you cannot pass arguments to a function in Bash. Arrays are not really first class objects and all of the solutions above just end up being workarounds with lots of leaky abstractions.

The key issue you will find is that the array is expanded and all indexes are lost when passed. So for instance, having an array with empty strings is not possible to pass into another function: they will all be lost on expansion.

There are workaround, like adding separators, and this Unix Stackexchange answer has some more background, but essentially the answer is that Bash does not support it directly.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .