What's in the Pipeline?

Learning SED

Chapter 12 — SED in Shell Scripts and Pipelines

SED as a Pipeline Stage

SED's greatest strength is that it reads from stdin and writes to stdout — it fits naturally anywhere in a Unix pipeline. Every tool in the pipeline handles one concern, and SED handles the text transformation step.

source
file / cmd
grep
filter lines
|
sed
transform text
|
awk / cut
extract fields
|
sort / uniq
aggregate
output
file / var
# Typical pipeline: filter → transform → aggregate
grep "ERROR" app.log \
  | sed -E 's/.*\[([0-9:T-]+)\].*/\1/' \
  | sort | uniq -c | sort -rn

# SED reading from a command's output via process substitution
sed 's/foo/bar/g' <(curl -s "https://example.com/data.txt")

# SED writing to a variable via command substitution
CLEANED=$(sed 's/[[:space:]]*$//' <<< "  hello world  ")
echo "'$CLEANED'"
'  hello world'

Using SED Inside Shell Functions

Wrapping SED in shell functions creates readable, reusable utilities. The function handles argument validation and quoting; SED handles the transformation.

Basic wrapper pattern

# Function: trim whitespace from both ends of a string
trim() {
    sed 's/^[[:space:]]*//; s/[[:space:]]*$//' <<< "$1"
}

# Usage
trim "   hello world   "
hello world

# Capture the result
RESULT=$(trim "  data  ")

Functions that process files or stdin

# Function: strip all comments and blank lines from a config file
strip_config() {
    local file="${1:--}"   # default to stdin if no arg
    sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' "$file"
}

# Use on a file
strip_config /etc/ssh/sshd_config

# Use on stdin via pipeline
cat app.conf | strip_config

# ──────────────────────────────────────────────────────
# Function: replace a key=value in a file (safe in-place)
set_config_key() {
    local key="$1" value="$2" file="$3"
    if [[ -z "$key" || -z "$file" ]]; then
        echo "Usage: set_config_key KEY VALUE FILE" >&2
        return 1
    fi
    # Escape the value so it's safe inside the sed replacement
    local escaped_value
    escaped_value=$(sed 's/[\/&]/\\&/g' <<< "$value")
    sed -i "s|^\($key\s*=\s*\).*|\1$escaped_value|" "$file"
}

# Usage
set_config_key "max_connections" "200" db.conf
set_config_key "host" "db.example.com" app.conf

Passing shell variables into SED expressions

# Use double quotes to expand shell variables in the sed expression
OLD="localhost"
NEW="10.0.0.5"
sed "s/$OLD/$NEW/g" config.txt

# Problem: if variables contain / you need to escape or change delimiter
OLD_URL="http://old.example.com/api"
NEW_URL="https://new.example.com/v2"
sed "s|$OLD_URL|$NEW_URL|g" config.txt   # use | as delimiter

# Safer: escape the variable content before inserting into the pattern
escape_sed_pattern() {
    # Escapes special BRE characters in a string for use in a sed pattern
    sed 's/[]\/$*.^[]/\\&/g' <<< "$1"
}
escape_sed_replacement() {
    # Escapes special replacement characters (& and \)
    sed 's/[\/&]/\\&/g' <<< "$1"
}

# Safe substitution using the escape helpers
PATTERN=$(escape_sed_pattern "$OLD_URL")
REPLACE=$(escape_sed_replacement "$NEW_URL")
sed "s|$PATTERN|$REPLACE|g" config.txt
Never use single quotes when you need variable expansion, and never use double quotes when the pattern contains characters the shell will misinterpret (like ! in history-enabled shells). For complex patterns containing both shell metacharacters and SED special characters, build the expression in a variable first, then pass it.

Safe In-Place Editing Patterns

In-place editing with -i is powerful but destructive if something goes wrong. In scripts you should always protect against failures.

# Always take a backup with -i.bak before modifying in scripts
sed -i.bak 's/foo/bar/g' important.conf

# Verify success, restore on failure
if ! sed -i.bak 's/foo/bar/g' important.conf; then
    echo "sed failed — restoring backup" >&2
    mv important.conf.bak important.conf
    exit 1
fi

# Atomic edit pattern: write to temp file, then move (POSIX mv is atomic)
atomic_sed_edit() {
    local expr="$1" file="$2"
    local tmp
    tmp=$(mktemp)
    if sed "$expr" "$file" > "$tmp"; then
        mv "$tmp" "$file"
    else
        rm -f "$tmp"
        echo "ERROR: sed edit failed on $file" >&2
        return 1
    fi
}

# Usage
atomic_sed_edit 's/version=1\.0/version=2.0/' app.conf
Why atomic? With sed -i, if the process is killed mid-write, the file is left partially written and the original is already gone. The temp-file-then-move pattern keeps the original intact until the new version is fully written. On the same filesystem, mv is a single inode rename — instantaneous and atomic.

Error Handling and Exit Codes

# SED exit codes
# 0 — success (even if no substitutions were made)
# 1 — error (bad syntax, file not found)
# 2 — usage error (GNU sed)

# Check if sed succeeded
if sed -n '/pattern/p' file.txt > output.txt; then
    echo "Processing complete"
else
    echo "sed encountered an error" >&2
    exit 1
fi

# Note: sed returns 0 even if no lines matched — it is not grep
# To detect whether a substitution actually happened, use the w flag + wc -l
COUNT=$(sed -n 's/foo/bar/w /dev/stdout' file.txt | wc -l)
echo "$COUNT lines were changed"

# Validate a sed expression before using it on real data
if echo "" | sed -n "$EXPR" 2>/dev/null; then
    echo "Expression is valid"
else
    echo "Invalid sed expression: $EXPR" >&2
    exit 1
fi

Performance on Large Files

SED is fast — it processes line by line with minimal memory overhead. But there are patterns that hurt performance, and strategies to avoid them.

What slows SED down

Anti-patternProblemFix
sed 'complex/pattern/' huge.log Applying expensive patterns to every line Pre-filter with grep -F to reduce line count first
sed -i 's/x/y/' file in a loop Opens and rewrites the file once per iteration Combine all substitutions into one sed call or a script file
Chained sed | sed | sed Multiple processes, multiple passes through the data Merge into one sed call with multiple -e or a script file
sed 'N; P; D' on huge files Multi-line commands load two lines at once — still fine, but beware N on the last line Use $!N to protect the last line
sed '1,/pattern/{...}' range that never closes Every line after line 1 is tested against the regex forever Ensure the closing pattern will actually be found, or use line numbers

Practical performance patterns

# BAD: three separate sed processes
sed 's/foo/bar/g' data.txt | sed 's/baz/qux/g' | sed '/^#/d'

# GOOD: one process, three expressions
sed 's/foo/bar/g; s/baz/qux/g; /^#/d' data.txt

# BEST for large files: pre-filter then transform
grep -F "foo" huge.log | sed 's/foo/bar/g'

# Use q/Q to stop early when you only need the first match
sed -n '/pattern/{p;q}' huge.log   # stop at first match — don't read the rest

# Process only a known range to avoid scanning the whole file
sed -n '1000,2000p' huge.log   # print lines 1000-2000 then stop

# For enormous files: use LC_ALL=C to avoid multibyte locale overhead
LC_ALL=C sed 's/foo/bar/g' 10gb.log

SED with find, xargs and Loops

# Edit all .conf files in a directory tree in-place
find /etc -name '*.conf' -exec sed -i.bak 's/old/new/g' {} \;

# Faster: use xargs to batch files (fewer sed invocations)
find /etc -name '*.conf' | xargs sed -i.bak 's/old/new/g'

# Handle filenames with spaces safely
find . -name '*.txt' -print0 | xargs -0 sed -i 's/foo/bar/g'

# Shell loop with sed — useful when you need per-file logic
for file in src/*.js; do
    if grep -q "TODO" "$file"; then
        echo "Processing: $file"
        sed -i 's/TODO/FIXME/g' "$file"
    fi
done

# Edit files modified in the last 24 hours
find . -name '*.py' -mtime -1 | xargs -0r sed -i 's/\t/    /g'

Combining SED with Other Tools

Knowing when to hand off to a different tool is as important as knowing how to use SED. Here is a practical guide to the most common partnerships:

Task Best tool Why not just sed?
Simple pattern filter grep Faster, simpler, returns correct exit code on match/no-match
Field extraction by column cut / awk SED regex for fields gets unwieldy quickly
Arithmetic on values awk / bc SED has no arithmetic capability
Sorting, deduplication sort / uniq SED can't sort — it is strictly ordered line processing
Structured data (JSON/YAML) jq / yq / python SED treats everything as text — will break on nested structures
CSV with quoted fields awk / python csv / miller SED cannot handle embedded commas or newlines in quoted fields
Complex multi-field transforms awk awk has variables, arrays, arithmetic, printf — SED has none
Line-by-line text substitution sed This is exactly what SED is built for
Multi-line pattern matching sed (with N/P/D) or perl Doable in SED but verbose; perl -0777 is simpler for whole-file
In-place file editing sed -i Perl -i also works; most other tools require temp files

Classic SED + awk pipeline

# SED strips noise, awk does the field work and arithmetic
grep "RESPONSE_TIME" app.log \
  | sed -E 's/.*RESPONSE_TIME=([0-9.]+)ms.*/\1/' \
  | awk '{ sum+=$1; n++ } END { printf "avg: %.2fms\n", sum/n }'

# SED normalises the format, cut extracts the column
sed 's/  */ /g' data.txt | cut -d ' ' -f3

# sed + sort + uniq for a frequency count
sed -E 's/.*status=([0-9]{3}).*/\1/' app.log | sort | uniq -c | sort -rn

Portability Checklist for Scripts

If your script must run on both Linux (GNU sed) and macOS (BSD sed), test against this checklist:

FeatureGNU sedBSD sed (macOS)Safe alternative
-i in-place-i ''Requires -i '' (empty string arg)sed -i '' (works on both if empty string given)
-E ERE flag-E or -r-EUse -E (not -r)
\+ \? in BRESupportedNot supportedUse -E with + ?
\| alternation BRESupportedNot supportedUse -E with |
\w \s \bSupportedNot supportedUse [[:alnum:]_] etc.
\t in regexSupportedNot supportedUse $'\t' or POSIX [[:blank:]]
a\ appendBoth formsPOSIX backslash form onlyUse a\ text (backslash-newline)
T branch-if-not-substitutedSupportedNot supportedNo portable equivalent — use t with negation
z zap pattern spaceSupportedNot supportedUse s/.*//' or d
R W commandsSupportedNot supportedNo portable equivalent
# Portable in-place edit that works on both GNU and BSD sed
if sed --version 2>/dev/null | grep -q 'GNU'; then
    # GNU sed: -i accepts no argument (or empty string)
    sed -i 's/foo/bar/g' file.txt
else
    # BSD sed: -i requires an explicit extension argument (even if empty)
    sed -i '' 's/foo/bar/g' file.txt
fi

When to Stop Using SED

SED is the right tool for fast, line-oriented text transformations. Reach for something else when:

  • The data has structure (JSON, YAML, XML, real CSV) — use a parser that understands the format. SED treats everything as flat text.
  • You need arithmetic — SED cannot add, subtract, or compare numbers. Use awk or bc.
  • The regex would need lookahead/lookbehind — SED has no PCRE. Use grep -P, perl, or python.
  • The script exceeds ~20 lines — a SED script of 30+ lines is a maintenance burden. Rewrite in Python or awk where logic can be clearly expressed.
  • You need error handling, conditionals, or data structures — SED has none of these in any meaningful way. Use a real scripting language.
  • The transformation depends on values from other lines (e.g. database-style joins, lookups) — SED's hold space is a single buffer; use awk or a scripting language.
The SED sweet spot: one-liners and short scripts performing text substitution, line filtering, or simple reformatting on line-oriented input. Anything beyond that — reach for awk first, then Python. Use SED where it is fast and clear; stop before it becomes a puzzle.

Quick Reference — Chapter 12

SED in Pipelines

cmd | sed 'expr' Transform output of a command
sed 'expr' <(cmd) Process substitution — treat command output as a file
VAR=$(sed 'expr' <<< "$STR") Capture sed output into a variable
LC_ALL=C sed 'expr' file Force C locale for maximum speed on ASCII files

Safe Scripting Habits

sed -i.bak 'expr' file Always backup before in-place edit in scripts
tmp=$(mktemp); sed … > "$tmp" && mv "$tmp" file Atomic edit — original safe until new version is complete
sed "s/$VAR/…/" Double-quote to expand variables; escape / with | delimiter
find … | xargs -0 sed -i Null-delimited xargs handles filenames with spaces
grep -F 'literal' | sed 'expr' Pre-filter with grep on large files for a big speed gain

Course Complete — Full Chapter Index

01Introduction to SED — the processing cycle, basic syntax, first commandsfoundations
02The Substitute Command — flags, delimiters, replacement sequencescore
03Addresses and Line Selection — line, pattern, range, step, negationcore
04Delete, Print and Quit — d, p, =, q, Q and silent modecore
05Append, Insert and Change — a, i, c with portable syntaxcore
06The Hold Space — h, H, g, G, x and classic accumulation recipesintermediate
07Multi-line Commands — N, P, D and the sliding window patternintermediate
08Branching and Labels — :label, b, t, T and loop patternsintermediate
09The y Command and Other Commands — y, r/R, w/W, e, z, = (full reference)intermediate
10Regular Expressions in SED — BRE/ERE, POSIX classes, quantifiers, trapsregex
11SED Scripts and Real-World Recipes — script files, log/config/code recipesapplied
12SED in Shell Scripts and Pipelines — functions, safety, performance, portabilityapplied

SED Course Complete

All 12 chapters complete. You now have a thorough understanding of SED — from its processing model and core commands through to real-world scripting, pipeline integration, and performance-aware usage.