Linux & Bash · Part I · Module 14 · Finding Files and Searching Their Contents
Choosing Between grep, find, sed and awk
Fifteen chapters have introduced tools whose jobs overlap enough that any of them can be made to do most things. The skill that remains is picking from the question rather than from whichever one you used last — and knowing when the answer is none of them.
Intermediate17 min readLinuxPart IFundamentalsSearching
Part I · Module 14 · Chapter 14.8 · Choosing Between grep, find, sed and awk
Chapter 13.9 was about assembling tools. This is about picking them.
1. Four Different Questions
The overlap is real — you can count lines with grep -c, awk 'END{print NR}', wc -l or sed -n '$='. That makes "which tool" feel like a matter of taste. It is not, because each one is shaped around a different question:
| The question | The tool |
|---|---|
| Which files exist? | find |
| Which files contain something? | grep |
| Which part of each line? | sed |
| What do the numbers add up to? | awk |
2. The Overlaps, Resolved
Several jobs can be done three ways. These are the ones worth settling.
| Job | Prefer | Over | Because |
|---|---|---|---|
| Count matching lines | grep -c | grep | wc -l | One process, no pipeline status |
| Count occurrences | grep -o | wc -l | awk | grep -c counts lines, not matches |
| Print matching lines | grep | awk '/x/', sed -n '/x/p' | Clearer and faster |
| First match only | grep -m1 | grep | head -1 | No pipe, no SIGPIPE under pipefail |
| Unique sorted values | sort -u | sort | uniq | One process |
| A field by position | cut -d, -f2 | awk -F, '{print $2}' | Simpler, much faster |
| A field from aligned text | awk '{print $2}' | cut -d' ' | cut returns nothing — 13.2 |
| Fields reordered | awk | cut | cut cannot reorder |
| Replace text in a line | sed | tr | tr is one character for one character |
| Replace one character | tr | sed | No pattern engine needed |
| Condition on a field | awk -F, '$3=="FAIL"' | grep FAIL | grep matches the word anywhere |
| Files by name | find -name | ls | grep | 13.1 — do not parse ls |
The general principle: prefer the simplest tool that does the job, and prefer one tool over two.
Two tools where one would do is a wasted process, an extra thing to get wrong, and — when the pipeline has a status — Chapter 12.8's problem.
3. Combining Them Properly
The common shapes, and which direction the data flows.
Find files, then search inside them:
find . -name '*.v' -newer last_build -print0 | xargs -0 grep -ln 'clk_div'find answers "which files", grep answers "which of those contain it". The Chapter 14.7 pair is not optional.
Search for files, then act on exactly those:
grep -rlZ --include='*.v' 'OLD_MACRO' . | xargs -0 sed -i.bak 's/OLD_MACRO/NEW_MACRO/g'grep -l narrows to the files that need changing, so sed runs on twelve files rather than four thousand.
Search, extract, then calculate:
grep -h 'SIMULATION TIME' */sim.log | awk '{s += $3} END {printf "%.1f hours\n", s/3600}'grep selects the lines, awk does the arithmetic. Each does what it is best at.
Search, normalise, then count:
grep -h ERROR *.log | sed 's/[0-9][0-9]*/N/g' | sort | uniq -c | sort -rn | headThe Chapter 13.4 pipeline, with the normalisation step that makes it useful.
4. What Each One Cannot Do
Knowing the limits is faster than discovering them.
find cannot look inside a file. It matches on name, type, time, size, permissions and ownership — never contents. Pair it with grep.
grep cannot do arithmetic, cannot address a field by number, and cannot match across lines. grep '$3 == "FAIL"' is not a thing; that is awk. And a pattern spanning two lines cannot match at all, because grep works a line at a time.
sed cannot address a field by number without an unreadable pattern, and cannot calculate. It also has no portable in-place edit (Chapter 13.6's -i trap).
awk cannot edit a file in place portably, and is the wrong tool once the program passes about three lines.
None of them understand nesting. JSON, XML and real CSV have records that span lines and structure that line-based tools cannot see (Chapter 13.1). jq exists for JSON precisely because this toolkit does not fit it.
5. When It Stops Being a One-Liner
Chapter 13.9 put the limit at about six stages. The other signals are about shape rather than length.
The transition is usually painless, because a script can contain the pipeline:
#!/bin/bash
set -uo pipefail
runs="${1:?usage: $0 <runs-dir>}"
# Which runs failed — a question about file contents.
failed=$(grep -rl 'TEST FAILED' "$runs"/*/sim.log | wc -l)
# Which tests failed most often — narrow, transform, summarise.
echo "=== most frequent failures ==="
grep -rl 'TEST FAILED' "$runs"/*/sim.log \
| sed 's|.*/run_||; s|/sim.log||; s/_seed[0-9]*$//' \
| sort | uniq -c | sort -rn | head -5
# How much time they consumed — a calculation, so awk.
echo "=== time spent on failures ==="
grep -h 'SIMULATION TIME' $(grep -rl 'TEST FAILED' "$runs"/*/sim.log) \
| awk '{s += $3} END {printf "%.1f minutes\n", s/60}'
[ "$failed" -eq 0 ]Each block is a pipeline with a comment saying which question it answers. Nothing was lost by making it a script — the pipelines are unchanged — and it gained names, comments, an argument and an exit status.
6. A Worked Decision
"Which modules changed since the last release still use the deprecated reset macro, and how many times?"
Four questions in one sentence, which §5 says is a script — but it is a good illustration of taking a sentence apart.
"changed since the last release" — existence and time. find:
find rtl/ -name '*.v' -newer .last_release"still use the deprecated macro" — contents, and which files:
find rtl/ -name '*.v' -newer .last_release -print0 | xargs -0 grep -l 'OLD_RESET'"how many times" — a count per file:
find rtl/ -name '*.v' -newer .last_release -print0 | xargs -0 grep -c 'OLD_RESET' | grep -v ':0$'"ranked" — the sorting from Chapter 13.3:
find rtl/ -name '*.v' -newer .last_release -print0 \
| xargs -0 grep -c 'OLD_RESET' \
| grep -v ':0$' \
| sort -t: -k2,2 -rnFour stages, each one answering one clause of the original sentence, built up in the Chapter 13.9 way. It is at the limit of what should stay a one-liner, and a comment above it would be the right next step.
7. A Reference for Both Modules
| Question | Command |
|---|---|
| Files by name | find . -name '*.v' |
| Files by age | find . -mmin -60 / find . -newer marker |
| Files by size | find . -size +100M |
| Files containing text | grep -rl 'pattern' . |
| Files not containing text | grep -rL 'pattern' . |
| Matching lines with context | grep -B5 -A10 'ERROR' run.log |
| Count matching lines | grep -c 'pattern' file |
| Count occurrences | grep -o 'pattern' file | wc -l |
| A whole word only | grep -w 'clk' file |
| Either of two patterns | grep -E '(A|B)' file |
| A column | cut -d, -f2 file |
| A column from aligned text | awk '{print $2}' file |
| Columns reordered | awk -F, '{print $3, $1}' file |
| A condition on a field | awk -F, '$3=="FAIL"' file |
| A total | awk '{s += $3} END {print s}' file |
| Replace text | sed 's/old/new/g' file |
| Replace a character | tr 'a' 'b' < file |
| Delete lines | sed '/pattern/d' file |
| A line range | sed -n '100,120p' file |
| Sort by a numeric column | sort -t, -k4,4n file |
| Most frequent values | sort | uniq -c | sort -rn | head |
| Act on found files | find . -name '*.x' -exec cmd {} + |
| Act, in parallel | find . -print0 | xargs -0 -P 8 -n 1 cmd |
Practice
mkdir -p /tmp/ch/{rtl,runs/run_alu_seed3,runs/run_fifo_seed9} && cd /tmp/ch
printf 'module alu;\n OLD_RESET;\n OLD_RESET;\nendmodule\n' > rtl/alu.v
printf 'module fifo;\n NEW_RESET;\nendmodule\n' > rtl/fifo.v
printf 'TEST FAILED: timeout after 1203 cycles\nSIMULATION TIME 262.4 seconds\n' > runs/run_alu_seed3/sim.log
printf 'TEST PASSED\nSIMULATION TIME 130.0 seconds\n' > runs/run_fifo_seed9/sim.log
touch .last_release; sleep 1; touch rtl/alu.v1 — Name the tool for each sentence before running anything:
- Which
.vfiles are larger than 1 kilobyte? - Which files mention
OLD_RESET? - How many seconds did all the runs take in total?
- What is the seed in each run directory name?
2 — Then check yourself.
find rtl -name '*.v' -size +1c
grep -rl OLD_RESET rtl/
grep -h 'SIMULATION TIME' runs/*/sim.log | awk '{s+=$2} END {print s}'
ls runs/ | sed 's/.*_seed//'The third needs care — count the fields before trusting $2.
3 — Resolve an overlap three ways.
grep -c OLD_RESET rtl/alu.v
grep OLD_RESET rtl/alu.v | wc -l
awk '/OLD_RESET/ {n++} END {print n+0}' rtl/alu.vSame answer. Say which you would commit.
4 — Count lines versus occurrences.
printf 'OLD_RESET OLD_RESET\n' > rtl/double.v
grep -c OLD_RESET rtl/double.v
grep -o OLD_RESET rtl/double.v | wc -l5 — Build the worked example.
find rtl/ -name '*.v' -newer .last_release
find rtl/ -name '*.v' -newer .last_release -print0 | xargs -0 grep -l OLD_RESET
find rtl/ -name '*.v' -newer .last_release -print0 | xargs -0 grep -c OLD_RESET | grep -v ':0$'6 — Narrow, transform, summarise.
grep -rl 'TEST FAILED' runs/*/sim.log
grep -rl 'TEST FAILED' runs/*/sim.log | sed 's|.*/run_||; s|/sim.log||; s/_seed[0-9]*$//'
grep -rl 'TEST FAILED' runs/*/sim.log | sed 's|.*/run_||; s|/sim.log||; s/_seed[0-9]*$//' | sort | uniq -c7 — Spot the wrong tool. Each of these works and each has a better form. Write the better one:
ls rtl/ | grep '\.v$'
cat rtl/alu.v | grep OLD_RESET
grep OLD_RESET rtl/alu.v | head -1
sed -n '/OLD_RESET/p' rtl/alu.v8 — Clean up. cd /tmp && rm -rf /tmp/ch
Check Your Understanding
1. What question is each of the four tools shaped around?
2. grep FAIL results.csv and awk -F, '$3=="FAIL"' results.csv can both find failures. When does the difference matter?
3. Why prefer grep -c to grep | wc -l?
4. What is the usual order of stages in a pipeline, and why?
5. Name three signs that a one-liner should become a script.
6. Why is none of these the right tool for JSON?
8. Remember
- Each tool answers a different question: which files exist, which contain something, which part of each line, what the numbers add up to.
- Say the question as a sentence first. The sentence names the tool; two tools means it is a script.
- Prefer the simplest tool, and one tool over two.
grep -c,sort -u,grep -m1each replace a pipeline. grepmatches anywhere on the line;awkcan test one field. That difference is the whole reason to useawkfor structured data.cutfor strictly delimited,awkfor aligned or reordered.- Never parse
ls.findasks the filesystem. - The usual order is narrow, transform, summarise — and narrowing first is also the fast way.
findcannot see inside a file;grepcannot calculate;sedcannot address a field;awkcannot edit in place.- None of them understand nesting. JSON and real CSV need a tool built for them.
- Stop at a conditional, a reused intermediate, a third
awk, or anything you cannot comment. - A script loses nothing — the pipelines go inside it unchanged, and gain names and an exit status.
- In a real repository, deciding where to search matters more than which tool. Chapter 23.5 measures a search whose answer changed from three files to two because of which
grepwas installed.
That completes Module 14, and with it the searching and text-processing half of Part I. You can locate a file by any property the filesystem records, find text anywhere in a tree and shape the answer to the question, write a pattern rather than copy one, act on what you found without breaking on a space, and choose between a dozen overlapping tools on purpose.
Module 15 stops typing commands one at a time. Everything from here — scripts, conditionals, loops, functions, arrays and defensive automation — is about putting what you now know into a file that somebody else can run.
Where this fits
Part of the Linux & Bash curriculum.
