1

I have a code snippet to print out an array in a shell script:

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

I wanted to create a function out of it

printArray() {
    for i in "${$1[@]}"; do
      echo "$i"
      done
}

but when I call my function with the array name (which is also available in the shell script), I get an error: ${$1[@]}: bad substitution

What I found out ist that curly braces expand first, probably trying to expand "$1[@]" literally.

I only found answers for numeric expansion like from 1 to 5. So is it possible to replace an array name with a variable inside curly braces?

I expect to be able to put a variable instead of a specific array name in my function.

Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
eifcgn24
  • 23
  • 6
  • You're probably looking for `nameref` via `declare -n` – Jetchisel Mar 30 '23 at 08:32
  • You several answers now. Please evaluate each once carefully for your needs, and upvote each one you find useful or helpful. Mark correct whichever one best answers your question, assuming one does, even if it's your own answer. – Gabriel Staples Mar 30 '23 at 19:03

4 Answers4

1

You can make use of the nameref attribute (introduced with version 4.3 of bash) using the -n option to the declare or local builtin commands to accomplish this job:

#!/bin/bash

printArray() {
    if [[ $1 != 'aref' ]]; then
        local -n aref=$1 || return
    fi
    printf '%s\n' "${aref[@]}"
}

echo '*** array=( -n -e -E ); printArray array ***'
array=( -n -e -E ); printArray array

echo '*** aref=( 8 9 10 ); printArray aref ***'
aref=( 8 9 10 ); printArray aref

echo '*** i=( 8 9 10 ); printArray i; declare -p i ***'
i=( 8 9 10 ); printArray i; declare -p i

Search for nameref in the Bash Reference Manual.

M. Nejat Aydin
  • 9,597
  • 1
  • 7
  • 17
  • 1
    The `printArray` function can fail in a few ways. Try: `array=( -n -e -E ); printArray array`. Try: `aref=( 8 9 10 ); printArray aref`. Try: `i=( 8 9 10 ); printArray i; declare -p i`. – pjh Mar 30 '23 at 17:28
  • @pjh I've modified the answer. – M. Nejat Aydin Mar 30 '23 at 17:55
  • 1
    There's still a minor issue in that the function will print an empty line (instead of nothing) if given an empty array, but that probably doesn't matter. Upvoted. – pjh Mar 30 '23 at 20:02
1

Here's how I pass and print bash arrays:

Print a bash array by passing parameters by value

arrays_join_and_print.sh from my eRCaGuy_hello_world repo:

Update: even better, see: array_print.sh now, which I just wrote. Sourcing (importing) array_print.sh via . array_print.sh gives you direct access to my two print functions below.

# Function to print all elements of an array.
# Example usage, where `my_array` is a bash array:
#       my_array=()
#       my_array+=("one")
#       my_array+=("two")
#       my_array+=("three")
#       print_array "${my_array[@]}"
print_array() {
    for element in "$@"; do
        printf "    %s\n" "$element"
    done
}

# Usage

# Build an array
array1=()
array1+=("one")
array1+=("two")
array1+=("three")

# Print it
echo "array1:"
print_array "${array1[@]}"

Sample output:

array1:
    one
    two
    three

Print a bash array by passing the array by reference

print_array2() {
    # declare a local **reference variable** (hence `-n`) named `array_ref`
    # which is a reference to the first parameter passed in
    local -n array_ref="$1"

    for element in "${array_ref[@]}"; do
        printf "    %s\n" "$element"
    done
}

# Usage

print_array2 "array1"
# or
print_array2 array1

Output: same as above.

Going further:

For passing arrays as parameters in Bash, I present 3 solutions which can also be used for printing arrays:

  1. [my favorite] Pass arrays by reference (regular arrays and associative bash arrays)
  2. Manually serialize and deserialize arrays
  3. For associative arrays: pass by reference and manually serialize/deserialize
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • If anything on the call chain of the `print_array` function uses a variable called `element`, its value may be changed unexpectedly by the call. This is a very common cause of difficult-to-debug errors in shell code. – pjh Mar 30 '23 at 20:26
  • 2
    The `print_array2` function will fail if given an array called `array_ref` or an array called `element`. It's also worth noting that it won't work at all with versions of Bash prior to 4.3 (which introduced namerefs). – pjh Mar 30 '23 at 20:29
  • @pjh, interesting. I was aware of the Bash version issue for passing by reference, but not the variable name overlap problem. – Gabriel Staples Mar 30 '23 at 20:41
  • Seems like the solution for most bash problems is "don't use bash." :) But, I find it much easier and faster to use than Python when stringing together command-line commands, and therefore I use Bash much more often than Python. – Gabriel Staples Mar 30 '23 at 20:42
0

I posted my code in the following shell check: https://www.shellcheck.net/

And it gave me the following answer: https://www.shellcheck.net/wiki/SC2082

# Expand variable names dynamically
var_1="hello world"
n=1
name="var_$n"
echo "${!name}"

So I first expanded array name and then put it with an exclamation mark in curly braces. After this the error disappeared and the array got printed out.

printArray() {
    array="$1[@]"
    for i in "${!array}"; do
      echo "$i"
    done
}
eifcgn24
  • 23
  • 6
  • 1
    The `printArray` function can fail in a few ways. Try: `arr=(-n -e -E); printArray arr`. Try: `array=(8 9 10); printArray array`. Try: `i=(8 9 10); printArray i; declare -p i`. – pjh Mar 30 '23 at 17:23
0

This Shellcheck-clean code should work with any version of Bash:

#! /bin/bash -p

printArray() {
    [[ -z ${1-} || $1 == [[:digit:]]* || $1 == *[^[:alnum:]_]* ]] && return 1
    set -- "$1[@]"
    if [[ -o nounset ]]; then
        set +o nounset
        set -- "${!1}"
        set -o nounset
    else
        set -- "${!1}"
    fi
    (( $# == 0 )) || printf '%s\n' "$@"
}
  • To avoid variable name clashes, the function uses no variables except for the positional parameters ($1, $2, ...).
  • [[ -z ${1-} || $1 == [[:digit:]]* || $1 == *[^[:alnum:]_]* ]] && return 1 ensures that the argument ($1) is a valid variable name. This is necessary because several Bash mechanisms for accessing variables through other variables (including indirect expansion, namerefs, and eval) can cause code injection if they are used with invalid variable names.
  • set -- "$1[@]" replaces the first positional parameter ($1) (e.g. arrname) with a string that can be used with indirect expansion to expand to the list of elements in an array (e.g. arrname[@]).
  • set -- "${!1}" replaces the positional parameters with the elements of the array.
  • The code relating to nounset is necessary because of a long-standing bug in Bash. Using set -o nounset (or set -u) in Bash code causes it to treat accesses to unset variables as errors. Some people and teams do that for all Bash programs because it can help to avoid bugs due to uninitialized variables. However, the feature has historically caused problems for code that uses arrays because Bash, incorrectly, treats empty arrays as uninitialized variables in some contexts. Most of the problems were fixed with Bash 4.4, but I've found that they persist with indirect expansions even in Bash 5. set -- "${!1}" causes an error if the referenced array is empty and nounset is enabled. The code works around the problem by disabling nounset before doing the expansion and re-enabling it afterwards, if it was enabled already.
  • Finally, the codes uses printf to print all of the positional parameters, one-per-line. The (( $# == 0 )) test is necessary to prevent a blank line being printed if the array is empty (and so there are no positional parameters).

WARNING

Although I think the function above is very robust, I don't think that it is useful. The output of the function may not give you an accurate representation of what is in the array. For instance, it will print the same output for both of these arrays: array=(1 2 3) (3 elements), and array=($'1\n2\n3') (1 element).

Bash has a built-in mechanism for printing array contents in an unambiguous way: declare -p arrname. I would always use that instead of an array-printing function.

pjh
  • 6,388
  • 2
  • 16
  • 17