Associative Arrays in Bash (AKA Key-Value Dictionaries)
Starting with Bash 4+ you can assign arrays with keys and values. Here's how to declare, update and loop over them.
I do try my best to create POSIX compliant shell scripts but once in a while you have to break out and use Bash features. This is one of those cases where it’s completely worth it because trying to do this is a lot more complex otherwise.
Associative arrays are great for when you have a number of key / value pairs that you want to work with, such as looping over them to reduce duplication.
You’ll need Bash 4+ to do this, you can check what you have by running: bash --version
If you’re using macOS, by default you’ll have Bash 3.2 but you can update by
running brew install bash
. Bash 4 has been available since 2009! It’s
available just about everywhere. macOS has an old version due to licensing
issues.
# Working with Associative Arrays
Let’s go over working with them for a number of common use cases.
Declaring the Variable
declare -A colors
The -A
flag denotes that we want an associative array as opposed to -a
which is to declare a regular array.
Setting Values (Hard Coded)
colors["red"]="#ff0000"
colors["green"]="#00ff00"
colors["blue"]="#0000ff"
Technically the quotes for keys are optional but I like adding them in case your keys have spaces. In general explicitly using quotes is a good idea in my opinion.
Looking up Values by Key (Hard Coded)
echo "${colors["red"]}"
=> #ff0000
It works as you expect but keep in mind using $colors["red"]
will return back
[red]
. That’s one more reason to always use curly braces when accessing
variables!
Setting Values (Variables)
red="r"
colors["${red}"]="#FF0000"
This example is a bit contrived but you get the idea. We’ll see a more general use case of this when we loop over colors.
Looking up Values by Key (Variables)
echo "${colors[${red}]}"
=> #FF0000
Since ${red}
is really r
that means you can also do ${colors["r"]}
to get
the same value.
Updating Values by Key
colors["green"]="#0f0"
echo "${colors["green"]}"
=> #0f0
For the most part everything works similar to what you may have seen in other languages.
Deleting Values by Key
unset colors["r"]
echo "${colors["r"]}"
=>
Notice how when unsetting the variable you don’t use $
or curly braces.
That’s just how unset
works. You pass in the name of the variable, not a
reference to it as if you were echoing it. The value is empty because nothing
exists there anymore.
Checking if a Key Exists
# This won't echo that it exists because we deleted this key previously.
[ -v colors["r"] ] && echo "'r' exists"
# But this exists so we'll get the echo statement.
[ -v colors["red"] ] && echo "'red' exists"
Listing all Values
echo "${colors[@]}"
=> #0000ff #ff0000 #00ff00
Using @
will list them as an array. Using [*]
will convert them to a single
string. When echoing them the output will look the same but technically they
are different data types.
One gotcha here is notice the ordering of the values. That’s not the same order as we defined them. We defined RGB but it got output as BRG. The ordering is deterministic in the sense that if you ran this 100 times you’ll always get the same order with these keys but it’s not necessarily the order you supplied.
If you want to preserve ordering there’s a couple of solutions in https://stackoverflow.com/a/29161460. I don’t have a preference or opinion here.
If ordering is important I won’t use associative arrays. If something starts to get complex then I’ll use other standard command line tools to help manipulate the data or consider using Python instead of Bash.
Listing all Keys
echo "${!colors[@]}"
=> blue red green
The !
in this context is documented as “indirect
expansion”.
To be honest this is one of those things where I know to add the !
because
that’s what you need to make it work.
In this context is gets the indexes of the array, which in our case are the keys.
Looping over Key / Values
for color in "${!colors[@]}"; do
echo "${color} => ${colors[${color}]}"
done
# The above produces this output:
blue => #0000ff
red => #ff0000
green => #0f0
Instead of having both the key and value available in the loop we reach in and
grab the value by looking it up by key. If you didn’t want to access the value
then you wouldn’t reference it, ${color}
gives you the key by itself.
# Real World Use Case
This came up recently for client work where I wanted to quickly check which Helm versions were available for the charts we use.
Rather than manually check each one I used an associative array with a few loops that ran a couple of different Helm commands. This ended up making it easy to add Helm repos and check a number of chart versions.
We had 6 different charts, but here’s a minimal version to showcase how it works:
declare -A HELM_REPOS
HELM_REPOS["sealed-secrets"]="https://bitnami-labs.github.io/sealed-secrets"
HELM_REPOS["eks"]="https://aws.github.io/eks-charts"
for repo in "${!HELM_REPOS[@]}"; do
helm repo add "${repo}" "${HELM_REPOS[${repo}]}"
done
for repo in "${!HELM_REPOS[@]}"; do
echo
helm repo update "${repo}"
done
for repo in "${!HELM_REPOS[@]}"; do
echo
helm search repo "${repo}"
done
The above will add, update and search through all of your repos. In the real
solution these loops were put into separate commands. The end result was being
able to run ./run helm:search-all-repos
and it would perform an update and
search in 1 loop.
The add loop had its own command since that’s a 1 off task that’s usually only run when new repos are added.
Now it takes 1 command and about 5 seconds to see if new versions are available to use. A human (AKA me) can review that, assess the changelogs and perform the updates in a controlled manner. These commands are basic but it brings me developer happiness.
# Demo Video
Timestamps
- 0:14 – You need at least Bash 4 to use them
- 1:02 – Defining the variable
- 1:21 – Defining our keys and values
- 1:55 – Looking up a value by key
- 2:50 – Using a variable as a key name
- 4:37 – Updating a value for a specific key
- 5:20 – Deleting a key
- 6:13 – Checking if a key exists
- 7:01 – Listing all values
- 7:50 – Listing all keys
- 8:02 – Ordering is not guaranteed
- 10:07 – Looping over the keys and values
- 12:05 – Going over a real world use case with Helm
- 14:47 – When was the last time you used an associate array?
How often do you use associative arrays with Bash? Let me know below.