awk: Pattern Scanning Basics
awk is a text-processing language and command-line tool built around a simple idea: scan a file line by line, test each line against a pattern, and run an action when the pattern matches. It ships on every Linux system and is one of the most powerful tools for working with structured, column-based text — log files, CSV-like data, /etc/passwd, output from other commands — without writing a full program. Where grep only finds matching lines and sed only edits text, awk understands a line’s structure as a set of fields, so you can select, transform, and compute over columns of data directly from the shell.
Overview: How awk Works
awk (named after its creators Aho, Weinberger, and Kernighan) is built around records and fields. By default, awk treats each line of input as one record, and splits that record into fields wherever it finds one or more spaces or tabs. The whole line is available as $0; the first field is $1, the second is $2, and so on, up to the last field, $NF, where NF (Number of Fields) is a built-in variable holding the field count for the current record. Another built-in variable, NR (Number of Records), counts how many lines awk has read so far — it behaves like a running line number.
A complete awk program is a sequence of pattern { action } pairs. For every record awk reads, it checks each pattern in order; if a pattern matches (or is left out, which matches every line), the associated action runs. A pattern can be a regular expression, a numeric or string comparison, or a special keyword: BEGIN runs its action once before any input is read (useful for setting up variables or printing headers), and END runs its action once after all input has been processed (useful for printing totals). If you write only a pattern with no action, awk performs the default action, which is {print $0} — print the whole matching line, much like grep.
Internally, awk works as a loop: read one record, split it into fields using the field separator FS (default: whitespace), evaluate every pattern-action pair against that record, then move to the next record. Because the same variables (like a running sum, or a counter you increment) stay alive from one line to the next, tasks like totalling a column or counting matches feel natural in awk — something grep and sed cannot do on their own.
Syntax
The general form of an awk invocation is shown below (this is a syntax diagram, not a command to run as-is — replace pattern, action, and file with real values):
awk 'pattern { action }' file
A fuller program can include BEGIN and END blocks, and multiple pattern-action pairs:
awk 'BEGIN { setup }
pattern1 { action1 }
pattern2 { action2 }
END { cleanup }' file
Always wrap the awk program in single quotes so the shell passes $1, $2, and similar references straight to awk instead of trying to expand them itself. Common command-line options and built-in variables:
| Item | Meaning |
|---|---|
-F fs |
Set the field separator (e.g. -F: for colon-delimited files like /etc/passwd) |
-v var=value |
Set an awk variable before the program runs |
-f file.awk |
Read the awk program from a file instead of the command line |
$0 |
The entire current record (line) |
$1 … $NF |
Individual fields of the current record |
NF |
Number of fields in the current record |
NR |
Number of the current record (running line count) |
FS |
Input field separator (default: whitespace) |
OFS |
Output field separator used by print when joining fields with commas |
FILENAME |
Name of the file currently being read |
Examples
Example 1: Print specific fields from a colon-delimited file
/etc/passwd stores one user account per line, with fields separated by colons. Field 1 is the username and field 7 is the user’s login shell:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
maria:x:1000:1000:Maria Lopez:/home/maria:/bin/bash
awk -F: '{print $1, $7}' /etc/passwd
Output:
root /bin/bash
daemon /usr/sbin/nologin
sync /bin/sync
maria /bin/bash
The -F: option tells awk to split each line on colons instead of whitespace. With that field separator, $1 is the username and $7 is the shell field; the comma in print $1, $7 joins them with a single space, the default OFS.
Example 2: Filter rows by a numeric field
Suppose a simplified web server log at /var/log/app/access.log has space-separated fields: date, time, client IP, HTTP method, path, status code, and bytes sent:
2026-08-04 10:15:32 203.0.113.5 GET /index.html 200 512
2026-08-04 10:15:35 203.0.113.9 POST /login 500 128
2026-08-04 10:16:01 203.0.113.5 GET /about.html 200 1024
2026-08-04 10:16:20 198.51.100.7 GET /missing.html 404 0
awk '$6 >= 400 {print $3, $6}' /var/log/app/access.log
Output:
203.0.113.9 500
198.51.100.7 404
Field 6 ($6) holds the status code. The pattern $6 >= 400 is checked against every record; only the two lines with server or client errors match, so the action {print $3, $6} only runs for those, printing the client IP and status code.
Example 3: Total a column with BEGIN/END
awk '{sum += $7} END {printf "Total bytes: %d\n", sum}' /var/log/app/access.log
Output:
Total bytes: 1664
Here the action {sum += $7} has no pattern, so it runs on every line, adding field 7 (bytes sent) to the variable sum. Because awk variables persist across records, sum keeps accumulating as each line is read. The END block runs once, after the last record, and printf formats the total with an explicit newline (\n), unlike print, which adds a newline automatically but gives you no control over formatting.
How awk Processes Input, Step by Step
- Before reading any input, awk runs every
BEGINblock, in the order they appear. - awk reads one line of input and stores it in
$0; it incrementsNRby one. - awk splits
$0into fields using the current field separatorFS, filling in$1through$NFand settingNFto the field count. - awk evaluates each pattern in the program, top to bottom, against the current record. For every pattern that matches, it runs the associated action immediately, then checks the next pattern — a single line can trigger several actions if it matches several patterns.
- Once all patterns have been checked, awk goes back and reads the next line. This repeats until the input is exhausted.
- After the last line has been processed, awk runs every
ENDblock, in the order they appear, then exits.
This read-split-match-act loop is why awk is described as a pattern scanning and processing language: you almost never write an explicit loop yourself — awk supplies the loop over lines, and you just describe what should happen on each one.
Common Mistakes
Mistake 1: Double-quoting the awk program
Wrapping the program in double quotes lets the shell — not awk — expand $1 before awk ever sees it. Since a plain shell usually has no $1 set, the field reference silently disappears:
awk "{print $1}" file.txt
Use single quotes so the dollar signs reach awk untouched:
awk '{print $1}' file.txt
Mistake 2: Using = instead of ==
A single = is assignment, not comparison. Used in a pattern, it assigns a value to the field on every line — and the assignment itself is truthy — so every line matches, and the field’s original value is overwritten:
awk '$6 = 500 {print}' /var/log/app/access.log
Use == to compare instead of assign:
awk '$6 == 500 {print}' /var/log/app/access.log
Mistake 3: Forgetting to set the field separator
awk’s default field separator is whitespace. Running an awk program against a colon-delimited file like /etc/passwd without -F: means there is no whitespace to split on, so the entire line becomes $1:
awk '{print $1}' /etc/passwd
Set the field separator explicitly to match the file’s actual delimiter:
awk -F: '{print $1}' /etc/passwd
Best Practices
- Always wrap the awk program in single quotes so field references like
$1and$NFreach awk instead of being expanded by the shell. - Set
-Fexplicitly whenever the input isn’t whitespace-separated — don’t rely on the default. - Use
printfinstead ofprintwhen you need specific formatting, such as fixed decimal places or no trailing newline. - Use a pattern like
NR==1 {next}to skip a header line before processing data rows. - Prefer
$NFand$(NF-1)over hardcoded field numbers when the field count can vary between lines. - For programs longer than a line or two, save them in a
.awkfile and run them withawk -f script.awk file— far easier to read and version-control than a long one-liner. - Reach for awk when you need to select, compare, or compute across columns; reach for
grepwhen you only need to find matching lines, andsedwhen you only need to substitute text.
Practice Exercises
- Using
/etc/passwd, print the usernames of every account whose user ID (field 3) is 1000 or greater — these are typically the regular human user accounts, as opposed to system accounts. Hint: you’ll need-F:and a numeric comparison pattern. - Using a log file like the one in Example 2, count how many requests returned status
200. Hint: use a pattern to match the status field, increment a counter variable in the action, and print the counter in anENDblock. - Write an awk command that prints the last field of every line in a file, without knowing in advance how many fields each line has. Hint:
NFalways holds the current line’s field count.
Summary
- awk scans input one record (line) at a time, splits each record into fields, and runs an action for every pattern that matches.
$0is the whole line,$1through$NFare individual fields,NFis the field count, andNRis the running record number.-Fsets the field separator; the default is whitespace, so remember to change it for delimited files like/etc/passwd.BEGINruns once before input is read;ENDruns once after all input is processed — useful for setup and totals.- Always single-quote the awk program so the shell doesn’t expand
$1-style field references itself. - Use
==for comparison; a bare=assigns and will make every pattern match.
