2. Arrays and dictionaries โ
๐ฏ What you will learn in this chapter:
- Indexed arrays โ
arr=(a b c), appending, removing, iterating- Associative arrays (dictionaries) โ
declare -A(Bash 4+)"${arr[@]}"vs"${arr[*]}"โ the critical difference- CSV and string parsing with
IFS- A real example โ a small TODO list manager
โฑ Time: ~30 minutes ๐งช Exercises:
bashlings watchโ 6 interactive exercises ready (exercises/07_arrays/)
2.1. Why arrays? โ
Imagine your script deploys to three servers. The first approach:
servers="alpha beta gamma"
for s in $servers; do
deploy "$s"
doneIt works. But what if a server name contains a space? Like "beta server"? Bash splits it into 2 separate elements:
servers="alpha beta server gamma"
# deploy: alpha, beta, server, gamma โ 4 of them!An array, on the other hand, is a proper data structure โ each element is stored in its own cell:
servers=("alpha" "beta server" "gamma")
for s in "${servers[@]}"; do
deploy "$s"
done
# deploy: alpha, "beta server", gamma โ 3 of them, correct โKey idea
In Bash, whenever you need to store multiple values โ use an array, not a string. It protects you from spaces, special characters and the word-splitting trap.
2.2. Indexed arrays โ declaring โ
Indexed arrays are the simplest type: elements are stored by integer indices (starting from 0).
Syntax variants โ
# Empty array
arr=()
# With several elements
fruits=("olma" "anor" "uzum")
# Range (brace expansion)
nums=({1..10}) # 1 2 3 4 5 6 7 8 9 10
# With mixed-type values
mixed=("matn" 42 3.14 "yana matn")Defining with declare -a โ
declare -a cities
cities[0]="Toshkent"
cities[1]="Samarqand"
cities[2]="Buxoro"declare -a โ explicitly marks it as an indexed array in bash. Inside a function it is used as local -a.
Indices start from 0 โ
fruits=("olma" "anor" "uzum")
# โ0 โ1 โ2Be careful with command output
files=($(ls *.txt)) # โ DANGEROUSThe reason: if a file name contains a space, bash itself word-splits the output of ls. my file.txt โ "my" and "file.txt" โ it ends up as two elements.
The correct way (Bash 4+):
mapfile -t files < <(ls *.txt)2.3. Working with elements โ
Accessing a single element โ
fruits=("olma" "anor" "uzum")
echo "${fruits[0]}" # olma
echo "${fruits[1]}" # anor
echo "${fruits[-1]}" # uzum (last โ Bash 4.2+)โ Note: $fruits (without ${fruits}, without an index) returns the first element, not all of them. This is a classic trap.
echo "$fruits" # olma โ many people think this means "all"
echo "${fruits[@]}" # olma anor uzum โArray length (element count) โ
echo "${#fruits[@]}" # 3Adding an element (append) โ
fruits+=("shaftoli") # appends to the end
echo "${fruits[@]}" # olma anor uzum shaftoliAdding several elements at once:
fruits+=("nok" "olcha")Changing an element โ
fruits[1]="banan"
echo "${fruits[@]}" # olma banan uzum shaftoli ...Removing an element โ
unset 'fruits[1]'
echo "${fruits[@]}" # olma uzum shaftoli ...unset leaves a "live state"
unset arr[1] โ removes the element, but does not re-index the array:
arr=(a b c d)
unset 'arr[1]'
echo "${arr[@]}" # a c d (b is gone)
echo "${arr[2]}" # c (index 2 is preserved)
echo "${arr[1]}" # (empty โ because it was removed)To compact the indices you need to rewrite it:
arr=("${arr[@]}")Removing the whole array โ
unset fruits # the array disappears entirely2.4. Iteration โ
The most common โ for ... in โ
fruits=("olma" "anor" "uzum")
for f in "${fruits[@]}"; do
echo "Meva: $f"
doneDon't forget the quotes!
for f in ${fruits[@]} โ without quotes โ word-splits each element. Always write it as "${fruits[@]}".
With indices โ
When you need the element + its position:
fruits=("olma" "anor" "uzum")
for i in "${!fruits[@]}"; do
echo "$i: ${fruits[$i]}"
done
# 0: olma
# 1: anor
# 2: uzum${!arr[@]} โ returns the list of indices (not the values).
Slicing โ partial cutting โ
nums=(10 20 30 40 50 60 70)
# ${arr[@]:start:count}
echo "${nums[@]:2:3}" # 30 40 50 (3 elements starting from position 2)
echo "${nums[@]:4}" # 50 60 70 (from position 4 to the end)
echo "${nums[@]: -2}" # 60 70 (last 2 โ a space is required!)Negative slicing
${nums[@]:-2} โ this is the default-value operator, not slicing! A negative offset requires a space: ${nums[@]: -2}.
With while (less common) โ
i=0
while [[ $i -lt ${#fruits[@]} ]]; do
echo "${fruits[$i]}"
((i++))
done2.5. "${arr[@]}" vs "${arr[*]}" โ the critical difference โ
This is the most confusing aspect of bash. Both are understood as "all elements", but inside quotes they behave completely differently.
arr=("salom dunyo" "bash" "uz")
# @ โ each element separately
for x in "${arr[@]}"; do echo "[$x]"; done
# [salom dunyo]
# [bash]
# [uz]
# * โ a single string, joined by the IFS character (default IFS = space)
for x in "${arr[*]}"; do echo "[$x]"; done
# [salom dunyo bash uz] โ a single string!| Syntax | Result inside quotes |
|---|---|
"${arr[@]}" | Each element โ a separate argument |
"${arr[*]}" | A single string, joined with IFS |
${arr[@]} | Unquoted โ risk of word-splitting |
Rule
In 99% of cases use "${arr[@]}". Use "${arr[*]}" only when you need to turn it into a string.
Custom separator with IFS โ
${arr[*]} uses the IFS character:
arr=("a" "b" "c")
IFS=, echo "${arr[*]}"
# a,b,c
IFS='|' echo "${arr[*]}"
# a|b|cThis is essentially the technique for turning a list into a CSV line.
2.6. Associative arrays โ dictionaries (Bash 4+) โ
An associative array โ stores by string keys. In other languages this is called a "dictionary", "map" or "hashtable".
The macOS problem
On macOS the default bash is 3.2 (from 2007). It does not support associative arrays.
The fix:
brew install bash
which bash # /opt/homebrew/bin/bash#!/usr/bin/env bash at the start of a script โ env finds the newer bash from PATH.
Checking the version:
bash --version # should be 5.xDeclaring and filling โ
declare -A user
user[name]="Ali"
user[age]=25
user[city]="Toshkent"Or all at once:
declare -A user=(
[name]="Ali"
[age]=25
[city]="Toshkent"
)Accessing values โ
echo "${user[name]}" # Ali
echo "${user[age]}" # 25A nonexistent key โ returns an empty string (not an error):
echo "${user[phone]}" # (empty)Keys and values โ
echo "${!user[@]}" # name age city (keys)
echo "${user[@]}" # Ali 25 Toshkent (values)
echo "${#user[@]}" # 3 (total)Order is not guaranteed
In an associative array the order of keys depends on the internal structure of the hash table. The iteration order is not reliable. If you need order, store the keys in a separate indexed array.
Iteration โ
for key in "${!user[@]}"; do
echo "$key = ${user[$key]}"
done
# name = Ali
# age = 25
# city = ToshkentRemoving an element โ
unset 'user[city]'A real example โ a counter for a set of files โ
#!/usr/bin/env bash
declare -A count
# Count the extensions of files in the current directory
for f in *; do
ext="${f##*.}"
((count[$ext]++))
done
for ext in "${!count[@]}"; do
printf '%-10s %d\n' "$ext" "${count[$ext]}"
doneResult:
md 12
sh 8
txt 32.7. IFS and string parsing โ
IFS (Internal Field Separator) โ bash's word-splitting character. Its default value: space + tab + newline.
read -ra โ splitting a string into an array โ
csv="ali,25,toshkent"
IFS=',' read -ra fields <<< "$csv"
echo "${fields[0]}" # ali
echo "${fields[1]}" # 25
echo "${fields[2]}" # toshkent-r โ do not interpret backslashes (always use it). -a fields โ write the result into the fields array. <<< โ here-string (single-line stdin).
Joining an array into a string โ
words=("salom" "dunyo" "bash")
# With spaces
result="${words[*]}"
echo "$result" # salom dunyo bash
# With a custom separator
(IFS=','; result="${words[*]}"; echo "$result")
# salom,dunyo,bashBe careful with IFS and subshells
We put (IFS=','; ...) in parentheses so that changing IFS stays only within this block. Outside, IFS does not change.
From a file into an array, line by line โ
# The cleanest way for Bash 4+
mapfile -t lines < /etc/passwd
echo "${#lines[@]}" # number of lines
echo "${lines[0]}" # first linemapfile -t โ makes each line a separate element, and -t strips the newline.
For older bash:
lines=()
while IFS= read -r line; do
lines+=("$line")
done < /etc/passwd2.8. A real example โ a TODO list manager โ
A complete example demonstrating arrays in practice:
#!/usr/bin/env bash
#
# todo.sh โ a small TODO list manager
#
# Usage:
# todo.sh add "Buyruq"
# todo.sh list
# todo.sh done 2
#
set -euo pipefail
readonly TODO_FILE="${TODO_FILE:-$HOME/.todo}"
# Load the file (as an array)
load_tasks() {
if [[ -f "$TODO_FILE" ]]; then
mapfile -t tasks < "$TODO_FILE"
else
tasks=()
fi
}
# Save the array to the file
save_tasks() {
printf '%s\n' "${tasks[@]}" > "$TODO_FILE"
}
cmd_add() {
local item="$*"
[[ -z "$item" ]] && { echo "Foydalanish: todo add <matn>" >&2; exit 1; }
tasks+=("$item")
save_tasks
echo "โ Qo'shildi: $item"
}
cmd_list() {
if [[ ${#tasks[@]} -eq 0 ]]; then
echo "(ro'yxat bo'sh)"
return
fi
for i in "${!tasks[@]}"; do
printf '%2d. %s\n' "$((i + 1))" "${tasks[$i]}"
done
}
cmd_done() {
local n="$1"
local idx=$((n - 1))
if [[ -z "${tasks[$idx]:-}" ]]; then
echo "โ #$n topilmadi" >&2
exit 1
fi
echo "โ Bajarildi: ${tasks[$idx]}"
unset 'tasks[idx]'
tasks=("${tasks[@]}") # re-index and compact
save_tasks
}
# --- Main ---
load_tasks
case "${1:-list}" in
add) shift; cmd_add "$@" ;;
list) cmd_list ;;
done) cmd_done "$2" ;;
*) echo "Foydalanish: $0 {add|list|done}"; exit 1 ;;
esacLet's try it out:
$ todo.sh add "non sotib olish"
โ Qo'shildi: non sotib olish
$ todo.sh add "kitobni o'qish"
โ Qo'shildi: kitobni o'qish
$ todo.sh list
1. non sotib olish
2. kitobni o'qish
$ todo.sh done 1
โ Bajarildi: non sotib olish
$ todo.sh list
1. kitobni o'qishWhat was used in this example? โ
| Technique | Where |
|---|---|
mapfile -t (file โ array) | load_tasks |
printf '%s\n' "${arr[@]}" | save_tasks (writing to file) |
tasks+=(...) appending | cmd_add |
"${!arr[@]}" indices | cmd_list |
unset 'arr[i]' + recompacting | cmd_done |
${var:-default} empty-value check | cmd_list |
2.9. Common mistakes โ
Classic traps
$arrโ only the first element.basharr=(a b c) echo "$arr" # a โ echo "${arr[@]}" # a b c โUnquoted iteration.
bashfor x in ${arr[@]}; do ... # โ word-splitting for x in "${arr[@]}"; do ... # โFilling an array from
$(ls).bashfiles=($(ls)) # โ file names with spaces break mapfile -t files < <(ls) # โunsetdoes not give the indices back. Afterunset 'arr[1]',${#arr[@]}is not 2, and${arr[2]}still exists. The fix: compact witharr=("${arr[@]}").Associative arrays don't work on macOS. The default bash 3.2 โ
declare -Athrows an error. You needbrew install bash.Mixing up negative slicing and the default value.
bash"${arr[@]:-2}" # โ default value "${arr[@]: -2}" # โ last 2 elements (the space!)Unquoted string joining.
bashIFS=',' echo "${arr[*]}" # โ echo doesn't see the IFS (IFS=','; echo "${arr[*]}") # โ inside a subshell
2.10. Exercises โ
๐งช Bashlings โ interactive exercises
This chapter's 6 exercises with auto-checking via the bashlings CLI:
bashlings watch # start from the first pending exercise
bashlings run arr1 # check a single exercise
bashlings hint arr1 # step-by-step hintSource: exercises/07_arrays/
And try the following conceptual exercises by hand yourself:
reverse_arrayโ write a function that prints an array in reverse order. Hint:for ((i=${#arr[@]}-1; i>=0; i--))unique_itemsโ return the elements of an array without duplicates. Hint: combine withsort -u.csv_to_dictโ a function that converts a string in the format"key1=val1,key2=val2,..."into an associative array.top_nโ takes a numeric array and an argumentn, and returns the largestnnumbers. Thesort -rn | headpattern.group_by_extโ group the files in the current directory by their extension (associative array). Example result:md: [README.md doc.md] sh: [build.sh test.sh]
2.11. Summary โ
| Concept | Key point |
|---|---|
| Indexed array | arr=(a b c) or declare -a |
| Associative array | declare -A (required, Bash 4+) |
| All elements | "${arr[@]}" (each element separately) |
| A single string | "${arr[*]}" (joined with IFS) |
| Element count | ${#arr[@]} |
| Keys/indices | "${!arr[@]}" |
| Appending | arr+=("yangi") |
| Slicing | "${arr[@]:start:count}" |
| File โ array | mapfile -t arr < fayl.txt |
| Splitting a string | IFS=',' read -ra arr <<< "$s" |
| Macro quoting | Always "${arr[@]}" โ unquoted is dangerous |
Key ideas โ
- An array = storing multiple values. A string is a single value.
- Always use
@โ use*only for joining into a string. - macOS bash 3.2 โ associative arrays don't work, you need brew bash.
mapfileโ the safest way from a file into an array.unsetleaves residue โ compact witharr=("${arr[@]}").
๐ Now you have the skill to use powerful data structures in Bash. In the next chapter we will learn industrial-grade text processing with sed and awk.
Next page: 3. Mastering sed, awk and grep โ