Bash function with an array input and output

2020-05-01 05:41发布

问题:

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.

回答1:

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[@]}")


回答2:

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