1

If I define an array in bash shell:

a=()
a+=("A")
a+=("B")
a+=("C")

I can interact with it as expected:

echo "${a[0]}"
# Returns "A"

echo "${a[1]}"
# Returns "B"

But when I run that same array through a function, I must be do something wrong. First, I'll define my function:

function sort_array {
  declare -a array=("${!1}")
  local sorted=()

  sorted+=("1")
  sorted+=("2")
  sorted+=("3")

  echo "${sorted[@]}"
}

Now let's call it and inspect the results:

b=()
b=$(sort_array a[@])
echo "${b[0]}"

# Returns "1 2 3"
# But I'm expecting b[0] == 1

What am I doing wrong? I realize my example could remove the function parameter entirely, but my end goal is to write a bash sort_array() function that I can pass an array to and get an array back for.

5
  • What is being returned is the echo inside the function, not the result of b... it returns empty. Commented Feb 22, 2017 at 1:44
  • 1
    adding b=($b) right after the function call gives the expected result. A string assignment to array variable would place that value in the first index only. Commented Feb 22, 2017 at 1:47
  • 1
    @hbagdi that did it! I ultimately changed the assignment to b=($(sort_array a[@])). If you want to post that as an answer I'll accept it. Commented Feb 22, 2017 at 1:55
  • That doesn't work in general; it will split an element containing a space into multiple elements, which usually defeats the purpose of using an array. Commented Feb 22, 2017 at 2:02
  • I have read the question several times and the answers below, but there is something troubling me ... in all proposed solutions the final sorted array has absolutely nothing to do with the original array being passed in?? Also, there is no sorting of any kind going on either. It appears everyone has been looking at how to pass an array around in bash only. chepner did achieve at least something to do with the original array, but all other solutions, whilst setting an initial array to what was passed in, then create another array with no relationship at all and return it Commented Feb 22, 2017 at 4:37

2 Answers 2

6

bash does not have array values. The statement echo "${sorted[@]}" does not "return" an array value, it simply writes each element of the array to standard output, separated by a single space. (More specifically, the array expansion produces a sequence of words, one per element, that are then passed to echo as arguments.)

It is somewhat difficult to simulate in bash. You have to create a global array parameter, something you couldn't do inside a function until bash 4.2. Working with said array was difficult until namerefs were introduced in bash 4.3.

sort_array () {
    declare -n input=$1     # Local reference to input array
    declare -ga "$2"        # Create the output array
    declare -n output="$2"  # Local reference to output array

    # As a simple example, just reverse the array instead
    # of sorting it.
    n=${#input[@]}
    for((i=n-1; i>=0; i--)); do
        echo "*** ${input[i]}"
        output+=( "${input[i]}" )
    done
}

Now, you pass sort_array two arguments, the names of the input and output arrays, respectively.

$ a=("foo 1" "bar 2" "baz 3")
$ sort_array a b
$ echo "${b[0]}"
baz 3
Sign up to request clarification or add additional context in comments.

Comments

2

As @chepner said, bash doesn't have array values. When you pass an array to a function, what you're really doing is passing each element of the array as a separate argument to that function.

All shell functions can ever return is a one-byte exit code value, 0-255. The only way they can return anything else is to output it, with echo or printf or whatever; the caller then has to capture that output in any of the usual ways (command substitution, process substitution, redirection into a file to read, etc).

That said, your original code would work if you just added a bit of syntax to the call:

b=($(sort_array "${a[@]}"))

But that relies on the elements of the sorted array being strings that parse as individual words. A safer version would be to change the sort_array function to print out one element per line; the caller can then read those lines into an array using the mapfile builtin (alias readarray; requires Bash 4.x). That looks something like this:

function sort_array {
  declare -a array=("$@")
  local sorted=()

  sorted+=("1")
  sorted+=("2")
  sorted+=("3")

  printf '%s\n' "${sorted[@]}"
}
mapfile -t b < <(sort_array "${a[@]}")

That says to read the array b from the output of the command inside <(...); the -t tells it not to include the newlines in the array values.

Even safer would be to use null characters instead of newlines; easiest if you have bash 4.4 which added an option to mapfile to use a different character in lieu of newline:

function sort_array {
  declare -a array=("$@")
  local sorted=()

  sorted+=("1")
  sorted+=("2")
  sorted+=("3")

  printf '%s\0' "${sorted[@]}"
}
mapfile -t -d '\0' b < <(sort_array "${a[@]}")

4 Comments

Note that mapfile didn't get a -d option until bash 4.4, though.
Also, you are still treating $1 as a[@], but using "${a[0]}" in the example calls.
Thanks, @chepner. Fixed.
Thanks for this very helpful explanation! This both directly answers my question and offers a better solution overall.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.