Module 6 — Finding Things: find and grep

Updated 2 September 2026

Module 6 — Finding Things: find and grep

Two questions dominate real operations work: where is the file? and which files contain this? find answers the first by walking the tree; grep answers the second by reading contents. Between them sits the skill interviewers probe hardest — regular expressions. This module goes deep on all three; its sibling, Module 7, does the same for editing text.

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

Before you start

You need Modules Module 1 — What Linux IsModule 5 — The Shell, Properly — pipes, exit codes, quoting, and globbing are load-bearing here. Build the playground by pasting this whole block (the cat > file <<'EOF' construct collects the lines you paste, until the EOF line, into the file — a Module 10 construct on early loan):

bash
mkdir -p ~/search/app/logs ~/search/app/config ~/search/notes
cd ~/search
cat > app/logs/app.log <<'EOF'
2026-09-02 10:00:01 INFO  Service starting
2026-09-02 10:00:02 INFO  Config loaded from /etc/app/app.conf
2026-09-02 10:03:15 WARN  Slow response from db-01 (1200ms)
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
2026-09-02 10:04:47 error retry 2 failed: connection refused
2026-09-02 10:05:00 INFO  Failover to db-02
2026-09-02 10:05:01 INFO  Connection to db-02 established
EOF
cat > app/config/app.conf <<'EOF'
listen_port=8080
db_host=db-01
db_port=5432
# temporary override
db_host_backup=db-02
EOF
echo 'remember: rotate keys' > notes/todo.txt
touch -d '2 days ago' notes/old.txt
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 — grep: reading for you

A1. grep fundamentals

🧠 grep pattern file prints every line containing a match for the pattern. The daily options: -i ignore case, -n show line numbers, -c count matching lines instead of printing them, -w match whole words only. And one behaviour that matters enormously to automation: grep's exit code speaks (Module 5): 0 — found something; 1 — found nothing (not an error!); 2 — actual trouble (unreadable file). "No matches" being exit 1 makes grep a test you can build conditions on — and a trap inside strict scripts, revisited in this module's tickets.

Real-world analogy — the assistant with the highlighter

Hand an assistant a 400-page transcript: "copy out every line that mentions the defendant". Back comes one tidy page of quotes. grep is that assistant — tireless, literal, and done in milliseconds.

Where the analogy stops working. A human assistant copies the surrounding sentences when a quote would mislead alone. grep copies exactly the matching lines and nothing else — context is lost unless you explicitly ask for it (A4), and a match mid-word is still a match unless you say -w.

🧪 Exercise A1.1 — First reads of a real log
bash
cd ~/search
grep ERROR app/logs/app.log
grep -i error app/logs/app.log      # case-insensitive: catches the sloppy lowercase line
grep -c -i error app/logs/app.log   # just the count
grep -n refused app/logs/app.log    # with line numbers
Expected result — click to reveal
javascript
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
2026-09-02 10:04:47 error retry 2 failed: connection refused
3
4:2026-09-02 10:04:44 ERROR Connection to db-01 refused
5:2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
6:2026-09-02 10:04:47 error retry 2 failed: connection refused

(First command: only the two uppercase ERROR lines; shown above from -i onward.)

What to read out of it:

  • The case-sensitive search missed a real error — the lowercase error line a hurried developer logged. Real logs are inconsistent; -i is the difference between "2 errors" and the truth. This single character has changed incident timelines.
  • -c counts matching lines, not occurrences — a line saying "error error error" counts once. Precision about what is being counted is a habit interviewers test.
  • -n's line numbers are for your editor: nano +5 app.log jumps straight there.

A2. -v and grep as a pipeline filter

Official docs: grep(1)

🧠 -v inverts: print lines that do not match. And because grep reads stdin when given no file, it slots into Module 5's pipelines as the universal filter: command | grep pattern | grep -v noise. Include-then-exclude chains read like sentences: "lines about the db, but not the healthy ones".

Real-world analogy — the two-door screening

Airport security in two stages: the first door admits only ticket-holders (include), the second pulls aside anyone on the fast-track list (exclude). Chaining greps is stacking doors; each stage sees only what the previous one passed.

Where the analogy stops working. Doors screen people one at a time in order of arrival; a grep chain preserves line order but each stage is a separate process running concurrently (Module 5's pipes) — the second door is already screening while the first is still admitting. Also: -v rejects whole lines — there is no "remove just the bad word", which is Module 7's job (sed).

🧪 Exercise A2.1 — Sculpt the log down to the incident
bash
cd ~/search
grep -v INFO app/logs/app.log                    # drop the routine chatter
grep -i error app/logs/app.log | grep -v Retry   # errors, but not the retries
env | grep -i home                               # filter ANY command's output
Expected result — click to reveal
javascript
2026-09-02 10:03:15 WARN  Slow response from db-01 (1200ms)
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
2026-09-02 10:04:47 error retry 2 failed: connection refused
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:04:47 error retry 2 failed: connection refused
HOME=/home/zaeem

What to read out of it:

  • Dropping INFO left the story: a warning, then the failure, then the retries. Subtracting noise is often faster than describing signal — -v earns its keep in incident work hourly.
  • The chain kept the lowercase retry line — -v Retry is case-sensitive and the sloppy line says retry. The same inconsistency that bit you in A1.1, now biting the exclude side. Fix: grep -vi retry. Logs punish assumptions symmetrically.
  • env | grep — the pattern you will type most: any command's output, filtered. (Your env line may differ; some systems also match XDG_CONFIG_HOME and friends — more lines is normal.)

A3. -r and -l — searching whole trees

Official docs: grep(1)

🧠 grep -r pattern directory descends recursively, reading every file, prefixing each match with its filename. -l (list) prints only the names of files containing a match — the right output when the question is "which files?" rather than "which lines?". The pair -rl is the config-audit workhorse: which files under /etc mention this host?

Real-world analogy — the search party

grep -r is a search party sweeping a building room by room, opening every drawer. Thorough, guaranteed current — and as slow as the building is big, every single time.

Where the analogy stops working. A search party remembers where it looked yesterday. grep -r has no memory and no index: each run re-reads every byte. For "find by name, fast", D1's locate keeps an index; for content, honest re-reading is the price of truth.

🧪 Exercise A3.1 — Which files know about db-01?
bash
cd ~/search
grep -r db-01 .
grep -rl db-01 .
Expected result — click to reveal
javascript
./app/config/app.conf:db_host=db-01
./app/logs/app.log:2026-09-02 10:03:15 WARN  Slow response from db-01 (1200ms)
./app/logs/app.log:2026-09-02 10:04:44 ERROR Connection to db-01 refused
./app/config/app.conf
./app/logs/app.log

What to read out of it (entry order can differ between machines — the filesystem decides):

  • Every match now wears its filename — and the answer spans both config and logs: db-01 is configured in one place and failing in another, which is precisely the shape of real diagnosis.
  • -l collapsed the same search to two names. Feed that to other commands (-exec, xargs, editors) and you are operating on "all files mentioning X" — a set you computed rather than guessed.

A4. Context — the lines around the match

Official docs: grep(1)

🧠 A match alone often misleads; the story sits next to it. -A n prints n lines After each match, -B n Before, -C n both. Between separate match regions grep prints a -- separator.

Real-world analogy — the film strip

A single crime-scene photo shows a broken window. The frames before and after show how. -B/-A are the adjacent frames of the log's film strip; incidents are diagnosed from sequences, not stills.

Where the analogy stops working. Film frames are evenly spaced in time; log lines are not — "one line before" might be an hour before. Context flags buy adjacency, and adjacency is only sometimes causality; timestamps arbitrate.

🧪 Exercise A4.1 — What led to the failover?
bash
cd ~/search
grep -B 1 Failover app/logs/app.log      # what happened just before?
grep -A 2 'refused$' app/logs/app.log    # a regex sneak preview: lines ENDING in refused
Expected result — click to reveal
javascript
2026-09-02 10:04:47 error retry 2 failed: connection refused
2026-09-02 10:05:00 INFO  Failover to db-02
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
2026-09-02 10:04:47 error retry 2 failed: connection refused
2026-09-02 10:05:00 INFO  Failover to db-02
2026-09-02 10:05:01 INFO  Connection to db-02 established

What to read out of it:

  • -B 1 answered the question in two lines: the failover followed the final failed retry. Cause, adjacent to effect.
  • The second command's $ anchored the match to line-ends — your first deliberate regex character, formally introduced in the next Part. Notice the three refused-lines' contexts merged into one block: overlapping context regions are printed once, not duplicated.

Part A — Interview questions

🎯 "What is the grep command?" — asked verbatim in GeeksforGeeks' Linux Interview Questions (70+ questions, updated July 2026); WeCreateProblems (2026) asks "What is the use of the grep command?"

grep searches text — files or stdin — for lines matching a pattern (fixed string or regular expression) and prints the matching lines. The name is from the old editor command g/re/p: globally search for a regular expression and print. Core flags: -i case-insensitive, -v invert, -r recursive, -n line numbers, -c count lines, -l names only, -w whole words, -A/-B/-C context, -E extended regex. As a pipeline filter it is the most-composed command on any Linux system.

The details that separate candidates: the exit-code contract (0 match, 1 no match, 2 error) and what it means for scripting; -c counting lines-not-occurrences; and one incident habit — -i first on human-written logs, because log level spelling is never consistent.

🎯 "Explain how to use 'grep' to find lines that match a pattern, but exclude lines that contain another pattern." — asked verbatim in Adaface's 96 Linux Commands interview questions (September 2024)

Chain an include with an exclude: grep 'apple' fruits.txt | grep -v 'orange' — first stage passes lines mentioning apple; second removes those also mentioning orange. Both stages accept full regexes, -i applies per-stage, and the chain extends indefinitely.

The details that separate candidates: mentioning the single-command alternatives for interviewers who push — grep -E 'apple' file | grep -vE 'orange|pear' for multiple excludes, or awk for conditions grep can't express (awk '/apple/ && !/orange/' — Module 7); and noting each stage is a concurrent process, so the chain scales to huge streams without memory cost.

🎯 "What command would you use to search for a specific text in multiple files?" — asked verbatim in WeCreateProblems' 100+ Linux Commands Interview Questions (2026)

grep -r 'the text' /path/ for a whole tree (add -l for filenames only, -n for locations); grep 'the text' file1 file2 file3 or a glob for a known set. For code trees, grep -rn --include='*.conf' narrows by filename pattern while searching content.

The details that separate candidates: choosing output form by question-l for "which files", -n for "where exactly", -c for "how widespread"; and the honesty note that content search is O(everything) — for repeated searching at scale, purpose-built indexers exist, but grep -rl is the zero-setup answer that always works.

Part B — Regular expressions: the pattern language

B1. Regex is not glob — same star, different language

🧠 Module 5's globs and grep's regexes share symbols and nothing else — the most confused pair in Linux. In a regex: every ordinary character matches itself; . matches any one character; ^ anchors to line start, $ to line end; and * means "zero or more of the previous thing" — it is a modifier, not a wildcard. So glob *.log and regex .*\.log$ say the same thing in two languages, and regex ERROR* means ERRO followed by any number of Rs — almost never what the writer meant.

One shell rule before anything else: single-quote every regex (grep 'ERROR.*db' file). Unquoted, the shell may glob-expand or split your pattern before grep sees it — Module 5's B2 rule, now load-bearing.

Counter-intuitive: a regex matches anywhere inside the line by default. grep 'port' matches listen_port, db_port, important, reported. Globs match whole filenames; regexes match substrings — the second great asymmetry. Whole-word intent needs -w; whole-line intent needs both anchors (^...$). Half of all "grep matched too much" surprises are this default.
Real-world analogy — false friends between languages

Spanish embarazada does not mean embarrassed. Glob-* and regex-* are false friends: identical spelling, unrelated grammar. Fluent speakers keep the languages apart by context — filename position speaks glob; grep/sed/awk patterns speak regex.

Where the analogy stops working. Human listeners catch false-friend slips from context and laugh. grep executes your mistranslation without comment — ERROR* quietly matches ERRO, and the only symptom is results that are subtly, silently wrong. Mistranslations here run.

🧪 Exercise B1.1 — Anchors and the dot
bash
cd ~/search
grep '^2026' app/logs/app.log | wc -l    # lines STARTING with 2026 (all of them — it's a log)
grep 'refused$' app/logs/app.log         # lines ENDING with refused
grep 'db.01' app/logs/app.log            # . matches any ONE character
Expected result — click to reveal
javascript
8
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
2026-09-02 10:04:47 error retry 2 failed: connection refused
2026-09-02 10:03:15 WARN  Slow response from db-01 (1200ms)
2026-09-02 10:04:44 ERROR Connection to db-01 refused

What to read out of it:

  • All 8 lines start with 2026^ turned "contains" into "starts with", the standard trick for matching log timestamps and config keys (^db_host won't match # db_host comment… wait, it will match db_host_backup — anchors and word-ends combine: ^db_host=).
  • db.01 matched db-01 — the . stood for the hyphen. It would equally match db 01, db801, dbX01: power and imprecision together. To match a literal dot, escape it: \. — the unescaped-dot bug classically bites IP-address patterns, where 10.0.0.1 happily matches 10a0b0c1.

B2. Classes, quantifiers, and -E

Official docs: regex(7) · grep(1)

🧠 [abc] matches one character from the set; [0-9], [a-z] are ranges; [^0-9] negates (inside brackets, ^ flips meaning!). Quantifiers modify the previous item: * zero-or-more, + one-or-more, ? zero-or-one, {3} exactly three. History's tax: basic grep treats + ? { } | ( ) as literal characters; grep -E (extended regex) makes them operators. The modern habit: reach for -E whenever a pattern goes beyond literals, dots, stars, anchors, and classes — it removes a whole category of "why is my + not working".

Real-world analogy — the crossword clue

[0-9]{4}-[0-9]{2} reads like a crossword constraint: "four digits, a hyphen, two digits". Each bracket-set is a cell's allowed letters; each quantifier says how many cells. Building a regex is writing constraints for a scanner that checks every position of every line against them.

Where the analogy stops working. Crossword slots are fixed-length; + and * make regex slots elastic, and elastic patterns match greedily — as much as they can. The scanner also starts over at every character position, which is why an imprecise elastic pattern on a long line can match far more than the crossword picture suggests.

🧪 Exercise B2.1 — Precision tools on the log
bash
cd ~/search
grep 'db-0[12]' app/logs/app.log            # db-01 or db-02, one class
grep -E 'Retry [0-9]+' -i app/logs/app.log  # a number after the word — needs -E for +
grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}' app/logs/app.log | wc -l   # a real date shape
Expected result — click to reveal
javascript
2026-09-02 10:03:15 WARN  Slow response from db-01 (1200ms)
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:05:00 INFO  Failover to db-02
2026-09-02 10:05:01 INFO  Connection to db-02 established
2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
2026-09-02 10:04:47 error retry 2 failed: connection refused
8

What to read out of it:

  • One class, [12], covered both hosts — and pointedly did not match the 5432 in the config or anything else. Classes are precision instruments; . is a shotgun.
  • The [0-9]{4}-[0-9]{2}-[0-9]{2} date shape is your first reusable regex — it appears in log tooling everywhere. Say it aloud as the crossword clue and it stops being line noise.
  • Try the middle command without -E: zero matches, because basic grep hunts a literal plus sign. Same pattern, two dialects — when a regex mysteriously fails, "which dialect am I in?" is question one.

B3. Alternation, grouping, and -w

Official docs: grep(1) · regex(7)

🧠 With -E: | means orgrep -E 'ERROR|WARN' catches both levels; parentheses group — and grouping matters because alternation binds loosely: ^ERROR|WARN means "(starts with ERROR) or (contains WARN)"; you almost always want ^(ERROR|WARN). For matching a word exactly without regex ceremony, -w wraps the pattern in word boundaries: grep -w error matches error but not errors.

Real-world analogy — the either/or on the order form

"Deliver Monday or Tuesday, morning" — does morning apply to both days or just Tuesday? English is ambiguous; forms add brackets. Regex alternation has the same ambiguity and the same fix: parentheses decide what the or spans.

Where the analogy stops working. A human courier asks when the form is ambiguous. The regex engine never asks — it has one fixed precedence rule (alternation binds last) and applies it, so the unbracketed pattern works, matches something, and the something is wrong. Ambiguity in regex is resolved silently, which is worse than an error.

🧪 Exercise B3.1 — Or, properly bracketed
bash
cd ~/search
grep -E 'ERROR|WARN' app/logs/app.log
grep -wi error app/logs/app.log        # the WORD error, any case — not part of a longer word
Expected result — click to reveal
javascript
2026-09-02 10:03:15 WARN  Slow response from db-01 (1200ms)
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
2026-09-02 10:04:44 ERROR Connection to db-01 refused
2026-09-02 10:04:45 ERROR Retry 1 failed: connection refused
2026-09-02 10:04:47 error retry 2 failed: connection refused

What to read out of it:

  • The first three lines are the alternation's: it caught the WARN line the ERROR searches always missed — severity filters in one pattern. Note what it did not catch: the lowercase error line, because ERROR|WARN is still case-sensitive. Alternation widens the pattern, not the casing.
  • The last three lines are -wi's: every error line regardless of case — and it would ignore an errors_total metric line, since word boundaries end the match at the word's edge. Cheap precision; most log searches want it.

Part B — Interview questions

🎯 "Which grep command option would you use to display lines that do not match a specified pattern in a file?" — a multiple-choice question, verbatim from Adaface's 96 Linux Commands interview questions (September 2024)

-v (invert match). The distractors are the other daily flags — -i (ignore case) and -c (count) — so the question is really testing whether you have used grep or merely read about it.

The details that separate candidates: in the live follow-up, combining them fluently (grep -vic pattern — count of non-matching lines, case-insensitive) and knowing -v operates on whole lines, which is why removing a word from output is sed's job, not grep's.

🎯 Corpus note — regular expressions

Published Linux question banks barely test regex directly — it hides inside grep/sed/awk questions and in live exercises ("write a pattern that matches an IP address", "why does your pattern match too much?"). The high-yield preparation: be able to write, on a whiteboard, the date pattern [0-9]{4}-[0-9]{2}-[0-9]{2}, an anchored key match ^db_host=, an alternation ^(ERROR|WARN), and to explain the three classic bugs — unescaped dot, glob-star confusion, and unbracketed alternation. Interviewers care less about exotic syntax than about whether you know what your pattern actually matches.

Part C — find: asking the filesystem questions

C1. find by name and type

🧠 find startpoint tests walks the tree from the start point and prints every path passing the tests. The two everyday tests: -name 'pattern' (a glob, matched against the filename — single-quoted so the shell doesn't expand it first, Module 5's lesson) and -type f/-type d (files/directories). Tests combine by AND when juxtaposed; -o gives OR; ! negates; -iname ignores case. Unlike globs, which expand only one directory level at a time, find descends everything.

Real-world analogy — the building inspector

find is an inspector walking every room, closet, and crawlspace, clipboard of criteria in hand, calling out each item that qualifies. Ask for "every fire extinguisher" (name) or "every door" (type) or both, and nothing in the building escapes the walk.

Where the analogy stops working. An inspector files a report at the end. find streams — each match is printed the moment the walk reaches it, which is why find pipes so well (results flow onward while the walk continues) and why a find on a huge tree "hangs": it isn't stuck, it's walking.

🧪 Exercise C1.1 — Walk your playground
bash
cd ~/search
find . -name '*.log'
find . -type d
find . -name '*.conf' -o -name '*.txt'
Expected result — click to reveal
javascript
./app/logs/app.log
.
./app
./app/config
./app/logs
./notes
./app/config/app.conf
./notes/todo.txt
./notes/old.txt

What to read out of it (entry order can differ between machines — the filesystem decides):

  • The glob found the log wherever it lived — no need to know the depth. Compare Module 5's *.log, which only sees the current directory: find is the glob that descends.
  • -type d listed . itself — the start point is part of the walk. Scripts that process find output learn to expect it.
  • The -o line returned both kinds. One subtlety to bank for C4: AND binds tighter than OR, so mixing -o with actions needs parentheses — the classic trap detonates in that section.

C2. find by time and size

Official docs: find(1)

🧠 The operational pair. Time: -mtime -1 — modified less than 1 day ago; -mtime +7 — more than 7 days; -mmin -30 — the minutes version. (These read Module 3's mtime.) Size: -size +100M, -size +1G — bigger than; - for smaller. These two tests power the two eternal jobs: what changed recently? (incidents) and what is eating the disk? (cleanups).

Counter-intuitive: -mtime counts in whole 24-hour buckets, and rounds in a way nobody expects. -mtime 1 (no sign) means "modified between 24 and 48 hours ago" — a file from 30 hours ago matches -mtime 1 but neither -mtime -1 nor -mtime +1. The everyday phrase "one day old" has three different find spellings. When precision matters, sidestep the buckets entirely with -mmin.
Real-world analogy — sorting the mail by postmark

"-mtime -1" is sweeping everything postmarked in the last day into a tray; "-size +100M" is pulling every parcel over the weight limit. Two sorts every mailroom — and every ops team — runs daily.

Where the analogy stops working. Postmarks are read as calendar dates; find measures elapsed 24-hour periods from this exact moment and truncates. A file modified yesterday evening can fail a -mtime -1 run this evening — "yesterday" and "less than 24 hours ago" quietly diverge, and cleanup scripts scheduled at midnight live and die on that divergence.

🧪 Exercise C2.1 — Recent things, old things, big things
bash
cd ~/search
find . -type f -mtime -1        # touched in the last 24 hours
find . -type f -mtime +1        # older than 48 hours (remember old.txt from setup)
find . -type f -size +100c      # bigger than 100 bytes (c = bytes; try M for megabytes on real trees)
Expected result — click to reveal
javascript
./app/config/app.conf
./app/logs/app.log
./notes/todo.txt
./notes/old.txt
./app/logs/app.log

(First three lines: the fresh files from the -mtime -1 run. Line four: the back-dated file, alone under -mtime +1. Last line: the size test's single hit. Order within each group may differ.)

What to read out of it:

  • old.txt — back-dated two days in setup with touch -d (Module 3's timestamp-setting, weaponized for testing) — fell past the +1 boundary exactly as designed. You now have a private time machine for testing date logic before trusting it in cleanups.
  • Only the log exceeded 100 bytes. On a real host, find /var -size +500M -type f is the first command of every disk-full incident (Module 12 makes it a ritual).

C3. find by owner and permissions

Official docs: find(1)

🧠 -user alice and -group www-data test ownership (Module 4's columns, queryable at last). -perm tests modes, in three grammars: -perm 644exactly 644; -perm -4000at least these bits (all named bits set); -perm /222any of these bits. The one to memorize tonight: find / -perm -4000 -type f — every setuid file on the system, Module 4's promised security sweep.

Real-world analogy — auditing the key cabinet

The -perm -4000 sweep is the quarterly master-key audit: walk the building, list every door that opens with borrowed authority, compare against the approved list. New, unexplained entries are how compromises announce themselves.

Where the analogy stops working. Keys are counted; permissions are combined bit patterns, and "has permission 755" is genuinely ambiguous in a way "has a key" is not — exactly 755? at least 755's bits? any of them? find forces you to choose a grammar (755 vs -755 vs /755), and choosing wrong silently audits the wrong question.

🧪 Exercise C3.1 — The setuid sweep, for real
bash
sudo find /usr/bin -perm -4000 -type f 2>/dev/null
Expected result — click to reveal
javascript
/usr/bin/fusermount3
/usr/bin/sudo
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/passwd
/usr/bin/su
/usr/bin/umount
...

What to read out of it (your list will vary slightly with installed packages):

  • Every name here is Module 4 vocabulary: passwd and sudo you predicted; mount/umount (Module 12), su, and the password-adjacent chfn/chsh/gpasswd complete the usual suspects. A dozen-odd entries is healthy; this list is supposed to be boring.
  • The 2>/dev/null (Module 5) discards permission-denied noise from unreadable corners — the standard companion of any wide find. On a security sweep proper, you'd keep stderr and read it: unreadable directories are themselves findings.
Now imagine this at 500 hosts. Run the setuid sweep on one host and you have trivia; run it on 500 and diff the results, and you have security monitoring — the odd host out is the interesting one. Fleet tooling (Module 10's scripts, later config management) is largely this pattern industrialized: run a find/grep question everywhere, compare answers, alert on divergence.

C4. Acting on what you find: -exec, -delete, xargs

Official docs: find(1) · xargs(1)

🧠 find can act, not just list. -exec command {} \; runs the command once per match ({} = the path; \; ends the command — escaped so the shell doesn't eat it). -exec command {} + batches many paths per invocation — hundreds of times faster for per-file-cheap commands. -delete removes matches. And the pipeline route: find ... -print0 | xargs -0 command — the null-separated handoff that survives any filename, spaces included (Module 5's word-splitting, finally fully defused).

Trap — expression order is execution order. find evaluates left to right, acting as it walks. find . -delete -name '*.tmp' deletes everything — the -delete fires before the name test is consulted. The professional ritual for destructive finds, no exceptions: run the exact expression with -print (or nothing) first, read the list, then — and only then — swap in -delete/-exec rm. The one-minute version of this mistake has erased whole home directories; the ritual costs ten seconds.
Real-world analogy — the inspector with a toolbox

-exec upgrades the inspector: not just noting each faulty smoke detector but replacing it on the spot (\;), or collecting a cartload and fixing them in batches (+). Same walk, work done en route.

Where the analogy stops working. A human inspector reads the whole checklist before acting. find reads its checklist while walking, in written order — put the action before the criteria and it "fixes" every room it enters. The tool trusts your ordering completely; the analogy's common sense is exactly what it lacks.

🧪 Exercise C4.1 — Act per-file, then in batch, then through the pipe
bash
cd ~/search
find . -name '*.log' -exec wc -l {} \;
find . -type f -name '*.txt' -print0 | xargs -0 wc -l
find . -name '*.tmp' -delete            # deleting nothing — no .tmp files exist; note the silence
echo $?
Expected result — click to reveal
javascript
8 ./app/logs/app.log
 1 ./notes/todo.txt
 0 ./notes/old.txt
 1 total
0

What to read out of it:

  • The -exec ran wc per file; the xargs version handed all txt files to one wc, which is why a total line appears — one invocation, many arguments. Per-file vs batched is invisible at three files and decisive at three hundred thousand.
  • -print0 | xargs -0 is one memorized unit: paths separated by null bytes, immune to spaces and newlines in names. Plain find | xargs corrupts on the first release notes.txt it meets.
  • The delete of nothing succeeded silently, exit 0 — find's verdict is about the walk, not the match count. Automation wanting "fail if nothing matched" must count results itself (a Module 10 pattern).

Part C — Interview questions

🎯 "How do you search for files?" — asked verbatim in GeeksforGeeks' Linux Interview Questions (70+ questions, updated July 2026); WeCreateProblems (2026) asks "What does the find command do?" and "How do you search for a file in Linux?"

find path tests walks a directory tree live, testing each entry: -name '*.conf' (glob, quoted), -iname case-insensitive, -type f/d, -mtime/-mmin for age, -size +100M, -user/-perm for ownership and mode; tests AND by default, -o for OR, ! to negate. It streams results as the walk proceeds and can act en route with -exec/-delete.

The details that separate candidates: contrasting find (live walk, always true, O(tree)) with locate (indexed, instant, possibly stale); one composed real command — "find /var/log -name '*.gz' -mtime +30 -delete, after a -print dry run"; and the -mtime bucket subtlety, which almost nobody volunteers and every senior recognizes.

🎯 "How can you find all files modified in the last 24 hours, and then compress them into a single archive?" — asked verbatim in Adaface's 96 Linux Commands interview questions (September 2024); their harder variant adds "…but exclude files in specific directories like /tmp"

find . -type f -mtime -1 -print0 | tar -czf recent.tar.gz --null -T - — find selects (null-separated for hostile filenames), tar's -T - reads the file list from stdin. The exclusion variant inserts -not -path './tmp/*' before the tests. Simpler-but-fragile spelling interviewers also accept: tar -czf recent.tar.gz $(find . -type f -mtime -1) — worth offering with its caveat (word splitting mangles spaced names; argument limits cap huge lists).

The details that separate candidates: knowing why the naive $(...) version breaks (M5's word splitting) and reaching for -print0/--null unprompted; the -mtime -1 vs -mtime 1 distinction; and mentioning the dry-run ritual before anything destructive touches the selection.

🎯 "Which find command option is used to locate files that have been modified within the last 24 hours?" — a multiple-choice question, verbatim from Adaface (September 2024); options include -mmin, -mtime, -atime, -ctime

-mtime -1 — with the minus sign meaning "less than one 24-hour unit ago". The distractors are all real: -mmin is minutes (-mmin -1440 is the precise 24-hour spelling), -atime tests last read, -ctime last inode change — Module 3's three timestamps, resurfacing as find tests.

The details that separate candidates: the sign grammar (-1 less-than, +1 more-than, bare 1 the 24–48h bucket) stated crisply, and mapping atime/ctime back to their meanings instead of guessing — the MCQ is really a Module 3 exam in disguise.

Part D — The wider search toolkit

D1. locate — trading freshness for speed

Official docs: locate(1)

🧠 locate name answers filename searches instantly — because it searches a pre-built index, not the disk. The index is rebuilt periodically (a scheduled updatedb, typically daily), which fixes locate's character completely: blazing on yesterday's truth, blind to today's. Files created since the last updatedb don't exist to it; deleted files haunt it. Not installed everywhere (sudo apt install plocate on Ubuntu if absent) — minimal servers usually skip it, and DevOps engineers default to find for anything that must be correct now.

Real-world analogy — the card index versus walking the stacks

locate is the library's card index: any title located in seconds. find is walking every shelf: slow, but it sees the book someone shelved ten minutes ago and doesn't list the one stolen last night. The index is rebuilt overnight by the night clerk (updatedb).

Where the analogy stops working. A librarian tells you the index is "as of yesterday". locate presents stale answers with total confidence — no timestamp, no caveat. You must carry the staleness in your head, or force a rebuild (sudo updatedb) before trusting it in anything that matters.

🧪 Exercise D1.1 — Feel the staleness (the miss is the lesson)
bash
sudo apt install -y plocate 2>/dev/null || echo "install needs Module 9 — skip if it fails"
sudo updatedb                      # build/refresh the index NOW
locate app.conf | grep search      # your playground file — indexed
touch ~/search/brand-new.txt
locate brand-new.txt               # created AFTER the index build
echo $?
Expected result — a miss, on purpose — click to reveal
javascript
/home/zaeem/search/app/config/app.conf
1

What to read out of it (your home path will differ):

  • The indexed file appeared instantly. The file you created after updatedb produced nothing and exit code 1 — locate is not lying, it is answering from its snapshot, faithfully.
  • Run sudo updatedb again, and locate brand-new.txt finds it. The whole tool in one experiment: an index, a rebuild schedule, and a freshness window you must always mentally subtract.

D2. find + grep — the combined sweep

Official docs: find(1) · grep(1)

🧠 The two tools compose into the full question: which files of this kind, anywhere, contain this text? Canonical form: find /etc -name '*.conf' -exec grep -l 'pattern' {} + — find selects by name/metadata, grep selects by content, -l reports filenames. (For code trees, grep -r --include='*.conf' reaches the same place; the find form wins when metadata tests — age, size, owner — join the question.)

Real-world analogy — panning in two passes

Gold panning is two sieves: the coarse mesh keeps only stones of the right size and shape (find: name, age, size), then the swirl looks for the glint inside what remains (grep: content). Neither pass alone answers the question; the sequence does.

Where the analogy stops working. A panner loses a little gold at every pass, unpredictably. The pipeline loses only what the tests exclude — deterministic, repeatable, and auditable: run it twice, get the identical answer, paste the command into the ticket as evidence.

🧪 Exercise D2.1 — Files named like configs, containing db-01
bash
cd ~/search
find . -name '*.conf' -exec grep -l 'db-01' {} +
find . -mtime -1 -type f -exec grep -li 'error' {} +   # recent files mentioning errors — an incident one-liner
Expected result — click to reveal
javascript
./app/config/app.conf
./app/logs/app.log

What to read out of it:

  • Line one: only the conf file — the log also contains db-01 but failed the -name '*.conf' test. Metadata narrowed, content confirmed.
  • Line two composes three selections — recency, file-ness, content — into one incident question: "what recent files talk about errors?" This composition habit, more than any single flag, is what Module 6 is for.

D3. Search etiquette on production machines

Official docs: find(1)

🧠 Wide searches on live machines have manners. Scope tight: start find at the narrowest plausible directory, never / out of laziness — a root-anchored find crawls every mounted filesystem (-xdev stops it crossing filesystem boundaries; meaning in Module 12). Silence the noise you expect: 2>/dev/null for permission-denied chatter when running unprivileged — but remember Module 5: the exit code still tells the truth. Mind the load: find and grep -r are I/O-hungry; on a struggling host during an incident, a whole-disk search is additional incident. And exclude what you must not read: -not -path '*/secrets/*' keeps sweeps out of directories that auditors care about.

Real-world analogy — searching the shared workshop

Looking for a tool in a workshop others are using, you check the likely bench first, work quietly, and leave the padlocked cabinet alone — not because you must, but because that is how shared spaces stay workable.

Where the analogy stops working. A rummaging human is self-limiting — arms tire, patience ends. find has no fatigue and no judgement: a carelessly scoped command reads every byte of every disk at full speed until done, competing with production for I/O the whole way. The restraint a workshop gets from etiquette, a search command must get from its own arguments.

🧪 Exercise D3.1 — A polite wide search
bash
find /etc -name '*.conf' -mtime -30 2>/dev/null | wc -l   # recently-changed configs, noise discarded
Expected result — click to reveal

A single number — anywhere from 0 to a few dozen depending on the machine's recent history (a fresh VM may show few; a hand-tended server, many).

What to read out of it:

  • This is drift detection in embryo: "what configuration changed this month?" is the first question after "it worked last month". Module 9's package tools will let you ask the sharper version — "changed from what the package shipped".
  • Unprivileged, some of /etc was unreadable; the discard kept output clean while the count stayed honest for what you can see. Run with sudo and compare counts — the delta is the permission-protected zone.

Part D — Interview questions

🎯 "What is the locate command?" — asked verbatim in GeeksforGeeks' Linux Interview Questions (70+ questions, updated July 2026); Adaface (2024) asks "Explain how to use the 'locate' command to quickly find files by name and how to update the locate database."

locate pattern searches a pre-built filename index — near-instant regardless of disk size — with the index refreshed by updatedb, normally on a daily schedule (sudo updatedb forces it). The trade is explicit: speed for freshness. Files newer than the last rebuild are invisible; deleted files linger. find is the live-walk complement: always current, cost proportional to the tree.

The details that separate candidates: stating the trade as a decision rule — locate for "where does this thing usually live?", find for anything feeding automation or incident response; knowing updatedb respects permission boundaries (users can't locate files they couldn't see); and that minimal server images often omit locate entirely, so find fluency is the portable skill.

🎯 "Which command will find and delete all empty directories within the current directory and its subdirectories?" — a multiple-choice question, verbatim from Adaface's 96 Linux Commands interview questions (September 2024)

find . -type d -empty -exec rmdir {} \; — select directories, test emptiness, remove each. (Modern spellings also accepted: -empty -delete, which processes depth-first automatically, or add -depth with rmdir so children go before parents.)

The details that separate candidates: explaining why rmdir is the safe effector here — Module 2's "timid tool" refuses anything non-empty, so even a botched selection can't cascade; spotting the depth-order subtlety (a directory containing only empty directories empties as its children go); and reciting the dry-run ritual — -print first, always — before any find that deletes.

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
A script using grep dies when the log is cleangrep exits 1 on "no matches"; strict scripts treat nonzero as fatalgrep pattern file; echo $? on a clean fileHandle 1 explicitly: grep pattern file || true, or test the count
find output drowning in Permission deniedUnprivileged walk hitting protected directories — stderr noise, stdout fineRe-run with 2>/dev/nullDiscard the noise, or sudo if the protected zone is actually in scope
Argument list too longA glob or $(find …) expanded past the kernel's argv limitCount first: find … | wc -lStream instead of expanding: find … -exec cmd {} + or -print0 | xargs -0
Cleanup ran but "yesterday's" files survived — or too many died-mtime's 24-hour buckets vs the calendar-day you meantfind … -mtime -1 -print vs -mmin -1440 -print; compare listsUse -mmin for precision; test with back-dated touch -d files
locate shows a file that isn't there (or misses one that is)Index staleness — answers are as-of the last updatedbls -l the path locate gave; sudo updatedb and retryRebuild, or use find when currency matters
Your regex matches too much — or nothingGlob/regex confusion, wrong dialect (missing -E), or shell ate the patternTest the pattern against a known line: echo 'sample' | grep -E 'pattern'Single-quote always; -E for + ? | (); anchor and escape dots deliberately

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.
🎫 Ticket 1 — "Disk usage jumped 40 GB overnight — find the culprit"

Ticket text: "Monitoring shows /var grew ~40 GB since yesterday on vm-app-2. Find what appeared or grew, without installing anything."

Worked answer: two sweeps, composed from C2. Recent and large first: sudo find /var -type f -mtime -1 -size +500M -exec ls -lh {} + 2>/dev/null — files both new-ish and big, human-readable sizes for the ticket. If that's quiet, drop the size test and sort the survivors by size: sudo find /var -type f -mtime -1 -size +50M -exec ls -l {} + | head -40. Typical culprits announce themselves by path: a log that lost its rotation (/var/log/app/app.log at 38 GB), a runaway core-dump directory, a cache. Then the content question — tail -50 the giant log (Module 3) to see what it's screaming about, because the disk symptom is usually a symptom of that. Module 12 adds du-based triage; the find version works everywhere today.

🎫 Ticket 2 — "Postmortem: the cleanup find deleted the whole staging tree"

Ticket text: "Intended: delete .tmp files older than 7 days under /srv/staging. Result: /srv/staging is empty. Explain precisely, and give the replacement procedure." The command that ran: find /srv/staging -delete -name '*.tmp' -mtime +7

Worked answer: mechanism — find evaluates its expression left to right per file, while walking; -delete was the first term, so it fired unconditionally on every path visited, and the name/time tests were never consulted (they stood after the action, like a signature line after the demolition order). Not a bug: documented behaviour plus fatal ordering. Replacement procedure, verbatim into the runbook: 1) build the selection with no action: find /srv/staging -name '*.tmp' -mtime +7 -print; 2) read the list — count it, spot-check it; 3) re-run the identical expression with -print replaced by -delete (order preserved: tests first, action last); 4) for anything beyond .tmp files, prefer quarantine over deletion: -exec mv -t /srv/staging/_to_delete/ {} +, empty it a week later. And restore staging from backups — Module 3's rule about what deletion means has not softened.

🎫 Ticket 3 — "Which of our configs still reference the old database host?"

Ticket text: "We migrated from db-01 to db-02 last quarter. Things keep 'mysteriously' connecting to db-01. Audit /etc and /srv/app on this host for every file still referencing it."

Worked answer: content sweep with filenames as the deliverable: sudo grep -rl 'db-01' /etc /srv/app 2>/dev/null. Then precision passes on the hits: grep -n 'db-01' each_file for exact lines, and -w if db-01x style near-names exist. Add the metadata angle for the report: ls -l each offender — the modification dates tell you which are pre-migration fossils and which were edited after the migration (someone re-adding the old host is a different, more interesting problem). Watch for false positives worth excluding (grep -rl 'db-01' | grep -v '\.bak$'), and for the one habit that prevents the recurrence: config in version control, where this audit is git grep and runs in milliseconds.

🎫 Ticket 4 — "Security asks: verify no world-writable files exist under /etc"

Ticket text: "Compliance sweep item 4.1.3: no file under /etc may be world-writable. Produce the evidence, and explain what you'd do with a hit."

Worked answer: the query is a -perm grammar exercise — "world-writable" means the others-write bit is set, whatever else is: sudo find /etc -type f -perm -o+w -exec ls -l {} + (symbolic spelling of "at least o+w"; -perm -002 is the octal twin). Empty output is the evidence — capture it with the command line and timestamp into the ticket. On a hit: ls -l is already in hand; identify the owner and the writing process (why is it world-writable — lazy chmod 777 from an old incident?), fix with the narrowest change (chmod o-w, Module 4's dictum), and check the same path across the fleet — one host's misconfiguration is usually a template's (Module 4's orange callout, in practice). Bonus points in the compliance answer: schedule the sweep and diff its output over time, converting a one-off audit into monitoring.

E3. Documentation reference

TopicAuthoritative sourceVerified link
Content searchgrep(1)grep(1)
Pattern languageregex(7)regex(7)
File searchfind(1)find(1)
Batch executionxargs(1)xargs(1)
Indexed lookuplocate(1)locate(1)

E4. Self-assessment

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

  1. grep exits 0, 1, and 2 — meanings, and why the middle one breaks naive automation. (A1)
  2. A case-sensitive search "proved" there were two errors; there were three. What single flag, and what does the miss teach about logs? (A1–A2)
  3. When do you reach for -l versus -n versus -c? Frame each as a question being answered. (A1, A3)
  4. Regex * versus glob * — explain the difference via what ERROR* actually matches. (B1)
  5. Why must regexes be single-quoted, and what are the three classic regex bugs from this module? (B1–B3)
  6. Write from memory: a pattern for lines starting with a date like 2026-09-02, and one for the word error in any case. (B2–B3)
  7. -mtime -1, -mtime 1, -mtime +1 — three different files match each. Explain the buckets, and the precise alternative. (C2)
  8. Recite the three -perm grammars and which one asks "is the setuid bit set?" (C3)
  9. find . -delete -name '*.tmp' — narrate the disaster, then the four-step safe procedure. (C4, Ticket 2)
  10. Why -print0 | xargs -0, and what breaks without it? Which module planted that landmine? (C4)
  11. locate answered instantly and wrongly. Explain its architecture, the freshness window, and the decision rule for locate vs find. (D1)
  12. Compose from memory: recently-modified conf files anywhere under /etc that mention a given hostname. (D2)

E5. Sources

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

GeeksforGeeks — Linux Interview Questions (70+) (updated July 2026) · Adaface — 96 Linux Commands interview questions (September 2024) · WeCreateProblems — 100+ Linux Commands Interview Questions (2026).

Corpus honesty note: find and grep are unusually well-served by published questions — including scenario and multiple-choice forms, several quoted above verbatim. Regular expressions, by contrast, are almost never asked as questions; they are the medium in which grep/sed/awk answers are judged. The regex depth here exceeds the published corpus deliberately: it is where the live evaluation actually happens.

All documentation links on this page were fetched and confirmed reachable on 2 September 2026.

Next: you can find anything. Now learn to change it — Module 7 — Text Surgery: sed and awk.
Spotted a mistake or want something added? Send me a note.