🧠 Bash Arrays: From First Steps to Real-World Mastery

/system-design

Bash arrays aren't just containers, they're a way to control how data flows through the shell.

This guide builds from zero to production-grade patterns, using real examples like file processing and reverse lookups.


1. 🌱 The Basics (Quick but Solid)

Creating arrays

  
  
arr=(apple banana "cherry pie")
  
  

Access

  
  
echo "${arr[0]}"
echo "${arr[@]}"
echo "${#arr[@]}"
  
  

👉 Always use "${arr[@]}" — this preserves elements.


2. ⚠️ The Rule That Changes Everything

"${arr[@]}" vs ${arr[@]}

  
  
arr=("a b" "c d")

for x in ${arr[@]}; do echo "$x"; done   # ❌ broken

a
b
c
d

for x in "${arr[@]}"; do echo "$x"; done # ✅ correct

a b
c d

  
  

👉 Without quotes, Bash:

  • splits on spaces
  • expands wildcards

3. 📥 Real Data: Reading Files Safely

Let's start doing something real.

❌ Wrong way

  
  
arr=($(ls))
  
  

Breaks on:

  • spaces
  • special characters

✅ Correct way

  
  
mapfile -t arr3 < <(ls)
  
  
  • -t flag means strip all \n for every newline, so no elements have a trailing \n

Now:

  
  
for i in "${!arr3[@]}"; do
  echo "$i -> ${arr3[$i]}"
done
  
  

Example output:

  
  
0 -> 2026-02-14 16-36-43.mkv
1 -> 2026-02-14 16-39-36.mkv
...
  
  

4. 🔁 Real Pattern: Build a Reverse Lookup Map

Goal: 👉 Given a filename → find its index

❌ Naive (broken)

  
  
declare -A map
for i in "${!arr3[@]}"; do
  map[${arr3[$i]}]=$i   # ❌ WRONG
done
  
  
  • -A is for an associative array -> hashmap
  • -a is for an index array

Why it fails:

  • filenames contain spaces
  • Bash splits them into multiple words

✅ Correct version

  
  
declare -A map

for i in "${!arr3[@]}"; do
  map["${arr3[$i]}"]=$i
done
  
  

🔍 Use it

  
  
file="2026-02-14 16-40-28.mkv"
echo "${map["$file"]}"
  
  

👉 O(1) lookup instead of scanning the array.


5. 🧠 Debugging Gotcha

If you write:

  
  
echo "$k -> map[$k]"
  
  

You'll get:

  
  
file -> map[file]
  
  

👉 That's just a string.

✅ Correct

  
  
echo "$k -> ${map["$k"]}"
  
  

👉 ${...} triggers evaluation.


6. 🔁 Iteration Patterns (Real Usage)

Process files safely

  
  
for file in "${arr3[@]}"; do
  echo "Processing: $file"
done
  
  

With index (important for mapping)

  
  
for i in "${!arr3[@]}"; do
  file=${arr3[$i]}
  echo "$i -> $file"
done
  
  

7. ⚙️ Real Use Case: Filtering Files

Example: keep only .mkv

  
  
filtered=()

for f in "${arr3[@]}"; do
  [[ $f == *.mkv ]] && filtered+=("$f")
done
  
  

8. ✂️ Transformations

Rename preview

  
  
for f in "${arr3[@]}"; do
  echo "${f/.mkv/.mp4}"
done
  
  

Apply to array

  
  
new=("${arr3[@]/.mkv/.mp4}")
  
  

9. 🧱 Associative Arrays = Power Tool

Real use: deduplicate files

  
  
declare -A seen
unique=()

for f in "${arr3[@]}"; do
  if [[ -z ${seen["$f"]} ]]; then
    unique+=("$f")
    seen["$f"]=1
  fi
done
  
  

10. ⚠️ Subshell Trap (VERY Important)

❌ Broken

  
  
cat file.txt | while read -r line; do
  arr+=("$line")
done
  
  

👉 arr is empty after loop.

✅ Correct

  
  
while read -r line; do
  arr+=("$line")
done < file.txt
  
  

11. ⚡ Arrays as Safe Command Builders

Real-world safe command

  
  
args=(-l -h --color=auto)
ls "${args[@]}"
  
  

Dynamic command

  
  
cmd=(grep -i "error" logfile.txt)
"${cmd[@]}"
  
  

👉 No eval, no injection risk.


12. 🧬 Subtle Tricks

Reverse array

  
  
rev=()
for ((i=${#arr3[@]}-1; i>=0; i--)); do
  rev+=("${arr3[i]}")
done
  
  

Stack behavior

  
  
stack=()

stack+=("a")              # push
last="${stack[-1]}"       # peek
unset 'stack[-1]'         # pop
  
  

Join safely

  
  
join_by() {
  local IFS="$1"
  shift
  echo "$*"
}

join_by "," "${arr3[@]}"
  
  

Here there is a lot going on. In fact, a simpler version could be:

  
  
$ arr=("a b" "c" d)
$ IFS=","
$ echo "${arr[*]}"
a b,c,d
  
  

This works because "${arr[*]}" joins all elements into a single string using the first character of IFS.

But the function is more general, because it is intended to be used as:

  
  
$ join_by "," "${arr[@]}"
  
  

Here, the separator is given as the first argument, which is why we do:

local IFS="$1"

Then we shift, to remove the first argument (the separator), so that the remaining arguments are only the elements to join.

And because inside a function the arguments are not in an array but in positional parameters:

  
  
$1, $2, … → individual arguments
$@ → all arguments (as separate elements)
$* → all arguments as a single string
  
  

Using:

  
  
echo "$*"
  
  

joins all remaining arguments into one string using IFS.

So effectively:

  
  
"$*"
  
  

inside the function behaves like:

  
  
"${arr[*]}"
  
  

but applied to the function arguments instead of a specific array.


13. ⚡ Performance Reality (Important)

Arrays in Bash are:

  • slow compared to real languages
  • memory-heavy (strings everywhere)

Use arrays when:

  • handling argument lists
  • small/medium datasets
  • mapping (like the reverse lookup example)

Avoid arrays when:

  • processing huge files
  • streaming is possible

14. 🧭 Mental Model Upgrade

The key insight:

Bash arrays are not "data structures" — they are word control mechanisms

They exist to:

  • preserve boundaries between values
  • prevent splitting/globbing bugs
  • safely build commands

🚀 Final Real Example (Everything Together)

  
  
mapfile -t arr3 < <(ls)

declare -A map

# Build reverse lookup
for i in "${!arr3[@]}"; do
  map["${arr3[$i]}"]=$i
done

# Use it
target="${arr3[3]}"
echo "Index of '$target' is ${map["$target"]}"

# Process safely
for file in "${arr3[@]}"; do
  echo "Processing: $file"
done
  
  

💬 Final Thought

You're already touching the hard parts:

  • quoting
  • expansion
  • associative arrays with real data

That's where most Bash scripts break — and where good ones are made.