70

I am planning a script to manage some pieces of my Linux systems and am at the point of deciding if I want to use or .

I would prefer to do this as a Bash script simply because the commands are easier, but the real deciding factor is configuration. I need to be able to store a multi-dimensional array in the configuration file to tell the script what to do with itself. Storing simple key=value pairs in config files is easy enough with bash, but the only way I can think of to do a multi-dimensional array is a two layer parsing engine, something like

array=&d1|v1;v2;v3&d2|v1;v2;v3

but the marshall/unmarshall code could get to be a bear and its far from user friendly for the next poor sap that has to administer this. If i can't do this easily in bash i will simply write the configs to an xml file and write the script in python.

Is there an easy way to do this in bash?

thanks everyone.

codeforester
  • 39,467
  • 16
  • 112
  • 140
scphantm
  • 4,293
  • 8
  • 43
  • 77

14 Answers14

61

Bash does not support multidimensional arrays, nor hashes, and it seems that you want a hash that values are arrays. This solution is not very beautiful, a solution with an xml file should be better :

array=('d1=(v1 v2 v3)' 'd2=(v1 v2 v3)')
for elt in "${array[@]}";do eval $elt;done
echo "d1 ${#d1[@]} ${d1[@]}"
echo "d2 ${#d2[@]} ${d2[@]}"

EDIT: this answer is quite old, since since bash 4 supports hash tables, see also this answer for a solution without eval.

Nahuel Fouilleul
  • 18,726
  • 2
  • 31
  • 36
  • 35
    Just a note. bash does support hashes (associative arrays) starting from version 4. more info: http://mywiki.wooledge.org/BashFAQ/006 – poncha Apr 21 '13 at 16:00
  • FYI associative arrays can have some of the 'declare' attributes set on them, such as 'set to uppercase on assignment' however, they cannot have the -A [associative] or -a [numeric] array set to them, nor the -n reference set, but you CAN tag your variable name with a number, and use a variable in place: MYARR_$i_[$j] would be the closest thing to this, however it isnt a true md array, but this is the best you are going to get. You could also use functions as a pseudo-array system if you were desperate enough :) – osirisgothra May 02 '14 at 11:31
  • 13
    and there it is... evil... sorry i meant eval – Angry 84 Dec 31 '15 at 03:17
  • 1
    You could always `declare -a "$elt"` instead of `eval $elt`. – ghoti Jul 20 '17 at 16:54
  • 1
    @ghoti, yes the answer is quite old, another similar answer https://stackoverflow.com/a/44831174/1454708, I supposed that the OP knows what he is doing and input is checked – Nahuel Fouilleul Jul 20 '17 at 17:43
34

Bash doesn't have multi-dimensional array. But you can simulate a somewhat similar effect with associative arrays. The following is an example of associative array pretending to be used as multi-dimensional array:

declare -A arr
arr[0,0]=0
arr[0,1]=1
arr[1,0]=2
arr[1,1]=3
echo "${arr[0,0]} ${arr[0,1]}" # will print 0 1

If you don't declare the array as associative (with -A), the above won't work. For example, if you omit the declare -A arr line, the echo will print 2 3 instead of 0 1, because 0,0, 1,0 and such will be taken as arithmetic expression and evaluated to 0 (the value to the right of the comma operator).

Jahid
  • 21,542
  • 10
  • 90
  • 108
25

This works thanks to 1. "indirect expansion" with ! which adds one layer of indirection, and 2. "substring expansion" which behaves differently with arrays and can be used to "slice" them as described https://stackoverflow.com/a/1336245/317623

# Define each array and then add it to the main one
SUB_0=("name0" "value 0")
SUB_1=("name1" "value;1")
MAIN_ARRAY=(
  SUB_0[@]
  SUB_1[@]
)

# Loop and print it.  Using offset and length to extract values
COUNT=${#MAIN_ARRAY[@]}
for ((i=0; i<$COUNT; i++))
do
  NAME=${!MAIN_ARRAY[i]:0:1}
  VALUE=${!MAIN_ARRAY[i]:1:1}
  echo "NAME ${NAME}"
  echo "VALUE ${VALUE}"
done

It's based off of this answer here

MarcH
  • 18,738
  • 1
  • 30
  • 25
Paul Sheldrake
  • 7,505
  • 10
  • 38
  • 50
  • it should be `NAME=${!MAIN_ARRAY[i]:0:1}` and `VALUE=${!MAIN_ARRAY[i]:1:1}` if I'm not mistaken for this to work (ie. exchange `URLS` for `MAIN`). – Christian Oct 27 '15 at 10:41
  • 3
    I haven't yet looked into why - but this works fine on bash v4.3+ but has trouble with earlier versions. (prints individual characters instead of full words) – dtmland May 17 '19 at 13:33
  • I wish with would work without having to define `SUB_0` and `SUB_1` before `MAIN_ARRAY` – freethebees Nov 12 '19 at 14:52
  • @dtmland what seems to happen with older bash versions is plain substring expansion instead of the array slicing alternative. – MarcH Jul 02 '20 at 03:39
  • If you have to define literal names with the index like SUB_0 and SUB_1 it is not really a 'universal' multidimensional array. – PePa May 10 '22 at 08:07
16

If you want to use a bash script and keep it easy to read recommend putting the data in structured JSON, and then use lightweight tool jq in your bash command to iterate through the array. For example with the following dataset:

[

    {"specialId":"123",
    "specialName":"First"},

    {"specialId":"456",
    "specialName":"Second"},

    {"specialId":"789",
    "specialName":"Third"}
]

You can iterate through this data with a bash script and jq like this:

function loopOverArray(){

    jq -c '.[]' testing.json | while read i; do
        # Do stuff here
        echo "$i"
    done
}

loopOverArray

Outputs:

{"specialId":"123","specialName":"First"}
{"specialId":"456","specialName":"Second"}
{"specialId":"789","specialName":"Third"}
Dustin
  • 281
  • 2
  • 7
8

Independent of the shell being used (sh, ksh, bash, ...) the following approach works pretty well for n-dimensional arrays (the sample covers a 2-dimensional array).

In the sample the line-separator (1st dimension) is the space character. For introducing a field separator (2nd dimension) the standard unix tool tr is used. Additional separators for additional dimensions can be used in the same way.

Of course the performance of this approach is not very well, but if performance is not a criteria this approach is quite generic and can solve many problems:

array2d="1.1:1.2:1.3 2.1:2.2 3.1:3.2:3.3:3.4"

function process2ndDimension {
    for dimension2 in $*
    do
        echo -n $dimension2 "   "
    done
    echo
}

function process1stDimension {
    for dimension1 in $array2d
    do
        process2ndDimension `echo $dimension1 | tr : " "`
    done
}

process1stDimension

The output of that sample looks like this:

1.1     1.2     1.3     
2.1     2.2     
3.1     3.2     3.3     3.4 
yaccob
  • 1,230
  • 13
  • 16
  • 1
    Doesn't work very well if the values contain spaces or colons, though. :) – dannysauer Jun 01 '16 at 14:40
  • 1
    @dannysauer - should be fine if you encode spaces and colons as an entity or hex code. %20 and %3a for example. It should be easy enough to process input to and output from the array through such functions if that's a concern for you. – ghoti Sep 13 '16 at 15:40
  • 3
    So then the values don't contain spaces or colons. :D – dannysauer Sep 13 '16 at 16:17
  • Can I have one loop and one function instead of two? – Timo Jun 07 '21 at 07:11
7

After a lot of trial and error i actually find the best, clearest and easiest multidimensional array on bash is to use a regular var. Yep.

Advantages: You don't have to loop through a big array, you can just echo "$var" and use grep/awk/sed. It's easy and clear and you can have as many columns as you like.

Example:

$ var=$(echo -e 'kris hansen oslo\nthomas jonson peru\nbibi abu johnsonville\njohnny lipp peru')

$ echo "$var"
kris hansen oslo
thomas johnson peru
bibi abu johnsonville
johnny lipp peru

If you want to find everyone in peru

$ echo "$var" | grep peru
thomas johnson peru
johnny lipp peru

Only grep(sed) in the third field

$ echo "$var" | sed -n -E '/(.+) (.+) peru/p'
thomas johnson peru
johnny lipp peru

If you only want x field

$ echo "$var" | awk '{print $2}'
hansen
johnson
abu
johnny

Everyone in peru that's called thomas and just return his lastname

$ echo "$var" |grep peru|grep thomas|awk '{print $2}'
johnson

Any query you can think of... supereasy.

To change an item:

$ var=$(echo "$var"|sed "s/thomas/pete/")

To delete a row that contains "x"

$ var=$(echo "$var"|sed "/thomas/d")

To change another field in the same row based on a value from another item

$ var=$(echo "$var"|sed -E "s/(thomas) (.+) (.+)/\1 test \3/")
$ echo "$var"
kris hansen oslo                                                                                                                                             
thomas test peru                                                                                                                                          
bibi abu johnsonville
johnny lipp peru

Of course looping works too if you want to do that

$ for i in "$var"; do echo "$i"; done
kris hansen oslo
thomas jonson peru
bibi abu johnsonville
johnny lipp peru

The only gotcha iv'e found with this is that you must always quote the var(in the example; both var and i) or things will look like this

$ for i in "$var"; do echo $i; done
kris hansen oslo thomas jonson peru bibi abu johnsonville johnny lipp peru

and someone will undoubtedly say it won't work if you have spaces in your input, however that can be fixed by using another delimeter in your input, eg(using an utf8 char now to emphasize that you can choose something your input won't contain, but you can choose whatever ofc):

$ var=$(echo -e 'field one☥field two hello☥field three yes moin\nfield 1☥field 2☥field 3 dsdds aq')

$ for i in "$var"; do echo "$i"; done
field one☥field two hello☥field three yes moin
field 1☥field 2☥field 3 dsdds aq

$ echo "$var" | awk -F '☥' '{print $3}'
field three yes moin
field 3 dsdds aq

$ var=$(echo "$var"|sed -E "s/(field one)☥(.+)☥(.+)/\1☥test☥\3/")
$ echo "$var"
field one☥test☥field three yes moin
field 1☥field 2☥field 3 dsdds aq

If you want to store newlines in your input, you could convert the newline to something else before input and convert it back again on output(or don't use bash...). Enjoy!

n00p
  • 269
  • 4
  • 11
  • This may work for two dimensions, but for a generic case with n dimensions this is getting hard. – U. Windl Nov 16 '17 at 09:18
  • That's true. When i wrote this i didn't realise the difference between 2d, 3d, 4d and all i'd ever needed was good tabular support/db. Now, I still haven't needed anything more than 2d, but if one needs more then i suppose this won't work. – n00p Nov 17 '17 at 02:27
  • "Of course looping works too if you want to do that" - your looping example isn't doing what you think it's doing. It's processing one single value, the entire array, at once, rather than each item in the array. Eg try `for i in "$var"; do echo "row=$i"; done` and you'll see only one row. – davmac Dec 04 '22 at 05:22
4

I am posting the following because it is a very simple and clear way to mimic (at least to some extent) the behavior of a two-dimensional array in Bash. It uses a here-file (see the Bash manual) and read (a Bash builtin command):

## Store the "two-dimensional data" in a file ($$ is just the process ID of the shell, to make sure the filename is unique)
cat > physicists.$$ <<EOF
Wolfgang Pauli 1900
Werner Heisenberg 1901
Albert Einstein 1879
Niels Bohr 1885
EOF
nbPhysicists=$(wc -l physicists.$$ | cut -sf 1 -d ' ')     # Number of lines of the here-file specifying the physicists.

## Extract the needed data
declare -a person     # Create an indexed array (necessary for the read command).                                                                                 
while read -ra person; do
    firstName=${person[0]}
    familyName=${person[1]}
    birthYear=${person[2]}
    echo "Physicist ${firstName} ${familyName} was born in ${birthYear}"
    # Do whatever you need with data
done < physicists.$$

## Remove the temporary file
rm physicists.$$

Output: Physicist Wolfgang Pauli was born in 1900 Physicist Werner Heisenberg was born in 1901 Physicist Albert Einstein was born in 1879 Physicist Niels Bohr was born in 1885

The way it works:

  • The lines in the temporary file created play the role of one-dimensional vectors, where the blank spaces (or whatever separation character you choose; see the description of the read command in the Bash manual) separate the elements of these vectors.
  • Then, using the read command with its -a option, we loop over each line of the file (until we reach end of file). For each line, we can assign the desired fields (= words) to an array, which we declared just before the loop. The -r option to the read command prevents backslashes from acting as escape characters, in case we typed backslashes in the here-document physicists.$$.

In conclusion a file is created as a 2D-array, and its elements are extracted using a loop over each line, and using the ability of the read command to assign words to the elements of an (indexed) array.

Slight improvement:

In the above code, the file physicists.$$ is given as input to the while loop, so that it is in fact passed to the read command. However, I found that this causes problems when I have another command asking for input inside the while loop. For example, the select command waits for standard input, and if placed inside the while loop, it will take input from physicists.$$, instead of prompting in the command-line for user input. To correct this, I use the -u option of read, which allows to read from a file descriptor. We only have to create a file descriptor (with the exec command) corresponding to physicists.$$ and to give it to the -u option of read, as in the following code:

## Store the "two-dimensional data" in a file ($$ is just the process ID of the shell, to make sure the filename is unique)
cat > physicists.$$ <<EOF
Wolfgang Pauli 1900
Werner Heisenberg 1901
Albert Einstein 1879
Niels Bohr 1885
EOF
nbPhysicists=$(wc -l physicists.$$ | cut -sf 1 -d ' ')     # Number of lines of the here-file specifying the physicists.
exec {id_file}<./physicists.$$     # Create a file descriptor stored in 'id_file'.

## Extract the needed data
declare -a person     # Create an indexed array (necessary for the read command).                                                                                 
while read -ra person -u "${id_file}"; do
firstName=${person[0]}
familyName=${person[1]}
birthYear=${person[2]}
echo "Physicist ${firstName} ${familyName} was born in ${birthYear}"
# Do whatever you need with data
done

## Close the file descriptor
exec {id_file}<&-
## Remove the temporary file
rm physicists.$$

Notice that the file descriptor is closed at the end.

Giuseppe
  • 171
  • 1
  • 8
  • It's very much the same as user7909577's solution. However what you do is write a sequence of lines in a text file. So the dimension 1 is the line number. Dimension 2 is the word in a line. A truely generic solution, however, would allow to put any array in any array. And that won't work. OK, you could implement dimensions by indenting the lines, but that's still not very elegant. – U. Windl Nov 16 '17 at 09:22
  • @U. Windi "put any array in any array. And that won't work.": That's why I stress in my answer that it is to simulate 2D-ARRAYS. "It's very much the same as user7909577's solution": I don't think so, he/she parses a string with sed/grep/awk in his answer, and I do it by "transforming" the string into an array first, then using indices (that is why it simulates the behaviour of an array). – Giuseppe Nov 16 '17 at 13:38
  • @Guiseppe: But both solutions use a text file as representation for a two dimensional array with the dimensions as I had described. The question specifically was about _multi_-dimensional arrays (There's a separate question for two-dimensional arrays). – U. Windl Nov 16 '17 at 14:15
  • @U. Windl: I agree, but as I understand user7909577's solution, it's never really an array that you have, but a long list where you have to put separators (like newline). So each time you start a new loop, you are stuck with one element of your long list. On the contrary with my solution, at each loop you work with a real 1D-array (a line of the simulated 2D-array), and you can therefore access any of its elements, which is much more convenient in my opinion. I didn't notice the other question on two-dimensional arrays, so if you know how to move answers I am interested. – Giuseppe Nov 16 '17 at 18:02
3

Bash does not supports multidimensional array, but we can implement using Associate array. Here the indexes are the key to retrieve the value. Associate array is available in bash version 4.

#!/bin/bash

declare -A arr2d
rows=3
columns=2

for ((i=0;i<rows;i++)) do
    for ((j=0;j<columns;j++)) do
        arr2d[$i,$j]=$i
    done
done


for ((i=0;i<rows;i++)) do
    for ((j=0;j<columns;j++)) do
        echo ${arr2d[$i,$j]}
    done
done
rashok
  • 12,790
  • 16
  • 88
  • 100
2

Expanding on Paul's answer - here's my version of working with associative sub-arrays in bash:

declare -A SUB_1=(["name1key"]="name1val" ["name2key"]="name2val")
declare -A SUB_2=(["name3key"]="name3val" ["name4key"]="name4val")
STRING_1="string1val"
STRING_2="string2val"
MAIN_ARRAY=(
  "${SUB_1[*]}"
  "${SUB_2[*]}"
  "${STRING_1}"
  "${STRING_2}"
)
echo "COUNT: " ${#MAIN_ARRAY[@]}
for key in ${!MAIN_ARRAY[@]}; do
    IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]}
    echo "VALUE: " ${val[@]}
    if [[ ${#val[@]} -gt 1 ]]; then
        for subkey in ${!val[@]}; do
            subval=${val[$subkey]}
            echo "SUBVALUE: " ${subval}
        done
    fi
done

It works with mixed values in the main array - strings/arrays/assoc. arrays

The key here is to wrap the subarrays in single quotes and use * instead of @ when storing a subarray inside the main array so it would get stored as a single, space separated string: "${SUB_1[*]}"

Then it makes it easy to parse an array out of that when looping through values with IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]}

The code above outputs:

COUNT:  4
VALUE:  name1val name2val
SUBVALUE:  name1val
SUBVALUE:  name2val
VALUE:  name4val name3val
SUBVALUE:  name4val
SUBVALUE:  name3val
VALUE:  string1val
VALUE:  string2val
Vigintas Labakojis
  • 1,039
  • 1
  • 15
  • 21
  • 1
    So you can't store strings that contain spaces (not mentioning newlines). Also your lack of quotes makes it impossible to safely have glob characters. – gniourf_gniourf Feb 12 '15 at 10:50
  • Yes, I've just realised it will break down strings with spaces into separate sub-values, but I, personally can live with that if it's just config values... But I think that's about the best that we can do in bash. I'm open to suggestions/improvements though. Added the quotes, thanks for pointing out :) – Vigintas Labakojis Feb 12 '15 at 11:03
  • Hi, can you explain how does `IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]}` work. What is IFS? Is it an variable being defined there? Why read is appended to the variable definition? – Boyang Feb 09 '16 at 03:19
  • @CharlesW. See [Bash Reference Manual, 5.1 Bourne Shell Variables](https://www.gnu.org/software/bash/manual/bash.html#index-IFS): "`IFS` – A list of characters that separate fields; used when the shell splits words as part of expansion." and [6.7 Arrays](https://www.gnu.org/software/bash/manual/bash.html#Arrays): "If the word is double-quoted, `${name[*]}` expands to a single word with the value of each array member separated by the first character of the `IFS` variable, ...". – Gerold Broser Jun 08 '16 at 00:37
2

Lots of answers found here for creating multidimensional arrays in bash.

And without exception, all are obtuse and difficult to use.

If MD arrays are a required criteria, it is time to make a decision:

Use a language that supports MD arrays

My preference is Perl. Most would probably choose Python. Either works.

Store the data elsewhere

JSON and jq have already been suggested. XML has also been suggested, though for your use JSON and jq would likely be simpler.

It would seem though that Bash may not be the best choice for what you need to do.

Sometimes the correct question is not "How do I do X in tool Y?", but rather "Which tool would be best to do X?"

Jared Still
  • 136
  • 7
1

I do this using associative arrays since bash 4 and setting IFS to a value that can be defined manually.

The purpose of this approach is to have arrays as values of associative array keys.

In order to set IFS back to default just unset it.

  • unset IFS

This is an example:

#!/bin/bash

set -euo pipefail

# used as value in asscciative array
test=(
  "x3:x4:x5"
)
# associative array
declare -A wow=(
  ["1"]=$test
  ["2"]=$test
)
echo "default IFS"
for w in ${wow[@]}; do
  echo "  $w"
done

IFS=:
echo "IFS=:"
for w in ${wow[@]}; do
  for t in $w; do
    echo "  $t"
  done
done
echo -e "\n or\n"
for w in ${!wow[@]}
do
  echo "  $w"
  for t in ${wow[$w]}
  do
    echo "    $t"
  done
done

unset IFS
unset w
unset t
unset wow
unset test

The output of the script below is:

default IFS
  x3:x4:x5
  x3:x4:x5
IFS=:
  x3
  x4
  x5
  x3
  x4
  x5

 or

  1
    x3
    x4
    x5
  2
    x3
    x4
    x5
rocksteady
  • 2,320
  • 5
  • 24
  • 40
1

Using GNU bash, version 5.2.2(1)-release

I needed to loop through multiple Associate arrays within a single Indexed array.

#!/usr/bin/env bash

declare -A bch=( [port]="8432" [node]="bchd-node" )
declare -A btc=( [port]="8332" [node]="bitcoind-node" )
declare -a currencies=(bch btc)

for ((i=0; i<"${#currencies[*]}"; i++)); do
    curr="${currencies[$i]}"
    port="${curr}[port]"
    node="${curr}[node]"
    echo "${curr} port is ${!port} -- ${curr} node is ${!node}"
done

Result:

bch port is 8432 -- bch node is bchd-node
btc port is 8332 -- btc node is bitcoind-node
skyehash
  • 31
  • 1
0

I've got a pretty simple yet smart workaround: Just define the array with variables in its name. For example:

for (( i=0 ; i<$(($maxvalue + 1)) ; i++ ))
  do
  for (( j=0 ; j<$(($maxargument + 1)) ; j++ ))
    do
    declare -a array$i[$j]=((Your rule))
  done
done

Don't know whether this helps since it's not exactly what you asked for, but it works for me. (The same could be achieved just with variables without the array)

-4
echo "Enter no of terms"
read count
for i in $(seq 1 $count)
do
  t=` expr $i - 1 `
  for j in $(seq $t -1 0)
  do
    echo -n " "
  done
  j=` expr $count + 1 `
  x=` expr $j - $i `
  for k in $(seq 1 $x)
  do
    echo -n "* "
  done
  echo ""
done
Izzy
  • 1,364
  • 3
  • 32
  • 65
  • 3
    Mind to add some explaining words on what the code is supposed to do? While some formatting is helpful as well, some comments would improve the answer even more. Though I can see multiple arrays, I see no multi-dimensional array here. – Izzy Nov 06 '14 at 15:06