Cooking with SED
Learning SED
Chapter 11 — SED Scripts and Real-World Recipes
From One-Liners to Script Files
So far every example has used the -e flag or a single quoted string. For serious work — multi-step transformations, reusable utilities, version-controlled text processing tools — you write a SED script file and invoke it with sed -f scriptfile.sed input.txt.
A script file is plain text. Each line is a SED command, exactly as you would write after -e, but without the quoting and with full freedom to use whitespace, blank lines, and comments.
anatomy of a sed script file (cleanup.sed)
#!/usr/bin/sed -foptional shebang — makes it self-executable
# ── normalise_config.sed ──────────────────comment: whole line starting with #
# Remove blank lines and commentsdescribe intent, not mechanics
/^[[:space:]]*$/ddelete blank lines
/^[[:space:]]*#/ddelete comment lines
blank lines are fine — improve readability
# Trim trailing whitespace
s/[[:space:]]*$//strip trailing spaces / tabs
# Normalise multiple spaces to one
s/[[:space:]]\+/ /gcollapse runs of whitespace
Invoking a script file
# Basic usage sed -f normalise_config.sed input.conf # In-place edit sed -i -f normalise_config.sed *.conf # Combine a script file with an extra -e command sed -f normalise_config.sed -e 's/localhost/10.0.0.1/g' input.conf # ERE mode with a script file sed -E -f myscript.sed input.txt # Self-executable script (shebang must be first line) chmod +x normalise_config.sed ./normalise_config.sed input.conf
Comments in script files vs one-liners: The
# comment character works in -f script files. In a -e expression, # is not a comment — it is a literal character or part of a regex delimiter. Only use comments in script files.
Structuring Complex Scripts
Using braces for clarity
# Group related commands under an address — visually clear in script files /^[[:space:]]*\[/ { # Lines that look like INI section headers s/[[:space:]]//g # strip all spaces s/\[/[/ # rewrite opening bracket (noop here but explicit) y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/ # uppercase } # Process lines NOT in the header range /^[[:space:]]*\[/! { s/^[[:space:]]*// # trim leading whitespace s/[[:space:]]*$// # trim trailing whitespace }
Labels and branching for loops in scripts
# Script: collapse repeated blank lines into a single blank line # Algorithm: join current line to next with N, check for blank+blank, # loop until no longer two consecutive blanks. /^$/ { N /^\n$/b blankloop # if still blank after join, keep looping P D } b # non-blank line: fall through to default print :blankloop N /^\n$/b blankloop P D
Log File Processing
Real-World Recipe Category 1Filter and extract from logs
# Show only ERROR and FATAL lines (suppress everything else) sed -n '/\(ERROR\|FATAL\)/p' app.log # Same in ERE sed -En '/(ERROR|FATAL)/p' app.log # Delete DEBUG and TRACE lines in-place (quiet the noise) sed -i '/\(DEBUG\|TRACE\)/d' app.log # Anonymise IP addresses (replace last two octets) sed -E 's/([0-9]{1,3}\.[0-9]{1,3})\.[0-9]{1,3}\.[0-9]{1,3}/\1.x.x/g' access.log # Extract just the timestamp column (first field before space) sed 's/[[:space:]].*//' app.log # Add a prefix to every log line for pipeline tagging sed 's/^/[APP] /' app.log
Reformat Apache / Nginx combined log format
# Combined log format: # IP - - [timestamp] "METHOD /path HTTP/1.1" status bytes # Extract: IP, status, path sed -E 's/^([^ ]+) [^ ]+ [^ ]+ \[[^]]+\] "[^ ]+ ([^ ]+)[^"]*" ([0-9]+).*/\1 \3 \2/' access.log # Count each unique status code (pipe to sort | uniq -c for frequency) sed -E 's/^.* "[^"]*" ([0-9]{3}) .*/\1/' access.log | sort | uniq -c # Keep only 4xx and 5xx errors sed -En '/\" [45][0-9]{2} /p' access.log
Multi-line log entry processing
# Java stack traces span multiple lines — collect them # Strategy: when a line starts with "at " or "Caused by:", append it # to the previous line so the full trace stays together # Simple version: join lines beginning with whitespace (indented continuations) sed '/^[[:space:]]/{H;d}; /^[[:space:]]/!{x;s/\n/ /g;p}' -n app.log
Config File Manipulation
Real-World Recipe Category 2INI / key=value files
# Set a specific key to a new value (key=value format) sed 's/^\(max_connections\s*=\s*\).*/\1100/' db.conf # Same with ERE — cleaner sed -E 's/^(max_connections\s*=\s*).*/\1100/' db.conf # Comment out a specific key (prepend #) sed 's/^\(listen_address\s*=\)/#\1/' db.conf # Uncomment a specific key (remove leading #) sed 's/^#\(listen_address\s*=\)/\1/' db.conf # Delete a key entirely sed '/^old_deprecated_key\s*=/d' db.conf # Append a new key after an existing one sed '/^max_connections/a new_param = 50' db.conf # Insert a key before an existing one sed '/^\[database\]/i # Database section begins here' db.conf
Work safely within a specific section only
# Only modify keys inside the [database] section # Strategy: range from section header to the next section header (or EOF) sed -E '/^\[database\]/,/^\[/ s/^(host\s*=\s*).*/\1newhost/' app.conf # Caution: the closing /^\[/ also matches the NEXT section header line itself # To avoid modifying that line, negate it: sed -E '/^\[database\]/,/^\[/ { /^\[database\]/! { /^\[/! s/^(host\s*=\s*).*/\1newhost/ } }' app.conf
XML / HTML attribute updates
# Update a version attribute in a single-line XML tag sed -E 's/(version=")[^"]*/\11.2.3/' pom.xml # Replace a URL inside quotes (uses | as delimiter to avoid escaping /) sed -E 's|href="[^"]*"|href="https://new.example.com"|g' index.html # Remove a specific HTML attribute entirely sed -E 's/ style="[^"]*"//g' index.html
CSV and Tabular Data
Real-World Recipe Category 3
SED is line-oriented and not CSV-aware. It does not understand quoted fields that contain commas or newlines. For production CSV with embedded commas, use
awk, Python's csv module, or miller. The recipes below work reliably only on simple delimiter-separated data with no embedded delimiters.
# Extract the third comma-separated field sed -E 's/^([^,]*,){2}([^,]*).*/\2/' data.csv # Delete the first field (up to and including the first comma) sed 's/[^,]*,//' data.csv # Replace comma delimiter with pipe sed 's/,/|/g' data.csv # Add a header line if the file lacks one sed '1i name,age,city' data.csv # Skip the header line and process only data rows sed '1d; s/,/ /g' data.csv # Surround every field with double quotes (simple CSV → quoted CSV) sed -E 's/([^,]+)/"\1"/g' data.csv # Trim spaces around comma separators sed 's/[[:space:]]*,[[:space:]]*/,/g' data.csv
Source Code Transformations
Real-World Recipe Category 4Rename a function or variable across a codebase
# Safe rename: only match whole identifiers (not substrings) sed -i 's/\bgetUser\b/fetchUser/g' src/*.js # Rename across all .py files recursively (using find + xargs) find . -name '*.py' | xargs sed -i 's/\bold_func\b/new_func/g'
Add / remove language constructs
# Add a semicolon to every non-blank, non-comment line (JS minification prep) sed '/^[[:space:]]*$/d; /^[[:space:]]*\/\//d; s/$/;/' code.js # Remove C-style single-line comments (// to end of line) sed 's|//.*$||' code.c # Add "export " prefix to every function declaration sed '/^function /s/^/export /' module.js # Indent every line by 4 spaces (e.g. when inserting into another file) sed 's/^/ /' snippet.py # Remove all indentation (dedent) sed 's/^[[:space:]]*//' snippet.py # Convert tabs to 4 spaces sed 's/\t/ /g' code.py # Remove trailing whitespace (common pre-commit hook fix) sed -i 's/[[:space:]]*$//' *.py
Version number bumping
# Bump patch version X.Y.Z → X.Y.Z+1 is tricky without arithmetic # But you can replace a known version string precisely: sed 's/version = "1.2.3"/version = "1.2.4"/' pyproject.toml # Replace any version string matching the pattern (capture major.minor, replace patch) # Note: SED cannot do arithmetic — only string replacement sed -E 's/(version = ")([0-9]+\.[0-9]+\.)[0-9]+/\11.2.4/' pyproject.toml
Text Formatting and Document Processing
Real-World Recipe Category 5Line spacing
# Double-space a file (add a blank line after every line) sed 'G' file.txt # Remove all blank lines sed '/^[[:space:]]*$/d' file.txt # Remove consecutive blank lines (leave at most one blank line) sed '/./,/^$/!d' file.txt # classical one-liner # Number every line sed '=' file.txt | sed 'N; s/\n/\t/' # Number only non-blank lines sed '/./=' file.txt | sed '/./N; s/\n/\t/'
Reverse a file (tac equivalent)
# Pure SED tac: accumulate all lines in hold space, then print reversed sed -n '1!G; h; $p' file.txt # 1!G — on every line except the first: prepend hold space to pattern space # h — copy pattern space to hold space # $p — on the last line: print (now contains all lines reversed)
Prepend and append boilerplate
# Insert a copyright header at the very top of a file sed -i '1i # Copyright 2024 Example Corp. All rights reserved.' *.py # Append a footer line at the very end sed -i '$a # End of file' file.txt # Replace lines 1-3 (existing header) with a new header sed -i '1,3c\# New header line 1\n# New header line 2' file.txt # Wrap entire file content in XML tags sed '1i <document>; $a </document>' content.txt
Markdown and plain-text utilities
# Convert Markdown headings to HTML sed -E 's/^### (.+)/<h3>\1<\/h3>/; s/^## (.+)/<h2>\1<\/h2>/; s/^# (.+)/<h1>\1<\/h1>/' doc.md # Convert *word* to <em>word</em> sed -E 's/\*([^*]+)\*/<em>\1<\/em>/g' doc.md # Remove all Markdown emphasis markers (strip * and _) sed 's/[*_]//g' doc.md # Centre-align text within 72 characters (approximate) sed 's/^/ /; s/^\(.\{36\}\)\(.*\)/\1\2/' title.txt
Data Masking and Security
Real-World Recipe Category 6# Mask credit card numbers (keep last 4 digits) sed -E 's/\b([0-9]{4}[- ]?){3}([0-9]{4})\b/****-****-****-\2/g' report.txt # Redact email addresses sed -E 's/[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}/[REDACTED]/g' data.txt # Mask all but the first and last character of a password field sed -E 's/(password=")([^"])(.*)(.")(\s*")/\1\2***\4\5/g' config.xml # Remove any line containing an API key pattern sed -i '/api[_-]key\s*[:=]/Id' config.txt # I flag = case-insensitive (GNU) # Scrub phone numbers (various formats) sed -E 's/(\+?1[-. ]?)?(\(?\d{3}\)?[-. ]?)(\d{3}[-. ]?\d{4})/[PHONE]/g' data.txt
The Essential SED One-Liner Library
These are the most referenced SED one-liners — memorise or bookmark them:
Delete blank lines
sed '/^$/d'
Remove trailing whitespace
sed 's/[[:space:]]*$//'
Remove leading whitespace
sed 's/^[[:space:]]*//'
Trim both ends
sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
Print lines 5 to 10
sed -n '5,10p'
Print last line
sed -n '$p'
Double-space
sed 'G'
Number lines
sed '=' | sed 'N;s/\n/\t/'
Reverse file (tac)
sed -n '1!G;h;$p'
Print between patterns
sed -n '/START/,/END/p'
Delete between patterns
sed '/START/,/END/d'
Remove duplicate adjacent lines
sed '$!N;/^\(.*\)\n\1$/!P;D'
In-place backup edit
sed -i.bak 's/foo/bar/g' file
Prepend text to every line
sed 's/^/PREFIX: /'
Convert Windows line endings
sed 's/\r$//'
Replace Nth occurrence on a line
sed 's/foo/bar/2' # 2nd only
Building Reusable SED Libraries
For teams or personal toolkits, it pays to organise frequently used SED scripts as self-documenting files:
#!/usr/bin/sed -Ef # ────────────────────────────────────────────────────── # sanitise_csv.sed # Purpose : Normalise a messy CSV before import # Usage : sed -Ef sanitise_csv.sed input.csv > output.csv # Requires: GNU sed (uses \s, -E, -f together) # ────────────────────────────────────────────────────── # 1. Remove UTF-8 BOM if present (first line, first three bytes = EF BB BF) 1s/^\xef\xbb\xbf// # 2. Normalise Windows CRLF to LF s/\r$// # 3. Trim whitespace around each comma s/[[:space:]]*,[[:space:]]*/,/g # 4. Remove trailing comma if present s/,$// # 5. Remove completely empty lines /^$/d
Good script file habits: Always include a usage comment, document the GNU/POSIX requirements, group related commands with a blank line and comment, and name script files with a
.sed extension so editors apply syntax highlighting.
Quick Reference — Chapter 11
Script File Invocation
sed -f script.sed file
Run a SED script file against a file
sed -i -f script.sed file
In-place edit using a script file
sed -E -f script.sed file
ERE mode with a script file
sed -f s1.sed -f s2.sed file
Chain multiple script files
sed -f script.sed -e 's/x/y/'
Script file plus inline expression
Script File Best Practices
# comment text
Whole-line comments — only work in -f files, not -e
#!/usr/bin/sed -f
Shebang for self-executable scripts (first line only)
Blank lines between groups
Ignored by SED — use them freely for readability
.sed extension
Convention — enables editor syntax highlighting
What is coming next: Chapter 12 — the final chapter — covers SED in shell scripts and pipelines: building robust wrappers, using SED inside functions, combining SED with grep/awk/cut in production pipelines, performance on large files, error handling, and when to stop using SED and reach for a different tool.