Shell Lesson 6 of 42

Arrays: Indexed, Associative, Slicing, mapfile/readarray & the Cardinality Discipline That Replaces Word-Splitting Hacks Forever

If you’ve spent years writing shell that pretends arrays don’t exist — using space-separated strings as fake lists, parsing the output of ls, or shoving “structured” data into colon-separated KEY=VALUE blobs — this is the lesson that changes how you write shell. Bash has had real indexed arrays since the early 90s and real associative arrays (hashes) since version 4 (released 2009). They are first-class values, fast, and dramatically safer than every string-based hack people use to avoid them.

This lesson covers both kinds of array, the iteration patterns that go with them, the mapfile/readarray builtin for reading lines into an array safely, the all-important "${arr[@]}" vs "${arr[*]}" distinction, and the file-list collection pattern that should replace every for f in $(ls) you’ve ever written.

A few macOS users will hit a wall here: macOS ships with bash 3.2 (it’s been frozen there since 2007 because of GPLv3 licensing). Associative arrays don’t work on stock macOS bash. The fix is brew install bash, then either chsh -s /opt/homebrew/bin/bash or just call your scripts with the new bash. Every example in this lesson assumes bash 4+; we’ll note bash 3.2 limitations where they matter.


1. Indexed arrays: the basics

An indexed array is an ordered collection of strings, indexed by non-negative integers starting at 0. Bash arrays are sparse — you can have indices 0, 1, 5, 100 with nothing in between, and the array still works.

Declaration and assignment

# Implicit declaration with the parentheses syntax
FRUITS=(apple banana cherry)

# Explicit declaration
declare -a FRUITS=(apple banana cherry)

# Empty array
EMPTY=()

# With explicit indices (allows sparse arrays)
SPARSE=([0]=zero [5]=five [10]=ten)

# With computed values
TODAY=$(date +%a)
DAYS=(Mon Tue Wed Thu Fri Sat Sun "$TODAY")

The parentheses-with-spaces syntax is the canonical form. Each space-separated token becomes one element. The same word-splitting and quoting rules from L2 apply — quote elements that contain spaces:

NAMES=(alice bob "carol jones" david)
echo "${#NAMES[@]}"          # 4 — four elements
echo "${NAMES[2]}"           # carol jones

If you wrote NAMES=(alice bob carol jones david) (no quotes around "carol jones"), you’d get five elements with carol and jones as separate tokens.

Accessing elements

FRUITS=(apple banana cherry)

echo "${FRUITS[0]}"          # apple — first element (index 0!)
echo "${FRUITS[1]}"          # banana
echo "${FRUITS[2]}"          # cherry
echo "${FRUITS[-1]}"         # cherry — bash 4.3+: negative indices count from end
echo "${FRUITS[-2]}"         # banana
echo "${FRUITS}"             # apple — same as ${FRUITS[0]}, NOT all elements!

The last form is a notorious pitfall. $FRUITS and ${FRUITS} and ${FRUITS[0]} are all the same — they give you the first element, not the array. To get all elements, you need the special [@] or [*] index. We’ll cover the difference between those in the next section.

Getting all elements

FRUITS=(apple banana cherry)

echo "${FRUITS[@]}"          # apple banana cherry — all elements as separate words
echo "${FRUITS[*]}"          # apple banana cherry — all elements joined by IFS[0]
echo "${#FRUITS[@]}"         # 3 — number of elements
echo "${!FRUITS[@]}"         # 0 1 2 — list of indices (note the leading !)

The [@] form expands to one shell-word per element. The [*] form joins all elements with the first character of IFS (default: a space). When unquoted, the difference is invisible. When quoted, the difference is everything. Section 4 of this lesson is dedicated to that distinction — don’t skim it.

Appending

FRUITS=(apple banana)
FRUITS+=(cherry)             # append one
FRUITS+=(date elderberry)    # append several
echo "${FRUITS[@]}"          # apple banana cherry date elderberry

The += operator with (...) appends. The parentheses are essential — without them, += does string concatenation on the first element only:

FRUITS+=cherry               # WRONG: appends "cherry" to ${FRUITS[0]}, giving "applecherry"
echo "${FRUITS[0]}"          # applecherry

Always use arr+=(value) to append, not arr+=value.

Modifying and deleting

FRUITS=(apple banana cherry)
FRUITS[1]="berry"            # replace index 1
echo "${FRUITS[@]}"          # apple berry cherry

unset 'FRUITS[1]'            # remove index 1
echo "${FRUITS[@]}"          # apple cherry
echo "${#FRUITS[@]}"         # 2
echo "${!FRUITS[@]}"         # 0 2 — note: 1 is gone, 2 didn't shift down (sparse!)

unset 'FRUITS[1]' removes the element but does not renumber the remaining elements. The array is now sparse. This is sometimes surprising. To get a “compact” array after deletion:

FRUITS=("${FRUITS[@]}")      # rebuild without gaps
echo "${!FRUITS[@]}"         # 0 1 — now indices are contiguous

The quoting around 'FRUITS[1]' matters: bash’s globbing might expand [1] as a character class against files in your current directory, breaking the unset. Always quote the argument to unset for arrays.

To delete the entire array:

unset FRUITS                 # delete the variable entirely
FRUITS=()                    # set to empty (variable still defined as array)

2. Iterating over arrays

The canonical iteration form:

FRUITS=(apple banana cherry)

for fruit in "${FRUITS[@]}"; do
  echo "$fruit"
done

Notice the double quotes around "${FRUITS[@]}". This expansion produces one quoted shell-word per array element, preserving every byte of every element including spaces, tabs, newlines. This is the only correct iteration form for arrays.

Without quotes:

NAMES=(alice "bob jones" carol)
for n in ${NAMES[@]}; do      # WRONG: bash word-splits each element, giving 4 names
  echo "$n"
done
# Output:
# alice
# bob
# jones
# carol

With quotes:

for n in "${NAMES[@]}"; do    # CORRECT: 3 names preserved
  echo "$n"
done
# Output:
# alice
# bob jones
# carol

This is the same lesson as L2 — quoting suppresses word splitting. Arrays don’t change the rule.

Iterating with index access

FRUITS=(apple banana cherry)

for i in "${!FRUITS[@]}"; do
  echo "${i}: ${FRUITS[$i]}"
done
# Output:
# 0: apple
# 1: banana
# 2: cherry

${!FRUITS[@]} expands to the list of indices. Useful when you need both the index and the value (like Python’s enumerate).

Iterating with a counter

FRUITS=(apple banana cherry)
for ((i=0; i<${#FRUITS[@]}; i++)); do
  echo "${i}: ${FRUITS[$i]}"
done

C-style loop for when you want to skip elements, walk in reverse, or process in pairs:

# Process in pairs of two
PAIRS=(name alice age 30 role engineer)
for ((i=0; i<${#PAIRS[@]}; i+=2)); do
  echo "${PAIRS[$i]} = ${PAIRS[$i+1]}"
done

For most cases, for x in "${arr[@]}" is cleaner. Drop to indexed iteration only when you need the index for arithmetic.


3. Associative arrays (bash 4+ hashes)

An associative array (also called a “hash” or “map”) indexes by string keys rather than integer positions. You declare them with declare -A:

declare -A USER

USER[name]="alice"
USER[age]=30
USER[role]="engineer"

echo "${USER[name]}"         # alice
echo "${USER[age]}"          # 30

Or with the parentheses form:

declare -A CAPITALS=(
  [USA]=Washington
  [UK]=London
  [France]=Paris
  [Japan]=Tokyo
)

echo "${CAPITALS[USA]}"      # Washington
echo "${CAPITALS[Japan]}"    # Tokyo

Critical: you must declare -A before assigning. Without declare -A, bash treats USER[name]=alice as an indexed-array assignment with the string name evaluated as an arithmetic expression (which gives 0). You’d get USER[0]=alice — silently wrong.

# WRONG — without declare -A first
USER[name]="alice"           # assigns to USER[0] because "name" arithmetic-evaluates to 0
echo "${USER[anything]}"     # also "alice" — every key looks like 0
echo "${USER[name]}"         # alice (still index 0)

# RIGHT
declare -A USER
USER[name]="alice"
echo "${USER[name]}"         # alice
echo "${USER[role]:-unknown}"  # unknown

This is one of the most common bugs in associative-array code. Always declare -A first. When in doubt, do it explicitly at the top of your function:

process_user() {
  declare -A user            # local-scoped associative array
  user[name]="$1"
  user[age]="$2"
  # ...
}

In bash 4.4+ you can combine: declare -A is implicitly local inside a function, but local -A user is more explicit and works in all bash 4.x.

Iterating

Iteration is the same shape as indexed arrays:

declare -A CAPITALS=(
  [USA]=Washington
  [UK]=London
  [France]=Paris
)

# Iterate over keys
for country in "${!CAPITALS[@]}"; do
  echo "${country}: ${CAPITALS[$country]}"
done

The ${!ARR[@]} form gives the list of keys for both indexed and associative arrays — for indexed, those are integers; for associative, they’re strings.

Key order is unspecified. Bash does not guarantee any particular ordering for associative-array iteration. If you need sorted output:

for country in $(echo "${!CAPITALS[@]}" | tr ' ' '\n' | sort); do
  echo "${country}: ${CAPITALS[$country]}"
done

Or, more robustly with a real array:

mapfile -t SORTED_KEYS < <(printf '%s\n' "${!CAPITALS[@]}" | sort)
for country in "${SORTED_KEYS[@]}"; do
  echo "${country}: ${CAPITALS[$country]}"
done

Membership test

declare -A FLAGS=([verbose]=1 [debug]=1)

if [[ -v FLAGS[verbose] ]]; then
  echo "verbose is set"
fi

if [[ -n "${FLAGS[debug]+x}" ]]; then
  echo "debug exists (POSIX-friendly form)"
fi

The -v test (bash 4.2+) checks “is this variable/key set” — works for both regular variables and array keys. The ${VAR+x} form is the older trick: expands to the literal x if VAR is set, empty if not.

Counting

echo "${#CAPITALS[@]}"       # number of key-value pairs

Removing a key

unset 'CAPITALS[USA]'        # delete one key
echo "${!CAPITALS[@]}"       # USA is gone

The same quoting rule applies: quote the argument to prevent globbing.


4. The "${arr[@]}" vs "${arr[*]}" distinction — read this carefully

This is the single most-misunderstood piece of array syntax in bash. It also appears for "$@" vs "$*" (which we covered in L2), and for the same reason. The rule is clean once you see it.

ARR=(a b "c d" e)
Expansion Behaviour
${ARR[@]} Splits each element on IFS; effectively gives 5 words: a b c d e
${ARR[*]} Joins all elements with first char of IFS, then splits on IFS; same 5 words
"${ARR[@]}" Each element becomes one quoted word; 4 args: a, b, c d, e
"${ARR[*]}" All elements joined by first char of IFS; 1 quoted word: a b c d e

The rule:

Practical examples:

ARGS=(--verbose --port 8080 "--name=Alice Smith")

# CORRECT — 4 separate arguments, "--name=Alice Smith" stays intact
my-tool "${ARGS[@]}"

# WRONG — 1 argument: "--verbose --port 8080 --name=Alice Smith"
my-tool "${ARGS[*]}"

# WRONG — 5+ arguments because "--name=Alice Smith" word-splits
my-tool ${ARGS[@]}

When in doubt: use "${arr[@]}". The [*] form is correct only when you specifically want a single string.

Joining elements with a custom separator

The first character of IFS controls the join character for "${arr[*]}". So you can join with any character:

ARR=(one two three)

IFS=','      ; echo "${ARR[*]}"    # one,two,three
IFS=' | '    ; echo "${ARR[*]}"    # one |two |three   (only first char of IFS used)
IFS=$'\n'    ; echo "${ARR[*]}"    # one<NL>two<NL>three

The first-character rule is annoying — you can’t join with a multi-character separator this way. For multi-char joins, use printf or a loop:

join_by() {
  local sep="$1"; shift
  local out=""
  local first=1
  local elem
  for elem in "$@"; do
    if (( first )); then
      out="$elem"
      first=0
    else
      out+="${sep}${elem}"
    fi
  done
  printf '%s' "$out"
}

ARR=(one two three)
join_by ' | ' "${ARR[@]}"     # one | two | three

This is a common helper to keep around.


5. Slicing and substring operations

Bash supports slicing on arrays with the same syntax as substring extraction on strings (covered in L2).

ARR=(zero one two three four five)

echo "${ARR[@]:1:3}"         # one two three — start at index 1, take 3 elements
echo "${ARR[@]:2}"           # two three four five — start at index 2 to end
echo "${ARR[@]: -2}"         # four five — last 2 (note the leading space!)

The slice syntax: ${arr[@]:OFFSET:LENGTH}. Both are arithmetic expressions. Negative offsets count from the end (need a leading space to disambiguate from :-default).

For associative arrays, slicing on [@] doesn’t quite work the same way (key order is unspecified), but slicing on the list of keys does:

declare -A USER=([name]=alice [age]=30 [role]=engineer)

KEYS=("${!USER[@]}")           # snapshot keys into an indexed array
echo "${KEYS[@]:0:2}"          # first two keys (whatever those are)

String operations on array elements

All the parameter-expansion operators from L2 work on array elements — and on the entire array at once with [@].

PATHS=(/var/log/a.log /var/log/b.log /tmp/c.log)

# Apply a transformation to every element
echo "${PATHS[@]##*/}"         # a.log b.log c.log — basename of every element
echo "${PATHS[@]%.log}"        # /var/log/a /var/log/b /tmp/c — strip .log suffix
echo "${PATHS[@]/log/LOG}"     # /var/LOG/a.log /var/LOG/b.log /tmp/c.LOG — replace first "log" each
echo "${PATHS[@]//log/LOG}"    # /var/LOG/a.LOG /var/LOG/b.LOG /tmp/c.LOG — replace all "log"s

This is enormously powerful. You can do basename, dirname, replacement, case folding, and length operations across an entire array in one expansion, with no fork. It’s much faster than piping through sed or awk.

# Fast: bash builtin, no fork
LOWERED=("${ARR[@]}")
LOWERED=("${LOWERED[@],,}")    # all lowercase

# Slow: forks one awk per element
LOWERED=()
for x in "${ARR[@]}"; do
  LOWERED+=("$(echo "$x" | tr '[:upper:]' '[:lower:]')")
done

In a loop with thousands of iterations, the bash-builtin form can be 100× faster.


6. mapfile (also called readarray) — the right way to load files into arrays

mapfile -t ARR < FILE reads a file line by line and stores each line as one array element. The -t flag strips trailing newlines from each line (almost always what you want).

mapfile -t LINES < /etc/hostname
echo "${LINES[0]}"           # the hostname
echo "${#LINES[@]}"          # 1

mapfile -t USERS < users.txt
for u in "${USERS[@]}"; do
  echo "Processing user: $u"
done

mapfile is the modern, byte-safe replacement for while read line; do arr+=("$line"); done and the various arr=( $(cat file) ) hacks people write. It handles every line correctly: leading whitespace, trailing whitespace, embedded glob characters — every edge case is handled by being byte-exact and not subject to word splitting.

Reading from a command instead of a file

mapfile -t SERVICES < <(systemctl list-units --type=service --no-legend | awk '{print $1}')
echo "${#SERVICES[@]}"

The < <(cmd) is process substitution (covered in L4 and revisited in L7). It runs cmd and presents its output as a file the array can read from. The whole thing happens in the parent shell, so the array is populated correctly (no subshell trap from L4).

Useful mapfile flags

The NUL-separated form is essential for filename-safe collection:

mapfile -d '' -t FILES < <(find /var/log -type f -name '*.log' -print0)
for f in "${FILES[@]}"; do
  echo "Processing: $f"
done

This handles every legal filename, including ones with newlines or weird characters. This is the right way to collect a list of files into an array — far better than parsing find’s output as text.

readarray is exactly the same builtin as mapfile. Bash defines them as aliases. Use whichever name you prefer; this course uses mapfile.


7. Sorting, deduplicating, and the read-into-array idioms

Bash has no built-in sort. You shell out to sort:

NAMES=(charlie alice bob alice david)

# Sort alphabetically, in place
mapfile -t SORTED < <(printf '%s\n' "${NAMES[@]}" | sort)
echo "${SORTED[@]}"          # alice alice bob charlie david

# Sort and dedupe
mapfile -t UNIQUE < <(printf '%s\n' "${NAMES[@]}" | sort -u)
echo "${UNIQUE[@]}"          # alice bob charlie david

# Numeric sort
NUMBERS=(10 2 30 4 100)
mapfile -t SORTED_NUM < <(printf '%s\n' "${NUMBERS[@]}" | sort -n)
echo "${SORTED_NUM[@]}"      # 2 4 10 30 100

# Reverse sort
mapfile -t SORTED_REV < <(printf '%s\n' "${NAMES[@]}" | sort -r)

The printf '%s\n' "${arr[@]}" idiom is worth memorising — it prints each array element on its own line, byte-exact. Combined with mapfile -t ... < <(...), it gives you a clean “transform array via Unix tool” pipeline.

Deduplication preserving original order

NAMES=(charlie alice bob alice david bob)

mapfile -t UNIQUE < <(printf '%s\n' "${NAMES[@]}" | awk '!seen[$0]++')
echo "${UNIQUE[@]}"          # charlie alice bob david

awk '!seen[$0]++' is a famous one-liner: tracks each line in seen, prints it the first time only. Preserves original order, unlike sort -u.

Membership test (linear scan)

Bash has no built-in “is this element in the array” check. The simple form:

contains() {
  local needle="$1"; shift
  local x
  for x in "$@"; do
    [[ "$x" == "$needle" ]] && return 0
  done
  return 1
}

FRUITS=(apple banana cherry)
if contains "banana" "${FRUITS[@]}"; then
  echo "found"
fi

This is O(N) — fine for arrays under a few thousand elements. For larger collections where membership tests are frequent, use an associative array as a set:

declare -A FRUIT_SET=([apple]=1 [banana]=1 [cherry]=1)

if [[ -n "${FRUIT_SET[banana]:-}" ]]; then
  echo "found"
fi

O(1) lookup. The associative array is the right choice for set semantics.


8. The file-list collection pattern

This is the canonical pattern this course wants you to know cold. It replaces every “iterate over files” hack you’ve ever seen.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

# Step 1: collect files into an array, NUL-safe
mapfile -d '' -t FILES < <(find /var/log -type f -name '*.log' -print0)

# Step 2: report the count before doing anything destructive
echo "Found ${#FILES[@]} log files"

# Step 3: iterate safely
for f in "${FILES[@]}"; do
  echo "Processing: $f"
  # do something with $f
done

# Step 4: bulk operations are also safe
gzip -- "${FILES[@]}"

Why this is the right shape:

  1. find -print0 + mapfile -d '' handles every legal filename (including newlines, glob chars, spaces).
  2. -type f filters out directories and weird filesystem entries before we even look at them.
  3. The intermediate array is inspectable. You can echo "${#FILES[@]}" to see how many you got, before doing anything destructive. This is the single most-important guard against accidental mass-modification.
  4. gzip -- "${FILES[@]}" invokes gzip once with all filenames as arguments, instead of one fork per file. Hugely faster for many files.

Compare to the wrong forms you may have written before:

for f in $(ls *.log); do gzip "$f"; done           # WRONG: spaces, globs, fork-per-file
for f in *.log; do gzip "$f"; done                 # OK for one directory; no recursion; doesn't handle empty match
find . -name '*.log' -exec gzip {} \;              # forks per file; slower
find . -name '*.log' | while read f; do ...; done  # subshell trap from L4

The array-pattern form gets all of these right and is the canonical advanced-shell idiom.


9. Real-world example: a service-status report

#!/usr/bin/env bash
# service-report.sh — print a sorted table of services with their statuses
set -euo pipefail
IFS=$'\n\t'

# 1. Collect service names into an indexed array
mapfile -t SERVICES < <(systemctl list-units --type=service --no-legend --no-pager \
  | awk '{print $1}' | sort)

# 2. Build a parallel associative array of statuses
declare -A STATUS
for svc in "${SERVICES[@]}"; do
  STATUS[$svc]=$(systemctl is-active "$svc" 2>/dev/null || echo "unknown")
done

# 3. Print a summary
COUNT_RUNNING=0
COUNT_FAILED=0
COUNT_OTHER=0
printf '%-50s  %s\n' "Service" "Status"
printf '%-50s  %s\n' "-------" "------"
for svc in "${SERVICES[@]}"; do
  printf '%-50s  %s\n' "$svc" "${STATUS[$svc]}"
  case "${STATUS[$svc]}" in
    active)   (( COUNT_RUNNING++ )) ;;
    failed)   (( COUNT_FAILED++ )) ;;
    *)        (( COUNT_OTHER++ )) ;;
  esac
done

# 4. Footer
echo
echo "Running: $COUNT_RUNNING"
echo "Failed:  $COUNT_FAILED"
echo "Other:   $COUNT_OTHER"

# 5. Exit non-zero if anything is failed
(( COUNT_FAILED == 0 ))

Things to notice:

This is the shape of structured shell code with arrays. It’s clean, fast, byte-safe, and readable.


10. Pitfalls and edge cases

Bash 3.2 (macOS) — no associative arrays

If your script must run on stock macOS bash, you can’t use declare -A. Workarounds:

The cleanest fix for any non-trivial script is brew install bash and shebang as #!/usr/bin/env bash (which finds /opt/homebrew/bin/bash ahead of /bin/bash). Don’t try to write portable shell that supports bash 3.2; it’s not worth the contortions.

The [*] vs [@] pitfall, again

ARR=(one "two three" four)

cmd ${ARR[@]}        # 4 args: one two three four
cmd ${ARR[*]}        # 4 args: one two three four (same — IFS-split after expansion)
cmd "${ARR[@]}"      # 3 args: one, two three, four
cmd "${ARR[*]}"      # 1 arg:  "one two three four"

Default to "${ARR[@]}". Use "${ARR[*]}" only when you specifically want a single joined string.

${ARR} is not the array

ARR=(one two three)
echo "${ARR}"        # "one" — first element only
echo "${ARR[@]}"     # "one two three" — all elements

A bare $ARR or ${ARR} is ${ARR[0]}. This is a constant source of bugs; if you mean “the array”, spell it "${ARR[@]}".

Spaces around = in declare -A

The same rule as L2 — no spaces around =. But there’s a twist for arrays:

declare -A USER=([name]=alice)            # WORKS
declare -A USER = ([name]=alice)          # WRONG: bash interprets this differently

# Inside an array assignment, no quotes around the key in [...]:
declare -A USER=([name]="Alice Smith")    # WORKS

# Quoted key works too:
declare -A USER=(["name"]="Alice Smith")  # WORKS

When in doubt, write the elements one per line for clarity:

declare -A USER=(
  [name]="Alice Smith"
  [age]=30
  [role]=engineer
)

Iterating over associative arrays gives keys, not values

declare -A CAPITALS=([USA]=Washington [UK]=London)

for c in "${CAPITALS[@]}"; do        # Washington London (values, in indeterminate order)
  echo "$c"
done

for c in "${!CAPITALS[@]}"; do       # USA UK (keys)
  echo "$c -> ${CAPITALS[$c]}"
done

Almost always you want ${!ARR[@]} (keys) and look up the value by key. Pure value iteration is rare for hashes.

Sorting is always external

There is no built-in array sort in bash. You always go through sort. The pattern is:

mapfile -t SORTED < <(printf '%s\n' "${UNSORTED[@]}" | sort [-options])

For numeric sort, sort -n. For reverse, sort -r. For sort-and-dedupe, sort -u. For preserving original order while deduping, awk '!seen[$0]++'.


11. Fourteen array idioms to memorise

# 1. Declare and populate
ARR=(one two three)

# 2. Append
ARR+=(four)
ARR+=(five six)

# 3. Length
COUNT="${#ARR[@]}"

# 4. All elements (the canonical iteration)
for x in "${ARR[@]}"; do
  echo "$x"
done

# 5. List of indices / keys
for i in "${!ARR[@]}"; do
  echo "${i}: ${ARR[$i]}"
done

# 6. Slice
FIRST_TWO=("${ARR[@]:0:2}")

# 7. Last element (bash 4.3+)
LAST="${ARR[-1]}"

# 8. Apply transformation across array
LOG_PATHS=(/var/log/a.log /var/log/b.log)
BASENAMES=("${LOG_PATHS[@]##*/}")     # a.log b.log

# 9. Read file lines into array
mapfile -t LINES < file.txt

# 10. Read NUL-separated input (filename-safe)
mapfile -d '' -t FILES < <(find /path -type f -print0)

# 11. Sort
mapfile -t SORTED < <(printf '%s\n' "${ARR[@]}" | sort)

# 12. Deduplicate, preserving order
mapfile -t UNIQUE < <(printf '%s\n' "${ARR[@]}" | awk '!seen[$0]++')

# 13. Associative array (set membership)
declare -A SET=([apple]=1 [banana]=1)
[[ -n "${SET[banana]:-}" ]] && echo "in set"

# 14. Forwarding to a command
my-command "${ARR[@]}"

Internalise these and you’ve replaced 90% of the string-hack code you’d otherwise write.


12. What you must internalise before lesson 7

If any felt fuzzy, re-read. With L1–L6 you have all of bash’s data-handling primitives. L7 takes you into I/O — file descriptors, redirection, here-docs, process substitution.


What’s next

Lesson 7 covers I/O redirection in depth: file descriptors (0, 1, 2 and beyond), the < > >> 2> &> 2>&1 operators, here-docs (<<EOF), here-strings (<<<), tee, exec for FD remapping, and process substitution as the elegant alternative to temporary files. Bring everything from L1–L6 — every redirection is a process-and-quoting decision.

shellbasharraysassociative-arraysmapfilereadarrayslicingindexed-arraysfundamentalslinux
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments