grep, sed and awk Basics: A Practical Introduction with Examples
On this page
grep, sed and awk are the three classic text tools on Linux. Each has a clear job:
- grep finds lines that match a pattern.
- sed edits text as it flows past, most often find and replace.
- awk splits lines into fields, so you can pick columns, filter on values and add things up.
You do not need to master any of them to get a lot of value. This guide covers the parts you will actually use, with examples you can run. To follow along, save these lines as access.log:
203.0.113.5 - - [25/Oct/2026:10:01:02 +0000] "GET / HTTP/1.1" 200 5120
198.51.100.7 - - [25/Oct/2026:10:01:05 +0000] "GET /about HTTP/1.1" 200 2048
203.0.113.5 - - [25/Oct/2026:10:02:11 +0000] "GET /missing HTTP/1.1" 404 512
192.0.2.44 - - [25/Oct/2026:10:03:30 +0000] "POST /login HTTP/1.1" 302 0
203.0.113.5 - - [25/Oct/2026:10:04:00 +0000] "GET /about HTTP/1.1" 200 2048
grep: find matching lines
Give grep a pattern and one or more files, and it prints every line that contains the pattern:
$ grep POST access.log
192.0.2.44 - - [25/Oct/2026:10:03:30 +0000] "POST /login HTTP/1.1" 302 0
The options you will reach for most:
| Option | Meaning |
|---|---|
-i | Ignore case: error also matches ERROR |
-n | Show line numbers |
-v | Invert: show lines that do not match |
-c | Count matching lines instead of printing them |
-w | Match whole words only: -w cat skips concatenate |
-r | Search all files under a directory |
-l | Print only the names of files that match |
-o | Print only the matching part, not the whole line |
-E | Extended regular expressions (|, +, ?, {n} without backslashes) |
-F | Treat the pattern as fixed text, not a regex |
-A n, -B n, -C n | Also show n lines after, before, or around each match |
Some real examples:
$ grep -c " 404 " access.log # how many 404s
1
$ grep -rn "TODO" src/ # every TODO with file and line number
$ grep -rl "mysql_connect" --include="*.php" . # which PHP files use it
$ grep -Ev '^(#|$)' /etc/ssh/sshd_config # config without comments and blank lines
$ grep -E "404|500" access.log # either status code
$ grep -C 2 "Traceback" app.log # each error with 2 lines of context
-o with -E is great for extracting things. This pulls every IPv4-looking address from a file:
$ grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' access.log | sort -u
192.0.2.44
198.51.100.7
203.0.113.5
In scripts, grep -q prints nothing and only sets the exit status, which is perfect for if tests:
if grep -q "^alice:" /etc/passwd; then
echo "alice exists"
fi
*, $ or |, so the shell passes them to grep unchanged. GNU grep also has -P for Perl-style regexes like \d+, but macOS grep does not.sed: find and replace
sed reads text line by line, applies your commands, and prints the result. The original file is not changed unless you ask. The command you will use 90% of the time is s for substitute:
$ echo "hello world" | sed 's/world/there/'
hello there
By default s replaces only the first match on each line. Add g to replace all of them:
$ echo "a a a" | sed 's/a/b/'
b a a
$ echo "a a a" | sed 's/a/b/g'
b b b
Useful variations:
$ sed 's/http:/https:/g' links.txt # replace everywhere, print result
$ sed 's/error/ERROR/gi' app.log # case-insensitive match (GNU sed)
$ sed 's|/usr/local|/opt|g' paths.txt # any delimiter works; handy for paths
$ echo "hello world" | sed 's/world/[&]/' # & is the whole match
hello [world]
Use -E for extended regexes and capture groups, referenced as \1, \2:
$ echo "2026-10-25" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3.\2.\1/'
25.10.2026
Printing and deleting lines
sed commands can be limited to certain lines, called addresses. Combine them with -n (do not print every line automatically) and p (print) to show part of a file:
$ sed -n '10,20p' file.txt # lines 10 to 20
$ sed -n '$p' file.txt # the last line
$ sed -n '/ERROR/p' app.log # lines matching a pattern, like grep
$ sed '1d' data.csv # everything except the first line (header)
$ sed '/^#/d; /^$/d' app.conf # remove comments and blank lines
$ sed '/START/,/END/d' file.txt # delete from START through END
$ sed '5s/foo/bar/' file.txt # substitute only on line 5
Editing files in place
The -i option writes changes back to the file. This is where GNU sed (Linux) and BSD sed (macOS) differ:
$ sed -i 's/foo/bar/g' file.txt # GNU sed (Linux)
$ sed -i '' 's/foo/bar/g' file.txt # BSD sed (macOS): empty backup suffix required
$ sed -i.bak 's/foo/bar/g' file.txt # works on both; keeps file.txt.bak
sed -i overwrites the file with no undo. Run the command without -i first to check the output, or use -i.bak so you keep a backup. Be especially careful combining it with find or wildcards across many files.awk: work with columns
awk splits each line into fields. By default, fields are separated by runs of spaces or tabs, and they are named $1, $2 and so on. $0 is the whole line. An awk program is a list of pattern { action } pairs, run for every line.
$ awk '{print $1}' access.log # first column: the IP address
$ awk '{print $1, $9}' access.log # IP and status code
$ awk '$9 == 404 {print $7}' access.log # paths that returned 404
/missing
Count requests per IP, a task you will meet again and again with log files:
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn
3 203.0.113.5
1 198.51.100.7
1 192.0.2.44
Choosing the field separator
Use -F for files separated by something other than whitespace:
$ awk -F: '{print $1}' /etc/passwd # usernames
$ awk -F, '{print $2}' data.csv # second column of a simple CSV
csv module or mlr (Miller).NR, NF and totals
awk has built-in variables. NR is the current line number, and NF is the number of fields on the line, so $NF is the last field. BEGIN and END blocks run before the first line and after the last.
$ awk 'NR == 3' access.log # just line 3
$ awk 'NR > 1' data.csv # skip a header line
$ awk '{print $NF}' access.log # last field: bytes sent
$ awk 'NF > 0' file.txt # drop empty lines
$ awk '{bytes += $10} END {print bytes}' access.log
9728
With a file budget.csv containing a header line and then rent,1200, food,450.50 and net,60:
$ awk -F, 'NR > 1 {total += $2} END {printf "Total: %.2f\n", total}' budget.csv
Total: 1710.50
$ awk -F, 'NR > 1 {printf "%-8s %8.2f\n", $1, $2}' budget.csv
rent 1200.00
food 450.50
net 60.00
printf works like in C: %s for a string, %d for an integer, %.2f for two decimals, and a number sets the width (- means left-aligned). Unlike print, it does not add a newline, so include \n.
awk also has associative arrays, which make "group by" counts easy:
$ awk '{count[$9]++} END {for (s in count) print s, count[s]}' access.log
200 3
302 1
404 1
The order of for (s in count) is not guaranteed, so pipe to sort if order matters.
Putting them together
The tools shine in pipelines. Here, grep picks the 200 responses, awk extracts the path, and sort | uniq -c counts them:
$ grep '" 200 ' access.log | awk '{print $7}' | sort | uniq -c | sort -rn
2 /about
1 /
The pipes and xargs guide explains how pipelines work, and the one-liner cheatsheet has many more ready-made examples. For full details see the sed(1) and the grep(1) man page.
Quick reference
grep -rni "text" dir/: search a directory, any case, with line numbers.grep -v pattern: lines that do not match.grep -oE 'regex': print only the matches.sed 's/old/new/g' file: replace all, print to screen.sed -i.bak 's/old/new/g' file: replace in place with a backup (GNU and macOS).sed -n '10,20p' file: print a range of lines.awk '{print $1}': first column.awk -F, '{print $2}': second comma-separated column.awk '{s += $3} END {print s}': sum a column.