5

In bash, I've an array with values:

declare -A my_array
my_array['key1']='value1'
my_array['key2']='value2'

echo "-----my_array-----"
echo key1: ${my_array['key1']}
echo key2: ${my_array['key2']}

what works well:

-----my_array----- 
key1: value1 
key2: value2

Although, I'd like to have for a key, a list of values. I've tried something like:

my_array['key2']=('value2.1', 'value2.2')  

this gives the error:

my_array['key2']: cannot assign list to array member

What is the Syntax to add a list into an array element and then to access an index values for a key?

4
  • 2
    bash arrays/lists are one dimensional. Commented Feb 24, 2022 at 0:45
  • 4
    It's time to use a different language, one with actual support for data structures. Commented Feb 24, 2022 at 0:48
  • 1
    @TedLyngmo Lists in most languages are one-dimensional; the difference is that the values they store aren't restricted to strings, as in bash. Commented Feb 24, 2022 at 0:49
  • Does this answer your question? Multi-dimensional arrays in Bash Commented Feb 24, 2022 at 0:55

2 Answers 2

2

Sub arrays in bash

No. Thechnically, shell variables are only intended to hold strings. But...

Creating an array of arrays (even associative), under

In this answer, I will use disk free's output (df -k) to create an associative array, indexed by device. As tmpfs pseudo device is used for many differents behaviour, this field have to contain a list.

1. Simpliest way, using special delimiter.

Here I use spaces to delimit contents of fields as they are space delimited by command. Then I use bell special character (\007) to delimit sub arrays.

Here is a sample, using df output, sorting creating lists by device. As result, all tmpfs will be stored together:

declare -A dfVars='()'
{
    read _;
    while read -r dev _ use free _ mpnt ;do
        dfVars["${dev##*/}"]+="$use $((use+free)) $mpnt"$'\07'
    done
} < <(
    LANG=C df -lk
)

You could parse:

for dev in ${!dfVars[@]} ;do
    IFS=$'\a\n' read -d '' -ra fields <<<"${dfVars["$dev"]%$'\a'}"
    for line in "${fields[@]}";do
        read -r use tot mpnt <<<"$line"
        printf "%-20s %11d %11d  %s\n" $dev $use $tot $mpnt
    done
done

May output someting like:

tmpfs                          0      188928  /dev/shm
tmpfs                      19284      188928  /run
tmpfs                          4        5120  /run/lock
tmpfs                          0      188928  /sys/fs/cgroup
tmpfs                          0       37784  /run/user/33
tmpfs                          0       37784  /run/user/1001
devtmpfs                       0      184596  /dev
mmcblk0p1                  23193       42136  /boot
root                   115253500   117652696  /

Everything are stored into $dfVars:

declare -p dfVars
declare -A dfVars=([tmpfs]=$'0 188928 /dev/shm\a19284 188928 /run\a4 5120 /run/l
ock\a0 188928 /sys/fs/cgroup\a0 37784 /run/user/33\a0 37784 /run/user/1001\a' [d
evtmpfs]=$'0 184596 /dev\a' [mmcblk0p1]=$'23193 42136 /boot\a' [root]=$'11529034
8 117652696 /\a' )

Note: The separator I entered \07 is shown as \a in this output.

2. Stronger: Using nameref for indexing arrays:

With this method, you create independant variables, wich could be array, associative array or even strings...

uuvar() { printf -v "$1" %s%09d "$2" $((++uuVarCnt));}
declare -A dfVars='()'
{
     read _
     while read -r dev _ use free _ mpnt ;do
         uuvar vname _df_
         dfVars["${dev##*/}"]+=$vname' ' 
         read -a $vname <<<"$use $((use+free)) $mpnt"
         done
} < <(LANG=C df -lk)

To parse this:

for dev in ${!dfVars[@]} ;do
    for sub in ${dfVars["$dev"]};do
        declare -n subDf=$sub
        printf "%-20s %11d %11d  %s\n" "$dev" "${subDf[@]}"
    done
done
tmpfs                          0      188928  /dev/shm
tmpfs                      19284      188928  /run
tmpfs                          4        5120  /run/lock
tmpfs                          0      188928  /sys/fs/cgroup
tmpfs                          0       37784  /run/user/33
tmpfs                          0       37784  /run/user/1001
devtmpfs                       0      184596  /dev
mmcblk0p1                  23193       42136  /boot
root                   115254628   117652696  /

Where $dfVars and sub variables look like:

declare -p dfVars ${!_df_*}
declare -A dfVars=([tmpfs]="_df_000000003 _df_000000004 _df_000000005 _df_000000
006 _df_000000008 _df_000000009 " [devtmpfs]="_df_000000002 " [mmcblk0p1]="_df_0
00000007 " [root]="_df_000000001 " )
declare -- uuVarCnt="9"
_df_000000001=([0]="115335328" [1]="117652696" [2]="/")
_df_000000002=([0]="0" [1]="184596" [2]="/dev")
_df_000000003=([0]="0" [1]="188928" [2]="/dev/shm")
_df_000000004=([0]="19284" [1]="188928" [2]="/run")
_df_000000005=([0]="4" [1]="5120" [2]="/run/lock")
_df_000000006=([0]="0" [1]="188928" [2]="/sys/fs/cgroup")
_df_000000007=([0]="23193" [1]="42136" [2]="/boot")
_df_000000008=([0]="0" [1]="37784" [2]="/run/user/33")
_df_000000009=([0]="0" [1]="37784" [2]="/run/user/1001")

3. Same, but as associative array of associative sub arrays:

declare -A dfVars='()'
uuvar() { printf -v "$1" %s%09d "$2" $((++uuVarCnt));}
{
    read _
    while read -r dev _ use free _ mpnt ;do
        uuvar vname _df_
        dfVars["${dev##*/}"]+=$vname' '
        declare -A $vname
        printf -v $vname[used] %d $use
        printf -v $vname[total] %d $((use+free))
        printf -v $vname[mount\ point] %s "$mpnt"
    done
} < <(LANG=C df -lk)
for dev in ${!dfVars[@]} ;do
    for sub in ${dfVars["$dev"]} ;do
        declare -n subDf=$sub
        printf "%-20s %11d %11d  %s\n" "$dev" \
            "${subDf[used]}" "${subDf[total]}" "${subDf["mount point"]}"
    done
done
## < snip same output >

Then

declare -p dfVars ${!_df_*}
declare -A dfVars=([tmpfs]="_df_000000003 _df_000000004 _df_000000005 _df_000000
006 _df_000000008 _df_000000009 " [devtmpfs]="_df_000000002 " [mmcblk0p1]="_df_0
00000007 " [root]="_df_000000001 " )
declare -- uuVarCnt="9"
_df_000000001=([used]="115358360" [total]="117652696" ["mount point"]="/" )
_df_000000002=([used]="0" [total]="184596" ["mount point"]="/dev" )
_df_000000003=([used]="0" [total]="188928" ["mount point"]="/dev/shm" )
_df_000000004=([used]="19284" [total]="188928" ["mount point"]="/run" )
_df_000000005=([used]="4" [total]="5120" ["mount point"]="/run/lock" )
_df_000000006=([used]="0" [total]="188928" ["mount point"]="/sys/fs/cgroup" )
_df_000000007=([used]="23193" [total]="42136" ["mount point"]="/boot" )
_df_000000008=([used]="0" [total]="37784" ["mount point"]="/run/user/33" )
_df_000000009=([used]="0" [total]="37784" ["mount point"]="/run/user/1001" )
Sign up to request clarification or add additional context in comments.

1 Comment

That is a nice workaround :D
1

It's not possible directly but there is a way to do this via variable expansion, like this:

declare -A my_array
my_array['key1']='value1'
my_array['key2']='value2'

value1="true value1"
value2="true value2"

Check:

$ echo ${!my_array['key1']}
true value1

$ echo ${!my_array['key2']}
true value2

It's more complicated if value* is an array, but also possible:

...
value1=("true value1.1" "true value1.2")
value1=("true value2.1" "true value2.2")
index="${my_array['key1']}[0]"

$ echo "${!index}"
true value2.1

This could be wrapped in a function:

fun(){
    local index1=${!1}
    local index2="$index1[$2]"
    echo  "${!index2}"
}

$ fun "my_array['key2']" 1
true value2.2

But IMO you should accept the limitation and rethink the design or consider to use another language.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.