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.
file / cmd
filter lines
transform text
extract fields
aggregate
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
! 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
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-pattern | Problem | Fix |
|---|---|---|
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:
| Feature | GNU sed | BSD 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 | -E | Use -E (not -r) |
\+ \? in BRE | Supported | Not supported | Use -E with + ? |
\| alternation BRE | Supported | Not supported | Use -E with | |
\w \s \b | Supported | Not supported | Use [[:alnum:]_] etc. |
\t in regex | Supported | Not supported | Use $'\t' or POSIX [[:blank:]] |
a\ append | Both forms | POSIX backslash form only | Use a\ text (backslash-newline) |
T branch-if-not-substituted | Supported | Not supported | No portable equivalent — use t with negation |
z zap pattern space | Supported | Not supported | Use s/.*//' or d |
R W commands | Supported | Not supported | No 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
awkorbc. - The regex would need lookahead/lookbehind — SED has no PCRE. Use
grep -P,perl, orpython. - 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.
Quick Reference — Chapter 12
SED in Pipelines
Safe Scripting Habits
Course Complete — Full Chapter Index
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.