Where grep finds lines and sed edits them, awk understands that a line has columns. That one idea is why it survives: any time your data has fields — log files, CSVs, the output of ps or df — awk lets you pick them out, do arithmetic on them, and summarise them, in a single line.

It is a full programming language, which scares people off. You can ignore almost all of it. Ninety per cent of real use is printing a field or adding up a column.

Fields

awk splits every line on whitespace and numbers the pieces from 1:

echo "alice 34 engineer" | awk '{print $2}'
34
VariableMeaning
$0The whole line
$1, $2, …Individual fields
NFNumber of fields on this line
$NFThe last field — extremely useful
NRRecord number, i.e. the current line number
FSInput field separator (default: whitespace)
OFSOutput field separator (default: a space)

$NF is worth committing to memory. It gets you the last column without knowing how many there are, which matters because log lines are rarely uniform.

Changing the separator

awk -F: '{print $1}' /etc/passwd          # colon-separated
awk -F, '{print $3}' data.csv            # comma
awk -F'\t' '{print $2}' data.tsv         # tab

Default whitespace splitting is smarter than a literal space: it collapses runs of spaces and tabs, and ignores leading whitespace. That is why ps aux | awk '{print $2}' works on neatly aligned columns without any fiddling.

Patterns and actions

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.

awk '/error/'                       # print lines matching "error" (like grep)
awk '/error/ {print $1}'            # first field of matching lines only
awk '$3 > 100'                      # lines where field 3 exceeds 100
awk '$1 == "root" {print $NF}'      # last field where field 1 is exactly root
awk 'NR > 1'                        # skip a header row
awk 'NF == 0'                       # blank lines
awk 'length($0) > 80'               # lines longer than 80 characters

That third example is the thing awk does that grep cannot: compare a field numerically. A regex has no idea that 100 is bigger than 99.

BEGIN and END

Two special patterns: BEGIN runs before the first line, END after the last. END is where summaries happen.

# Sum a column
awk '{sum += $3} END {print sum}' data.txt

# Average
awk '{sum += $1} END {print sum/NR}' numbers.txt

# Count lines matching a condition
awk '/timeout/ {n++} END {print n+0}' app.log

# Add a header, then the data
awk 'BEGIN {print "USER\tSHELL"} {print $1 "\t" $7}' FS=: /etc/passwd

Note print n+0 in the third example. If nothing matched, n was never set, and printing it alone gives an empty line rather than 0. Adding zero forces it to a number — a small trick that saves confusion in scripts.

Associative arrays

This is where awk stops being a field printer and starts being genuinely powerful. Arrays are indexed by strings, and you get grouping and counting almost for free.

# Count requests per IP in an access log
awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -rn

# Total bytes transferred per status code
awk '{bytes[$9] += $10} END {for (code in bytes) print code, bytes[code]}' access.log

# Count shells in use
awk -F: '{shells[$7]++} END {for (s in shells) print shells[s], s}' /etc/passwd | sort -rn

The equivalent with sort | uniq -c works for simple counting, but the moment you need to sum a second column per group, awk is the shortest correct answer.

Formatting output

awk '{print $1, $3}'                       # comma inserts OFS (a space)
awk '{print $1 "-" $3}'                    # no comma: joined with no separator
awk -v OFS=, '{print $1, $2}'              # output as CSV
awk '{printf "%-20s %8.2f\n", $1, $2}'     # aligned columns, 2 decimal places

printf follows the C convention and does not add a newline, so you must include \n yourself. -v sets a variable before the program runs, which is also how you pass shell values in safely:

threshold=500
awk -v t="$threshold" '$3 > t' data.txt

One-liners worth keeping

# Disk usage above 80% (see the disk space page)
df -h | awk 'NR>1 && $5+0 > 80 {print $6, $5}'

# The PIDs of every nginx process
ps aux | awk '/[n]ginx/ {print $2}'

# Top 10 IPs hitting a site
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head

# Print columns 2 to the end
awk '{$1=""; sub(/^ /, ""); print}' file.txt

# Remove duplicate lines without sorting first
awk '!seen[$0]++' file.txt

# Sum the sizes reported by ls
ls -l | awk '{total += $5} END {printf "%.1f MB\n", total/1024/1024}'

awk '!seen[$0]++' deserves a moment. It is the shortest way to deduplicate a file while preserving order, which sort -u cannot do. The array records each line it has seen; the ! makes the first occurrence true (printing it) and every later one false.

Gotchas

Use single quotes

awk’s $1 and the shell’s $1 look identical. Inside double quotes the shell substitutes first and awk receives an empty string. Always wrap the program in single quotes, and pass shell values with -v.

Numbers and strings are the same thing, until they are not

awk decides on context, which occasionally surprises. $5 > 80 on a column containing 90% compares strings, so "9" > "8" happens to give the right answer while "100%" > "80%" does not. Force numeric comparison with $5+0 > 80.

Modifying a field rewrites the line

Assigning to $1 causes awk to rebuild $0 using OFS, which collapses your original spacing. If you were relying on the input alignment, it is gone.

Do not parse CSV with quoted commas

-F, splits on every comma, including ones inside "quoted, fields". It works on your sample and breaks on real data. Use a real CSV tool — csvkit, miller (mlr), or Python — when quoting is involved.

Know when to stop

awk has functions, loops and recursion, and people have written remarkable things in it. But if your one-liner has grown past a couple of lines and you find yourself reaching for string functions, a Python script will be easier to read in six months. Using awk well includes knowing where it ends.

Quick reference

awk '{print $2}' f              # second field
awk '{print $NF}' f             # last field
awk -F: '{print $1}' f          # custom separator
awk 'NR > 1' f                  # skip header
awk '$3 > 100' f                # numeric filter
awk '/pattern/ {print $1}' f    # regex plus field
awk '{s += $2} END {print s}' f # sum a column
awk '!seen[$0]++' f             # dedupe, keep order
awk -v OFS=, '{print $1,$2}' f  # CSV output
awk '{printf "%-10s %5d\n", $1, $2}' f   # aligned output

Related commands

  • grep — finding lines; awk takes over when you need the columns.
  • sed — editing lines; awk when the edit depends on a field’s value.
  • cut — simpler and faster for plain column extraction with a fixed delimiter.
  • sort and uniq — the usual companions in a counting pipeline.
  • mlr (Miller) — awk-like processing that genuinely understands CSV and JSON.