env.dev

sed & awk Cheat Sheet — Text Processing One-Liners

sed and awk one-liners for substitution, line ranges, field extraction, and aggregation — with the GNU vs BSD traps that break scripts on macOS.

By env.dev Updated

sed edits a stream line by line; awk splits each line into fields and runs a program against them. The split in practice: reach for sed when you are rewriting text, and awk when you care about columns or need to accumulate a total. Both ship on every Unix system, which is why they survive — but the GNU versions on Linux and the BSD versions on macOS disagree in ways that silently corrupt files, and that section is the one worth reading even if you skip the rest.

When to reach for this

  • Rewriting a config value across many files in place, without opening an editor — sed -i.
  • Pulling one column out of whitespace- or comma-separated output and summing or counting it — awk.
  • A script works on your Linux CI runner and mangles files on a colleague's Mac — the portability section below.

How Do You Substitute Text With sed?

CommandDescription
sed 's/old/new/' fReplace the first match on each line
sed 's/old/new/g' fReplace every match on each line
sed 's/old/new/2' fReplace only the second match on each line
sed 's/old/new/gI' fGlobal and case-insensitive (GNU; BSD uses I too)
sed 's|/usr|/opt|' fUse | as the delimiter when the text contains slashes
sed 's/.*/[&]/' fWrap the whole match — & is the matched text
sed -E 's/(a+)(b+)/\2\1/' fSwap capture groups; -E enables extended regex
sed 's/x/y/w out.txt' fWrite only the changed lines to out.txt

& in the replacement is the entire match, and \1 through \9 are capture groups. Use -E rather than -r for extended regex: -E works on both GNU and BSD, -r is GNU-only.

How Do You Target Specific Lines?

AddressSelects
sed -n 5p fLine 5 only (-n suppresses the default print)
sed -n 5,10p fLines 5 through 10
sed -n 5,$p fLine 5 to end of file
sed -n 0~3p fEvery third line (GNU only)
sed '/start/,/end/d' fDelete from the first match of start through end
sed '/^#/d' fDelete comment lines
sed '/^$/d' fDelete blank lines
sed '5!d' fDelete everything except line 5
sed '$d' fDelete the last line
sed '2i\text' fInsert a line before line 2
sed '2a\text' fAppend a line after line 2
sed '2c\text' fReplace line 2 entirely

sed -n '/start/,/end/p' is the quickest way to pull one stanza out of a long config or a single failing test out of CI output. Note that a range restarts: if start matches again after end, you get a second block.

What Does an awk Program Look Like?

An awk program is a list of pattern { action } pairs. Omit the pattern and the action runs on every line; omit the action and matching lines are printed.

CommandDescription
awk '{print $1}' fPrint the first whitespace-separated field
awk '{print $NF}' fPrint the last field
awk '{print $(NF-1)}' fPrint the second-to-last field
awk -F, '{print $2}' fSplit on commas instead of whitespace
awk -F'\t' '{print $3}' fSplit on tabs
awk 'NR==5' fPrint line 5
awk 'NR>1' fSkip a header row
awk '/error/ {print $0}' fPrint lines matching a pattern
awk '$3 > 100' fPrint rows where field 3 exceeds 100
awk 'NF' fDrop blank lines (NF is 0 on an empty line)
awk '!seen[$0]++' fDeduplicate without sorting, keeping first occurrence
awk -v n=5 '$1 > n' fPass a shell value in with -v

Built-in Variables

VariableMeaning
NRCurrent record number, counting across all input files
FNRRecord number within the current file
NFNumber of fields on the current line
FSInput field separator (same as -F)
OFSOutput field separator used by print with commas
RSInput record separator; set to "" for paragraph mode
ORSOutput record separator, newline by default
FILENAMEName of the file currently being read

OFS catches people out: changing it does nothing until a field is reassigned. The idiom is awk -v OFS=, '{$1=$1; print}' — the no-op assignment forces awk to rebuild the record with the new separator.

BEGIN, END, and Aggregation

CommandDescription
awk '{s+=$1} END {print s}' fSum a column
awk '{s+=$1} END {print s/NR}' fAverage a column
awk 'END {print NR}' fCount lines, like wc -l
awk '{c[$1]++} END {for (k in c) print c[k], k}' fCount occurrences per key
awk 'BEGIN {FS=":"} {print $1}' /etc/passwdSet the separator in BEGIN instead of -F
awk '{print > $1".txt"}' fSplit input into files named by field 1
awk 'BEGIN {print "start"} END {print "done"}' fRun once before and once after all input

The {c[$1]++} END {for (k in c) ...} pattern replaces sort | uniq -c | sort -rn and does it in a single pass, which matters once the input stops fitting in memory comfortably. Associative arrays are the reason awk outlived its replacements.

Which GNU vs BSD Differences Actually Bite?

macOS ships BSD sed and BSD awk; Linux ships GNU sed and gawk or mawk. The in-place edit flag is where scripts break, and it fails destructively rather than loudly.

FormGNU (Linux)BSD (macOS)
sed -i 's/a/b/' fEdits in place, no backupTreats the script as the backup suffix — breaks
sed -i '' 's/a/b/' fTreats '' as the suffix — breaksEdits in place, no backup
sed -i.bak 's/a/b/' fEdits in place, keeps f.bakEdits in place, keeps f.bak
sed -EExtended regexExtended regex
sed -rExtended regexNot supported
\b word boundarySupportedNot supported

Run the Linux form on a Mac and BSD sed reads 's/a/b/' as the required backup extension, then tries to parse your filename as the script, giving the famously unhelpful sed: 1: "f": invalid command code f. The variant sed -i -e 's/a/b/' f is worse: BSD takes -e as the suffix and leaves a stray f-e file behind while appearing to work.

The portable idiom is sed -i.bak 's/a/b/' f && rm f.bak. Both implementations parse a glued suffix identically, and the && keeps the backup if sed failed and left the original half-rewritten. If you would rather not think about it, install GNU sed on macOS with brew install gnu-sed and call gsed.

What Are the Common Gotchas?

SymptomCauseFix
sed: invalid command codeBSD sed consumed the script as the -i backup suffixUse sed -i.bak ... && rm file.bak
Substitution silently does nothingThe pattern contains an unescaped delimiter, or is a basic-regex construct needing -EChange the delimiter (s|a|b|) or add -E
awk prints nothing for a valid fileThe field separator is wrong, so $2 does not existCheck with awk "{print NF}" before indexing fields
awk arithmetic gives 0The field has stray whitespace or a currency symbol, so it coerces to 0Strip it first: gsub(/[^0-9.]/, "", $1)
OFS has no effect on outputawk only rebuilds the record when a field is assignedAdd a no-op assignment: {$1=$1; print}
sed -i destroys a symlinkIn-place edit writes a new file and replaces the linkResolve first with readlink -f, or edit the target directly

For the regex syntax both tools share, see the regex cheat sheet. When the input is JSON rather than lines and columns, stop reaching for these and use jq — parsing JSON with sed is the canonical way to produce a subtly wrong pipeline. For wiring these into scripts, the Bash scripting cheat sheet covers quoting and exit codes.

Was this helpful?

Frequently Asked Questions

Why does sed -i fail on macOS but work on Linux?

GNU sed treats the backup suffix as optional and glued to the flag, so sed -i works alone. BSD sed on macOS always requires a suffix argument, so it reads your script as the suffix and then fails with "invalid command code". The portable form is sed -i.bak 's/a/b/' file && rm file.bak.

When should I use sed instead of awk?

Use sed when you are rewriting text on a line — substitutions, deletions, insertions. Use awk when the line has structure you care about: extracting a column, filtering on a numeric field, or accumulating a total across records. If you find yourself counting fields in sed, switch.

How do I sum a column with awk?

awk '{s+=$1} END {print s}' file. Use $NF for the last column, and -F, to split on commas. If the result is 0, the field almost certainly contains a currency symbol or stray whitespace, which awk coerces to 0 rather than erroring.

Why does setting OFS not change my awk output?

awk only rebuilds the output record when a field is assigned. Setting OFS alone leaves the original line untouched. Force a rebuild with a no-op assignment: awk -v OFS=, '{$1=$1; print}'.

Should I use -E or -r for extended regex in sed?

-E. Both GNU and BSD sed accept it, while -r is GNU-only. GNU sed has supported -E as an alias since 4.2 specifically for portability.

How do I remove duplicate lines without sorting?

awk '!seen[$0]++' file. It keeps the first occurrence of each line in original order, unlike sort -u which reorders. The idiom works because the post-increment returns 0 (falsy) the first time a line is seen.