Module 10 — Arrays and Associative Arrays

Updated 3 September 2026

Module 10 — Arrays and Associative Arrays. Lists of hosts, maps of service→port — how bash stores collections, and how to loop over them without the quoting bugs that plague naive scripts.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start. You need Modules 1–9 — above all "$@" vs "$*" (Module 7: arrays reuse that exact distinction), loops (Module 6), and ${ } expansions (Module 9 — array syntax lives in the same workshop). Arrays are bash-only: none of this works under #!/bin/sh (Module 1 C2's dialect line, drawn again). Tools: nothing new.

Part A — Indexed arrays: a numbered list in one variable

A1. Create, read, count

Official docs: Bash manual — Arrays

An indexed array is one variable holding an ordered list of values, numbered from 0:

bash
hosts=(web-1 web-2 "db primary" cache-1)   # parentheses, space-separated; quote spacey members
echo "${hosts[0]}"        # one element, by number
echo "${#hosts[@]}"       # how many elements (the # is Module 9's length, applied to the collection)
echo "${!hosts[@]}"       # the indexes themselves (0 1 2 3)

Every access wears ${ } braces — $hosts[0] without them is $hosts (which quietly means element 0!) followed by literal text [0], a Module-2-style greedy-parse trap. And notice the third member: "db primary" went in as one element, spaces and all — arrays hold values, not words, which is the entire reason they exist and the subject of A2.

Real-world analogy — the numbered pigeonholes. An indexed array is a wall of numbered pigeonholes behind a reception desk: one label (the variable name), many slots, each slot holding one item intact — even an envelope with a long messy address occupies exactly one slot. The register asks: what's in slot 0? how many slots are used? which slot numbers are in use?

Where the analogy stops working. Pigeonhole walls are dense — slots 0 to N, no gaps. Bash arrays can be sparse: removing slot 1 leaves 0, 2, 3 — the numbering does not close up (A3 demonstrates, and it surprises everyone). "How many" and "up to what number" are different questions in bash.

🧪 Exercise 10.1 — the pigeonhole wall
bash
hosts=(web-1 web-2 "db primary" cache-1)
echo "first:   ${hosts[0]}"
echo "count:   ${#hosts[@]}"
echo "indexes: ${!hosts[@]}"
Expected result — click to reveal
plain text
first:   web-1
count:   4
indexes: 0 1 2 3

What to read out of it: four values, one of them containing a space, counted as exactly 4 — the collection knows its own boundaries, which no space-separated string ever could (that was Module 2's whole tragedy). The ! prefix meaning "give me the indexes, not the values" is a shape worth filing: it returns transformed for associative arrays in Part C, where the indexes are names.

A2. "${arr[@]}" — the only correct way to say "all of them"

Official docs: Bash manual — Arrays

Expanding a whole array replays Module 7's finale, note for note. "${arr[@]}" (quoted, @) → one word per element, boundaries intact — the faithful forward. "${arr[*]}" (quoted, *) → all elements welded into a single word — the log-line form. Unquoted either way → elements re-split on spaces, the collection's boundaries destroyed. The rule is the same reflex with brackets on: iterating or forwarding → "${arr[@]}"; building one display string → "${arr[*]}"; unquoted → never. It is not a coincidence: $@/$* are this mechanism — the positional parameters are simply bash's oldest array.

🧪 Exercise 10.2 — the probe settles it again

The third probe mangles on purpose.

bash
hosts=(web-1 web-2 "db primary" cache-1)
printf '[%s]\n' "${hosts[@]}"
echo "---"
printf '[%s]\n' "${hosts[*]}"
echo "---"
printf '[%s]\n' ${hosts[@]}     # ← unquoted: boundaries destroyed
Expected result — click to reveal (contains a deliberate mangling)
plain text
[web-1]
[web-2]
[db primary]
[cache-1]
---
[web-1 web-2 db primary cache-1]
---
[web-1]
[web-2]
[db]
[primary]
[cache-1]

What to read out of it: quoted-@ gave four brackets with db primary whole — the collection survived. Quoted-* gave one bracket — the weld. Unquoted gave FIVE brackets: bash expanded, then word-split, and the space inside db primary became a boundary — a fleet script just gained a host named db and another named primary, neither of which exists. Every array loop you ever write is for h in "${hosts[@]}" — the quoted-@ spelling, untouched, forever.

A3. Growing, shrinking, and the sparse surprise

Append with hosts+=(web-3) — the parentheses matter (hosts+=web-3 would glue text onto element 0, another brace-family trap). Remove one element with unset 'hosts[1]' (quoted, so the brackets reach unset instead of globbing — Module 2 B3 in a new costume). And then the surprise:

Counter-intuitive: unset 'hosts[1]' does not renumber. The indexes become 0, 2, 3 — a hole where 1 was. ${#hosts[@]} says 3 (it counts elements), while the highest index says otherwise, so any C-style loop for ((i=0; i<${#hosts[@]}; i++)) now misses the last element and reads the hole as empty. This is why production bash iterates arrays with for x in "${arr[@]}" (holes simply don't appear) and treats index-arithmetic loops over possibly-sparse arrays as bugs. To truly delete-and-compact, rebuild: hosts=("${hosts[@]}") — expand the survivors, re-collect densely.
🧪 Exercise 10.3 — grow, punch a hole, feel it
bash
hosts=(web-1 web-2 "db primary" cache-1)
hosts+=(web-3)
echo "count: ${#hosts[@]}"
unset 'hosts[1]'
echo "count: ${#hosts[@]}  indexes: ${!hosts[@]}"
hosts=("${hosts[@]}")
echo "count: ${#hosts[@]}  indexes: ${!hosts[@]}"
Expected result — click to reveal
plain text
count: 5
count: 4  indexes: 0 2 3 4
count: 4  indexes: 0 1 2 3

What to read out of it: after the unset, four elements — at indexes 0, 2, 3, 4. The hole is real and permanent until you act. The rebuild line — expand-quoted, re-collect — closes it: same four elements, dense numbering. If you take one habit from this section: "${arr[@]}" iteration never sees holes, so scripts that stick to it are immune to the whole sparse-array bug class.

Interview questions — Part A

🎯 "What are arrays in Bash, and how do you use them?" — asked verbatim at Zero To Mastery; Edureka asks "How to print the first array element?"

The direct answer: arr=(a b "c d") creates an indexed array; "${arr[0]}" reads one element; "${arr[@]}" expands all elements one-word-each; "${#arr[@]}" counts; arr+=(e) appends; for x in "${arr[@]}" iterates safely.

Going deeper: say why arrays over space-separated strings — elements keep their boundaries, so values with spaces survive; and the quoting law inherited from $@/$*: quoted-@ forwards, quoted-* welds, unquoted destroys. Note arrays are bash (and ksh/zsh) — not POSIX sh.

The details that separate candidates: the sparse-array behavior of unset (indexes don't renumber; count ≠ highest index; "${arr[@]}" iteration as the immunity); and the brace-trap that $arr alone means element 0 — both are the follow-ups strong interviewers reach for.

🎯 "How to print all array elements and their respective indexes?" — asked verbatim at Edureka

The direct answer: values: echo "${array[@]}"; indexes: echo "${!array[@]}".

Going deeper: Edureka's published answer writes both unquoted — which works for their spaceless sample and silently breaks on real data; you can now say precisely why, and that the robust display is printf '[%s]\n' "${array[@]}" (element boundaries made visible). The ! prefix generalizes: on associative arrays it yields the keys, making for k in "${!map[@]}" the universal map-iteration idiom.

The details that separate candidates: pairing values-and-indexes in one loop — for i in "${!arr[@]}"; do echo "$i: ${arr[$i]}"; done — the spelling that survives sparse arrays, where a naive 0..count loop breaks; naming that failure unprompted is the differentiator.

Part B — Filling arrays from the world

B1. mapfile: a file into an array, safely

Module 6 read files line-by-line in a streaming loop. When you instead want the whole list in memory — to count it, slice it, pass it around — the builtin mapfile (also spelled readarray) loads stdin into an array, one line per element: mapfile -t servers < hosts.txt. The -t trims each line's trailing newline and is effectively mandatory (without it every element carries an invisible newline that corrupts later comparisons). Globs fill arrays too — logs=(/var/log/app/*.log) — inheriting Module 6's rules (split-safe results, the no-match literal pattern, nullglob).

🧪 Exercise 10.4 — load, count, slice
bash
cd ~/bash-course
printf 'web-1\nweb-2\ndb primary\n' > hosts.txt
mapfile -t servers < hosts.txt
echo "loaded: ${#servers[@]}"
printf '[%s]\n' "${servers[@]}"
echo "first two: ${servers[@]:0:2}"
Expected result — click to reveal
plain text
loaded: 3
[web-1]
[web-2]
[db primary]
first two: web-1 web-2

What to read out of it: three lines became three elements — db primary intact, no IFS ceremony required (mapfile splits on newlines only, by design; it is the collection-shaped sibling of while IFS= read -r). The last line is Module 9's slice syntax promoted to collections: ${servers[@]:0:2} — offset and length, but counting elements. Choose by need: streaming loop for transform-as-you-go over big inputs; mapfile for "I need the whole list as a thing."

Interview questions — Part B

🎯 "How do you read the lines of a file into an array?" — the natural follow-up to Module 6's file-reading question; asked in screens more than in published lists (tutorials at Baeldung and LinuxConfig cover the mechanics)

The direct answer: mapfile -t arr < file — one line per element, -t stripping newlines. From a command: mapfile -t arr < <(command) (process substitution, Module 12's tool, previewed).

Going deeper: name the anti-pattern it replaces — arr=($(cat file)) splits on all whitespace and glob-expands, Module 6's cat-loop disease in array form — and the pre-mapfile portable spelling (a while-read loop appending with arr+=("$line")), worth knowing for old bash. State the streaming-vs-loading trade: mapfile holds everything in memory; while-read streams.

The details that separate candidates: the < <(cmd) spelling and why the obvious cmd | mapfile -t arr fails silently — the pipeline puts mapfile in a subshell and the array evaporates with it (Module 12's headline gotcha, cited by name); knowing that one failure mode is the strongest signal on this question.

Part C — Associative arrays: labels instead of numbers

C1. declare -A: the map

Official docs: Bash manual — Arrays

An associative array indexes by name instead of number — service→port, host→role, code→meaning. Unlike indexed arrays it must be declared first: declare -A port — and this is a hard requirement, not politeness: without the declaration, port[web]=8080 is treated as an indexed array assignment where web is arithmetic (an unset variable, so 0), and every key silently lands on element 0, last write winning. The rest of the grammar mirrors Part A: ${port[db]} reads, "${!port[@]}" lists keys, "${#port[@]}" counts, for k in "${!port[@]}" iterates. Existence-of-key uses a Module 9 alumnus: [ -n "${port[web]+x}" ] — the +x form expands only if the key is set, distinguishing "unmapped" from "mapped to empty".

Counter-intuitive: associative arrays remember no order — not insertion order, not alphabetical, no order at all; the same map can list keys differently on different bash builds. Scripts that need deterministic output must impose order at read time: for k in $(printf '%s\n' "${!port[@]}" | sort) — or keep a parallel indexed array of keys in insertion order. Tests and diffs against map-iteration output are flaky by construction until you do.
Real-world analogy — the coat check vs the filing cabinet. Indexed arrays are a coat check: you get ticket #7, order is intrinsic. An associative array is a filing cabinet with labeled folders: you ask for "Hernandez," not "the 7th folder," and adding a folder anywhere is fine because label is location. The declare -A is telling the clerk "this cabinet files by name" before anything goes in.

Where the analogy stops working. A real cabinet has visible physical order (alphabetized drawers). Bash's cabinet has none — folders come back in whatever order the clerk grabs them, a different order tomorrow. And a cabinet mislabeled as a coat check (the missing declare -A) doesn't refuse your folders — it stuffs them all into slot 0 and smiles, which is exactly the silent-corruption failure the declaration exists to prevent.

🧪 Exercise 10.5 — the service map
bash
declare -A port
port[web]=8080
port[db]=5432
port[cache]=6379
echo "db port: ${port[db]}"
echo "count:   ${#port[@]}"
for svc in "${!port[@]}"; do
    echo "$svc -> ${port[$svc]}"
done
[ -n "${port[web]+x}" ]   && echo "web is mapped"
[ -z "${port[queue]+x}" ] && echo "queue is not mapped"
Expected result — click to reveal
plain text
db port: 5432
count:   3
db -> 5432
web -> 8080
cache -> 6379
web is mapped
queue is not mapped

What to read out of it: lookups read like sentences — the map's whole point. But stare at the loop's order: db, web, cache — neither insertion order (web, db, cache) nor alphabetical. That is the no-order rule showing itself; your machine may print another permutation, and that variability is the expected result. The last two lines are the existence idiom earning its keep: queue being unmapped is a checkable fact, not a silent empty string.

Now imagine this at 500 hosts. Fleet scripts run on collections: an indexed array is the host list (mapfile -t hosts < <(get-fleet)), and associative arrays carry per-host facts gathered in one pass — status[$host]=ok, version[$host]=2.14.7 — then reported in a second pass. The discipline that makes this scale: quoted-@ everywhere (one spacey hostname breaks everything else), keys sorted at report time (so tonight's diff against last night's report is meaningful), and existence-checks instead of empty-string checks (an unprobed host and a probed-but-empty answer are different operational facts — the +x distinction is exactly that).

Interview questions — Part C

🎯 "What are associative arrays and when would you use them?" — asked in screens wherever bash 4+ is assumed; published lists rarely carry a fixed wording (Codecademy, Baeldung and LinuxConfig document the mechanics as tutorials)

The direct answer: string-keyed maps, declare -A map; map[key]=value; ${map[key]} — for lookups by name: service→port, env→URL, host→state. Iterate keys with "${!map[@]}"; count with "${#map[@]}".

Going deeper: the two sharp edges — the mandatory declare -A (without it, keys evaluate as arithmetic and silently collapse onto index 0) and no ordering guarantees (sort keys when output must be stable). Requires bash 4+; notably absent on stock macOS bash 3.2 (the Module 1 caveat, resurfacing).

The details that separate candidates: the key-existence idiom ${map[k]+x} and why it beats -n "${map[k]}" (empty value vs absent key are different facts); and the honest design boundary — nested structures don't exist in bash, so a map-of-lists means encoded values or parallel arrays, and "this data has outgrown bash, use jq/python" is a professional answer, not a confession.

Part D — Choosing your collection

D1. The decision

Note: the diagram below is a Mermaid code block. Notion does not render it automatically — click the block and switch it from "Code" to "Preview" (or "Split") to see the flowchart.
Diagram source
flowchart TD
    A["I have multiple values<br>to keep together"] --> B{"How are they<br>looked up?"}
    B -->|"by position /<br>just iterate them"| C["indexed array<br>arr=(a b c)"]
    B -->|"by name<br>(service, host, code)"| D["associative array<br>declare -A map"]
    C --> E{"where do the<br>values come from?"}
    E -->|"literals in<br>the script"| F["arr=(...)"]
    E -->|"lines of a file<br>or command"| G["mapfile -t arr < file<br>or < <(cmd)"]
    E -->|"matching files"| H["arr=(*.log)<br>+ nullglob"]
    D --> I{"output order<br>matters?"}
    I -->|"yes"| J["sort the keys<br>at report time"]
    I -->|"no"| K["iterate ${!map[@]}<br>as-is"]

Interview questions — Part D

🎯 "Why store hosts in an array instead of a space-separated string?" — the design-question form in which array knowledge is actually probed

The direct answer: because arrays preserve element boundaries. A string hosts="web-1 web-2" cannot hold a value containing a space, cannot be counted reliably, and every use re-splits it; an array holds each value intact, counts itself ("${#hosts[@]}"), and iterates safely ("${hosts[@]}").

Going deeper: the string version is the Module 2 word-splitting hazard institutionalized — it works until the first spacey value, then fails at every use site simultaneously. Arrays move the boundary decision to fill time (mapfile, globs, explicit quoting) and make every later use safe by construction.

The details that separate candidates: naming the one place strings still win — exporting to child processes (arrays don't export; a child needs the data serialized, e.g. newline-joined, and re-parsed — a real bash limitation worth admitting); and the observation that positional parameters are themselves an array, which is why "$@" and "${arr[@]}" obey identical laws.

Part E — Production

E1. 🏭 Production practices

These are the practices in production — how the things this module taught are actually done on real systems:

Collections live in arrays from the moment they enter the script. Hostlists via mapfile -t, file sets via globs (with nullglob), never space-joined strings that re-split at every use.

"${arr[@]}" is the only iteration spelling. Quoted-@ in every loop and forward; "${arr[*]}" only inside log lines; unquoted array expansion is a review-blocking defect.

Associative arrays are declared at the top, next to the constants — declare -A STATUS VERSION — so the mandatory -A can never be forgotten mid-file.

Map output is sorted before humans or diffs see it. Key order is noise; reports, metrics, and test fixtures sort at the boundary.

Sparse traps are designed out. Element removal is followed by a compacting rebuild, or (better) iteration never depends on index arithmetic in the first place.

Bash-version reality is checked. Associative arrays need bash 4+; scripts that must run on stock macOS or ancient AMIs either guard ((( BASH_VERSINFO[0] >= 4 )) || die …) or avoid them.

E2. Production-practice table

SymptomWhat is really happeningWhat to runThe fix
Fleet loop targets a host named db and another named primary — neither existsUnquoted ${hosts[@]} re-split a spacey elementprintf '[%s]\n' both spellings and count brackets"${hosts[@]}" — quoted-@, everywhere
Every key of a "map" reads back the same valueMissing declare -A — keys evaluated as arithmetic, all writes landed on index 0declare -p map — shows declare -a (indexed!) with one elementdeclare -A map before first write; declare maps at the top of the script
C-style loop misses the last element / prints an empty slotThe array is sparse — an unset punched a hole; count ≠ highest indexecho "${!arr[@]}" — look for gapsIterate "${arr[@]}" or "${!arr[@]}"; compact with arr=("${arr[@]}") after removals
Array loaded from a command is mysteriously emptycmd | mapfile -t arr — the pipeline's subshell took the array to its graveecho "${#arr[@]}" right after → 0mapfile -t arr < <(cmd) — process substitution keeps mapfile in the current shell
Comparisons fail although elements "look identical"mapfile without -t — every element carries an invisible trailing newlineprintf '[%s]\n' "${arr[0]}" — the bracket closes on the next linemapfile -t, always
Map-driven report ordering changes run to run; diffs are noiseAssociative arrays guarantee no key orderRun the report twice; compareSort keys at report time; or keep an insertion-order index array alongside

E3. 🎓 Capstone — four tickets from the queue

Work each ticket yourself before opening the answer. Everything needed was taught in Modules 1–10.
🎓 Ticket 1 — "Rollout script loads targets with targets=($(cat hosts.txt)). Since networking added edge gateway 3 (a jump-host entry with spaces) the script has been attempting SSH to hosts named edge, gateway, and 3."

Diagnosis. Module 6's cat-loop disease, array edition: $( ) substituted the file, unquoted expansion word-split it, and the array collected words, not lines. Three fake hosts from one real line.

Work the steps: printf '[%s]\n' "${targets[@]}" versus wc -l hosts.txt — element count exceeds line count; the brackets show exactly where lines shattered.

Fix and prevention: mapfile -t targets < hosts.txt — line-per-element by construction. Then the boundary guard: validate each element ([[ "$t" =~ ^[a-zA-Z0-9.-]+$ ]] || die "bad hostname: $t") so the next surprising entry fails loudly at load time instead of weirdly at SSH time.

🎓 Ticket 2 — "Inventory script: declare -A role was deleted in a 'cleanup' commit because 'the script worked without it in testing.' Now role[web]=frontend; role[db]=database — and every service reports role database."

Diagnosis. The missing-declare collapse (C1): without -A, role became an indexed array; web and db were evaluated as arithmetic — unset variable names, both worth 0 — so every write landed on element 0 and the last writer won. (Keys with hyphens like web-1 fail differently: bad array subscript, arithmetic refusing a negative index — loud, and in a way luckier.) It "worked in testing" because testing read back the same key it had just written — the one pattern that can't expose last-write-wins.

Work the steps: declare -p role — the output opens declare -a (indexed) with a single element; that one line is the entire diagnosis.

Fix and prevention: restore declare -A role (top of script, beside the constants — E1's placement rule exists to make such deletions visible in review). Add the cheap tripwire test that testing lacked: write two keys, read both back, compare count to 2.

🎓 Ticket 3 — "Nightly fleet report loops a status map and emails the table. The on-call lead complains: 'the host order shuffles every night — I can't diff today against yesterday to see what changed.'"

Diagnosis. C1's no-order rule meeting a workflow that assumes stability: for h in "${!status[@]}" emits whatever order the hash table feels like, per run. Nothing is broken in bash; the interface (a diffable report) has a requirement the data structure never promised.

Work the steps: run the report twice in a row; diff — the shuffle reproduces without any data change.

Fix and prevention: impose order at the boundary: for h in $(printf '%s\n' "${!status[@]}" | sort); do echo "$h: ${status[$h]}"; done — or iterate the source hostlist array (which has stable order) and look each host up in the map. General rule for the runbook: maps are for lookup; any output humans read or diff gets ordered deliberately on the way out.

🎓 Ticket 4 — "A cleanup pass removes decommissioned hosts from the in-memory array with unset 'hosts[i]' inside a C-style index loop, then a second C-style loop pushes config to ${hosts[i]}. Some hosts get skipped; occasionally the push targets an empty string."

Diagnosis. The sparse trap (A3) plus index arithmetic: each unset punches a hole; the second loop walks 0..count-1, where count has shrunk but the surviving indexes haven't moved — so it stops before the tail (skipped hosts) and dereferences holes on the way (empty-string pushes). Two symptoms, one cause: index math over a sparse array.

Work the steps: after the removal pass: echo "${!hosts[@]}" — gaps visible; echo "${#hosts[@]}" vs the highest index — mismatch confirmed.

Fix and prevention: either compact after removals (hosts=("${hosts[@]}")) or — structurally better — stop removing mid-structure: build a new array of keepers (for h in "${hosts[@]}"; do keep "$h" && new+=("$h"); done; hosts=("${new[@]}")), and iterate with "${hosts[@]}" so holes can't exist in the first place. Filter-into-new beats delete-in-place in bash every time.

E4. Documentation reference

TopicAuthoritative referenceWhat you'll find there
Arrays, indexed and associativeBash manual — ArraysCreation, subscripts, @ vs * expansion, ! for indexes/keys, unset behavior
mapfile / readarray, declareBash manual — Bash Builtinsmapfile's options (-t and friends); declare -A and declare -p for inspection
The @/* law these obeyBash manual — Special ParametersThe original $@/$* semantics that array expansion mirrors

E5. Self-assessment

Answer from memory, out loud or on paper. Every answer is in this module.

  1. Create a four-element array where one element contains a space; read element 0, the count, and the index list.
  2. Why is $hosts[0] without braces wrong twice over?
  3. "${arr[@]}" vs "${arr[*]}" vs unquoted — map each to its Module 7 twin and its use.
  4. What does unset 'arr[1]' do to the indexes, the count, and a C-style loop? Why is the unset quoted?
  5. How do you compact a sparse array, and which iteration style never notices holes at all?
  6. Load a file's lines into an array safely; what does -t prevent, and what does the arr=($(cat file)) anti-pattern do instead?
  7. Why does cmd | mapfile -t arr leave the array empty, and what is the working spelling?
  8. What goes wrong if you skip declare -A before using string keys — and which one command diagnoses it instantly?
  9. What ordering do associative arrays guarantee, and what are the two production remedies when output must be stable?
  10. Distinguish "key mapped to empty" from "key absent" — the exact test for each.
  11. Which bash versions lack associative arrays, where does that bite in practice, and what is the guard line?
  12. Name the one thing space-separated strings can still do that arrays cannot, and the workaround.

E6. Sources

Interview questions in this module were captured verbatim from:

Zero To Mastery — Bash Interview Prep: 31 Essential Questions and Answers — "What are arrays in Bash, and how do you use them?" (published June 18, 2026)

Edureka — Top 60 Shell Scripting Interview Questions and Answers — "How to print all array elements and their respective indexes?", "How to print the first array element?" (page updated Dec 9, 2024)

A note on the corpus: published lists cover indexed-array basics thinly and associative arrays almost not at all — the working material circulates as tutorials (Baeldung, LinuxConfig, Codecademy, LinuxJunkies). The associative-array and design questions above are therefore labeled as screen-style questions without canonical published wording, rather than given invented citations. No questions were invented. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2; associative-array key order is explicitly machine-varying and flagged as such where shown.

Next: collections in hand, it is time for the seven tools that power nearly every log-analysis one-liner in every DevOps interview: grep, cut, sort, uniq, tr, xargs, and working sed/awk: Module 11 — Text Processing in Pipelines.
Spotted a mistake or want something added? Send me a note.