sed Basics: Address, Command, Flag

Every sed instruction follows the same structure: an optional address, a command letter, and arguments. The address selects which lines to act on. Without an address, the command applies to every line.

The substitution command `s` is the one you will use 90% of the time. It takes the form `s/pattern/replacement/flags`. The delimiter does not have to be a forward slash - any character works, which matters when you are substituting paths.

Check your installed version first. GNU sed and BSD sed differ in ways that break scripts when you move between Linux and macOS.

sed --version | head -1
# GNU sed 4.8

# Basic substitution - replace first match per line
sed 's/foo/bar/' file.txt

# Replace ALL matches per line (g flag)
sed 's/foo/bar/g' file.txt

# Use a different delimiter - useful with paths
sed 's|/etc/old|/etc/new|g' deploy.conf

# Address by line number - only modify line 5
sed '5s/foo/bar/' file.txt

# Address by regex - only modify lines containing 'ERROR'
sed '/ERROR/s/old/new/' app.log

In-Place Editing with -i

The `-i` flag writes changes back to the file instead of printing to stdout. This is where GNU sed and BSD sed diverge in a way that breaks scripts.

GNU sed (Linux): `-i` with no argument edits in place with no backup. `-i.bak` creates a backup with `.bak` extension.

BSD sed (macOS): `-i ''` is required for no-backup in-place editing. `-i` alone is a syntax error. This single difference causes more cross-platform sed breakage than any other issue.

In our experience, the safest portable pattern for scripts that run on both Linux and macOS is to write to a temp file and move it, or to detect the platform and branch. For pure Linux production environments, use GNU sed's `-i` directly.

Always test on a copy or with a backup flag before running in-place edits against production configs.

# GNU sed - in-place, no backup
sed -i 's/DEBUG=true/DEBUG=false/' /etc/app/config.env

# GNU sed - in-place with .bak backup
sed -i.bak 's/DEBUG=true/DEBUG=false/' /etc/app/config.env

# BSD sed (macOS) equivalent - note the space and empty string
sed -i '' 's/DEBUG=true/DEBUG=false/' /etc/app/config.env

# Portable approach - write to temp, then move
tmp=$(mktemp)
sed 's/DEBUG=true/DEBUG=false/' /etc/app/config.env > "$tmp" && mv "$tmp" /etc/app/config.env

# In-place on multiple files at once
sed -i 's/v1\.0/v2.0/g' configs/*.conf

Multiple Expressions with -e

Running sed multiple times against the same file is slower and less readable than chaining expressions in a single pass. Use `-e` to apply multiple substitutions, or use a semicolon to separate commands within a single expression string.

Order matters. sed processes expressions in sequence for each line. If expression one changes a string that expression two would have matched, expression two misses it. Design your expression chain with this in mind.

For complex multi-step transformations applied repeatedly, write a sed script file and invoke it with `-f`. This is much easier to maintain and review in pull requests than a 200-character one-liner.

# Two substitutions in one pass
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# Semicolon syntax - equivalent
sed 's/foo/bar/g; s/baz/qux/g' file.txt

# sed script file - save as fix_config.sed
# s/localhost/10.0.1.100/g
# s/port=8080/port=443/g
# /^#/d
# /^$/d

# Invoke the script file
sed -f fix_config.sed /etc/app/config.conf

# In-place with script file
sed -i -f fix_config.sed /etc/app/config.conf
// advertisement

Deleting Lines: The d Command

Stripping comment lines, blank lines, or log entries matching a pattern is one of the most common sysadmin use cases for sed. The `d` command deletes lines matching an address and moves to the next line without printing.

Combining a negated address with `d` is the inverse: delete everything that does NOT match. The `!` character negates any address.

Range addresses use a comma between two patterns or line numbers. `sed '5,10d'` deletes lines 5 through 10. You can mix line numbers and regex in a range.

# Delete blank lines
sed '/^$/d' file.txt

# Delete comment lines (starting with #)
sed '/^#/d' file.txt

# Delete both blank lines and comments in one pass
sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' file.txt

# Delete lines 5 through 10
sed '5,10d' file.txt

# Delete from a pattern to end of file
sed '/^BEGIN CERTIFICATE/,$d' file.pem

# Keep ONLY lines matching a pattern (delete everything else)
sed '/ERROR/!d' app.log

# Delete trailing whitespace
sed 's/[[:space:]]*$//' file.txt

Print Command, Line Numbers, and Suppressing Default Output

By default sed prints every line after processing. The `-n` flag suppresses this behavior, printing only when you explicitly use the `p` command. This is how you use sed as a grep replacement with context, or extract specific line ranges without loading the whole file into memory.

The `=` command prints the current line number. Combined with `-n` and `p`, you can build targeted extractors for large log files.

For extracting lines between two patterns, the range address with `-n` and `p` is faster than awk for simple cases, though awk gives you more control once you need field processing.

# Print only lines matching a pattern (-n suppresses default output)
sed -n '/ERROR/p' app.log

# Print line numbers of matching lines
sed -n '/ERROR/=' app.log

# Print lines 100 through 200 only
sed -n '100,200p' hugefile.log

# Print from pattern to pattern (inclusive)
sed -n '/START/,/END/p' file.txt

# Print line number AND the line
sed -n '/ERROR/{=;p}' app.log

# Equivalent to head -20
sed -n '1,20p' file.txt

# Quit after line 20 (faster on large files - sed stops reading)
sed '20q' file.txt

Regex in sed: Character Classes, Backreferences, Extended Regex

GNU sed uses basic regex (BRE) by default. The `-E` flag (or `-r` in older versions) enables extended regex (ERE), which removes the need to escape `+`, `?`, `|`, `(`, and `)` with backslashes. Always use `-E` for anything beyond simple literal substitutions - it makes patterns readable.

Backreferences let you capture groups and reuse them in the replacement string. In BRE, capture groups use `\(` and `\)`. In ERE with `-E`, they use `(` and `)`. Either way, `\1` refers to the first captured group in the replacement.

POSIX character classes like `[[:alpha:]]`, `[[:digit:]]`, `[[:space:]]` are portable across GNU and BSD sed. Prefer them over `\w`, `\d`, `\s` which are not guaranteed in all sed implementations.

# ERE with -E flag
sed -E 's/[0-9]+/NUM/g' file.txt

# Backreference - swap first and second word on each line
sed -E 's/^([[:alpha:]]+)[[:space:]]+([[:alpha:]]+)/\2 \1/' file.txt

# Capture an IP address group and reformat
sed -E 's/([0-9]{1,3}\.){3}([0-9]{1,3})/[REDACTED]/g' access.log

# Extract email addresses (print only matching lines)
sed -En 's/.*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}).*/\1/p' file.txt

# Wrap a value in quotes
sed -E 's/(VARIABLE=)(.*)/\1"\2"/' config.env

# Case-insensitive match (GNU sed only, I flag)
sed 's/error/ERROR/gI' app.log
// advertisement

The Hold Space: Advanced Line Manipulation

sed has two buffers: the pattern space (the current line being processed) and the hold space (persistent storage across line cycles). Most sed usage never touches the hold space. When you need to reorder lines, join lines, or reference a previous line, hold space commands become necessary.

The key commands are: `h` (copy pattern space to hold), `H` (append pattern space to hold), `g` (copy hold to pattern space), `G` (append hold to pattern space), `x` (exchange pattern and hold spaces).

A practical example: reverse the order of two adjacent lines, or join a continuation line to the previous line. These operations are unusual but appear in real parsing tasks for multiline config formats.

# Join lines ending with backslash to the next line
# (common in shell scripts and Makefiles)
sed -E ':a; /\\$/ { N; s/\\\n//; ba }' Makefile

# Double-space a file (add blank line after each line)
sed 'G' file.txt

# Reverse two adjacent lines using hold space
sed -n 'h;n;p;g;p' file.txt

# Print the line BEFORE a pattern match
sed -n '/ERROR/ { g; p }; h' app.log

# Delete duplicate consecutive lines (like uniq)
sed '$!N; /^\(.*\)\n\1$/!P; D' file.txt

Append, Insert, and Change Commands

Beyond substitution, sed can insert new lines before a match (`i`), append new lines after a match (`a`), and replace an entire matched line with new content (`c`). These are underused but solve real problems in config management.

A common deployment use case: insert a comment block before a specific directive, or add a line after a section header. These operations are far less error-prone in sed than in Python scripts that read and rewrite files manually.

Note that GNU sed allows single-line syntax with `\n` in the argument. BSD sed requires a literal newline after the command character. For cross-platform scripts, use the newline form.

# Append a line after every line matching 'ServerName'
sed '/ServerName/a\    ServerAlias www.example.com' httpd.conf

# Insert a line before the matching line
sed '/^Port /i\# Port configured by deploy script' sshd_config

# Replace an entire matching line
sed '/^Port 22$/c\Port 2222' sshd_config

# GNU sed single-line append syntax
sed '/\[database\]/a db_host=10.0.1.5\ndb_port=5432' app.ini

# Add a header line at the top of a file
sed '1i\# Auto-generated - do not edit' config.conf

# Add a footer at the end of a file
sed '$a\# End of configuration' config.conf

Real-World Sysadmin Workflows

In our experience, sed appears most often in three production contexts: config file modification during provisioning, log preprocessing before feeding data to monitoring tools, and bulk file transformation in deployment scripts.

For provisioning, sed handles parameterization of config templates more reliably than heredocs when the replacement values contain special characters. Pair it with environment variables using shell interpolation.

For log preprocessing, sed strips ANSI escape codes, normalizes timestamps, or redacts sensitive fields before logs hit aggregators like Elasticsearch or Loki. This is significantly faster than processing in Python when dealing with multi-gigabyte log files.

For DevOps automation pipelines, teams at organizations using platforms like taskbotshub.ai often integrate sed into their shell-based automation steps for exactly this kind of deterministic, fast text transformation before handing data to more complex tooling.

When managing infrastructure-as-code repositories where project names and slugs appear across dozens of config files, a sed replacement across the tree is the fastest migration path. If you are registering a new project domain and need to update references throughout your configs, services like nicename.me can help find available names, and sed handles the bulk rename in the repo in seconds.

# Inject environment variable into config during provisioning
export DB_HOST=10.0.1.100
sed "s/DB_HOST=.*/DB_HOST=${DB_HOST}/" /etc/app/config.env

# Strip ANSI escape codes from logs
sed 's/\x1b\[[0-9;]*m//g' colored.log

# Redact Authorization headers from access logs
sed -E 's/(Authorization: Bearer )[A-Za-z0-9._-]+/\1[REDACTED]/g' access.log

# Bulk rename a project slug across all config files
find ./infra -name '*.conf' -exec sed -i 's/old-project-name/new-project-name/g' {} +

# Extract slow query times from MySQL slow log
sed -n 's/^# Query_time: \([0-9.]*\).*/\1/p' mysql-slow.log | sort -n | tail -20

# Remove all XML/HTML tags
sed 's/<[^>]*>//g' page.html
// advertisement

Performance: sed vs awk vs perl for Large Files

On a 2 GB nginx access log in our testing on a 4-core VM running Linux 6.8, a simple global substitution with sed completed in 4.1 seconds. The equivalent perl one-liner (`perl -pi -e`) completed in 5.8 seconds. awk was fastest for field-based operations but slower for pure regex substitution across entire lines.

sed outperforms perl for single-pass substitutions on large files because it has lower startup overhead and streams directly. The gap narrows when perl's regex engine can use optimizations that sed cannot.

For multi-gigabyte files, avoid `-i` on GNU sed without measuring first. GNU sed's in-place implementation writes a full temp file and renames it. On systems with slow disk, the rename is fine but the write time is the same as any tool. Consider `mmap`-based tools like `sd` (written in Rust) for sustained throughput on very large files where sed bottlenecks on I/O.

When a sed expression grows beyond three `-e` expressions or requires hold space manipulation, switch to awk or perl. The readability cost outweighs the marginal performance benefit.

# Time a sed substitution on a large file
time sed 's/GET/POST/g' access.log > /dev/null

# Compare with perl
time perl -pe 's/GET/POST/g' access.log > /dev/null

# For sustained large-file throughput, sd (Rust) syntax for comparison
# sd 'GET' 'POST' access.log

# Process in parallel chunks with GNU parallel + sed
parallel -j4 sed -n '{1}p' ::: $(seq 1 4) # example only - see parallel docs

# Check if mmap is being used (Linux)
strace -e trace=mmap sed 's/a/b/' largefile.txt 2>&1 | head -5

GNU sed-Specific Features Worth Knowing

GNU sed 4.x includes several extensions not in POSIX sed. The `R` command reads a line from a file into the pattern space. The `W` command writes the pattern space to a file. The `e` flag on the substitution command executes the pattern space as a shell command and replaces it with the output.

The `e` flag is powerful and dangerous. It runs arbitrary shell commands. Never use it with untrusted input. In controlled contexts, it enables sed to call external commands inline.

GNU sed also supports `\w`, `\W`, `\b` word boundaries, `\s` and `\S` for whitespace, and `\+` without needing ERE mode. These work in GNU sed 4.x but will break on BSD sed or older POSIX systems. Stick to POSIX character classes if portability matters.

# GNU sed e flag - execute pattern space as shell command
# WARNING: only use with controlled input
echo 'echo hello' | sed 'e'
# Output: hello

# Read from a file into the stream (R command, GNU only)
sed '/INJECT_HERE/R template_block.txt' config.conf

# Write matching lines to a separate file (w command, POSIX)
sed -n '/ERROR/w errors.log' app.log

# GNU word boundary \b
sed -E 's/\btest\b/TEST/g' file.txt

# Multiline mode with N - join next line into pattern space
sed 'N; s/\n/ /' file.txt