Text Processing: grep, sed, awk, and jq¶
What You'll Learn¶
- How pipelines and standard streams let small tools combine into powerful one-liners
- When to reach for
grep,sed,awk, orjq - Real operations examples: top error sources, request rates, config edits, and API output
- How to avoid the quoting and locale traps that make one-liners fragile
Streams and Pipelines¶
Every process has three standard streams: stdin (0), stdout (1), and stderr (2). A pipe connects one command's stdout to the next command's stdin.
command 2>/dev/null # discard errors
command > out.log 2>&1 # stdout and stderr to one file
command 2>&1 | tee run.log # see output and save it
command | head -5 # the pipeline stops early; later lines are never read
Pipelines and exit codes
By default a pipeline's exit status is the last command's. grep ERROR app.log | wc -l succeeds even if app.log doesn't exist. With set -o pipefail (part of strict mode), any failing command fails the pipeline.
Choosing the Tool¶
| Tool | Best at | Example |
|---|---|---|
grep |
Selecting lines that match a pattern | Find error lines |
sed |
Editing streams: substitute, delete, insert | Change a config value |
awk |
Fields and columns, counting, summing, reports | Requests per status code |
sort, uniq, cut, wc |
Ordering, de-duplicating, counting | Top 10 client IPs |
jq |
Parsing and transforming JSON | Extract fields from API or kubectl output |
grep¶
grep -i 'error' app.log # case-insensitive
grep -n -C 3 'Traceback' app.log # line numbers with 3 lines of context
grep -v 'healthcheck' access.log # invert: exclude matches
grep -c ' 500 ' access.log # count matching lines
grep -E 'timeout|refused|reset' app.log # extended regex: alternation
grep -o 'user_id=[0-9]\+' app.log # print only the match
grep -rIl 'password' /etc/myapp/ # recursive, skip binaries, filenames only
grep -F '[ERROR]' app.log # fixed string: no regex, no escaping brackets
zgrep 'ERROR' app.log.2.gz # search compressed rotated logs
sed¶
sed -n '100,120p' app.log # print a line range
sed 's/http:/https:/g' urls.txt # substitute every match on each line
sed -i.bak 's/^max_connections = .*/max_connections = 500/' postgresql.conf # in place, keep a backup
sed '/^\s*#/d; /^\s*$/d' nginx.conf # strip comments and blank lines
sed -n '/2026-09-14T10:00/,/2026-09-14T10:15/p' app.log # lines between two timestamps
sed -i on macOS
GNU sed (Linux) accepts sed -i 's/a/b/' file. BSD sed (macOS) requires an explicit backup suffix argument: sed -i '' 's/a/b/' file. Scripts that run on both should use sed -i.bak ... and remove the backup, or use perl -pi -e.
For anything more structured than a single key, prefer a real parser — yq for YAML, jq for JSON, or an Ansible module — over regex edits.
awk¶
awk splits each line into fields ($1, $2, … and $0 for the whole line) and runs a program against it.
Given an Nginx access log:
203.0.113.9 - - [14/Sep/2026:10:02:11 +0000] "GET /api/orders HTTP/1.1" 500 512 "-" "curl/8.9" 0.842
# Requests per status code
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# Top 10 client IPs
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# Only 5xx responses, printing time, path, and status
awk '$9 ~ /^5/ {print $4, $7, $9}' access.log
# Average response time (last field) per path
awk '{sum[$7] += $NF; n[$7]++} END {for (p in sum) printf "%.3f %s\n", sum[p]/n[p], p}' access.log | sort -rn | head
# Custom separator: users with a login shell from /etc/passwd
awk -F: '$7 !~ /nologin|false/ {print $1, $7}' /etc/passwd
sort, uniq, cut, and friends¶
cut -d, -f1,3 inventory.csv # columns 1 and 3 of a CSV (no quoted commas)
sort -t, -k3,3n inventory.csv # sort numerically by the third column
sort -u hosts.txt # sort and de-duplicate
uniq -c # count adjacent duplicates — always sort first
comm -13 <(sort old.txt) <(sort new.txt) # lines only in new.txt
tr '[:upper:]' '[:lower:]' < names.txt
xargs -n 1 -P 4 ./check-host.sh < hosts.txt # run 4 checks in parallel
jq¶
# Pretty-print and pick fields
curl -s https://api.github.com/repos/prometheus/prometheus/releases/latest | jq '{tag: .tag_name, published: .published_at}'
# Raw strings for use in shell variables
TAG=$(curl -s https://api.github.com/repos/prometheus/prometheus/releases/latest | jq -r .tag_name)
# Pods that are not Running, with their node
kubectl get pods -A -o json \
| jq -r '.items[] | select(.status.phase != "Running") | [.metadata.namespace, .metadata.name, .status.phase, .spec.nodeName] | @tsv'
# Container images across all pods, counted
kubectl get pods -A -o json | jq -r '.items[].spec.containers[].image' | sort | uniq -c | sort -rn
# Count structured log events by level
jq -r '.level' app.json.log | sort | uniq -c
# Filter JSON log lines by a field
jq -c 'select(.level == "ERROR" and .service == "orders-api")' app.json.log
# Build JSON safely from shell variables — never by string concatenation
jq -n --arg host "$HOSTNAME" --argjson disk "$(df --output=pcent / | tail -1 | tr -dc 0-9)" \
'{host: $host, disk_used_percent: $disk}'
Worked Example: An Incident One-Liner¶
"Which endpoints started returning 5xx in the last 15 minutes, and from which upstream?"
since=$(date -u -d '15 minutes ago' '+%d/%b/%Y:%H:%M')
awk -v since="$since" '
{ ts = substr($4, 2, 17) } # 14/Sep/2026:10:02
ts >= since && $9 ~ /^5/ { count[$7" "$9]++ }
END { for (k in count) print count[k], k }
' /var/log/nginx/access.log | sort -rn | head
String comparison of timestamps works here only because every line is within the same day. For anything longer, use structured JSON logs and query them in Loki or jq.
Common Mistakes¶
- Parsing
lsoutput instead of using globs orfind -print0 | xargs -0, and breaking on filenames with spaces. - Using
uniqwithoutsort, so non-adjacent duplicates aren't counted. - Writing
cat file | grepeverywhere — harmless, butgrep pattern fileis clearer and givesgrepthe filename. - Editing YAML or JSON with
sedregexes, then corrupting the file when formatting changes. - Building JSON with string interpolation instead of
jq --arg, producing invalid JSON when a value contains a quote. - Forgetting
pipefail, so a missing input file produces an empty result instead of an error.
Interview Questions¶
- How would you find the top 10 IP addresses hitting a web server from its access log?
- What's the difference between
grep,sed, andawk, and when would you use each? - How do you extract the names of all failing pods across namespaces with
kubectlandjq? - Why does
set -o pipefailmatter for scripts that use pipelines?
Next¶
Continue to Testing, Linting, and Debugging.