Three tools show up in nearly every Linux troubleshooting session: grep, awk, and sed. Whether you’re tailing logs at 2 AM, parsing CSV exports, or bulk-editing config files across a fleet, knowing which tool to reach for saves real time. Most sysadmins learn one well and fake their way through the other two. This guide fixes that.
Each tool has a sweet spot. grep finds lines matching a pattern, awk processes structured columnar data, and sed edits text streams on the fly. They overlap in places, but using the right one for the job means shorter commands and faster results. You’ll also see how combining all three in pipelines handles tasks that none of them can do alone. If you spend time on the Linux terminal, these three tools are non-negotiable.
Tested March 2026 | Ubuntu 24.04, Rocky Linux 10.1, GNU grep 3.11, GNU awk 5.3, GNU sed 4.9
When to Use Each Tool
Before getting into syntax, here’s the decision framework. Pick the right tool first, then worry about flags.
| Tool | Best for | Reads input as | Outputs |
|---|---|---|---|
grep | Finding lines matching a pattern | Lines | Matching lines |
awk | Processing columnar/structured data | Fields (columns) | Transformed data |
sed | Editing text streams (find/replace) | Lines | Modified text |
Think of it this way: grep answers “which lines match?”, awk answers “what’s in column 3?”, and sed answers “change every X to Y.” When you need two or three of those answers at once, pipe them together.
Sample Data Files
Create these three files to follow along with every example in this guide. All output shown below comes from running commands against these exact files.
Create the Apache-style access log:
cat << 'ENDOFFILE' > access.log
192.168.1.10 - admin [25/Mar/2026:10:15:32 +0000] "GET /dashboard HTTP/1.1" 200 5432
10.0.1.55 - - [25/Mar/2026:10:15:33 +0000] "POST /api/login HTTP/1.1" 401 128
192.168.1.10 - admin [25/Mar/2026:10:15:34 +0000] "GET /api/users HTTP/1.1" 200 2048
172.16.0.22 - - [25/Mar/2026:10:15:35 +0000] "GET /images/logo.png HTTP/1.1" 304 0
10.0.1.55 - - [25/Mar/2026:10:15:36 +0000] "GET /admin/config HTTP/1.1" 403 256
192.168.1.10 - admin [25/Mar/2026:10:15:37 +0000] "GET /api/reports HTTP/1.1" 200 8192
172.16.0.22 - - [25/Mar/2026:10:15:38 +0000] "DELETE /api/users/5 HTTP/1.1" 500 512
10.0.1.55 - - [25/Mar/2026:10:15:39 +0000] "GET /dashboard HTTP/1.1" 200 5432
ENDOFFILE
Next, the employee CSV:
cat << 'ENDOFFILE' > employees.csv
id,name,department,salary,city
1,Alice Johnson,Engineering,95000,Portland
2,Bob Smith,Marketing,72000,Seattle
3,Carol Davis,Engineering,102000,Portland
4,Dan Wilson,Sales,68000,Denver
5,Eve Martinez,Engineering,98000,Seattle
6,Frank Brown,Marketing,75000,Portland
7,Grace Lee,Sales,71000,Denver
8,Hank Taylor,Engineering,110000,Seattle
ENDOFFILE
And the INI config file:
cat << 'ENDOFFILE' > config.ini
[database]
host = 10.0.1.100
port = 5432
name = appdb
user = appuser
password = s3cureP@ss
[redis]
host = 10.0.1.101
port = 6379
maxmemory = 256mb
[app]
debug = false
workers = 4
log_level = info
secret_key = a1b2c3d4e5f6
ENDOFFILE
grep: Pattern Matching
grep searches files line by line and prints every line that matches a pattern. It’s the fastest of the three for simple searches, and it’s what you should reach for when the question is “does this text appear anywhere?” The GNU grep manual covers every edge case, but the flags below handle 95% of real-world use.
Basic Search
Find all lines containing “admin” in the access log:
grep "admin" access.log
This returns every line where “admin” appears anywhere, whether as a username or in a URL path:
192.168.1.10 - admin [25/Mar/2026:10:15:32 +0000] "GET /dashboard HTTP/1.1" 200 5432
192.168.1.10 - admin [25/Mar/2026:10:15:34 +0000] "GET /api/users HTTP/1.1" 200 2048
10.0.1.55 - - [25/Mar/2026:10:15:36 +0000] "GET /admin/config HTTP/1.1" 403 256
192.168.1.10 - admin [25/Mar/2026:10:15:37 +0000] "GET /api/reports HTTP/1.1" 200 8192
Notice that line 5 matched because the URL path contains “/admin/config”, not because of the username field. This is important to keep in mind when your search term can appear in multiple columns.
Case-Insensitive Search (-i)
Search for “get” regardless of case:
grep -i "get" access.log
All GET requests show up even though the pattern is lowercase:
192.168.1.10 - admin [25/Mar/2026:10:15:32 +0000] "GET /dashboard HTTP/1.1" 200 5432
192.168.1.10 - admin [25/Mar/2026:10:15:34 +0000] "GET /api/users HTTP/1.1" 200 2048
172.16.0.22 - - [25/Mar/2026:10:15:35 +0000] "GET /images/logo.png HTTP/1.1" 304 0
10.0.1.55 - - [25/Mar/2026:10:15:36 +0000] "GET /admin/config HTTP/1.1" 403 256
192.168.1.10 - admin [25/Mar/2026:10:15:37 +0000] "GET /api/reports HTTP/1.1" 200 8192
10.0.1.55 - - [25/Mar/2026:10:15:39 +0000] "GET /dashboard HTTP/1.1" 200 5432
Invert Match (-v)
Show all lines that do NOT contain “200” (non-successful requests):
grep -v "200" access.log
Only the 401, 403, 304, and 500 responses remain:
10.0.1.55 - - [25/Mar/2026:10:15:33 +0000] "POST /api/login HTTP/1.1" 401 128
172.16.0.22 - - [25/Mar/2026:10:15:35 +0000] "GET /images/logo.png HTTP/1.1" 304 0
10.0.1.55 - - [25/Mar/2026:10:15:36 +0000] "GET /admin/config HTTP/1.1" 403 256
172.16.0.22 - - [25/Mar/2026:10:15:38 +0000] "DELETE /api/users/5 HTTP/1.1" 500 512
Count Matches (-c) and Line Numbers (-n)
Count how many requests came from 192.168.1.10:
grep -c "192.168.1.10" access.log
The count confirms three requests from that IP:
3
To see exactly which lines matched, add -n for line numbers:
grep -n "192.168.1.10" access.log
Each match shows its position in the file:
1:192.168.1.10 - admin [25/Mar/2026:10:15:32 +0000] "GET /dashboard HTTP/1.1" 200 5432
3:192.168.1.10 - admin [25/Mar/2026:10:15:34 +0000] "GET /api/users HTTP/1.1" 200 2048
6:192.168.1.10 - admin [25/Mar/2026:10:15:37 +0000] "GET /api/reports HTTP/1.1" 200 8192
Only the Matching Part (-o)
Extract just the HTTP status codes using -o combined with extended regex:
grep -oE " [0-9]{3} " access.log
Only the matched portions are printed, one per line:
200
401
200
304
403
200
500
200
Extended Regex (-E) and Perl Regex (-P)
Extended regex (-E) lets you use +, ?, {}, and alternation | without escaping. Find all POST or DELETE requests:
grep -E "POST|DELETE" access.log
Both HTTP methods show up:
10.0.1.55 - - [25/Mar/2026:10:15:33 +0000] "POST /api/login HTTP/1.1" 401 128
172.16.0.22 - - [25/Mar/2026:10:15:38 +0000] "DELETE /api/users/5 HTTP/1.1" 500 512
Perl-compatible regex (-P) gives you lookaheads, lookbehinds, and \d shorthand. Extract IP addresses using a lookbehind for the start of line:
grep -oP "^\d+\.\d+\.\d+\.\d+" access.log
Clean IP extraction without surrounding text:
192.168.1.10
10.0.1.55
192.168.1.10
172.16.0.22
10.0.1.55
192.168.1.10
172.16.0.22
10.0.1.55
Context Lines (-A, -B, -C)
When you find an error in a log, you usually need the lines around it. Show 1 line after (-A) and 1 line before (-B) the 500 error:
grep -B1 -A1 "500" access.log
The context reveals what happened just before and after the error:
192.168.1.10 - admin [25/Mar/2026:10:15:37 +0000] "GET /api/reports HTTP/1.1" 200 8192
172.16.0.22 - - [25/Mar/2026:10:15:38 +0000] "DELETE /api/users/5 HTTP/1.1" 500 512
10.0.1.55 - - [25/Mar/2026:10:15:39 +0000] "GET /dashboard HTTP/1.1" 200 5432
Use -C2 as shorthand for “2 lines of context on both sides.”
Recursive Search (-r) and Files Matching (-l)
Search all files under /etc for a specific setting. This is invaluable when you know a value exists somewhere but not which file. If you manage servers via SSH, you’ll use this constantly:
grep -r "PermitRootLogin" /etc/ssh/
To get just the filenames without the matching content, use -l:
grep -rl "port" *.ini *.csv *.log
Only the filenames containing “port” are listed:
config.ini
employees.csv
Word Boundaries (-w)
Search for “port” as a whole word, not as part of “Portland” or “reports”:
grep -w "port" config.ini
Only lines where “port” stands alone as a word match:
port = 5432
port = 6379
Without -w, grep would also match “reports” in the access log or “Portland” in the CSV. The word boundary flag is essential when your search term is a substring of common words.
awk: Column Processing
awk treats each line as a record and splits it into fields. By default, whitespace is the delimiter. This makes it perfect for structured data like log files, CSVs, and command output. Where grep asks “does this line match?”, awk asks “what’s in column N of lines that match?”
Print Specific Columns
Pull the IP address (column 1) and URL path (column 7) from the access log:
awk '{print $1, $7}' access.log
Clean two-column output, ignoring everything else:
192.168.1.10 /dashboard
10.0.1.55 /api/login
192.168.1.10 /api/users
172.16.0.22 /images/logo.png
10.0.1.55 /admin/config
192.168.1.10 /api/reports
172.16.0.22 /api/users/5
10.0.1.55 /dashboard
Custom Delimiter (-F)
CSV data uses commas, not spaces. Set the field separator with -F:
awk -F, '{print $2, $3}' employees.csv
Names and departments, neatly extracted:
name department
Alice Johnson Engineering
Bob Smith Marketing
Carol Davis Engineering
Dan Wilson Sales
Eve Martinez Engineering
Frank Brown Marketing
Grace Lee Sales
Hank Taylor Engineering
Skip the header row by adding NR>1:
awk -F, 'NR>1 {print $2, $3}' employees.csv
The header line is gone:
Alice Johnson Engineering
Bob Smith Marketing
Carol Davis Engineering
Dan Wilson Sales
Eve Martinez Engineering
Frank Brown Marketing
Grace Lee Sales
Hank Taylor Engineering
Filter by Condition
Find employees earning more than 90,000:
awk -F, 'NR>1 && $4 > 90000 {print $2, $4}' employees.csv
Only the high earners pass the filter:
Alice Johnson 95000
Carol Davis 102000
Eve Martinez 98000
Hank Taylor 110000
Filter the access log for non-200 status codes (column 9):
awk '$9 != 200 {print $1, $7, $9}' access.log
Every failed or redirected request with its source IP and path:
10.0.1.55 /api/login 401
172.16.0.22 /images/logo.png 304
10.0.1.55 /admin/config 403
172.16.0.22 /api/users/5 500
Math: Sum, Average, Count
Calculate total salary across all employees:
awk -F, 'NR>1 {sum += $4} END {print "Total salary:", sum}' employees.csv
The END block runs after all lines are processed:
Total salary: 691000
Average salary and employee count:
awk -F, 'NR>1 {sum += $4; count++} END {printf "Employees: %d\nAverage salary: $%.2f\n", count, sum/count}' employees.csv
Both values in one pass:
Employees: 8
Average salary: $86375.00
Group By (Department Salary Averages)
This is where awk really shines compared to grep or sed. Calculate the average salary per department using associative arrays:
awk -F, 'NR>1 {sum[$3]+=$4; count[$3]++} END {for (dept in sum) printf "%s: $%.2f (%d employees)\n", dept, sum[dept]/count[dept], count[dept]}' employees.csv
Department averages with headcount:
Engineering: $101250.00 (4 employees)
Marketing: $73500.00 (2 employees)
Sales: $69500.00 (2 employees)
Try doing that with grep. You can’t. This kind of aggregation is exactly why awk exists.
Built-in Variables (NR, NF, $0)
awk exposes several useful built-in variables. NR is the current line number, NF is the number of fields on the current line, and $0 is the entire line.
awk -F, '{print NR, NF, $1}' employees.csv
Line number, field count, and first column for each row:
1 5 id
2 5 1
3 5 2
4 5 3
5 5 4
6 5 5
7 5 6
8 5 7
9 5 8
Use $NF to always print the last column, regardless of how many fields exist:
awk -F, 'NR>1 {print $2, $NF}' employees.csv
Names paired with cities (the last field):
Alice Johnson Portland
Bob Smith Seattle
Carol Davis Portland
Dan Wilson Denver
Eve Martinez Seattle
Frank Brown Portland
Grace Lee Denver
Hank Taylor Seattle
String Functions
awk has built-in string functions that eliminate the need to pipe through other tools:
awk -F, 'NR>1 {print toupper($3), length($2), substr($2,1,5)}' employees.csv
Department in uppercase, name length, and first 5 characters of each name:
ENGINEERING 13 Alice
MARKETING 9 Bob S
ENGINEERING 11 Carol
SALES 10 Dan W
ENGINEERING 12 Eve M
MARKETING 11 Frank
SALES 9 Grace
ENGINEERING 12 Hank
Printf Formatting
For clean tabular output, printf beats print:
awk -F, 'NR>1 {printf "%-15s %-12s $%9.2f\n", $2, $3, $4}' employees.csv
Neatly aligned columns with proper currency formatting:
Alice Johnson Engineering $ 95000.00
Bob Smith Marketing $ 72000.00
Carol Davis Engineering $102000.00
Dan Wilson Sales $ 68000.00
Eve Martinez Engineering $ 98000.00
Frank Brown Marketing $ 75000.00
Grace Lee Sales $ 71000.00
Hank Taylor Engineering $110000.00
Multiple Delimiters
Split on multiple characters using a regex delimiter. Extract the hour from the access log timestamps (split on : and [):
awk -F'[\\[:]' '{print $2, $3}' access.log
Date and hour from each log entry:
25/Mar/2026 10
25/Mar/2026 10
25/Mar/2026 10
25/Mar/2026 10
25/Mar/2026 10
25/Mar/2026 10
25/Mar/2026 10
25/Mar/2026 10
sed: Stream Editing
sed processes text line by line, applying transformations as the text flows through. It’s the tool you want for find-and-replace operations, deleting specific lines, or reformatting text on the fly. Where grep selects lines and awk selects columns, sed modifies the text itself.
Simple Substitution
Replace “admin” with “root” throughout the access log:
sed 's/admin/root/g' access.log
Every occurrence of “admin” is replaced, including in URL paths:
192.168.1.10 - root [25/Mar/2026:10:15:32 +0000] "GET /dashboard HTTP/1.1" 200 5432
10.0.1.55 - - [25/Mar/2026:10:15:33 +0000] "POST /api/login HTTP/1.1" 401 128
192.168.1.10 - root [25/Mar/2026:10:15:34 +0000] "GET /api/users HTTP/1.1" 200 2048
172.16.0.22 - - [25/Mar/2026:10:15:35 +0000] "GET /images/logo.png HTTP/1.1" 304 0
10.0.1.55 - - [25/Mar/2026:10:15:36 +0000] "GET /root/config HTTP/1.1" 403 256
192.168.1.10 - root [25/Mar/2026:10:15:37 +0000] "GET /api/reports HTTP/1.1" 200 8192
172.16.0.22 - - [25/Mar/2026:10:15:38 +0000] "DELETE /api/users/5 HTTP/1.1" 500 512
10.0.1.55 - - [25/Mar/2026:10:15:39 +0000] "GET /dashboard HTTP/1.1" 200 5432
The g flag means “global,” replacing all matches on each line. Without it, only the first match per line is replaced.
In-Place Editing (-i)
To modify a file directly instead of printing to stdout, use -i. Create a test copy first:
cp config.ini config_test.ini
sed -i 's/debug = false/debug = true/' config_test.ini
grep "debug" config_test.ini
The file is changed on disk:
debug = true
On macOS, sed -i requires an empty string argument: sed -i '' 's/old/new/' file. GNU sed on Linux doesn’t need it. This catches people off guard when switching between systems.
Delete Lines
Remove all comment lines and blank lines from a config file. This is useful for reading configs without the noise:
sed '/^\[/d' config.ini
Section headers are stripped, leaving only the key-value pairs:
host = 10.0.1.100
port = 5432
name = appdb
user = appuser
password = s3cureP@ss
host = 10.0.1.101
port = 6379
maxmemory = 256mb
debug = false
workers = 4
log_level = info
secret_key = a1b2c3d4e5f6
Delete a specific line by number (remove line 5, the password line):
sed '5d' config.ini
Print Specific Lines
Print only lines 2 through 4 from the employees file:
sed -n '2,4p' employees.csv
The -n flag suppresses default output, so only the explicitly printed lines appear:
1,Alice Johnson,Engineering,95000,Portland
2,Bob Smith,Marketing,72000,Seattle
3,Carol Davis,Engineering,102000,Portland
Print the last line of a file:
sed -n '$p' employees.csv
Just the final record:
8,Hank Taylor,Engineering,110000,Seattle
Insert and Append
Insert a line before line 1 (add a comment at the top of the config):
sed '1i\# Generated by deployment script' config.ini
The comment appears above the first section header:
# Generated by deployment script
[database]
host = 10.0.1.100
port = 5432
name = appdb
user = appuser
password = s3cureP@ss
[redis]
host = 10.0.1.101
port = 6379
maxmemory = 256mb
[app]
debug = false
workers = 4
log_level = info
secret_key = a1b2c3d4e5f6
Append a line after a pattern match (add a timeout setting after every “port” line):
sed '/^port/a\timeout = 30' config.ini
Each port line gets a timeout added below it:
[database]
host = 10.0.1.100
port = 5432
timeout = 30
name = appdb
user = appuser
password = s3cureP@ss
[redis]
host = 10.0.1.101
port = 6379
timeout = 30
maxmemory = 256mb
[app]
debug = false
workers = 4
log_level = info
secret_key = a1b2c3d4e5f6
Multiple Substitutions (-e)
Chain multiple edits in a single sed invocation:
sed -e 's/Engineering/Eng/g' -e 's/Marketing/Mkt/g' -e 's/Sales/Sls/g' employees.csv
All three department names are abbreviated in one pass:
id,name,department,salary,city
1,Alice Johnson,Eng,95000,Portland
2,Bob Smith,Mkt,72000,Seattle
3,Carol Davis,Eng,102000,Portland
4,Dan Wilson,Sls,68000,Denver
5,Eve Martinez,Eng,98000,Seattle
6,Frank Brown,Mkt,75000,Portland
7,Grace Lee,Sls,71000,Denver
8,Hank Taylor,Eng,110000,Seattle
Capture Groups and Backreferences
Rearrange the employee CSV to show “LastName, FirstName” format using capture groups:
sed -n 's/.*,\([A-Za-z]*\) \([A-Za-z]*\),.*/\2, \1/p' employees.csv
Names are swapped using \1 and \2 backreferences:
Johnson, Alice
Smith, Bob
Davis, Carol
Wilson, Dan
Martinez, Eve
Brown, Frank
Lee, Grace
Taylor, Hank
Print Lines Between Patterns
Extract only the [redis] section from the config file:
sed -n '/\[redis\]/,/^\[/p' config.ini | sed '$d'
Everything between [redis] and the next section header:
[redis]
host = 10.0.1.101
port = 6379
maxmemory = 256mb
The second sed removes the trailing section header that the range pattern includes. This technique works on any INI-style config file, which is common when managing systemd services and their associated configs.
Combining grep, awk, and sed
Each tool is powerful alone, but piping them together handles tasks that none can do individually. The key is using the right tool at each stage of the pipeline: grep to filter, awk to extract and compute, sed to transform.
Top IPs from the Access Log
Find which IP addresses generate the most traffic:
awk '{print $1}' access.log | sort | uniq -c | sort -rn
IPs ranked by request count:
3 192.168.1.10
3 10.0.1.55
2 172.16.0.22
awk extracts the IP, sort groups identical IPs together, uniq -c counts them, and the final sort -rn orders by count descending.
Failed Requests with Details
Pull the IP, URL, and status code for all 4xx and 5xx responses:
grep -E " (4[0-9]{2}|5[0-9]{2}) " access.log | awk '{print $1, $7, $9}'
grep filters to error status codes, then awk extracts the three relevant columns:
10.0.1.55 /api/login 401
10.0.1.55 /admin/config 403
172.16.0.22 /api/users/5 500
Generate Email Addresses from Employee Data
Transform Engineering team names into corporate email addresses:
awk -F, 'NR>1 && $3=="Engineering" {print $2}' employees.csv | sed 's/ /./g' | awk '{print tolower($0) "@company.com"}'
awk filters to Engineering and extracts names, sed replaces spaces with dots, and the final awk lowercases and appends the domain:
[email protected]
[email protected]
[email protected]
[email protected]
Parse Config to Environment Variables
Convert an INI config file into shell-compatible export statements:
grep -E "^[a-z]" config.ini | sed 's/ = /=/' | awk '{print "export " toupper($0)}'
grep selects only key-value lines (skipping section headers and blanks), sed removes the spaces around =, and awk uppercases and adds the export prefix:
export HOST=10.0.1.100
export PORT=5432
export NAME=APPDB
export USER=APPUSER
export PASSWORD=S3CUREP@SS
export HOST=10.0.1.101
export PORT=6379
export MAXMEMORY=256MB
export DEBUG=FALSE
export WORKERS=4
export LOG_LEVEL=INFO
export SECRET_KEY=A1B2C3D4E5F6
In production, you’d prefix each variable with its section name to avoid duplicates (two HOST keys). That’s a more advanced awk script with state tracking, but this demonstrates the pipeline pattern.
Performance Comparison
On small files, all three tools are effectively instant. Performance differences only matter when you’re processing large files (hundreds of megabytes or more). Here’s what to expect based on real-world testing.
grep is the fastest for simple pattern matching because it’s optimized for exactly that task. It uses Boyer-Moore and other algorithms to skip through text without examining every character. For searching a 1 GB log file for a string, grep will consistently outperform both awk and sed.
sed is the fastest for simple substitutions. It processes text with minimal overhead because it doesn’t need to split lines into fields. For a bulk find-and-replace across a large file, sed beats awk.
awk carries more overhead per line because it splits every line into fields. But for tasks that actually need columnar processing, it’s faster than chaining grep and sed together, since it avoids the overhead of spawning multiple processes and piping data between them.
The practical rule: use grep when you only need to find lines. Use sed when you need simple text transformations. Use awk when you need to work with columns, do math, or apply conditional logic. When you need all three, pipe them together and let each tool handle what it does best.
Quick Reference Table
Bookmark this table. It covers the commands used most in day-to-day system administration.
| Task | Command |
|---|---|
| Search for a string | grep "pattern" file |
| Case-insensitive search | grep -i "pattern" file |
| Invert match (exclude pattern) | grep -v "pattern" file |
| Count matching lines | grep -c "pattern" file |
| Show line numbers | grep -n "pattern" file |
| Only matching part | grep -o "pattern" file |
| Extended regex | grep -E "pat1|pat2" file |
| Perl regex | grep -P "\d+" file |
| Context (before/after) | grep -C3 "pattern" file |
| Recursive search | grep -r "pattern" /path/ |
| List matching files only | grep -rl "pattern" /path/ |
| Whole word match | grep -w "word" file |
| Print specific column | awk '{print $2}' file |
| Custom delimiter | awk -F, '{print $1}' file |
| Filter by condition | awk '$3 > 100 {print $1}' file |
| Sum a column | awk '{sum+=$1} END {print sum}' file |
| Count lines matching | awk '/pattern/ {count++} END {print count}' file |
| Group by and aggregate | awk '{a[$1]+=$2} END {for(k in a) print k,a[k]}' file |
| Print last column | awk '{print $NF}' file |
| Skip header row | awk 'NR>1' file |
| Formatted output | awk '{printf "%-10s %d\n", $1, $2}' file |
| Find and replace | sed 's/old/new/g' file |
| In-place edit | sed -i 's/old/new/g' file |
| Delete matching lines | sed '/pattern/d' file |
| Print line range | sed -n '5,10p' file |
| Insert before line | sed '3i\new text' file |
| Append after match | sed '/pattern/a\new text' file |
| Multiple substitutions | sed -e 's/a/b/' -e 's/c/d/' file |
| Extract between patterns | sed -n '/start/,/end/p' file |
| Backreference replace | sed 's/\(foo\)bar/\1baz/' file |