Control the Cycle

Learning SED

Chapter 4 — Delete, Print and Quit Commands

Three Commands That Control the Cycle

While the substitute command transforms text, the three commands in this chapter control something more fundamental — whether a line appears in the output at all, and when SED stops reading. Together with the -n flag, they give you precise control over what gets printed and when processing ends.

Command What it does Effect on cycle
d Delete the pattern space Skips the rest of the script and the default print; jumps immediately to the next cycle
p Print the pattern space Outputs the pattern space now (in addition to the default print at end of cycle)
q Quit after this line Prints the pattern space (unless -n), then exits immediately
Q Quit without printing Exits immediately without printing the pattern space (GNU sed)

The Delete Command: d

The d command deletes the pattern space and immediately starts the next cycle — it does not just suppress output, it aborts all remaining commands for the current line and jumps straight to reading the next line. No further commands in the script run after d fires.

Normal cycle (no d): read line → run commands → print → next line Cycle with d firing: read line → run commands → d fires → skip remaining commands → skip print → next line

Basic deletion

# Delete every line containing "TODO"
sed '/TODO/d' notes.txt

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

# Delete lines containing only whitespace
sed '/^[[:space:]]*$/d' file.txt

# Delete lines shorter than 3 characters (e.g. stray single-char lines)
sed '/^.\{0,2\}$/d' file.txt

# Delete the first line (strip a header row)
sed '1d' data.csv

# Delete the last line
sed '$d' file.txt

# Delete a range of lines
sed '3,7d' file.txt

Deleting comment lines and cleaning config files

# Remove shell-style # comments
sed '/^[[:space:]]*#/d' script.sh

# Remove both comments AND blank lines in one pass
sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' nginx.conf

# Remove inline comments (everything from # to end of line)
sed 's/[[:space:]]*#.*$//' config.txt
# Note: this uses s// not d — it removes the comment but keeps the line

Deleting between markers

# Delete a block including its opening and closing tags
sed '/<!-- BEGIN SKIP -->/,/<!-- END SKIP -->/d' page.html

# Delete from a pattern to end of file
sed '/^### Appendix/,$d' README.md

# Delete from line 1 to the first blank line (strip email headers)
sed '1,/^$/d' email.txt

Why d short-circuits the script

Because d aborts the rest of the script for that line, the order of your commands matters. A command placed after d in the same address block will never run on lines where d fires:

# The substitution on line 2 will NEVER run for lines matching /DEBUG/
sed '/DEBUG/ {
  d
  s/foo/bar/    # unreachable — d already ended this cycle
}' file.txt

# If you need to transform before deleting, put the transform first
sed '/DEBUG/ {
  s/DEBUG/INFO/   # runs first
  d               # then deletes (so the substitution is pointless here)
}' file.txt
# In practice, if you're deleting the line anyway, transforms before d are useless

The Print Command: p

The p command prints the current contents of the pattern space to standard output. Crucially, SED also prints the pattern space automatically at the end of each cycle (the default print). This means that without -n, using p causes each matching line to appear twice.

sed 'p' on a line "hello": p fires → prints "hello" ← explicit print end cycle → prints "hello" ← default print output: hello hello

This double-printing behaviour is not a bug — it becomes genuinely useful in specific patterns, and the fix when you don't want it is always -n.

The -n flag — suppressing the default print

The -n flag (sometimes called "silent mode" or "quiet mode") tells SED not to print the pattern space automatically at the end of each cycle. With -n active, the only output you get is from explicit p commands (or the p flag on s). This turns SED into a powerful line-extraction tool:

# Without -n: every line printed, matching lines printed twice
printf 'a\nb\nc\n' | sed '/b/p'
a
b
b
c

# With -n: only explicitly printed lines appear
printf 'a\nb\nc\n' | sed -n '/b/p'
b

Using -n with p for line extraction

# Print only line 7
sed -n '7p' file.txt

# Print lines 10 through 20
sed -n '10,20p' file.txt

# Print lines matching a pattern
sed -n '/^ERROR/p' logfile.txt

# Print lines matching either of two patterns (ERE alternation)
sed -En '/(ERROR|WARN)/p' logfile.txt

# Print a block between two markers (inclusive)
sed -n '/BEGIN/,/END/p' file.txt

# Print the last line (equivalent to tail -1)
sed -n '$p' file.txt

# Print every other line (odd lines)
sed -n '1~2p' file.txt

Using p intentionally without -n — showing before and after

Sometimes the double-print behaviour of p is deliberate. A classic use is showing what a substitution changed — print the original line with p first, then let the substitution and default print show the modified version:

# Show original and modified side by side for every changed line
sed '/foo/ { p; s/foo/bar/; }' file.txt
# Lines with "foo":
#   p prints the original    → "this foo line"
#   s replaces it            → "this bar line"
#   default print outputs it → "this bar line"
# Lines without "foo": only appear once (default print)

The p flag on the substitute command

As seen in Chapter 2, the p flag on s prints the pattern space when the substitution succeeds. Combined with -n, this is the cleanest way to see only lines that were actually changed:

# Print only lines where a substitution was made
sed -n 's/foo/bar/p' file.txt

# Practical: check which config values will change before committing with -i
sed -n 's/debug=true/debug=false/p' app.conf

The Line Number Command: =

Though not strictly a print variant, the = command is closely related — it prints the current line number to standard output, followed by a newline. It is worth covering here because it is most useful alongside p:

# Print line numbers for every line (like cat -n)
sed '=' file.txt | paste - -
# paste merges the number line and content line onto one line

# Print line numbers AND lines for lines matching a pattern (like grep -n)
sed -n '/ERROR/ { =; p }' logfile.txt
42
2024-01-15 ERROR connection refused
87
2024-01-15 ERROR disk full

# Count total lines in a file (print only the last line number)
sed -n '$=' file.txt
Counting lines without wc: sed -n '$=' file.txt is a handy alternative to wc -l file.txt. Unlike wc -l, it counts the number of newline-terminated lines without any trailing space or filename in the output — useful in scripts where you need a clean integer.

The Quit Commands: q and Q

The q command tells SED to stop processing after the current line. It prints the pattern space (unless -n is active) and exits. The Q command (GNU sed only) does the same but skips the final print — it exits silently.

sed 'q' at line 3 of a 100-line file: line 1: read → process → print → continue line 2: read → process → print → continue line 3: read → process → printq fires → EXIT lines 4–100: never read

Basic quit usage

# Print only the first 5 lines then stop (equivalent to head -5)
sed '5q' file.txt

# Print up to and including the first line matching a pattern, then stop
sed '/ERROR/q' logfile.txt

# Print the first line only (equivalent to head -1)
sed '1q' file.txt

# Extract just the HTTP status line from a curl response
curl -sI https://example.com | sed '1q'

Quit with an exit code

GNU sed allows an optional exit code after q and Q. This is useful in shell scripts where the exit status signals a result:

# Exit with code 1 if the pattern is NOT found (check for required config)
sed -n '/required_setting/q 0' config.txt; echo $?
# Exits 0 if "required_setting" is found, 1 otherwise (default)

The Q command — quit without printing

# Print lines 1 through 4, stop before line 5 (no print for line 5)
sed '5Q' file.txt
# With q: lines 1,2,3,4,5 are printed (q prints line 5 then quits)
# With Q: lines 1,2,3,4   are printed (Q quits before printing line 5)
The difference in practice:
  • Nq — prints lines 1 through N, then exits (equivalent to head -N)
  • NQ — prints lines 1 through N-1, then exits (like head -N minus one)
Use q when you want to include the triggering line in output; use Q when you want to stop just before it.

Performance: Why q and Q Matter on Large Files

SED reads files sequentially line by line. On a 500,000-line log file, if you only need the first match of a pattern, running sed -n '/pattern/p' reads all 500,000 lines even after finding the first match. Adding q stops as soon as the match is found:

# Slow on large files — reads the ENTIRE file even after finding the match
sed -n '/ERROR/p' huge.log | head -1

# Fast — stops reading as soon as the first match is found and printed
sed -n '/ERROR/ { p; q }' huge.log

# Even faster with Q — stops without the final print overhead
sed '/ERROR/ { p; Q }' huge.log
# (the p runs first, then Q exits — no default print attempted)
Always use q or Q when you only need the first match (or the first N lines) from a large file. The difference in speed can be dramatic — finding a match on line 100 of a million-line file using q is 10,000× faster than processing all million lines.

Combining d, p, and q

The real power emerges when these commands work together. Here are patterns you will use repeatedly:

grep-like filtering with SED

# Print only matching lines — equivalent to: grep "pattern" file
sed -n '/pattern/p' file.txt

# Print non-matching lines — equivalent to: grep -v "pattern" file
sed '/pattern/d' file.txt

# Print matching lines with line numbers — equivalent to: grep -n "pattern" file
sed -n '/pattern/ { =; p }' file.txt

head and tail equivalents

# First N lines — equivalent to: head -N file
sed 'Nq' file.txt          # replace N with a number

# Last N lines — equivalent to: tail -N file
# (requires knowing total line count or using a different approach)
lines=$(sed -n '$=' file.txt)
sed -n "$((lines - N + 1)),\$p" file.txt

# Skip first N lines — equivalent to: tail -n +N+1 file
sed '1,Nd' file.txt         # replace N with a number

Extracting a specific section

# Print lines 50–60 then stop (efficient — stops at line 60)
sed -n '50,60p'; '60q' file.txt

# Better: combine the address and quit in one expression
sed -n '50{ :loop; p; n; 60q; b loop }' file.txt

# Simplest: just use the range address — SED is smart enough
sed -n '50,60p' file.txt

Viewing a section of a log file around an error

# Print the first ERROR line and stop
sed -n '/ERROR/ { p; q }' app.log

# Print everything up to the first ERROR (exclusive) then stop
sed '/ERROR/Q' app.log

# Print everything from the first ERROR to end of file
sed -n '/ERROR/,${p}' app.log

Removing duplicate consecutive lines

# Delete a line if it is identical to the previous one
# (uses hold space — covered fully in Chapter 6)
sed '$!N; /^\(.*\)\n\1$/!P; D' file.txt
# Brief explanation: N appends next line; if the two lines match, D deletes
# the first without printing; P prints the first if they differ

Practical Recipes

Extract email addresses from a file

# Print only lines that look like email addresses
sed -En '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/p' contacts.txt

View only the active (uncommented) lines of a config

sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' /etc/ssh/sshd_config

Check if a pattern exists and exit appropriately

# Use in a shell script: exit 0 if pattern found, exit 1 if not
if sed -n '/required_key/q 0; $q 1' config.txt; then
  echo "Config OK"
else
  echo "ERROR: required_key missing from config"
fi

Print function signatures from a C or shell file

# Print lines that look like shell function declarations
sed -n '/^[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*()[[:space:]]*{/p' script.sh

# Print C function signatures (very simplified)
sed -n '/^[a-z].*([^;]*$/{/^#/d; p}' source.c

Fast search through a large compressed log

# Decompress on-the-fly and stop at first match
zcat archive.log.gz | sed -n '/CRITICAL/ { p; q }'

Strip a shebang line from a script

# Remove the first line if it starts with #!
sed '1{ /^#!/d }' script.sh

Command Interaction Summary

Command Prints pattern space? Continues script? Reads next line? POSIX?
d No No — aborts rest of script Yes (next cycle) Yes
p Yes (immediately) Yes — script continues Yes (at end of cycle) Yes
= No — prints line number Yes — script continues Yes (at end of cycle) Yes
q Yes (unless -n) No — exits SED No — SED terminates Yes
Q No No — exits SED No — SED terminates GNU only

Quick Reference — Chapter 4

Delete

d Delete pattern space; start next cycle immediately (no print)
/pattern/d Delete lines matching pattern
/pattern/!d Delete lines NOT matching pattern (keep only matches)
n,md Delete lines n through m

Print

p Print pattern space now (also prints at end of cycle unless -n)
-n … p With -n: only print explicitly — turns SED into a filter
= Print the current line number
sed -n '$=' Print total number of lines in file

Quit

q Print pattern space then exit (stop reading input)
Q Exit immediately without printing (GNU sed)
Nq Print first N lines then stop (like head -N)
q N Quit with exit code N (GNU sed)
/pat/ { p; q } Print first matching line then stop (efficient on large files)
What is coming next: Chapter 5 covers the a (append), i (insert), and c (change) commands — the tools for adding new lines to output and replacing entire lines or blocks. These are essential for tasks like inserting file headers, adding config directives, and replacing stale blocks of content.