Linux Basic — sed & awk Practical Patterns
Practical Patterns
Ten recipes you’ll reach for again and again. Each shows the goal, the command, and (where it matters) the why.
1. Clean a Config File (Strip Comments & Blank Lines)
sed -E '/^[[:space:]]*#/d; /^[[:space:]]*$/d' config.txt
d deletes; the two address regexes match (a) lines starting with optional whitespace then #, and (b) lines that are entirely whitespace.
To modify in place with backup:
sed -i.bak -E '/^[[:space:]]*#/d; /^[[:space:]]*$/d' config.txt
2. Replace a Setting in a Config File
# Set or update key=value
key="timeout"
new="60"
if grep -q "^${key}=" config.txt; then
sed -i.bak "s/^${key}=.*/${key}=${new}/" config.txt
else
echo "${key}=${new}" >> config.txt
fi
Or with awk in one pass (creates the line if missing):
awk -v k="$key" -v v="$new" '
BEGIN { FS=OFS="=" }
$1 == k { $2 = v; found=1 }
{ print }
END { if (!found) print k OFS v }
' config.txt > config.tmp && mv config.tmp config.txt
3. Top N HTTP Status Codes
awk '{ c[$9]++ } END { for (s in c) print c[s], s }' access.log \
| sort -rn \
| head -10
$9 is the status field in a default Combined Log Format. Adjust the column number for your log.
4. Per-IP Request Count + Bandwidth
awk '{ count[$1]++; bytes[$1] += $10 } END {
for (ip in count) printf "%-16s %6d req %12d B\n", ip, count[ip], bytes[ip]
}' access.log | sort -k2 -rn | head -20
Two accumulators per key — the awk way.
5. Extract a Field From a Path
Native is fastest:
path="/var/log/nginx/access.log"
echo "${path##*/}" # access.log
echo "${path%/*}" # /var/log/nginx
But if data lives in a file and you need it inside a pipeline:
echo "$path" | awk -F/ '{ print $NF }'
echo "$path" | sed 's|.*/||'
In a hot loop, use native. In a one-shot pipeline, use whatever reads best.
6. CSV Sum + Average
# col 3 = amount
awk -F, '
NR == 1 { next } # skip header
{ sum += $3; n++ }
END { printf "rows=%d sum=%.2f avg=%.2f\n", n, sum, sum/n }
' sales.csv
For multi-column averages:
awk -F, '
NR == 1 { next }
{ for (i=2; i<=NF; i++) { sum[i]+=$i; cnt[i]++ } }
END { for (i in sum) printf "col%d avg=%.2f\n", i, sum[i]/cnt[i] }
' data.csv
7. Filter Log by Time Range
# /var/log/syslog timestamps look like: Jun 17 09:12:34
start="Jun 17 09:00:00"
end="Jun 17 10:00:00"
awk -v s="$start" -v e="$end" '
{
ts = $1 " " $2 " " $3
if (ts >= s && ts <= e) print
}
' /var/log/syslog
awk does lexical comparison — it works as long as your timestamps sort lexically (zero-padded, year omitted means stable within the year).
8. Inject a Block into a File at a Marker
# Insert <new block> after a line matching "# CONFIG MARKER"
sed -i '/# CONFIG MARKER/r additional.conf' main.conf
r FILE reads the named file in after a matching address.
Or with awk:
awk '/# CONFIG MARKER/ { print; while ((getline line < "additional.conf") > 0) print line; next } { print }' \
main.conf > main.new && mv main.new main.conf
9. Find Lines Present in File A but Not in File B
awk 'NR==FNR { seen[$0]; next } !($0 in seen)' b.txt a.txt
The NR==FNR idiom loads b.txt into a set, then for each line of a.txt prints it only if it wasn’t in b.txt. Much faster than grep -vxF -f.
10. Extract Key Values from a JSON-Like Line (No jq)
For real JSON, use jq. For ad-hoc grepping when jq isn’t installed:
line='{"user":"alice","status":200,"ms":42}'
echo "$line" | sed -E 's/.*"user":"([^"]+)".*/\1/'
# alice
Or with awk:
echo "$line" | awk -F'"' '{ for (i=1; i<=NF; i++) if ($i=="user") print $(i+2) }'
These are brittle by design — they assume well-formed input. For anything user-facing, install jq (covered in Using jq in Chapter 18).
Bonus — Stream Editing With Watch
For long-running tail-and-transform:
tail -F app.log | awk '/ERROR/ { print strftime("%H:%M:%S"), $0; fflush() }'
fflush() flushes after every line — important because awk’s stdout is block-buffered when piped, which can delay output by tens of seconds otherwise. (GNU awk only.)
For mawk / BSD awk, use the line-buffering wrapper:
stdbuf -oL tail -F app.log | awk '/ERROR/ { print }' | tee err.log
The Decision Recap
| Situation | Tool |
|---|---|
| String already in a Bash var, simple transform | parameter expansion |
| String already in a Bash var, real regex (no replace) | [[ … =~ … ]] + BASH_REMATCH |
| Stream/file, line-by-line substitution | sed -E |
| Stream/file, column data / aggregation / state | awk |
| JSON / YAML / TOML | jq / yq / tq (next chapter or Chapter 18) |
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
cat file \| awk … |
awk … file |
useless use of cat |
grep pattern \| awk '…' |
awk '/pattern/ { … }' |
awk filters by itself |
Many seds piped |
one sed -e … -e … |
each pipe is a fork |
| Parsing JSON with sed/awk in production | use jq |
regex JSON is brittle |
awk -v x=$x (unquoted) |
awk -v x="$x" |
spaces break it |
Next → Practice Exercises