05 — Text Processing & Pipelines
Linux philosophy: write small tools that do one thing well, then combine them with pipes. This chapter covers the core text-processing toolkit — grep, sed, awk, cut, sort, uniq, tr, xargs — composed into production-grade one-liners. This is arguably the most important skill for a Linux engineer.
Pipes (Recap)
A pipe | connects one command's stdout to the next command's stdin. Each command runs concurrently:
# Complex Implementation: frequency-count top IPs in an access log
# — 5 stages, all running concurrently, streaming data through the pipe
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
# 1243 192.168.1.50
# 872 10.0.0.15
grep — Search
grep "root" /etc/passwd # basic search
grep -r "PermitRoot" /etc/ssh # recursive
grep -i "error" /var/log/syslog # case-insensitive
grep -v "DEBUG" app.log # invert (lines WITHOUT "DEBUG")
grep -n "PermitRoot" sshd_config # line numbers
grep -c "error" app.log # count matches
grep -E "^[0-9]+" file # extended regex (-E)
grep -w "the" file # whole-word match
grep -A 2 -B 2 "error" log # 2 lines After, 2 Before
grep -l "TODO" *.py # only filenames with matches
grep -P "(?<=foo)bar" file # PCRE (lookbehind) — GNU grep
grep -oE 'https?://[^ ]+' access.log # extract only matching portion
Performance Tip: LC_ALL=C grep for Huge Files
# NAIVE: locale-aware grep on a 10 GB log (slow — UTF-8 collation overhead)
grep "error" huge.log
# PRODUCTION: byte-wise grep (10x+ faster on large files)
LC_ALL=C grep "error" huge.log
# C locale = byte comparison, no Unicode processing
sed — Stream Editor
Substitution
sed 's/old/new/' file # replace first occurrence per line
sed 's/old/new/g' file # replace ALL occurrences per line (global)
sed 's/old/new/gi' file # global + case-insensitive
sed 's|/usr/local|/opt/app|g' file # use | as delimiter (easier with /)
sed 's/[[:space:]]*$//' file # trim trailing whitespace
sed 's/^[[:space:]]*//' file # trim leading whitespace
In-Place Editing
# Complex Implementation: safe in-place edit with backup
sed -i.bak 's/old/new/g' file # in-place + save original as file.bak
# Test WITHOUT -i first:
sed 's/old/new/g' file | head # verify output, then apply with -i.bak
# Anti-Pattern: sed -i without backup (no undo if regex is wrong)
# sed -i 's/old/new/g' file → if wrong, file is corrupted
Edge Case: sed Regex is BRE by Default
# NAIVE: sed 's/a+/b/g' replaces literal "a+" (BRE — + is literal)
# PRODUCTION: sed -E 's/a+/b/g' replaces "one or more a" (ERE)
sed -E 's/a+/b/g' file # ERE (extended regex)
awk — Field Processing
awk is a full programming language for column-based text processing. It auto-splits each line into fields ($1, $2, ..., $0 = whole line). Default field separator is whitespace.
Complex Implementation: Log Analysis with awk
# Sum HTTP status codes from an access log
awk -F'"' '{print $3}' access.log | awk '{print $1}' | sort | uniq -c | sort -rn
# 1243 200
# 872 404
# 15 500
# Top 10 requesting IPs with their request counts
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# Total bandwidth (bytes transferred = field 10 in Common Log Format)
awk '{sum += $10} END{printf "Total: %.2f MB\n", sum/1024/1024}' access.log
# Users with bash shell (field 7 in /etc/passwd)
awk -F: '$7 ~ /bash$/ {print $1}' /etc/passwd
# Average of column 2
awk '{sum += $2} END{print sum/NR}' file
BEGIN / END Blocks
# Print header + data + footer
awk -F: 'BEGIN{print "User\tUID\tShell"} {print $1"\t"$3"\t"$7} END{print "Total: " NR " users"}' /etc/passwd
sort — Sort Lines
sort -n file # numeric (10 after 9, not after 1)
sort -rn file # numeric reverse (top values first)
sort -u file # sort + unique (dedupe)
sort -k2 -n file # sort by field 2, numeric
sort -t: -k3 -n /etc/passwd # colon-delimited, sort by UID (field 3)
sort -h file # human-numeric (2K, 1M, 3G — for sizes)
sort -V file # version sort (v1.2.10 after v1.2.9)
Edge Case: sort is Locale-Aware
# NAIVE: locale-dependent sort (different order on different machines)
sort file
# In en_US.UTF-8, uppercase sorts differently than in C locale
# PRODUCTION: byte-order sort (consistent across systems)
LC_ALL=C sort file
uniq — Deduplicate Adjacent Lines
uniq only removes adjacent duplicates. Usually preceded by sort:
# Complex Implementation: frequency counter (the most reused pipeline in Linux)
sort file | uniq -c | sort -rn # count occurrences, sorted by frequency
# Edge Case: uniq without sort appears to do nothing
# NAIVE:
uniq file # only removes adjacent dups — a\nb\na → all 3 lines
# PRODUCTION:
sort file | uniq # dedupe correctly (sort first)
tr — Transliterate / Delete
tr works on characters, not lines:
echo "Hello" | tr 'a-z' 'A-Z' # HELLO (uppercase)
echo "hello world" | tr -s ' ' # hello world (squeeze repeats)
echo "hello world" | tr -cd 'a-zA-Z' # helloworld (delete non-alpha)
echo "1,2,3" | tr ',' '\n' # split CSV into lines
xargs — Build Command Lines from Stdin
# Complex Implementation: null-delimited pipeline (safe for any filename)
# — filenames can contain spaces, newlines, and quotes
find . -type f -print0 | xargs -0 grep "pattern"
# -print0: separate filenames with NUL (\0), not newline
# -0: xargs reads NUL-delimited input
# Anti-Pattern: newline-delimited (breaks on filenames with spaces)
# find . -name "*.txt" | xargs grep foo → breaks on "my notes.txt"
# Prefer -exec {} + (equivalent safety, no xargs needed):
find . -type f -name "*.txt" -exec grep "pattern" {} +
Composing Pipelines — Real-World Examples
Extract All URLs from a Log
grep -oE 'https?://[^ ]+' /var/log/nginx/access.log | sort -u
Count Files per Extension
find . -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn
Show Process Tree of a Service
ps aux | grep nginx | grep -v grep | awk '{print $2}' | head -1 | xargs -I{} pstree -p {}
💡 Tips & Tricks
- Idiom: use
sort | uniq -c | sort -rnas a frequency counter — the most reused pipeline in Linux. Counts how many times each line appears, sorted by frequency. - Idiom: use
awk '{print $NF}'for the last field —$NFis "field number NF" (the count), so it's always the last field regardless of how many fields the line has. - Idiom: use
tail -F(capital) instead oftail -ffor log watching —-Fhandles log rotation (re-opens if the file is moved/recreated). - Idiom: use
grep -Efor readability overgrepwith backslashes —grep -E "cat|dog"is clearer thangrep "cat\|dog". - Performance:
grepis faster thanawkfor plain matching — usegrepfor filtering,awkwhen you need field logic. - Performance:
sort -S 50% --parallel=4speeds up large sorts —-Ssets buffer size (50% of RAM),--paralleluses multiple cores.
⚠️ Edge Cases & Gotchas
uniqonly removes adjacent duplicates:echo -e "a\nb\na" | uniqoutputs all three lines. Alwayssortbeforeuniqunless you specifically want adjacent-only dedup.sed -ihas no undo:sed -i 's/old/new/g' fileoverwrites the file. If your regex is wrong, the file is corrupted. Always test without-ifirst, or usesed -i.bak.- Filenames with spaces break
xargs:find . -name "*.txt" | xargs grep foofails onmy notes.txt. Usefind -print0 | xargs -0orfind -exec {} +. sortis locale-aware:sortuses your locale (LC_COLLATE). For byte-order sorting (consistent across systems), useLC_ALL=C sort.grep -rvs-R: GNUgrep -rdoes NOT follow symlinks;-Rdoes. Watch for symlink loops with-R.awktreats numbers as strings sometimes:awk '$1 > 10'may do string comparison if$1is"9abc". Useawk '$1 + 0 > 10'to force numeric.cut -d" "treats consecutive spaces as multiple empty fields:echo "a b" | cut -d" " -f2gives empty. Useawk '{print $2}'instead (awk treats consecutive whitespace as one separator).- Pipelines hide intermediate failures:
cmd1 | cmd2 | cmd3— ifcmd1fails,cmd2andcmd3still run. The pipeline's exit status iscmd3's. Useset -o pipefailin scripts.
🧠 Spot the Bug
A sysadmin wants to count how many times each error code appears in a log, but this pipeline gives wrong counts — many error codes show count 1 that should be higher:
grep "ERROR" app.log | awk '{print $4}' | uniq -c | sort -rn | head
What's wrong, and how do you fix it?
Answer
uniq only deduplicates adjacent duplicates. The error codes from awk are in log order (not sorted), so E404 E500 E404 E404 E500 has no adjacent duplicates collapsed — uniq -c reports each run separately:
1 E404
1 E500
2 E404
1 E500
Fix: sort before uniq:
grep "ERROR" app.log | awk '{print $4}' | sort | uniq -c | sort -rn | head
Now sort groups identical codes together, and uniq -c counts each group correctly:
3 E404
2 E500