awk
Split lines into fields and compute over them
awk reads its input one record at a time (a line, by default), splits each record into
fields, and runs a small program against them. Where grep answers "which
lines match?" and sed answers "how do I transform matching lines?", awk
answers "how do I pull fields out of a line and compute something with them?" You want it
once a task stops being about lines and starts being about columns: totals,
averages, tallies, reordering, reformatting. It is not the tool for JSON, which has no columns
and no fixed idea of where its lines go: jq is the equivalent there.
Every program is a list of pattern { action } pairs
An awk program is a series of pattern { action } pairs. For every input record, awk tests
each pattern in order; if a pattern matches (or there is no pattern, meaning "always"), it runs
the associated action. Leave off the action and the default is { print }, so awk '/error/' file is a complete, working program: print every line matching /error/. This is why so many
awk one-liners look shorter than the equivalent grep/sed combination once you know the
shorthand.
awk '/error/' app.log # pattern only: same job as grep
awk '{ print $1 }' app.log # action only: same job as cut
awk '/error/ { print $1 }' app.log # both: grep + cut in one pass
How awk splits a record into fields
Each input line is a record, split on whitespace by default into fields: $1 is the
first field, $2 the second, and so on, $0 is the whole record, and $NF is always the last
field regardless of how many fields a line has. NF (number of fields) and NR (number of
records seen so far, i.e. the line number) are built-in variables updated automatically for every
record. Because field splitting is automatic, tasks that need cut -f2 | ... in one tool often
need nothing more than { print $2 } in awk.
The default field separator (FS) is "any run of whitespace," which quietly handles both
single spaces and columns padded with extra spaces. Set a different one with -F (or FS= in a
BEGIN block) for structured data: -F: for /etc/passwd-style files, -F, for CSV, or a
regex like -F'[0-9]+' when the separator itself varies. OFS (output field separator, a single
space by default) controls how fields are rejoined when you print them individually or rebuild
$0 by reassigning a field.
BEGIN and END: code that runs once
Most of an awk program runs once per record, but BEGIN { ... } runs once before any input is
read (good for setting FS/OFS or printing a header) and END { ... } runs once after the
last record (good for printing totals). A running total accumulated in a plain variable across
every record, then printed in END, is awk's signature move:
awk -F, 'NR > 1 { sum += $3 } END { print sum }' employees.csv
END runs even if BEGIN set things up but no records ever arrived, and any variable you never
explicitly initialise starts as both 0 and "", so sum += $3 works from the very first line
without a sum = 0 line first.
Variables, arrays, and control flow
awk variables need no declaration and no type: assign a string or a number, and later use the
same variable as the other, and awk converts as needed. Arrays are associative (keyed by
string, not just integer index), which makes them the natural structure for counting and
grouping: count[$1]++ builds a frequency table of field 1 across every record in a single pass,
no external sort | uniq -c needed. awk also has the if/else, for, and while you'd
expect from a general-purpose language, plus a ternary ?:, so logic that would need a sed
hold-space trick or a shell loop around grep often fits in one awk line instead.
print vs printf
print is the quick option: it writes its arguments joined by OFS and terminated by ORS
(a newline, by default), with sensible defaults for both. printf gives up those defaults in
exchange for control: field widths, decimal places, zero-padding, no trailing newline unless you
write \n yourself. Use printf the moment output needs to line up in columns or a number
needs a fixed number of decimal places.
Debian's awk is mawk, not gawk
Debian's awk is a symlink managed by update-alternatives, and on a fresh install it usually
points at mawk, a smaller and faster implementation that covers the POSIX language well
(everything on this page runs under it).
apt install gawk pulls in the GNU implementation,
which adds extensions such as gensub(), asort(), and in-place editing (-i inplace). If a
script you find online uses one of those and errors out with "calling undefined function," that's
almost certainly a gawk-only extension running under mawk.
Combining awk with grep and sed
The three tools compose naturally in a pipeline: grep narrows the lines, sed reshapes text
within a line, and awk extracts and computes over fields, often with
sort/uniq -c closing the loop. See
Pipes and redirection for why chaining small,
single-purpose tools like this works as well as it does.
Sample files used on this page
Every example below was run against these files. Recreate them to follow along.
employees.csv a header row plus 6 records: name,department,salary
name,department,salary
alice,engineering,95000
bob,design,72000
carol,engineering,110000
dave,sales,68000
erin,design,81000
frank,engineering,88000
app.log 8 syslog-style lines, 3 of them errors
Jul 5 09:14:20 deb1 app[312]: info: service starting
Jul 5 09:14:21 deb1 app[312]: info: listening on port 8080
Jul 5 09:14:22 deb1 app[312]: error: connection refused
Jul 5 09:14:23 deb1 app[312]: warning: retrying in 5s
Jul 5 09:14:28 deb1 app[312]: error: connection refused
Jul 5 09:14:33 deb1 app[312]: info: connected
Jul 5 09:15:01 deb1 app[313]: error: connection refused
Jul 5 09:15:05 deb1 app[313]: info: connected
access.log 11 fields per line - combined log format plus a trailing response time
198.51.100.7 - - [05/Jul/2026:09:12:01 +0000] "GET /index.html HTTP/1.1" 200 1024 0.021
203.0.113.5 - - [05/Jul/2026:09:12:03 +0000] "GET /style.css HTTP/1.1" 200 512 0.008
203.0.113.5 - - [05/Jul/2026:09:12:07 +0000] "GET /app.js HTTP/1.1" 200 2048 0.014
198.51.100.7 - - [05/Jul/2026:09:12:15 +0000] "POST /login HTTP/1.1" 401 128 0.045
203.0.113.9 - - [05/Jul/2026:09:12:22 +0000] "GET /index.html HTTP/1.1" 200 1024 0.019
198.51.100.7 - - [05/Jul/2026:09:12:40 +0000] "POST /login HTTP/1.1" 401 128 0.052
203.0.113.9 - - [05/Jul/2026:09:13:02 +0000] "GET /missing HTTP/1.1" 404 0 0.003
198.51.100.7 - - [05/Jul/2026:09:13:10 +0000] "GET /dashboard HTTP/1.1" 200 4096 0.081
config.conf a comment line plus four key=value settings
# Server configuration
host=localhost
port=8080
debug=false
timeout=30
passwd.txt an excerpt in /etc/passwd format, for the -F: examples
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
user:x:1000:1000:user:/home/user:/bin/bash
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
allowed.txt the names treated as already approved, for the two-file example
alice
bob
carol
Fields and records
Every line is a record, split automatically into numbered fields.
Print the whole record
awk '{ print }' config.conf
With no pattern, the action runs on every record. print with no arguments is shorthand for print $0, the whole line.
Show output
# Server configuration
host=localhost
port=8080
debug=false
timeout=30
Print a single field
awk '{ print $1 }' access.log
$1 is the first whitespace-separated field. Here it's the client IP at the start of each access log line.
Show output
198.51.100.7
203.0.113.5
203.0.113.5
198.51.100.7
203.0.113.9
198.51.100.7
203.0.113.9
198.51.100.7
Print two fields together
awk '{ print $1, $9 }' access.log
A comma between arguments to print joins them with a single space (the default OFS). Field 9 here is the HTTP status code, since the quoted request string splits into three of its own fields.
Show output
198.51.100.7 200
203.0.113.5 200
203.0.113.5 200
198.51.100.7 401
203.0.113.9 200
198.51.100.7 401
203.0.113.9 404
198.51.100.7 200
Print the last field with $NF
awk '{ print $NF }' access.log
NF is always the number of fields on the current line, so $NF is the last field regardless of how many fields a line has. Here it's the request duration in seconds.
Show output
0.021
0.008
0.014
0.045
0.019
0.052
0.003
0.081
Count the fields on each line
awk '{ print NF }' access.log
Every access log line here has 11 whitespace-separated fields, because the quoted request ("GET /path HTTP/1.1") counts as three of them.
Show output
11
11
11
11
11
11
11
11
Watch the field count change with the data
awk '{ print NF, $0 }' config.conf
config.conf has no spaces in most lines, so NF is 1 there, but the comment line has spaces and splits into 3 fields. Field splitting depends entirely on what's in the line, not what you expect.
Show output
3 # Server configuration
1 host=localhost
1 port=8080
1 debug=false
1 timeout=30
Line numbers and counting records
NR tracks how many records have been read so far.
Number every line
awk '{ print NR, $0 }' config.conf
NR (number of records) increments before every action runs, giving a cat -n-style line number for free.
Show output
1 # Server configuration
2 host=localhost
3 port=8080
4 debug=false
5 timeout=30
Count records total
awk 'END { print NR }' app.log
NR still holds its final value inside END, so printing it there is a one-line wc -l.
Show output
8
Print a specific line
awk 'NR==2' app.log
A bare expression is a pattern: true for line 2 only, so with no action the default { print } fires just once.
Show output
Jul 5 09:14:21 deb1 app[312]: info: listening on port 8080
Print a range of lines
awk 'NR==2,NR==4' app.log
A comma between two patterns makes a range: every record from the first match through the second, inclusive.
Show output
Jul 5 09:14:21 deb1 app[312]: info: listening on port 8080
Jul 5 09:14:22 deb1 app[312]: error: connection refused
Jul 5 09:14:23 deb1 app[312]: warning: retrying in 5s
Print the last line
awk 'END { print }' app.log
$0 still holds the most recent record once the main loop finishes, so print inside END prints the last line without knowing the file's length in advance.
Show output
Jul 5 09:15:05 deb1 app[313]: info: connected
Selecting records with patterns
A pattern with no action is a complete program: it prints whatever matches.
Filter lines like grep, with a bare pattern
awk '/error/' app.log
No action means the default { print }, so this is a full working program: print every record matching /error/, the same job grep error app.log does.
Show output
Jul 5 09:14:22 deb1 app[312]: error: connection refused
Jul 5 09:14:28 deb1 app[312]: error: connection refused
Jul 5 09:15:01 deb1 app[313]: error: connection refused
Invert a match
awk '!/error/' app.log
! negates the pattern, printing every record that does NOT match, the awk equivalent of grep -v.
Show output
Jul 5 09:14:20 deb1 app[312]: info: service starting
Jul 5 09:14:21 deb1 app[312]: info: listening on port 8080
Jul 5 09:14:23 deb1 app[312]: warning: retrying in 5s
Jul 5 09:14:33 deb1 app[312]: info: connected
Jul 5 09:15:05 deb1 app[313]: info: connected
Combine a pattern with an action
awk '/error|warning/ { print NR, $0 }' app.log
Once a pattern matches, the action can do more than the default print, here adding the line number in front.
Show output
3 Jul 5 09:14:22 deb1 app[312]: error: connection refused
4 Jul 5 09:14:23 deb1 app[312]: warning: retrying in 5s
5 Jul 5 09:14:28 deb1 app[312]: error: connection refused
7 Jul 5 09:15:01 deb1 app[313]: error: connection refused
Match a regex against a specific field
awk -F, '$1 ~ /^[ab]/' employees.csv
~ tests a field against a regex instead of the whole line. !~ is the negated form. This matches names starting with a or b.
Show output
alice,engineering,95000
bob,design,72000
Filter on a numeric field
awk -F, 'NR>1 && $3 > 90000' employees.csv
NR>1 skips the header row before the numeric comparison runs. Without it, the header sneaks through, see the next example for why.
Show output
alice,engineering,95000
carol,engineering,110000
Watch a header row sneak past a numeric filter
awk -F, '$3 > 90000' employees.csv
Without the NR>1 guard, $3 on the header line is the string "salary", which isn't numeric-looking, so awk falls back to a string comparison. "salary" sorts after "90000" lexically, and the header line matches.
Show output
name,department,salary
alice,engineering,95000
carol,engineering,110000
Combine two conditions with &&
awk -F, '$2=="engineering" && $3 > 90000 { print $1 }' employees.csv
&& requires that both conditions hold. String comparison with == and numeric comparison with > combine freely.
Show output
alice
carol
Combine two conditions with ||
awk -F, '$2=="design" || $2=="sales" { print $1 }' employees.csv
Either condition is enough to match, the awk equivalent of grep -E "design|sales" scoped to one field.
Show output
bob
dave
erin
Match an exact field value
awk -F, '$1=="alice"' employees.csv
== on a field requires an exact match, unlike ~ which tests a regex. Faster and clearer when you are not matching a pattern.
Show output
alice,engineering,95000
Changing the field separator
FS controls how a line splits into fields; -F sets it from the command line.
Split fields on commas
awk -F, 'NR>1 { print $1, $3 }' employees.csv
-F, sets the field separator to a comma instead of whitespace, so $1/$2/$3 line up with the CSV's own columns.
Show output
alice 95000
bob 72000
carol 110000
dave 68000
erin 81000
frank 88000
Split fields on colons, /etc/passwd-style
awk -F: '{ print $1, $NF }' passwd.txt
Colon-separated files are common on Debian: /etc/passwd, /etc/group, and similar. $1 is the username, $NF the login shell.
Show output
root /bin/bash
daemon /usr/sbin/nologin
user /bin/bash
www-data /usr/sbin/nologin
Filter a colon-separated file on a numeric field
awk -F: '$3 >= 1000 { print $1 }' passwd.txt
A classic Debian sysadmin one-liner: field 3 is the UID, and real (non-system) accounts start at 1000.
Show output
user
Set FS in a BEGIN block instead of -F
awk 'BEGIN{FS=","} NR>1{print $1}' employees.csv
-F and setting FS in BEGIN do the same thing. Prefer BEGIN when the separator is part of a longer script rather than a quick one-liner.
Show output
alice
bob
carol
dave
erin
frank
Split on a character that isn't a comma
awk -F= 'NR>1{print $1}' config.conf
Any single character works as FS, here = for key=value lines.
Show output
host
port
debug
timeout
Split on a regex separator
printf "a1b2c33d\n" | awk -F'[0-9]+' '{ print $1, $2, $3, $4 }'
When FS is more than one character, awk treats it as a regex. Here any run of digits, of any length, is a separator.
Show output
a b c d
Control the output separator with OFS
awk -F, 'BEGIN{OFS="|"} NR>1 { $1=$1; print }' employees.csv
Reassigning any field (even to itself) makes awk rebuild $0 from the fields, joined with OFS. Without that reassignment, $0 stays untouched and still has the original commas.
Show output
alice|engineering|95000
bob|design|72000
carol|engineering|110000
dave|sales|68000
erin|design|81000
frank|engineering|88000
BEGIN and END blocks
Code that runs once before the first record, or once after the last.
Run a program with no input file at all
awk 'BEGIN{print "starting report"}'
BEGIN runs before awk tries to read any input, so a BEGIN-only program never touches stdin or a file.
Show output
starting report
Print a header before the data
awk -F, 'BEGIN{print "name\tsalary"} NR>1{print $1"\t"$3}' employees.csv
The header line comes from BEGIN, which runs once, before record processing starts.
Show output
name salary
alice 95000
bob 72000
carol 110000
dave 68000
erin 81000
frank 88000
Accumulate a running total, print it in END
awk -F, 'NR>1{sum+=$3} END{print sum}' employees.csv
sum starts at 0 automatically (an unset variable is both 0 and "" until assigned), so no separate initialisation line is needed.
Show output
514000
Compute an average in END
awk -F, 'NR>1{sum+=$3; n++} END{printf "%.2f\n", sum/n}' employees.csv
A sum and a count accumulate separately and are combined once at the end. printf controls the decimal places; print sum/n would show far more digits.
Show output
85666.67
END still runs when nothing matched
awk '/nonexistent/{c++} END{print c+0}' app.log
c is never touched if the pattern never matches, so print c alone would print an empty string, not 0. Adding 0 forces numeric context, printing the 0 you wanted.
Show output
0
Associative arrays: counting and grouping
awk arrays are keyed by string, which makes them a natural fit for tallying and grouping in one pass.
Count occurrences of a field value
awk -F, 'NR>1{count[$2]++} END{for (d in count) print d, count[d]}' employees.csv | sort
count[$2]++ builds a frequency table keyed by department, all in a single pass, no external sort | uniq -c needed.
Show output
design 2
engineering 3
sales 1
Sum a field, grouped by another field
awk -F, 'NR>1{sum[$2]+=$3} END{for (d in sum) print d, sum[d]}' employees.csv | sort
Same idea as counting, but accumulating a value instead of a count: total salary per department.
Show output
design 153000
engineering 293000
sales 68000
Group and sum values from a log file
awk '{bytes[$1]+=$(NF-1)} END{for (ip in bytes) print ip, bytes[ip]}' access.log | sort
Bytes transferred (second-to-last field) totalled per client IP (first field). sort afterwards just orders the output, awk doesn't guarantee array iteration order.
Show output
198.51.100.7 5376
203.0.113.5 2560
203.0.113.9 1024
Tally values in a different field
awk '{count[$9]++} END{for (c in count) print c, count[c]}' access.log | sort
The same counting pattern applied to HTTP status codes instead of IPs. Field 9 lines up with the status code because the quoted request eats three fields first.
Show output
200 5
401 2
404 1
Test array membership with in
awk -F, 'NR>1{seen[$2]=1} END{print ("engineering" in seen) ? "found" : "missing"}' employees.csv
key in array tests for a key without creating it as a side effect, unlike referencing array[key] directly, which would.
Show output
found
Count distinct keys with length()
awk -F, 'NR>1{d[$2]=1} END{print length(d)}' employees.csv
length() on an array returns its element count, here the number of distinct departments.
Show output
3
Build a word-frequency table
printf "the quick fox the lazy fox the dog\n" | awk '{for(i=1;i<=NF;i++) count[$i]++} END{for (w in count) print count[w], w}' | sort -rn
A for loop over every field on the line, counting each word seen. Piping through sort -rn ranks the results afterwards.
Show output
3 the
2 fox
1 quick
1 lazy
1 dog
printf and output formatting
printf trades print's convenient defaults for exact control over layout.
Left- and right-align columns
awk -F, 'NR>1{printf "%-8s %-12s %8s\n", $1, $2, $3}' employees.csv
%-8s left-aligns a string in an 8-character field, %8s right-aligns. This is how awk builds aligned tabular output without an external formatter.
Show output
alice engineering 95000
bob design 72000
carol engineering 110000
dave sales 68000
erin design 81000
frank engineering 88000
Zero-pad a number
awk -F, 'NR>1{printf "%03d %s\n", NR-1, $1}' employees.csv
%03d pads a number to at least 3 digits with leading zeroes, useful for generating sortable sequence numbers.
Show output
001 alice
002 bob
003 carol
004 dave
005 erin
006 frank
printf doesn't add a newline for you
awk '{printf "%s", $0}' config.conf
Unlike print, printf never appends anything automatically. Every line here runs into the next, with no \n between them.
Show output
# Server configurationhost=localhostport=8080debug=falsetimeout=30
Add the newline back explicitly
awk '{printf "%s\n", $0}' config.conf
The fix for the previous example: write \n into the format string yourself.
Show output
# Server configuration
host=localhost
port=8080
debug=false
timeout=30
String functions
Text-manipulation functions available in every action, not just BEGIN/END.
Get a field's length
awk -F, 'NR>1{print $1, length($1)}' employees.csv
length() with no argument returns the length of $0; with an argument, the length of that string.
Show output
alice 5
bob 3
carol 5
dave 4
erin 4
frank 5
Extract a substring by position
echo "order-4821-confirmed" | awk '{print substr($0, 7, 4)}'
substr(string, start, length) pulls out a fixed-position chunk, here the 4-digit order number starting at character 7.
Show output
4821
Find a substring's position with index
awk -F, 'NR>1{print $1, index($1,"a")}' employees.csv
index(string, target) returns the 1-based position of the first match, or 0 if it isn't found at all.
Show output
alice 1
bob 0
carol 2
dave 2
erin 0
frank 3
Change case with toupper and tolower
echo "Hello World" | awk '{print toupper($0)}'
echo "Hello World" | awk '{print tolower($0)}'
Each takes a string and returns a new one, leaving its argument untouched.
Show output
HELLO WORLD
hello world
Replace the first match with sub()
echo "foo bar foo" | awk '{sub(/foo/,"FOO"); print}'
sub(regex, replacement) edits $0 in place (or a named field, if given a third argument), replacing only the first match. It's awk's equivalent of a single-shot sed substitution.
Show output
FOO bar foo
Replace every match and count them with gsub()
echo "foo bar foo baz foo" | awk '{n=gsub(/foo/,"FOO"); print n, $0}'
gsub() is sub()'s global counterpart, matching sed's g flag, and it returns the number of replacements made.
Show output
3 FOO bar FOO baz FOO
Split a string into an array
awk 'BEGIN{n=split("a:b:c",parts,":"); print n, parts[1], parts[3]}'
split(string, array, separator) is the manual version of field splitting, useful when the string to split isn't $0 itself. It returns the number of pieces produced.
Show output
3 a c
Build a formatted string with sprintf
awk -F, 'NR>1{line=sprintf("%-8s $%d",$1,$3); print line}' employees.csv
sprintf formats like printf but returns the string instead of printing it, so it can be stored in a variable and used later.
Show output
alice $95000
bob $72000
carol $110000
dave $68000
erin $81000
frank $88000
Extract matched text with match(), RSTART, and RLENGTH
echo "order-4821-confirmed" | awk '{if (match($0,/[0-9]+/)) print substr($0,RSTART,RLENGTH)}'
match() sets RSTART and RLENGTH to where the regex matched, which substr() can then use, useful when the interesting text isn't at a fixed position.
Show output
4821
Concatenate strings and fields
awk -F, 'NR>1{print $1 " works in " $2}' employees.csv
Placing values next to each other with nothing between them concatenates. No + or explicit operator needed.
Show output
alice works in engineering
bob works in design
carol works in engineering
dave works in sales
erin works in design
frank works in engineering
Control flow: conditionals and loops
if/else, a ternary, for, and while, for logic that doesn't fit a single expression.
Branch per record with if/else
awk -F, 'NR>1{if ($3>90000) print $1,"senior"; else print $1,"standard"}' employees.csv
Standard if/else, useful once the logic is more than a single pattern can express cleanly.
Show output
alice senior
bob standard
carol senior
dave standard
erin standard
frank standard
Use a ternary for a short branch
awk -F, 'NR>1{print $1, ($3>90000?"high":"normal")}' employees.csv
condition ? a : b is a compact alternative to if/else when the branch is a single value.
Show output
alice high
bob normal
carol high
dave normal
erin normal
frank normal
Loop over every field with for
echo "a b c d" | awk '{for(i=1;i<=NF;i++) print i, $i}'
A C-style for loop indexing $i from 1 to NF visits every field on the line, regardless of how many there are.
Show output
1 a
2 b
3 c
4 d
Loop with while
awk 'BEGIN{i=1; while(i<=5){printf "%d ", i; i++}; print ""}'
while is the other loop form, useful when the exit condition doesn't fit for's init/test/increment shape naturally.
Show output
1 2 3 4 5
Build a repeated-character string in a loop
awk 'BEGIN{for(i=0;i<20;i++) s=s "="; print s}'
Concatenating onto s inside a loop is awk's equivalent of Python's "=" * 20, no repeat operator exists, so a loop does the job.
Show output
====================
Trim a record down to N fields
echo "a b c d e" | awk '{NF=3; print}'
Assigning to NF truncates (or, if larger, extends) the record. Because NF changed, awk rebuilds $0 from the remaining fields, the same rebuild that reassigning any single field triggers.
Show output
a b c
Blank out a field but keep its place
awk -F, -v OFS=, 'NR>1{$2=""; print}' employees.csv
Setting a field to an empty string keeps its position (and delimiter) instead of removing it entirely, unlike deleting an array element.
Show output
alice,,95000
bob,,72000
carol,,110000
dave,,68000
erin,,81000
frank,,88000
Working with multiple files
NR counts across every file; FNR resets for each one.
Tell files apart with FNR and FILENAME
awk 'FNR==1{print FILENAME, "FNR="FNR, "NR="NR}' app.log access.log
FNR (file NR) restarts at 1 for every new input file, while NR keeps counting across all of them. FILENAME names whichever file is currently open.
Show output
app.log FNR=1 NR=1
access.log FNR=1 NR=9
Count records across every file
awk 'END{print NR}' app.log access.log
With two files given, NR in END is the combined total, 8 lines from app.log plus 8 from access.log.
Show output
16
Stop reading a file early with nextfile
awk 'FNR==1{print FILENAME; nextfile}' app.log access.log employees.csv
nextfile skips straight to the next input file, useful for peeking at just the first line (or first match) of each file in a batch without reading the rest.
Show output
app.log
access.log
employees.csv
Cross-reference two files
awk -F, 'NR==FNR{allowed[$1]=1; next} FNR>1 && !($1 in allowed){print $1}' allowed.txt employees.csv
NR==FNR is only true while reading the first file, so that block builds a lookup table from it; next skips straight to the following record. The second block then reports names from the second file that aren't in that table.
Show output
dave
erin
frank
CSV and tabular data
Field splitting makes awk a natural fit for CSV-shaped files, provided fields don't contain embedded commas.
Skip a header row
awk -F, 'NR>1' employees.csv
NR>1 alone, as a pattern with no action, prints every record after the first. The single most common guard in CSV-processing awk scripts.
Show output
alice,engineering,95000
bob,design,72000
carol,engineering,110000
dave,sales,68000
erin,design,81000
frank,engineering,88000
Reorder columns
awk -F, -v OFS=, 'NR>1{print $1,$3,$2}' employees.csv
Printing fields in a different order, with OFS set to match the input delimiter, rewrites the CSV's column order.
Show output
alice,95000,engineering
bob,72000,design
carol,110000,engineering
dave,68000,sales
erin,81000,design
frank,88000,engineering
Turn a CSV into an aligned table
awk -F, 'NR==1{next} {printf "%-6s %-12s %s\n",$1,$2,$3}' employees.csv
next skips straight to the next record without running the rest of the script, a tidy way to drop the header before a printf-formatted body.
Show output
alice engineering 95000
bob design 72000
carol engineering 110000
dave sales 68000
erin design 81000
frank engineering 88000
Apply a calculation to a column
awk -F, -v OFS=, 'NR>1{$3=sprintf("%.0f",$3*1.10); print}' employees.csv
Reassigning $3 to a computed value, then letting the field reassignment trigger a rebuild of $0, applies a 10% raise across every row.
Show output
alice,engineering,104500
bob,design,79200
carol,engineering,121000
dave,sales,74800
erin,design,89100
frank,engineering,96800
Analysing an access log
Log lines are field-rich by nature; awk is usually the fastest way to turn them into numbers.
Extract the timestamp from bracketed text
awk -F'[][]' '{print $2}' access.log
Using [ and ] themselves as the field separator (inside a character class, so they're literal, not a range) splits the line around the bracketed date, landing it in $2.
Show output
05/Jul/2026:09:12:01 +0000
05/Jul/2026:09:12:03 +0000
05/Jul/2026:09:12:07 +0000
05/Jul/2026:09:12:15 +0000
05/Jul/2026:09:12:22 +0000
05/Jul/2026:09:12:40 +0000
05/Jul/2026:09:13:02 +0000
05/Jul/2026:09:13:10 +0000
Strip the leading quote from the HTTP method
awk '{gsub(/"/,"",$6); print $6}' access.log
The quoted request splits across three default fields; $6 is the method with a leading " still attached. gsub on a specific field, not $0, cleans up just that one.
Show output
GET
GET
GET
POST
GET
POST
GET
GET
Filter to 4xx responses
awk '$9 ~ /^4/{print $1, $9}' access.log
Field 9 is the status code once the quoted request has consumed fields 6-8. Matching ^4 catches both 401s and 404s in one pattern.
Show output
198.51.100.7 401
198.51.100.7 401
203.0.113.9 404
Average response size and duration
awk '{bytes+=$(NF-1); time+=$NF} END{printf "avg_bytes=%.0f avg_time=%.3f\n", bytes/NR, time/NR}' access.log
Using $(NF-1) and $NF for bytes and duration is more robust than a fixed field number, since it doesn't depend on how many words the quoted request happens to contain.
Show output
avg_bytes=1120 avg_time=0.030
Rank status codes by frequency
awk '{count[$9]++} END{for (c in count) print count[c], c}' access.log | sort -rn
The same counting pattern as before, piped through sort -rn to put the most frequent status code first.
Show output
5 200
2 401
1 404
Practical one-liners and pipelines
awk rarely works alone; it's usually one stage in a chain with grep, sed, and sort.
Deduplicate a field while keeping first-seen order
awk '!seen[$1]++' access.log
seen[$1]++ is 0 (falsy) the first time a value appears and truthy after, so !seen[$1]++ matches only the first occurrence of each value, preserving original order, unlike sort -u which reorders.
Show output
198.51.100.7 - - [05/Jul/2026:09:12:01 +0000] "GET /index.html HTTP/1.1" 200 1024 0.021
203.0.113.5 - - [05/Jul/2026:09:12:03 +0000] "GET /style.css HTTP/1.1" 200 512 0.008
203.0.113.9 - - [05/Jul/2026:09:12:22 +0000] "GET /index.html HTTP/1.1" 200 1024 0.019
Narrow with grep, then extract fields with awk
grep error app.log | awk '{print $1, $2, $3}'
A common division of labour: grep filters the lines worth looking at, awk pulls out just the timestamp fields from what's left.
Show output
Jul 5 09:14:22
Jul 5 09:14:28
Jul 5 09:15:01
Reshape with sed, then filter and extract with awk
sed 's/refused/denied/' app.log | awk '/error/{print $3, $6, $7}'
sed rewrites text in place in the stream; awk downstream still matches and extracts fields from the edited text, not the original.
Show output
09:14:22 error: connection
09:14:28 error: connection
09:15:01 error: connection
Extract a field, then rank it with sort and uniq
awk '{print $9}' access.log | sort | uniq -c | sort -rn
The alternative to counting inside awk with an array: hand the extracted field to the standard sort | uniq -c | sort -rn tallying pipeline instead.
Show output
5 200
2 401
1 404
Read an environment variable
REPORT_TITLE="Weekly Summary" awk 'BEGIN{print ENVIRON["REPORT_TITLE"]}'
ENVIRON is an array of the process's environment variables, keyed by name, handy for parameterising a script without -v.
Show output
Weekly Summary
Load a script from a file
printf 'BEGIN { FS = ","; OFS = "\t"; print "name", "dept" }\nNR > 1 { print $1, $2 }\n' > report.awk
awk -f report.awk employees.csv
-f reads the program from a file instead of the command line. Worth it once a script has more than a line or two, or gets reused across files, the same trade-off as sed -f.
Show output
name dept
alice engineering
bob design
carol engineering
dave sales
erin design
frank engineering