Storing many values in a single variable with indexed arrays: declaration, per-index access, iterating all elements with `"${arr[@]}"`, counting, appending, slicing, and deleting elements. Includes a server-list ping practice and a backup-file-list practice along with the traps that commonly catch people.

In episode 13 we learned to wrap logic into functions — named, reusable code blocks. In this episode we cover the thing that makes functions and loops even more powerful: storing many values in a single variable via indexed arrays.
So far, one variable can only store one value: user="arman". What if you need to store a list of 20 servers, a list of 50 backup files, or 100 computed numbers? Creating 20 separate variables (server1, server2, ...) makes no sense — and that's exactly the problem arrays solve.
Imagine a filing cabinet with numbered drawers. Each drawer holds one document, and the drawer number is the key to retrieving it. An array is exactly that: one name (arr), many slots indexed by number (arr[0], arr[1], ...). Instead of carrying 20 separate folders in your hands, you carry one cabinet — far easier to move around and process.
The array + for loop combination (episode 11) is one of the most productive combinations in scripting: arrays store the data, loops process it. In this episode we'll master all the basic array operations — declaration, access, iteration, append, slice, and unset — then tie them together in two real practices: monitoring a server list and backing up a file list.
Before the syntax, let's reaffirm what problem arrays solve. Consider the no-array approach to managing several servers:
server1="web-01"
server2="web-02"
server3="db-01"
echo "$server1"
echo "$server2"
echo "$server3"This approach breaks down quickly: there's no easy way to iterate those variables, count how many there are, or add new ones without writing new code. With arrays, all those lists become one entity that can be counted, iterated, and extended dynamically. This is what's called data-driven scripting: the script no longer hard-codes each item, but processes the whole collection with one command.
Arrays in Bash are declared with parentheses, and elements are separated by spaces:
buah=("apel" "mangga" "pisang")
echo "${buah[0]}"
echo "${buah[1]}"
echo "${buah[2]}"
# apel
# mangga
# pisangThere are two things you must note in the access syntax:
0. The first element is arr[0], not arr[1]. This is the universal convention shared with C and almost all modern languages.{} are mandatory when accessing. ${buah[0]} — without the braces, Bash would mistakenly read buah[0] as a weird variable name. Braces also matter for regular variables (${nama}, remember the expansion episode), but in arrays they're absolutely required.Note
You can also fill an array element by element: buah[0]="apel", buah[1]="mangga". This is useful when new values are only known later. Bash even fills in empty slots automatically: if you write buah[5]="anggur" on an empty array, elements [0]–[4] become empty (treated as elements without values). Rarely desired, but good to know.
"${arr[@]}" and "${arr[*]}"To access the entire contents of an array, there are two notations that look alike but mean different things:
"${arr[@]}" — each element becomes a separate argument/word. This is almost always what you want."${arr[*]}" — all elements are joined into one string separated by the IFS character (default: space).daftar=("satu kata" "dua kata" "tiga kata")
for item in "${daftar[@]}"; do
echo "Item: $item"
done
echo "Gabungan: ${daftar[*]}"This difference is crucial when elements contain spaces. With "${daftar[@]}", the element "satu kata" stays as one item; with "${daftar[*]}", everything merges into one string "satu kata dua kata tiga kata". This is exactly analogous to the difference between "$@" and "$*" for script arguments, which you may have already encountered.
Important
Always write "${arr[@]}" with quotes when iterating or passing to commands — and never forget the curly braces. "$arr[@]" (without braces) isn't an array at all: Bash reads $arr (the first element) followed by the literal [@]. This mistake produces output you never expected. The formula to memorize cold: "${arr[@]}", three symbols — the quotes, the curly braces, and the @ — none of them may be left out.
Arrays aren't static. The most commonly used operations:
daftar=("a" "b" "c")
echo "Jumlah: ${#daftar[@]}" # 3
daftar+=("d")
daftar+=("e" "f")
echo "Jumlah: ${#daftar[@]}" # 6
unset 'daftar[1]'
echo "Jumlah: ${#daftar[@]}" # 5
echo "Isi: ${daftar[@]}" # a c d e fLet's break down each one:
${#daftar[@]} — the # sign in front counts elements. It's the "length" form of ${#str}, which counts string length.daftar+=("d") — the append syntax: adds one or more elements to the end of the array. This is the correct way, far cleaner than daftar[${#daftar[@]}]="d".unset 'daftar[1]' — deletes the element at index 1. Note the quotes: unset 'daftar[1]' protects the brackets from glob expansion. A deleted element leaves a "hole" — other indices don't shift, so daftar[1] is now empty and the count decreases.angka=(10 20 30 40 50)
sub=${angka[@]:1:2}
echo "Slice: $sub"
# 20 30
unset 'angka' # hapus seluruh array
echo "Jumlah: ${#angka[@]}" # 0Slice "${arr[@]:offset:length}" takes a chunk starting at index offset with length length — the array version of substring slicing ${str:offset:length}.
for — Whole Elements, Not SplitThis is where arrays become truly powerful: iterating all elements with for, and because we use "${arr[@]}", every element containing spaces stays intact:
server=("web-01" "web-02" "db-01")
for host in "${server[@]}"; do
echo "Menghubungi $host..."
doneCompare with the without-quotes-and-braces version — for host in $server — which would only print web-01 (the first element) because $server without an index is element [0], and for host in ${server[@]} which would split spaced elements. Only "${server[@]}" is correct. This is a pattern we've stressed since episode 11 and it applies here in full force.
Often the list a script needs comes from a command's output. There are two ways, with a very important difference.
The wrong way, for ls for example — using command substitution directly:
arr=($(ls *.txt))
echo "${#arr[@]}"The problem is the same one we covered in episode 11: the result of $(ls ...) is split on spaces (word splitting), and glob characters get expanded too. The file catatan penting.txt becomes two elements.
The right way is to use mapfile (or readarray), which reads output line by line as elements:
mapfile -t files < <(ls *.txt)
echo "Jumlah file: ${#files[@]}"
for f in "${files[@]}"; do
echo "File: $f"
donemapfile -t reads stdin line by line, trims the newline (-t), and stores each line as one element. The filling process itself already preserves line integrity, so spaces inside filenames aren't a problem.
Now let's tie it all together. First scenario: a script monitors a number of servers. The server list is defined once as an array, then iterated:
#!/usr/bin/env bash
server=("192.168.1.10" "192.168.1.11" "192.168.1.12")
ok=0
gagal=0
for host in "${server[@]}"; do
if ping -c 1 -W 2 "$host" >/dev/null 2>&1; then
echo "OK : $host"
ok=$((ok + 1))
else
echo "FAIL : $host"
gagal=$((gagal + 1))
fi
done
echo "Ringkasan: $ok OK, $gagal gagal dari ${#server[@]} server."Notice the accumulator pattern (ok and gagal) incrementing inside the loop, and the final report using ${#server[@]} for the total count. When the server list grows, the script doesn't change — just add one array element.
Second scenario: backing up several important files whose list is stored in an array, with a per-file report:
#!/usr/bin/env bash
backup_dir="$HOME/backup-$(date +%F)"
mkdir -p "$backup_dir"
target=("/etc/hostname" "/etc/hosts" "/etc/fstab")
for file in "${target[@]}"; do
if [ -f "$file" ]; then
cp "$file" "$backup_dir/"
echo "Backup OK : $file"
else
echo "Tidak ada : $file"
fi
done
echo "Backup selesai di: $backup_dir"This script shows a clean division of labor: data (the target array) is separate from logic (the loop and condition). That's what makes a script easy to extend — adding a new file only means changing one array line, without touching the logic at all.
Next, let's combine episode 13's material with arrays: how to pass an array into a function. It's important to understand from the start: Bash has no special syntax for "passing an array as an argument" — an array is a collection of values, not a single block of data. The correct idiom is to spread its elements with "${arr[@]}", then receive them inside the function via "$@":
#!/usr/bin/env bash
tampilkan_list() {
local label="$1"
shift
echo "$label:"
for item in "$@"; do
echo " - $item"
done
}
server=("web-01" "web-02" "db-01")
tampilkan_list "Server aktif" "${server[@]}"There are two details worth memorizing:
"${server[@]}" on the caller side spreads all elements into separate arguments for the function — exactly as it spreads elements for for.shift inside the function discards the first argument (label), so "$@" after it contains the remaining array elements. This is a common idiom when a function receives an "option/label" up front and "data" behind it.Without this pattern, you'd be tempted to write tampilkan_list "Server aktif" "$server" — which only passes the first element because $server without an index. With the "${arr[@]}" + "$@" combination, the entire array transfers intact into the function, and the function becomes reusable for any array.
Tip
The "${arr[@]}" + "$@" pattern is the bridge between the two most productive episodes: arrays as data, functions as logic. You can build an array-processing function library — daftar_server, daftar_file, print_list — that accepts any array without needing to know its contents. This is the simplest form of the genericity people often seek from programming languages, and it's available in Bash with this simple syntax.
| Mistake | Symptom | Solution |
|---|---|---|
"${arr[@]}" without quotes | Spaced elements split | Always "${arr[@]}" |
$arr without index and braces | Only the first element is used | Write ${arr[0]} or "${arr[@]}" |
Treating an array as a string (echo $arr) | Output is only the first element | Use "${arr[@]}" or "${arr[*]}" |
arr=($(cmd)) instead of mapfile | Output splits, glob expands | mapfile -t arr < <(cmd) |
Forgetting declare -a (rare) | The variable behaves as a string | Declare explicitly declare -a when needed |
unset arr[1] without quotes | Unexpected glob/interpretation | unset 'arr[1]' |
Caution
The most dangerous mistake is treating an array like an ordinary string: echo $arr only prints the first element, and "$arr" in a command only passes the first element. When a script receives only one of many elements without any error, that's the classic symptom — no error message, just a wrong result. When an array feels "incomplete", check first: does every use include ${...} and [@]/[*]?
In this episode 14 we added collective storage power to your scripts. With indexed arrays you can store many values in one variable: declaring (arr=("a" "b" "c")), accessing by index (${arr[0]}), retrieving everything ("${arr[@]}"), counting (${#arr[@]}), adding elements (arr+=("d")), slicing (${arr[@]:1:2}), and deleting (unset 'arr[1]'). We also built arrays from command output with mapfile and combined it all in real practices of monitoring a server list and backing up a file list.
The principle to take home: an array is one entity containing many values, always accessed via "${arr[@]}" — three symbols (quotes, braces, @) that become an automatic habit. With data structured in arrays and logic wrapped in functions, your scripts can now manage data collections cleanly.
There's still one more advanced array type: the associative array, which is indexed not by numbers but by string keys. In the next episode, episode 15, we'll cover it — key-value pairs in dictionary style, service-to-port mapping, key-value configuration, and frequency counters for counting word occurrences. That's where data that must be looked up by name, not number, feels far more natural. See you in the next episode!