Overview
Use find to select files, grep to select lines, awk for fields, and sed for line edits. Examples run on GNU tools; the BSD (macOS) differences are in the gotchas. Pure-bash alternatives are in bash-one-liners.
find
Put tests before actions: -delete placed before -name deletes everything.
| Expression | Matches or does |
|---|---|
find . -type f -name '*.md' | Markdown files, recursive. |
find . -type f \( -name '*.ts' -o -name '*.tsx' \) | Either pattern. |
find . -not -path '*/node_modules/*' | Exclude a subtree. |
find . -type f -size +10M | Larger than 10 MB. |
find . -type f -mtime -7 | Modified in the last 7 days. |
find . -mindepth 2 -maxdepth 3 -type f | Depth-bounded. |
find . -type f -name '*.tmp' -delete | Delete matches. |
find . -name '*.log' -exec grep -l ERROR {} + | Batch many files into one command. |
find . -type f -print0 | xargs -0 wc -l | Safe for names with spaces. |
grep
Use rg for repository searches and grep -E when you need portability.
| Expression | Does |
|---|---|
grep -rn "pat" . | Recursive with line numbers; skips symlinks found while walking. |
grep -rIn "pat" --include='*.py' --exclude-dir=node_modules . | Text files only, filtered by name. |
grep -l / -L / -c | Matching files / non-matching files / count per file. |
grep -v / -i / -w | Invert / ignore case / whole word. |
grep -A 3 -B 2 "pat" | Context after / before. |
grep -E "(foo|bar)" | Extended regex; no \d, use [0-9]. |
grep -P "\d{3}-\d{4}" | PCRE; GNU grep only. |
grep -o "pat" | Print only the matched text. |
Prefer rg (ripgrep) for repository searches; it honors .gitignore and skips binaries.
awk
Fields are $1, $2; $NF is the last; NR is the line number.
| Expression | Does |
|---|---|
awk '{print $1, $NF}' f | First and last field. |
awk -F: '{print $1}' /etc/passwd | Custom delimiter. |
awk 'NR>=3 && NR<=7' f | Lines 3 to 7. |
awk '/pat/' / awk '!/pat/' | Lines that match / do not match. |
awk '{s+=$2} END{print s}' f | Sum column 2. |
awk '!seen[$0]++' f | Remove duplicate lines, keeping order. |
awk '{gsub(/old/,"new")} 1' f | Replace all matches, print every line. |
awk 'NR==5' f / awk 'length($0) > 80' f | Line 5 / lines longer than 80 characters. |
sed
Use -i.bak so in-place edits work on both GNU and BSD.
| Expression | Does |
|---|---|
sed 's/old/new/g' f | Replace all matches per line; without g, only the first. |
sed -E 's/(a+)b/\1/' f | Extended regex with groups. |
sed -i.bak 's/old/new/g' f | In-place with backup; works on GNU and BSD. |
sed '/pat/d' / sed '1d' / sed '$d' | Delete matching / first / last line. |
sed -n '10,20p' | Print lines 10 to 20. |
sed -n '/start/,/end/p' | Print a pattern range. |
sed 's/^/> /' / sed 's/$/;/' | Prefix / suffix every line. |
sed '/pat/a\new line' | Append a line after each match (GNU one-line form). |
Pipelines
Run a pipeline without -i or -delete first, then add the destructive step.
# Top 10 client IPs in an access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# Rename a symbol across Python files (BSD sed: use -i '')
find . -name '*.py' -exec sed -i 's/old_fn/new_fn/g' {} +
# Files containing a TODO, deduplicated
grep -rn "TODO" . | awk -F: '{print $1}' | sort -uGotchas
Avoid these; each differs between GNU and BSD or breaks on unusual filenames.
| Gotcha | Fix |
|---|---|
Unquoted -name *.md is expanded by the shell first. | Quote it: -name '*.md'. |
-exec cmd {} \; runs once per file. | End with + to batch. |
sed -i is GNU; BSD needs an argument (-i ''). | Use -i.bak for portability, then delete the .bak files. |
grep -r skips symlinks and enters node_modules. | Use -R to follow links; add --exclude-dir. |
-P is GNU only (macOS grep lacks it), and \d is not POSIX: GNU grep -E matches a literal d. | Use [0-9] with -E, or install GNU grep for -P. |
awk '{print $1}' splits on spaces inside quoted CSV fields. | Use a CSV-aware tool such as mlr (Miller) or csvcut (csvkit). |
sed 's/./X/g' replaces every character. | Escape the dot: \.. |