Module 7 — Text Surgery: sed and awk

Updated 2 September 2026

Module 7 — Text Surgery: sed and awk

Module 6 found the text; this module changes and computes with it. sed edits streams — replace, delete, extract, at any scale. awk treats text as columns and rows — filter, reshape, count, sum. With their supporting cast (sort, uniq, cut, tr), they turn logs and configs into answers. These are the commands that make interviewers sit up.

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

Before you start

You need Modules Module 1 — What Linux IsModule 6 — Finding Things: find and grep — regexes, pipes, quoting, and Module 6's ~/search playground, which this module extends. Add the web-access log it will dissect (same paste-the-block technique as Module 6):

bash
cd ~/search
cat > app/logs/access.log <<'EOF'
10.0.0.5 GET /api/users 200 123
10.0.0.7 GET /api/orders 200 87
10.0.0.5 POST /api/orders 500 431
10.0.0.9 GET /health 200 5
10.0.0.5 GET /api/users 200 119
10.0.0.7 POST /api/login 401 12
10.0.0.9 GET /health 200 4
10.0.0.5 GET /api/orders 500 502
EOF

Columns, left to right: client IP, method, path, status code, response time (ms). Every exercise reads them.

This page uses Mermaid diagram blocks. Notion shows them as code by default — click the block and set it to Preview to see the diagram. This reminder appears once per page.

Part A — sed: the stream editor

A1. s/// — substitution, the 90% case

Official docs: GNU sed manual · sed(1)

🧠 sed 'script' file reads the file line by line, applies the script to each line, prints the result — the file itself is untouched; you are editing the stream. The script you will write most: s/old/new/ — substitute. Add g (s/old/new/g) to replace every occurrence on a line rather than just the first. The pattern side is a regex (Module 6's language, working overtime here); the script travels in single quotes (Module 5's rule — sed's $, *, and friends must reach sed intact). When your pattern contains slashes — file paths! — swap the delimiter for anything: s|/old/path|/new/path|.

Real-world analogy — find-and-replace in the photocopier

sed is a photocopier with find-and-replace built in: originals feed in one side, corrected copies emerge from the other, the originals returning to their folder unmarked. Editing happens in the copy stream — which is why sed composes with pipes so naturally.

Where the analogy stops working. A copier's replace would surely fix every occurrence on a page. sed replaces only the first match per line unless you append g — a pure convention with no physical intuition behind it, and the number-one sed bug: the report that fixed one of the two hostnames on the line.

🧪 Exercise A1.1 — Replace, and mind the g
bash
cd ~/search
sed 's/db-01/db-03/' app/config/app.conf     # stream out an edited copy
cat app/config/app.conf                       # original: untouched
echo "aaa bbb aaa" | sed 's/aaa/XXX/'         # first match only...
echo "aaa bbb aaa" | sed 's/aaa/XXX/g'        # ...vs all matches
Expected result — click to reveal
javascript
listen_port=8080
db_host=db-03
db_port=5432
# temporary override
db_host_backup=db-02
XXX bbb aaa
XXX bbb XXX

What to read out of it:

  • The streamed copy shows db-03; the cat (output omitted above — run it) still says db-01. Stream-editing is inherently safe: you can inspect the would-be result forever before committing anything (A3 commits).
  • The aaa pair is the g lesson in miniature — first-per-line versus all. Burn it in now; it costs real incidents later.
  • Note db_host_backup=db-02 sailed through unchanged — the regex db-01 matched precisely. When it must be even more precise (db-01 but not db-011), Module 6's word boundaries and anchors apply verbatim.

A2. Addresses — operating on chosen lines

Official docs: GNU sed manual

🧠 Any sed command can be restricted to an address: a line number (2), a range (1,3), or a pattern (/^#/). Combined with the commands p (print — used with -n, which silences the default line-echo) and d (delete from the stream), you get surgical extraction: sed -n '2p' — only line 2; sed -n '1,3p' — lines 1–3; sed '/^#/d' — everything except comments. Address + s/// also composes: sed '/^db/s/-01/-03/' — substitute only on lines starting with db.

Real-world analogy — the editor's blue pencil

A copy editor marks a manuscript: "strike every line beginning with a stage direction", "print me only paragraphs two through four". Addresses are the margin instructions; commands are the pencil strokes.

Where the analogy stops working. An editor flips back and forth through the manuscript. sed's pages stream past exactly once — an address can never mean "the line before the match" or "go back and fix line 3" (there are dark arts for lookback, but the honest answer is: that's awk's or an editor's job). One pass, forward only, is the price of handling infinite streams.

🧪 Exercise A2.1 — Extract and strike
bash
cd ~/search
sed -n '2p' app/config/app.conf         # line 2 only
sed -n '1,3p' app/config/app.conf       # a range
sed '/^#/d' app/config/app.conf         # strike the comments
Expected result — click to reveal
javascript
db_host=db-01
listen_port=8080
db_host=db-01
db_port=5432
listen_port=8080
db_host=db-01
db_port=5432
db_host_backup=db-02

What to read out of it:

  • -n plus p is "print nothing except what I ask" — without -n, sed echoes every line and prints the addressed ones again (run it; the doubled line 2 is a rite of passage).
  • The comment-strike is the classic "show me the effective config": sed '/^#/d; /^$/d' file (two commands, semicolon-joined, the second deleting blank lines) prints what the machine actually reads. This one-liner earns its keep weekly.

A3. -i — editing in place, with a seatbelt

Official docs: GNU sed manual

🧠 sed -i 'script' file writes the result back into the file. sed -i.bak 'script' file does the same and keeps the original as file.bak — and until Module 10 gives you version control everywhere, the suffix form is the only -i you should type. The ritual: stream first (A1), eyeball the output, then repeat with -i.bak, then diff file.bak file (Module 3) to certify exactly what changed.

Counter-intuitive: sed -i does not edit your file — it replaces it. Internally sed writes a brand-new file and renames it over the old name (Module 2's rename mechanics). Consequences, straight from Module 3: the inode changes; any hard link to the old file now points at stale data; and run against a symlink, -i replaces the link itself with a regular file, quietly disconnecting it from its target. A program holding the old file open keeps reading the ghost. "In place" is a user-interface fiction — and knowing that resolves a whole family of spooky aftermaths.
Real-world analogy — retyping the ledger page

The careful clerk never scribbles corrections into the ledger: they type a fresh page, staple the old one behind it (.bak), and swap the new page in. Same total effect as "editing", provably reversible.

Where the analogy stops working. The ledger binder holds pages by position, so a swapped page is seamless. Files are referenced by inode — and every other reference to the old page (hard links, open file handles, the symlink that pointed there) keeps holding the page you removed. The clerk's swap is invisible; sed's swap is invisible only to path-based access.

🧪 Exercise A3.1 — The full in-place ritual
bash
cd ~/search
cp app/config/app.conf /tmp/ritual.conf        # practice target
sed 's/8080/9090/' /tmp/ritual.conf            # 1: stream, inspect
sed -i.bak 's/8080/9090/' /tmp/ritual.conf     # 2: commit, with seatbelt
diff /tmp/ritual.conf.bak /tmp/ritual.conf     # 3: certify the change
Expected result — click to reveal
javascript
listen_port=9090
db_host=db-01
db_port=5432
# temporary override
db_host_backup=db-02
1c1
< listen_port=8080
---
> listen_port=9090

What to read out of it:

  • Three steps, three proofs: the preview showed intent, the -i.bak committed it, the diff certifies that exactly one line changed and which. Paste that diff into the change ticket — this is what auditable config editing looks like without version control.
  • Try ls -i /tmp/ritual.conf before and after a second edit: the inode number changes each time. The yellow callout, verified on your own machine in ten seconds.

A4. sed across many files

Official docs: GNU sed manual · find(1)

🧠 Module 6's find selects; sed edits; together they are mass surgery: find /etc/app.d -name '*.conf' -exec sed -i.bak 's/db-01/db-03/g' {} +. The professional sequence stretches A3's ritual fleet-wide: grep first (grep -rl 'db-01' /etc/app.d — know your blast radius), preview on one representative file, execute with backups, verify (grep -rl 'db-01' again — should now be empty), and keep the .bak files until the change has survived a restart.

Now imagine this at 500 hosts. The find+sed sweep is the tactical tool — perfect for one host, or an emergency. As a fleet's steady state it is an anti-pattern: 500 hosts hand-edited drift apart within weeks. The strategic answer is configuration management (templates render configs; hosts converge to them) — Ansible, which your Ansible covers, exists substantially to replace fleet-wide sed. Interviewers love hearing the distinction stated plainly: sed to fix, templates to keep fixed.
🧪 Exercise A4.1 — Blast radius, then surgery
bash
cd ~/search
grep -rl 'db-01' .                              # 1: who mentions it?
find . -name '*.conf' -exec sed -i.bak 's/db-01/db-03/g' {} +   # 2: edit configs only, with backups
grep -rl --exclude='*.bak' 'db-01' .            # 3: verify — logs still mention it, configs must not
find . -name '*.bak'                            # the seatbelts, present and accounted for
Expected result — click to reveal
javascript
./app/config/app.conf
./app/logs/app.log
./app/logs/app.log
./app/config/app.conf.bak

What to read out of it:

  • Before: config and log both matched. After: only the log still says db-01 — correct! Logs are history; rewriting them would be falsifying records. The -name '*.conf' scope expressed that policy in one test.
  • The verification grep is not paranoia; it is the completion criterion. A change is done when the check that motivated it comes back clean. (The --exclude='*.bak' keeps the just-created seatbelts — which of course still contain db-01 — out of the verdict.)
  • Undo, if ever needed: mv app/config/app.conf.bak app/config/app.conf. (Restore the playground now for later sections: run exactly that.)

Part A — Interview questions

🎯 "Describe a scenario where you would use 'sed' to replace all occurrences of a string in multiple files." — asked verbatim in Adaface's 96 Linux Commands interview questions (September 2024)

The canonical scenario: a hostname, IP, or copyright line must change across a config tree — e.g. decommissioning db-01: grep -rl 'db-01' /etc/app.d to scope, then find /etc/app.d -name '*.conf' -exec sed -i.bak 's/db-01/db-03/g' {} +, then the same grep to verify empty, keeping .bak files until the change proves out. The g flag matters (multiple occurrences per line), the suffix backup matters (rollback), and the scoping test matters (never sed what you haven't grepped).

The details that separate candidates: the four-step ritual (scope, preview, execute-with-backups, verify) narrated as a procedure; the logs-are-history scoping judgment; and the strategic caveat — at fleet scale this is what configuration management replaces, a sentence that reliably upgrades the interviewer's model of you.

🎯 Corpus note — sed's syntax itself

Question banks rarely quiz raw sed syntax; they test it live — "here's a config, delete the comments", "change this port". The muscle memory that covers nearly everything asked: s/old/new/g, alternate delimiters for paths (s|…|…|), -n with p for extraction, /pattern/d for deletion, -i.bak for committing, address+command composition (/^db/s/…/…/). If you can produce those six shapes cold, live sed rounds become dictation.

Part B — awk: text as columns and rows

B1. Fields — the spreadsheet view

Official docs: gawk(1) — GNU awk, the version on Linux

🧠 awk reads input line by line and, before your program even runs, splits each line into fields on runs of whitespace: $1 is the first field, $2 the second… $0 is the whole line, and NF holds the count of fields on this line. The program travels in single quotes — non-negotiable here, because in double quotes your shell would expand $1 first (Module 5's expansion order, with teeth): awk would receive {print } and print entire lines, mystifyingly. The minimal program: awk '{print $3}' file — for every line, print field three.

Real-world analogy — the instant spreadsheet

awk drops a spreadsheet grid over any text: each line a row, whitespace divides the columns. print $1, $4 is "show me columns A and D". No import wizard, no schema — the grid appears, you query it, it vanishes.

Where the analogy stops working. A spreadsheet enforces its column count; awk re-splits every line independently and cheerfully handles jagged rows — line 5 can have three fields and line 6 eleven. Freedom, and a trap: a log line with an unexpected space (a user-agent string, a city name) silently shifts every column after it. NF is how you catch the jagged rows.

🧪 Exercise B1.1 — Columns out of a log
bash
cd ~/search
awk '{print $3}' app/logs/access.log
awk '{print $1, $4}' app/logs/access.log
Expected result — click to reveal
javascript
/api/users
/api/orders
/api/orders
/health
/api/users
/api/login
/health
/api/orders
10.0.0.5 200
10.0.0.7 200
10.0.0.5 500
10.0.0.9 200
10.0.0.5 200
10.0.0.7 401
10.0.0.9 200
10.0.0.5 500

What to read out of it:

  • Column three — the paths — extracted in one breath; this is Adaface's verbatim interview question ("print the third word of each line"), answered.
  • The comma in print $1, $4 inserts the output separator (a space by default); without the comma the fields concatenate into one word. Small syntax, frequent bug.
  • Now break it on purpose: run the first command with double quotes. Whole lines come back — your shell expanded $3 to nothing before awk saw it. Diagnose-by-symptom: awk printing full lines = quoting bug, almost always.

B2. Patterns — filtering rows

Official docs: gawk(1)

🧠 The full shape of an awk program is pattern { action } — the action runs only on lines where the pattern holds. Patterns can be comparisons ($4 == 500), regex matches (/error/, or per-field: $3 ~ /api/), or combinations ($4 == 500 && $2 == "POST"). Omit the action and matching lines print whole (awk as a smarter grep); omit the pattern and the action runs on every line (B1). This one sentence — pattern selects rows, action shapes them — is the entire language's skeleton.

Real-world analogy — the spreadsheet filter

$4 == 500 is clicking the filter arrow on column D and ticking "500": the sheet shows only matching rows, and any columns you then read come from those rows alone. Filter, then look — the spreadsheet workflow, textified.

Where the analogy stops working. A spreadsheet filter sees the whole table at once and can be un-clicked. awk is a stream: each row is examined once, in order, and gone — there is no "unfilter and re-examine". Whole-table questions (sums, counts, top-N) must be accumulated during the pass and delivered at the end, which is exactly B4's machinery.

🧪 Exercise B2.1 — The 500s, three ways
bash
cd ~/search
awk '$4 == 500' app/logs/access.log                     # pattern alone: full matching rows
awk '$4 == 500 {print $2, $3}' app/logs/access.log      # pattern + action: reshape the matches
awk '$4 >= 400 && $1 == "10.0.0.5"' app/logs/access.log # compound: this client's failures
Expected result — click to reveal
javascript
10.0.0.5 POST /api/orders 500 431
10.0.0.5 GET /api/orders 500 502
POST /api/orders
GET /api/orders
10.0.0.5 POST /api/orders 500 431
10.0.0.5 GET /api/orders 500 502

What to read out of it:

  • Line-by-line: both 500s hit /api/orders — different methods, same endpoint. The reshaped second view says it loudest: the orders endpoint is the problem. You are now doing analysis, not just extraction.
  • The compound condition reads like the sentence it is: status at-least-400 AND this client. grep would need chained pipes and could not compare numbers at all — grep 500 would also match a response-time of 500 ms in the last column. Numeric column comparison is awk's home turf.

B3. -F — other delimiters

Official docs: gawk(1)

🧠 Not everything splits on whitespace: /etc/passwd uses :, configs use =, CSVs use ,. -F sets the field separator: awk -F: '{print $1}' /etc/passwd — every username on the system. -F= turns key=value configs into a two-column table. (The separator is itself a regex — -F'[=:]' splits on either.)

Real-world analogy — redrawing the ruled lines

A printed ledger's vertical rules decide where one box ends and the next begins. -F hands you the pen: rule the columns at every colon, every equals sign — the same page, gridded your way.

Where the analogy stops working. Ruled lines are dumb ink; -F's value is a regex, and awk's default splitting (any run of whitespace, leading blanks ignored) is special magic that no single -F value exactly reproduces. When output columns wander, the first suspect is always: where exactly did the rules get drawn?

🧪 Exercise B3.1 — Query a config like a table
bash
cd ~/search
awk -F= '{print $1}' app/config/app.conf        # column 1: the keys
awk -F= '/^db/ {print $2}' app/config/app.conf  # values of db* keys — pattern and -F together
Expected result — click to reveal
javascript
listen_port
db_host
db_port
# temporary override
db_host_backup
db-01
5432
db-02

What to read out of it:

  • The keys column includes the comment line, whole — no = on it, so the entire line is field one. awk never errors on shape surprises; it adapts, and your program must expect that (a /^#/ {next} guard, or B2's patterns, filters such lines).
  • The second command is a config query: all db-related values, one pipeline-ready value per line. Compare the Module 3 way (open in less, read with human eyes) — you have graduated from reading files to interrogating them.

B4. Computation — sums, counts, and END

Official docs: gawk(1)

🧠 awk has variables, arithmetic, and two special patterns: BEGIN (runs before input) and END (after the last line) — accumulate during the stream, report at the end: awk '{sum += $5} END {print sum}'. Its superpower is the associative array — an array indexed by strings: count[$1]++ builds a per-client tally as lines stream past, and END {for (ip in count) print ip, count[ip]} reports it. That idiom — group-and-count in one pass — is the beating heart of ad-hoc log analysis everywhere.

Real-world analogy — tally marks on the whiteboard

Counting visitors by company, reception keeps a whiteboard: each arrival, find the company's row (create it if new), add a stroke. One pass over the day's visitors, and the board holds the summary. count[$1]++ is precisely that board.

Where the analogy stops working. A whiteboard has row order — first company listed stays on top. awk's for (ip in count) walks the array in unspecified order, deliberately: sortedness costs, and awk makes you pay only if you ask. Want order? Pipe the report through sort (next Part) — small tools, composed, as ever.

🧪 Exercise B4.1 — Total, then tally
bash
cd ~/search
awk '{sum += $5} END {print sum}' app/logs/access.log        # total response-time ms
awk '{count[$1]++} END {for (ip in count) print ip, count[ip]}' app/logs/access.log
Expected result — click to reveal
javascript
1283
10.0.0.5 4
10.0.0.7 2
10.0.0.9 2

(The tally's three lines may arrive in any order — that is the unspecified-order rule, live.)

What to read out of it:

  • 1283 ms summed across the stream in one pass, no temporary files, constant memory. The same command handles eight lines or eight billion.
  • The tally says 10.0.0.5 dominates traffic — four of eight requests, including both 500s (cross-reference B2.1). Two one-liners in, you have a suspect. This is what "processing log files with awk" means in the Adaface question — and in the job.

Part B — Interview questions

🎯 "Explain how to use 'awk' to print the third word of each line in a file." — asked verbatim in Adaface's 96 Linux Commands interview questions (September 2024)

awk '{print $3}' filename — awk splits each line on runs of whitespace into $1…$NF; the action prints field three for every line (no pattern = all lines). Single quotes are mandatory: double-quoted, the shell expands $3 to nothing and awk prints whole lines.

The details that separate candidates: volunteering the quoting failure mode before being asked; knowing the default splitting subtleties (leading whitespace ignored, any run of spaces/tabs is one separator — so columns align even in ragged output); and the reach for -F the moment the delimiter isn't whitespace.

🎯 "How can you use the 'awk' command to process log files and extract specific information based on patterns?" — asked verbatim in Adaface's 96 Linux Commands interview questions (September 2024)

The skeleton is pattern { action }: regex patterns (/error/ {print $1} — timestamps of error lines), field comparisons ($4 == 500), compounds ($4 >= 500 && $3 ~ /api/). Add accumulation for summaries: '{count[$1]++} END {for (k in count) print k, count[k]}' groups requests by client in one pass; '{sum += $5} END {print sum/NR}' averages a column (NR = number of records). One pass, constant memory, no temp files — which is why awk survives on gigabyte logs where spreadsheet thinking dies.

The details that separate candidates: producing the group-and-count idiom from memory — it is the awk interview move; explaining why numeric comparisons beat grep for status codes (grep can't tell a status 500 from a 500 ms latency); and knowing END exists because streams can't be re-read.

Part C — The supporting cast

C1. sort — ordering, and the numeric trap

Official docs: sort(1)

🧠 sort orders lines. The flags that matter: -n numeric, -r reverse, -u unique (sort and deduplicate in one), -t set a field delimiter and -k pick the sort column (sort -t: -k3 -n /etc/passwd — accounts by UID), -h human-numeric (sorts 900M below 2G — pairs with du -h in Module 12). And -o file safely sorts a file onto itself — the sanctioned answer to Module 5's sort f > f disaster.

Counter-intuitive: sort's default is dictionary order, even for numbers. Unflagged, sort puts 100 before 2 before 9 — character by character, 1 < 2 < 9. Every engineer meets this exactly once with disbelief, then flags -n forever. The deeper lesson generalizes: text tools see text; numbers are an interpretation you must request.
Real-world analogy — filing by spelling

A filing clerk shelves folders strictly by label spelling: folder "100" files under one-zero-zero, before "2", because "1" precedes "2" in the alphabet of digits. Impeccable procedure, absurd result — unless someone tells the clerk these labels are quantities (-n).

Where the analogy stops working. You'd catch the clerk's error at the cabinet. sort's misordering hides inside pipelines — a top-10 list that is subtly, plausibly wrong (90 outranking 800) can survive review for months. Wrong order is quieter than wrong data.

🧪 Exercise C1.1 — The trap, sprung and disarmed
bash
printf '10\n9\n100\n2\n' | sort      # dictionary order
printf '10\n9\n100\n2\n' | sort -n   # numeric order
Expected result — click to reveal
javascript
10
100
2
9
2
9
10
100

What to read out of it:

  • First block: the dictionary logic, character by character — 10 and 100 cluster because they share a prefix; 9 exiles to the bottom. Second block: sanity, via -n.
  • (printf with \ns is the compact way to feed test lines into a pipe — a Module 10 regular making an early appearance; the quoting rules you know apply.)

C2. uniq — adjacent duplicates only

Official docs: uniq(1)

🧠 uniq collapses consecutive duplicate lines; uniq -c prefixes each survivor with its count. The word consecutive is the whole tool: unsorted input with scattered duplicates passes through nearly untouched. Hence the eternal marriage sort | uniq -c — sort gathers the duplicates into adjacency, uniq counts them. (sort -u deduplicates without counting; uniq -c exists for the counting.)

Real-world analogy — the toll booth counter

A toll operator counting "how many red cars in a row" clicks the counter only while red cars stream past consecutively. That is uniq: a streak counter, not a census. The census requires first parading all cars in color order — sort — past the booth.

Where the analogy stops working. The operator sees that reds are scattered and would object to the question. uniq has one line of memory — this line versus the previous — and cannot notice non-adjacent duplicates even in principle. It answers a different question than you asked, correctly, silently.

🧪 Exercise C2.1 — Wrong without sort, right with it
bash
cd ~/search
awk '{print $1}' app/logs/access.log | uniq -c          # unsorted: streaks, not totals
awk '{print $1}' app/logs/access.log | sort | uniq -c   # the census
Expected result — click to reveal
javascript
1 10.0.0.5
1 10.0.0.7
1 10.0.0.5
1 10.0.0.9
1 10.0.0.5
1 10.0.0.7
1 10.0.0.9
1 10.0.0.5
4 10.0.0.5
2 10.0.0.7
2 10.0.0.9

What to read out of it:

  • (In your terminal the counts arrive right-aligned in a 7-character column — uniq -c pads with leading spaces; the block above shows them flush-left.) The unsorted run "worked" — produced plausible-looking counts — and every one of them is a streak length, not a total. This is the most dangerous kind of wrong: no error, reasonable shape, false content. The sorted run gives the true 4/2/2.
  • Recite until reflexive: uniq without sort is a bug unless you specifically want streaks (rare, real: "how many identical lines in a row did the app log?" — that one time, it's the tool).

C3. cut — column scissors

Official docs: cut(1)

🧠 cut -d'delim' -f LIST snips fields: cut -d: -f1 /etc/passwd — usernames; cut -d' ' -f1,4 access.log — IP and status. Simpler than awk where it applies; the boundaries are instructive: cut's delimiter is one literal character (no "runs of whitespace" magic — two spaces means an empty field), and -f2,1 still prints fields in file order 1,2cut cannot reorder. The moment you need reordering, computation, or tolerant splitting, that is awk's cue.

Real-world analogy — the paper guillotine

cut is the office guillotine: set the blade positions once, feed the stack, identical strips fall out. Fast, dumb, uniform. awk is the person with scissors who reads each page first.

Where the analogy stops working. Even a guillotine's strips can be rearranged after cutting; cut's cannot — output order is input order, whatever your -f list says. And a page with columns in unexpected positions gets cut at the set blade positions regardless: literal-minded to the end.

🧪 Exercise C3.1 — Scissors, and their limit
bash
cd ~/search
cut -d' ' -f1,4 app/logs/access.log | head -3   # IP and status
cut -d' ' -f4,1 app/logs/access.log | head -3   # "reversed" — or is it?
Expected result — click to reveal
javascript
10.0.0.5 200
10.0.0.7 200
10.0.0.5 500
10.0.0.5 200
10.0.0.7 200
10.0.0.5 500

What to read out of it:

  • Identical outputs. -f4,1 silently normalized to file order — cut selects, never arranges. The command lied by omission; now you know its one lie.
  • Need status IP order? awk '{print $4, $1}'. Choosing the smallest tool that can actually do the job — and knowing when it can't — is the fluency being tested when interviewers ask "cut or awk?"

C4. tr — character-level translation

Official docs: tr(1)

🧠 tr transforms characters (never strings) in a stream: tr 'a-z' 'A-Z' uppercases; tr -d '-' deletes a character; tr -s ' ' squeezes character runs to one — the classic pre-cut normalizer for space-padded output. Position maps to position: tr 'ab' 'xy' sends every ax and every by — it does not replace the string "ab". String work is sed's; character work is tr's.

Real-world analogy — the cipher wheel

tr is a substitution-cipher wheel: line up two alphabets, and every letter converts to its opposite number, one at a time, mechanically. Caesar would recognize tr 'a-z' 'A-Z' on sight.

Where the analogy stops working. A cipher clerk still reads letter by letter — and so does tr, only ever letter by letter: tr 'ab' 'xy' turns "abba" into "xyyx", mapping positions, never the string "ab". The moment you think in words rather than characters, you have walked out of tr's shop and into sed's.

🧪 Exercise C4.1 — Three one-character jobs
bash
echo "Hello World" | tr 'a-z' 'A-Z'
echo "too   many    spaces" | tr -s ' '
echo "id-123-456" | tr -d '-'
Expected result — click to reveal
javascript
HELLO WORLD
too many spaces
id123456

What to read out of it:

  • Three transformations no regex required: case-fold, squeeze, strip. In pipelines these are the sandpaper passes before the joinery — tr -s ' ' | cut -d' ' -f2 tames space-padded tables that would defeat cut alone.

C5. The grand pipeline — top talkers

Official docs: sort(1) · uniq(1) · gawk(1)

🧠 The idiom this Part has been building toward — extract, gather, count, rank:

javascript
awk '{print $1}' log | sort | uniq -c | sort -rn | head

Read it as a sentence: take the client column; gather duplicates together; count each; order by count, largest first; show the leaders. This four-stage shape answers an astonishing fraction of operational questions — top IPs, top endpoints, top error messages, top user agents — by swapping only the first stage's column.

Real-world analogy — election night

Extract the candidate name from each ballot (awk), sort the ballots into piles (sort), count each pile (uniq -c), announce the largest first (sort -rn). Every returning officer runs this pipeline by hand; you run it at a million ballots a second.

Where the analogy stops working. An election's ballot box closes; logs never do. The pipeline answers as of now — run it again in a minute and the answer has moved. Module 14's dashboards are exactly this pipeline made continuous, which is why learning it here pays twice.

🧪 Exercise C5.1 — Top talkers, top failures
bash
cd ~/search
awk '{print $1}' app/logs/access.log | sort | uniq -c | sort -rn        # who talks most?
awk '$4 >= 400 {print $3}' app/logs/access.log | sort | uniq -c | sort -rn   # which endpoints fail most?
Expected result — click to reveal
javascript
4 10.0.0.5
2 10.0.0.9
2 10.0.0.7
2 /api/orders
1 /api/login

(Ties — the two 2-count lines — may order either way; sort -rn compares the numbers and leaves equal keys in whatever order they arrived.)

What to read out of it:

  • (As in C2.1, uniq -c's counts arrive right-aligned with leading spaces in your terminal.) Two commands, and the incident story is complete: client .5 dominates traffic, and /api/orders owns the failures. On a real outage this pipeline against the last ten thousand lines is often the first real evidence anyone has.
  • Notice the pipeline is B4's awk tally plus sorting — two roads to the same summit. The awk form wins on gigantic inputs (no sort of the raw stream); the pipeline form wins on composability and recall-under-pressure. Fluency in both is the interview flex.

Part C — Interview questions

🎯 "Enlist some Linux file content commands." — asked (as "Linux to file content commands", verbatim) in Turing's 100+ Linux interview questions (2025)

The reading set from Module 3 — cat, less, head, tail — plus this module's processors: grep (select lines), sed (edit streams), awk (fields and computation), sort, uniq, cut, tr, wc. The answer that lands is organized by job, not alphabet: view, select, transform, summarize.

The details that separate candidates: following the list with one composed example (awk '{print $1}' log | sort | uniq -c | sort -rn | head) and narrating it stage by stage — a list proves reading; a pipeline proves practice.

🎯 Corpus note — sort/uniq/cut/tr

No recent published bank asks these directly; they are tested inside pipeline questions — most commonly some variant of "find the top N X in this log", which is C5's idiom verbatim. Practice until you can type the four-stage pipeline in one breath and answer its two classic follow-ups: why must sort precede uniq (adjacency), and why -rn not -r (dictionary-order 9-beats-80 bug). Those two follow-ups are the interview.

Part D — Choosing the tool, and dodging the dialects

D1. grep vs sed vs awk — the decision tree

Official docs: grep(1) · sed(1) · gawk(1)

🧠 The three overlap enough to confuse and differ enough to matter. The clean split: grep selects lines; sed transforms lines; awk understands fields (and computes). Interviewers ask the comparison constantly — usually as "when would you use each?"

Diagram source
flowchart TD
    A["I need to..."] --> B{"just FIND lines?"}
    B -->|"yes"| C["grep"]
    B -->|"no"| D{"CHANGE text<br>in a stream/file?"}
    D -->|"yes"| E["sed"]
    D -->|"no"| F{"work with COLUMNS,<br>conditions, math?"}
    F -->|"yes"| G["awk"]
    F -->|"no"| H{"structured data?<br>JSON, YAML"}
    H -->|"yes"| I["jq / yq / python<br>(D3)"]
Real-world analogy — highlighter, correction tape, calculator-ruler

On the editor's desk: a highlighter (grep — mark what matters), correction tape (sed — fix in place as pages stream past), and a combination ruler-calculator (awk — measure columns, total them). All three touch the same page; reaching for the wrong one means doing a tool's job by hand.

Where the analogy stops working. Desk tools compose badly — tape over highlighter is a mess. These three compose perfectly (grep | sed | awk is a normal Tuesday), and each can impersonate the others at the edges (awk can select; sed can, painfully, count). Choose by center of gravity, not by possibility.

🧪 Exercise D1.1 — One question, three tools, one answer
bash
cd ~/search
grep -c 'GET /health' app/logs/access.log
sed -n '/GET \/health/p' app/logs/access.log | wc -l
awk '$2 == "GET" && $3 == "/health"' app/logs/access.log | wc -l
Expected result — click to reveal
javascript
2
2
2

What to read out of it:

  • Three tools, same count — proof of the overlap. Now the judgment: grep's version is shortest and clearest → correct choice for this question. The sed version needed an escaped slash (\/) inside its pattern — friction signaling wrong tool. The awk version earns its keep only when the question grows conditions ("...and slower than 100 ms": $5 > 100 — try it; the others simply cannot).
  • "Simplest tool that answers the question" is the review standard for pipelines, and saying it out loud in interviews lands well.

D2. Dialects — the portability trap

Official docs: GNU sed manual · standards(7)

🧠 Module 1 promised the family differences would be flagged when they bite; here is the sharpest tooth. Linux ships GNU sed/awk/grep; macOS and the BSDs ship older BSD lineages. Most daily syntax is shared (POSIX, Module 1's glue) — but the edges differ, and the most notorious edge is sed's -i: GNU accepts sed -i 's/a/b/' f and sed -i.bak …; BSD requires the suffix argumentsed -i '' 's/a/b/' f for "no backup" — so the GNU spelling fails on a Mac with invalid command code, and the BSD spelling fails on Linux. Other edges: grep -P (Perl regexes) is GNU-only; some \+-style escapes behave differently; awk on Ubuntu is actually mawk unless gawk is installed (fast, slightly fewer features — awk --version tells you which).

Trap — "works on my Mac" scripts. A deploy script written on macOS with sed -i '' breaks on every Linux host; the reverse breaks every Mac. Teams with mixed laptops hit this within the first month. Defenses: write to POSIX where possible, test scripts on the OS they target (containers make this cheap — Module 16), and in CI pin the tool explicitly. The interview version of this answer is one sentence: sed -i is not POSIX, and I've been burned.
🧪 Exercise D2.1 — Know which awk you're holding
bash
awk --version | head -1
sed --version | head -1
Expected result — click to reveal
javascript
mawk 1.3.4 20240123
sed (GNU sed) 4.9

(On some Ubuntu installs the first line reads GNU Awk 5.x instead — meaning gawk is installed and selected; both run everything in this module.)

What to read out of it:

  • Ubuntu's default awk is mawk — everything this module taught runs identically, but exotic gawk extensions (in-place editing, some time functions) would not. --version before relying on an extension is the thirty-second insurance policy.
  • On a Mac, both commands answer differently (BSD tools often lack --version entirely — itself the tell). The habit generalizes far beyond text tools: identify the dialect before trusting the syntax.

D3. Knowing when to stop — structured data

Official docs: gawk(1) — and know its limits

🧠 sed and awk assume line-oriented text. Modern infrastructure speaks JSON and YAML — where a value can contain newlines, quoting is layered, and "field three" means nothing. Regexing JSON works right up until it catastrophically doesn't (nested quotes, reordered keys, multi-line values). The professional boundary: for JSON reach for jq, for YAML yq, and past a screenful of logic, a real language (Python). Both jq and Python arrive properly in your other tracks; today's lesson is only the boundary itself — recognizing un-line-oriented data before your pipeline mangles it.

Real-world analogy — amending a contract with a highlighter

Text tools read documents the way a highlighter does: as characters on a page. Fine for finding the word "liability"; reckless for amending clause 4(b), because clauses are structure, and the same words may appear in clause 7 with the opposite meaning.

Where the analogy stops working. A paralegal senses when structure matters and slows down. sed and awk have no such instinct — they will cheerfully "amend" whatever characters align, and JSON reordered by a serializer aligns differently every time. The tool cannot know it is out of its depth; you are the depth gauge.

🧪 Exercise D3.1 — Watch line-thinking fail on JSON (the mismatch is the lesson)
bash
echo '{"host": "db-01", "note": "moved to db-02, do not use db-01"}' > /tmp/config.json
grep -c 'db-01' /tmp/config.json     # how many times does db-01 appear... per LINE
sed 's/db-01/db-03/' /tmp/config.json    # "replace the host"... did it?
Expected result — click to reveal
javascript
1
{"host": "db-03", "note": "moved to db-02, do not use db-01"}

What to read out of it:

  • grep said 1 — it counts lines, and JSON often arrives as one line; the true occurrence count (2) needed grep -o 'db-01' | wc -l. First mismatch.
  • sed changed the first occurrence (no g), which happened to be the host — this time. Reorder the keys and the same command corrupts the note while leaving the host wrong. Text tools cannot see JSON's structure, only its characters; that a regex worked once is luck wearing the costume of correctness.
  • The honest fix is structure-aware: jq '.host = "db-03"'. Say exactly that in interviews when handed JSON — reaching for jq at the right moment reads as seniority, not weakness.

Part D — Interview questions

🎯 The comparison question — "When would you use grep vs sed vs awk?" (a live staple; published banks describe the tools separately — e.g. Turing's set defines "sed: Stream editor for filtering and transforming text" and "awk: Text processing tool for manipulating and extracting data from files" — but the head-to-head is asked in person)

grep selects: fastest way to find or count matching lines, exit code doubles as a test. sed transforms: substitution and line surgery on streams or files, the tool for "change X to Y everywhere". awk computes: field-aware filtering, reshaping, sums and group-counts, the tool the moment columns or arithmetic appear. They compose in pipelines and overlap at the edges; choose by the question's center of gravity, and default to the simplest that answers it.

The details that separate candidates: one crisp example per tool from your own hands (a grep -c, a sed -i.bak ritual, an awk group-and-count); the numeric-comparison point (only awk can ask $4 >= 500); and volunteering the boundary beyond all three — structured data goes to jq/python, which turns a syntax question into an engineering-judgment answer.

🎯 Corpus note — the published record on sed/awk

Among all this track's topics, sed and awk have the widest gap between published questions (a handful — Adaface's three are the best of them, quoted in Parts A–B) and live interview weight (enormous: log-analysis rounds, "fix this config" rounds, pipeline whiteboards). Prepare accordingly: the six sed shapes (A-part corpus note), the awk skeleton sentence (pattern selects rows, action shapes them), the group-and-count idiom, and C5's four-stage pipeline. Those four memorized artifacts cover the overwhelming majority of what is actually asked.

Part E — Toolkit

Official docs: Part E is reference material — every source it draws on is linked in the E3 documentation table below.

E1. Production practice — symptoms and fixes

SymptomWhat is really happeningWhat to runThe fix
awk prints whole lines instead of the fieldDouble quotes let the shell expand $1 to nothing before awk ranLook at the quotes on the awk programSingle-quote every awk program, always
sed "fixed" the line but one occurrence surviveds/// replaces first-per-line; later matches untouchedgrep -n the pattern; count matches per lineAppend g: s/old/new/g
Ran the sed, file unchangedStream edit without -i — the result went to the screen and evaporatedRe-read the command for -iPreview → sed -i.bak → diff (the A3 ritual)
uniq -c counts look plausible and are wrongInput wasn't sorted — streak lengths masquerading as totalsCompare with sort | uniq -csort before uniq, every time
Top-10 list ranks 9 above 80Dictionary sort on numbersInspect the sort flagssort -rn (or -h for 2G/900M-style sizes)
Script's sed works on Linux, dies on macOS (or vice versa)GNU vs BSD -i dialects — suffix argument handling differssed --version (GNU answers; BSD typically errors)Target one OS explicitly, or restructure to stream + mv
After sed -i, a hard-linked or symlinked config "disconnected"-i writes a new file and renames: new inode; symlink replaced by a regular filels -li before/after; file the pathEdit the target of links, or use --follow-symlinks; re-create links after

E2. Capstone — four tickets

Work these like real tickets: read the ticket, write the commands and the explanation you would send, then open the worked answer. Everything needed was taught in this module (plus Modules 3, 5, 6).
🎫 Ticket 1 — "API errors spiking — who and what, from the access log, in five minutes"

Ticket text: "On vm-web-1, /var/log/app/access.log (format: IP METHOD PATH STATUS MS). Management wants: top clients, top failing endpoints, and whether failures are slow. Now."

Worked answer, three pipelines: whoawk '{print $1}' access.log | sort | uniq -c | sort -rn | head (top talkers); what failsawk '$4 >= 500 {print $3}' access.log | sort | uniq -c | sort -rn | head (failing endpoints, ranked); are failures slowawk '$4 >= 500 {sum+=$5; n++} END {if (n) print sum/n " ms avg over " n " errors"}' access.log versus the healthy baseline awk '$4 == 200 {sum+=$5; n++} END {print sum/n}'. Three numbers and two rankings, no tools installed, evidence paste-ready. The habit worth naming in the ticket reply: every claim ("orders endpoint, avg 466 ms on errors vs 68 ms healthy" — the playground's own numbers) arrives with the command that produced it, so anyone can re-derive it.

🎫 Ticket 2 — "Rename the metrics prefix across 40 config files — auditable, reversible"

Ticket text: "Every file under /etc/metricsd/conf.d must change prefix statsd.legacy. to metrics.v2. — dots included, hence the fear. Provide the exact procedure."

Worked answer, the A4 sequence with regex care: scopegrep -rl 'statsd\.legacy\.' /etc/metricsd/conf.d | wc -l (expect about 40 — a surprise count is a stop sign); preview onesed 's/statsd\.legacy\./metrics.v2./g' somefile | diff somefile - (dots escaped in the pattern — unescaped they'd match statsdXlegacyY; replacement side needs no escapes); executefind /etc/metricsd/conf.d -name '*.conf' -exec sed -i.bak 's/statsd\.legacy\./metrics.v2./g' {} +; verify — the scoping grep, re-run with --exclude='*.bak' (the fresh backups still contain the old prefix, by design), returns nothing, and diff file.bak file on two samples shows only intended lines; rollback planfor loop restoring .baks (Module 10 gives the loop; until then, the .bak files are the plan). Auditable = every step leaves evidence; reversible = originals exist until decommissioned.

🎫 Ticket 3 — "After an automated edit, the app reads a stale config — ghosts?"

Ticket text: "A vendor script ran sed -i on /etc/app/current.conf last night. The file looks correct in cat, but the running app still uses the old value, and our config framework says current.conf 'is no longer a symlink'. Explain both hauntings."

Worked answer: both are A3's yellow callout in production. Haunting one — the running app: it opened the old file at startup and holds it still; sed -i made a new inode and renamed it over the old path, but open file handles follow inodes, not paths (Module 3), so the app reads the ghost until restarted (Module 11's territory — schedule it). Haunting two — the framework's complaint: current.conf was a symlink into a release directory; GNU sed -i, run on a symlink, writes its new file at the link's own path — replacing the pointer with a regular file and orphaning the target. Fix: restore the link (ln -sfn releases/v42/app.conf /etc/app/current.conf), apply the edit to the target file, and file a bug against the vendor script: it should edit link targets (or use sed's --follow-symlinks). Prevention: config trees that use link-swapping (Module 3's deploy pattern) must never be touched by naive -i tooling.

🎫 Ticket 4 — "Monthly report: unique visitors per endpoint from the access log"

Ticket text: "Product wants, per PATH, the count of distinct client IPs — not requests. One-liner if possible; explain it line by line for the wiki."

Worked answer: distinct-then-count is a two-stage dedup: awk '{print $3, $1}' access.log | sort -u | awk '{count[$1]++} END {for (p in count) print count[p], p}' | sort -rn. Wiki narration: stage 1 emits endpoint-IP pairs; sort -u keeps each distinct pair once (dedup = distinctness); stage 3 counts surviving pairs per endpoint — which is now "distinct IPs per endpoint" by construction; stage 4 ranks. On the playground data: /api/orders 2, /api/users 1, /health 1, /api/login 1 — check by eye against the eight lines to convince yourself (both /health hits come from the same client, so its distinct count is 1 — distinctness doing its job), then trust the pipeline at scale. The teachable subtlety: "unique" questions always need what is unique made explicit as a key — the pair, not the IP — before any counting starts.

E3. Documentation reference

TopicAuthoritative sourceVerified link
sedGNU sed manual; sed(1)GNU sed manual · sed(1)
awkgawk(1)gawk(1)
Supporting castsort(1), uniq(1), cut(1), tr(1)sort(1) · uniq(1) · cut(1) · tr(1)
The pattern languageregex(7)regex(7)
Dialects and standardsstandards(7)standards(7)

E4. Self-assessment

Answer out loud, without notes. The section number tells you where to re-read.

  1. What does "stream editor" mean, and why is plain sed 's/a/b/' file inherently safe? (A1)
  2. The g flag: what exactly does its absence do, and what class of bug does that create? (A1)
  3. Produce from memory: print only lines 2–4 of a file; delete comments and blank lines; substitute only on lines starting with db. (A2)
  4. Narrate the full in-place ritual, and explain why "in place" is a fiction — with all three consequences for links and open files. (A3, Ticket 3)
  5. What is the four-step procedure for a mass sed across a config tree, and why do logs get excluded? (A4)
  6. Why must awk programs be single-quoted, and what is the tell-tale symptom of getting it wrong? (B1)
  7. State awk's skeleton sentence, and write: full rows where status ≥ 400; just method and path of those rows. (B2)
  8. From memory: the group-and-count idiom, and why its output order is unpredictable. (B4)
  9. Why is uniq without sort a bug, and what is the one case where it isn't? (C2)
  10. sort put 100 before 2, and cut -f4,1 ignored your order. Explain both tools' "lies". (C1, C3)
  11. Type C5's four-stage pipeline blind, then answer its two classic follow-ups. (C5)
  12. When do sed and awk stop being the right tools, and what is the boundary's tell-tale sign? (D3)

E5. Sources

Interview-question sources used in this module (fetched and quoted verbatim during research, September 2026):

Adaface — 96 Linux Commands interview questions (September 2024 — the three sed/awk scenario questions quoted in Parts A–B) · Turing — 100+ Linux Interview Questions (2025 — tool definitions quoted in Part C/D).

Corpus honesty note: this module has the thinnest published corpus in the track — GeeksforGeeks (71 questions), WeCreateProblems (100+), and Mindmajix (111) contain no dedicated sed/awk/sort/uniq questions at all; only Adaface asks them, three times. That scarcity is emphatically not a signal of low importance: live DevOps interviews lean on exactly this material for log-analysis and whiteboard-pipeline rounds. Where published questions ran out, this module teaches to the live format instead — and labels the difference honestly.

All documentation links on this page were fetched and confirmed reachable on 2 September 2026. (The GNU gawk web manual was persistently rate-limited during verification and is deliberately not linked; gawk(1) at man7.org covers the same ground and is verified.)

Next: text bends to your will. Time to meet the things that runModule 8 — Processes.
Spotted a mistake or want something added? Send me a note.