When you need to quickly modify the content of several files, perform mass
replacements or delete some lines based on a pattern, sed is the essential
tool. Designed for stream editing, sed searches, replaces, deletes or
inserts text straight from the command line, without opening the files by
hand.
What you will learn
- Replace text with the s/old/new/ command
- Delete, insert and modify targeted lines
- Use addresses (line numbers, patterns) to target precisely
- Edit files in place with
-i(safely)
The sed command in the Linux ecosystem
sed belongs to the text-processing commands of Linux. Each tool has its
speciality, and sed is the one that edits a stream:
| Command | Speciality | When to use it |
|---|---|---|
sed | Edit text as a stream | Replace, delete, insert text |
grep | Search for patterns | Filter the lines matching a pattern |
awk | Process tabular data | Extract/transform columns |
tr | Transliterate characters | Replace character by character |
cut | Extract columns | Split by delimiter or position |
find | Search for files | Find files by attributes |
Frequent combinations:
# grep + sed: filter then transformgrep "ERROR" log.txt | sed 's/ERROR/ALERTE/g'
# sed + awk: modify then extractsed 's/;/,/g' data.csv | awk -F',' '{print $2}'
# find + sed: modify several filesfind . -name "*.conf" -exec sed -i 's/old/new/g' {} +When sed is the right tool (and when to avoid it)
sed excels on unstructured text, line by line. Its limit is also its
definition: it ignores any structure. As soon as the data is hierarchical or
nested, sed works blind and produces fragile results. The right reflex is to
pick the tool that understands the format.
| Data to process | Avoid sed, use | Why |
|---|---|---|
| JSON | jq | jq knows the structure; sed breaks on nesting, spaces and key order |
| YAML | yq (mikefarah) | indentation and anchors are meaningful, invisible to a string substitution |
| HTML / XML | xmllint, xmlstarlet, pup | HTML is not a regular language: a regex cannot follow nesting |
| Multi-line / logic | awk or perl -0777 | fields, variables and state memory that sed lacks |
The rule fits in one sentence: sed for plain text, a dedicated tool for
structured data. On a minimal server without jq, a one-off sed on JSON
can help out, but never make it a production building block.
Understand the sed command in 2 minutes
The mental model below sums up how sed works: an input, a
command, an output.
🧠 Mental model: How sed works
sed = Read a line → Apply the commands → Print the result
Key points
- sed reads the file line by line (a stream, not all in memory)
- Each line goes through the commands in order
- By default, sed prints the result on stdout (the file is not modified)
- The s/old/new/ command replaces text
- Without the g flag, only the first occurrence per line is replaced
- With -i, sed modifies the file directly (danger!)
Golden rules
Essential vocabulary
- s/old/new/
- Substitution: replaces old with new (1st occurrence)
- s/old/new/g
- Global substitution (every occurrence)
- d
- Delete: deletes the line
- p
- Print: prints the line
- -i
- In-place: modifies the file directly
- -n
- No-print: prints nothing except with p
📚 Going further: 12 advanced options
- a\text
- Append: adds after the line
- i\text
- Insert: inserts before the line
- c\text
- Change: replaces the whole line
- /regex/
- Address: targets the lines matching the regex
- 1,10
- Range: lines 1 to 10
- /start/,/end/
- Range between two patterns
- -E
- Extended regex (ERE): +, ?, |, () without escaping
- &
- In s///, stands for the matched text
- \1, \2
- Backreferences: captured groups
- y/abc/xyz/
- Transliteration (like tr)
- q
- Quit: stops processing
- N
- Next: appends the next line to the buffer
sed reads the file line by line (a stream), applies the requested
transformations, then prints the result. By default, the original file is not
modified.
Minimal syntax
Every sed invocation fits in this form. Remember above all the single
quotes around the command: they stop the shell from interpreting $, * and
\ before sed sees them. With double quotes, sed "$d" becomes an empty
command, which copies the file without deleting anything, because bash will
first have replaced $d with a non-existent variable. The file is optional:
without it, sed reads standard input, which makes it a natural pipeline
link.
sed [OPTIONS] 'COMMANDE' [FICHIER]The 3 parts of a sed command
This breakdown serves for diagnosis: when a sed command does nothing, the
error almost always sits in a single one of these three parts. The Required
column flags the point most often forgotten: the default output is
stdout, so a sed that "does not modify the file" behaves normally, it is
simply missing the -i option.
| Step | Question | Examples | Required? |
|---|---|---|---|
| Input | Where does the text come from? | fichier.txt, stdin (pipe) | One of the two |
| Command | Which transformation? | s/old/new/g, d, p | ✓ Yes |
| Output | Where does the result go? | stdout (default) or file (-i) | ✗ No (default: stdout) |
The s/// substitution command
The most used command is s (substitution), which looks for a pattern
and replaces it with a text:
sed 's/ancien/nouveau/' fichier.txt| Element | Meaning |
|---|---|
s | Substitution command |
/ | Delimiter (can be #, @, |) |
ancien | Text or regex to search for |
nouveau | Replacement text |
By default, only the first occurrence per line is replaced. To replace
every occurrence, add the g flag:
sed 's/foo/bar/g' fichier.txtThe other main commands
Beyond substitution, sed has a handful of one-letter commands. Two of them
behave differently from the others: p duplicates the line instead of
printing it alone, because sed already prints each line by default; it is
therefore always paired with the -n option. And q stops reading the
file, which makes it the most efficient command to extract the first lines of a
multi-gigabyte file.
| Command | Effect | Example |
|---|---|---|
d | Deletes the line | /debug/d |
p | Prints the line | -n '/error/p' |
i\texte | Inserts before the line | 1i\# En-tête |
a\texte | Appends after the line | $a\# Fin |
c\texte | Replaces the whole line | 3c\Nouvelle ligne |
q | Quits after this line | 10q |
Addresses: target specific lines
By default, sed applies the command to every line. You can target with
addresses, placed before the command:
| Address | What it targets | Example |
|---|---|---|
5 | Line number 5 | sed '5d' f.txt |
$ | Last line | sed '$d' f.txt |
/pattern/ | Lines containing "pattern" | sed '/error/d' f.txt |
1,10 | Lines 1 to 10 | sed '1,10d' f.txt |
/début/,/fin/ | Range between two patterns | sed '/BEGIN/,/END/d' f.txt |
Substitution flags
Flags go after the last delimiter and stack up (gi replaces everywhere
regardless of case). The numeric flag is the least known and the most useful on
logs: s/a/X/2 only touches the second occurrence of each line. It also
combines with g in the form s/a/X/2g, which replaces from the second
occurrence to the end of the line. Watch the i flag: it is a GNU
extension, absent from POSIX.
| Flag | Effect | Example |
|---|---|---|
g | Global (every occurrence) | s/a/b/g |
i | Case-insensitive | s/error/warning/gi |
p | Prints if a substitution was made | -n 's/a/b/p' |
2 | Replaces the 2nd occurrence | s/a/b/2 |
Alternative delimiters
When the text contains /, use another delimiter. The character placed
right after the s becomes the delimiter, whatever it is: sed reserves
nothing for the slash. Compare the two forms below, they do exactly the same
thing; only the second stays readable six months later.
# ✗ Unreadable and error-pronesed 's/\/var\/log/\/opt\/logs/g' fichier.txt
# ✓ Much clearer with #sed 's#/var/log#/opt/logs#g' fichier.txtIn-place editing with -i
By default, sed prints the result on stdout without modifying the file.
To modify it directly, add -i, ideally with a backup suffix:
# Modifies the file (DANGER on error)sed -i 's/old/new/g' fichier.txt
# ✓ Creates a backup before modifyingsed -i.bak 's/old/new/g' fichier.txtGolden rule: always test without -i first!
# 1. Testsed 's/old/new/g' fichier.txt
# 2. Check the result, then only:sed -i.bak 's/old/new/g' fichier.txtTypical errors (and fixes)
These four errors account for most "my sed does not work" tickets. All of
them are diagnosed the same way: rerun the command without -i and look at
the output. The most misleading case is the last one, "nothing happens": sed
never reports the absence of a match and returns a 0 return code, so a
script that does not check its result goes on as if the change had been made.
| Error | Cause | Fix |
|---|---|---|
| Only the 1st occurrence is replaced | Missing g flag | Add g: s/old/new/g |
| Syntax error with paths | / in the text | Use # as the delimiter |
| File modified by mistake | -i without a prior test | Always test without -i first |
| Nothing happens | No match (case?) | Check with grep or use i (insensitive) |
Common substitution patterns
Now that you understand the logic of sed, here are sixteen ready-to-use
patterns. Each one gives its general formula and a concrete example.
Simple substitution
Replace the first occurrence of a text on each line. It is the basic
form, and also the most common trap: without the g flag, the following
occurrences on the same line stay intact.
sed 's/ancien/nouveau/' <fichier> # formulased 's/erreur/warning/' log.txt # exampleancien: text or regex to search fornouveau: replacement text
Global substitution
Replace every occurrence on each line. The g flag acts inside a
line, not on the whole file: sed already processes every line by default.
sed 's/ancien/nouveau/g' <fichier> # formulased 's/foo/bar/g' fichier.txt # exampleg: global flag, every occurrence of the line
Case-insensitive substitution
Replace a text whatever its case. This flag is a GNU extension absent from POSIX, so avoid it in a portable script meant to run on macOS or on an embedded system.
sed 's/ancien/nouveau/gi' <fichier> # formulased 's/error/warning/gi' log.txt # examplei: case-insensitive flag (GNU sed)
Alternative delimiter
Avoid escapes when the text contains /. Pick a character absent from
both the pattern and the replacement, otherwise you move the problem instead
of solving it.
sed 's#ancien#nouveau#g' <fichier> # formulased 's#/var/log#/opt/logs#g' paths.txt # example#: alternative delimiter (#,@, etc.) instead of/
Delete lines by pattern
Delete the lines containing a pattern. The deletion covers the whole line as soon as a match is found anywhere in it, not only at the start of the line.
sed '/motif/d' <fichier> # formulased '/DEBUG/d' log.txt # examplemotif: regex pattern to matchd: delete command
Delete empty lines
Clean up a file by removing the empty lines. Watch out, ^$ only matches
strictly empty lines: a line containing a space or a tab survives, you
then need /^[[:space:]]*$/d.
sed '/^$/d' <fichier> # formulased '/^$/d' texte.txt # example^$: regex of an empty line (start = end)
Target by line number
Apply a command only to a specific line. The number is counted over all
the files passed as arguments, not per file: with several files, line 5 only
exists once, except with the -s or -i option, which process each file
separately.
sed 'N<commande>' <fichier> # formulased '5d' fichier.txt # example: deletes line 5N: targeted line number
Range of lines
Apply a command to a set of lines. Both bounds are included, and $ can
replace the upper bound to go to the end of the file (2,$d).
sed 'N,M<commande>' <fichier> # formulased '2,5d' fichier.txt # example: deletes lines 2 to 5N,M: from line N to line M inclusive
Range between two patterns
Target the lines located between two patterns. Both delimiting lines are included in the range. If the end pattern never appears, the range runs to the end of the file.
sed '/début/,/fin/<commande>' <fichier> # formulased '/BEGIN/,/END/d' fichier.txt # example/début/,/fin/: range delimited by two patterns
In-place editing
Modify the file directly. No confirmation is requested and no going back
is possible: always run the command without -i first to check the
output.
sed -i '<commande>' <fichier> # formulased -i 's/old/new/g' config.conf # example-i: in-place, modifies the file. Always test without-ifirst.
In-place editing with a backup
Modify a file while keeping a backup copy. The suffix is glued to the
option, without a space: -i.bak works, -i .bak fails, because GNU sed then
takes .bak for the script and answers unknown command: '.'.
sed -i.bak '<commande>' <fichier> # formulased -i.bak 's/old/new/g' config.conf # example-i.bak: createsfichier.bakbefore modifying
Insert a line
Add text before a line. The condensed form on a single line is a GNU extension; the POSIX version requires a line break after the backslash.
sed '<adresse>i\<texte>' <fichier> # formulased '1i\# En-tête ajouté' fichier.txt # examplei\: insert command, adds before the targeted line
Append after a line
Add text after a line. Combined with the $ address, it adds a line at the
end of the file, a handy move to complete a configuration without
opening it in an editor.
sed '<adresse>a\<texte>' <fichier> # formulased '/END/a\# Commentaire' fichier.txt # examplea\: append command, adds after the targeted line
Print only the matching lines
Extract the lines of a pattern, like grep. The -n option goes
hand in hand with p: without it, sed already prints each line and the matched
lines appear twice.
sed -n '/motif/p' <fichier> # formulased -n '/ERROR/p' log.txt # example-n: disables automatic printingp: print command, prints the line
Backreferences (captured groups)
Reuse a captured part in the replacement. In basic regex, group
parentheses are escaped (\( and \)); with the -E option, they are
written without a backslash.
sed 's/\(motif\)/\1 ajout/' <fichier> # formulased 's/\([0-9]*\)/ID:\1/' fichier.txt # example\(\): capture group\1: reference to the first captured group
Reuse the matched text (&)
Insert the matched text in the replacement with &. To insert a
literal &, escape it as \&, otherwise sed copies the matched text in its
place.
sed 's/motif/[&]/g' <fichier> # formulased 's/[0-9]\+/(&)/g' fichier.txt # example&: stands for the whole matched text
Pitfalls to avoid
Seven errors come back constantly with sed. For each one: the faulty
command, the symptom, the cause and the correction.
Forgetting the g flag
Without the g flag, sed stops at the first occurrence of each
line.
sed 's/foo/bar/' fichier.txt # ✗ only replaces the 1st occurrence per lineSymptom: some occurrences are not replaced. Cause: without g, sed
only touches the first occurrence of each line.
sed 's/foo/bar/g' fichier.txt # ✓ global flagSlash in the text to replace
A slash in the text collides with the default delimiter of the s
command.
sed 's//var/log//opt/logs/' fichier.txt # ✗ syntax errorSymptom: unknown option to 's' error or unexpected result. Cause: the
/ of the text are taken for delimiters.
sed 's#/var/log#/opt/logs#g' fichier.txt # ✓ alternative delimiter-i editing without a preview
-i writes straight to the file, without any undo.
sed -i 's/important/supprimé/g' fichier.conf # ✗ run without a testSymptom: lost data, corrupted file. Cause: -i modifies the file
without any possible going back.
sed 's/old/new/g' fichier # ✓ test first, then sed -i.bakUnescaped regex metacharacter
The dot of a file name is a regex metacharacter in the pattern.
sed 's/fichier.txt/fichier.log/' fichier # ✗ the . matches any characterSymptom: unexpected matches (fichierXtxt is matched too). Cause: the
dot . is a regex metacharacter.
sed 's/fichier\.txt/fichier.log/g' fichier # ✓ escaped dot-i incompatibility between GNU and BSD
The same -i does not take the same argument on GNU and BSD.
sed -i 's/old/new/' fichier # ✗ fails on macOS (BSD sed)Symptom: invalid command code error on macOS. Cause: BSD sed requires
an extension after -i, even an empty one.
sed -i '' 's/old/new/' fichier # ✓ on macOS, or install GNU sedLine break in the replacement
\n in the replacement is not defined by POSIX.
sed 's/$/\n/' fichier # ✗ works with GNU sed, not guaranteed elsewhereSymptom: the command does produce an empty line after each line on a Linux
server, then a literal n appears on a colleague's macOS workstation.
Cause: POSIX does not define the meaning of \n in the replacement part.
GNU sed reads it as a real line break, BSD implementations do not
guarantee it. For a script that must run everywhere, inject the line break from
the shell.
sed 's/$/\'$'\n''/' fichier # ✓ portable: the line break comes from bash ($'\n')Greedy regex (.* too broad)
.* is greedy and swallows as much text as it can.
sed 's/<.*>//g' fichier # ✗ deletes from the first < to the last >Symptom: <a>texte<b> becomes empty instead of texte. Cause: .* is
greedy, it matches as much as possible.
sed 's/<[^>]*>//g' fichier # ✓ negated class: stops at the first >sed -i breaks symbolic links
-i replaces the path it receives, even when it is a symbolic
link.
sed -i 's/old/new/' /etc/service/config # ✗ if config is a symlink, the link is lostSymptom: the symbolic link disappears, replaced by a regular file, and the
original target stays unchanged. Cause: -i writes a temporary file then
renames it onto the path, which destroys the link.
sed -i --follow-symlinks 's/old/new/' /etc/service/config # ✓ GNU: follows the linkThe \d false friend
\d does not mean digit in sed.
sed 's/\d/X/g' fichier # ✗ \d does NOT mean "digit" in sedSymptom: letters are replaced instead of digits (\d matches the letter
d). Cause: unlike PCRE regex, sed does not know \d. Fix: the
portable POSIX class.
sed 's/[[:digit:]]/X/g' fichier # ✓ or [0-9]Windows line endings (CRLF)
A Windows file hides a \r at the end of each line.
sed 's/local$/remote/' config.txt # ✗ fails if the file is in CRLFSymptom: no replacement although the pattern looks right. Cause: an
invisible \r stays at the end of the line, between local and the real end,
so the local$ pattern never matches. Fix: remove the \r first.
sed 's/\r$//' config.txt # ✓ normalises to LF (GNU)Hands-on lab
Nothing replaces practice. Create the test environment below, then go
through the eight steps. At the end, clean up with rm -rf ~/sed-lab.
Prepare the ground
This script creates five test files in ~/sed-lab: a
configuration, a log, a CSV, a list of paths and a "draft" file to clean up.
# Create the labmkdir -p ~/sed-labcd ~/sed-lab
# Typical configuration filecat > config.conf << 'EOF'# Configuration serveurserver_host=localhostserver_port=8080debug_mode=truelog_level=INFOdatabase_url=postgres://localhost:5432/mydbmax_connections=100EOF
# Log filecat > access.log << 'EOF'192.168.1.10 - - [15/Jan/2025:10:30:00] "GET /index.html HTTP/1.1" 200 1234192.168.1.20 - - [15/Jan/2025:10:30:05] "POST /api/users HTTP/1.1" 201 567192.168.1.10 - - [15/Jan/2025:10:30:10] "GET /style.css HTTP/1.1" 200 890192.168.1.30 - - [15/Jan/2025:10:30:15] "GET /error HTTP/1.1" 404 123192.168.1.10 - - [15/Jan/2025:10:30:20] "DELETE /api/users/5 HTTP/1.1" 403 45EOF
# CSV filecat > users.csv << 'EOF'id,nom,email,role1,Alice,alice@example.com,admin2,Bob,bob@example.com,user3,Charlie,charlie@example.com,user4,Diana,diana@example.com,moderatorEOF
# File with pathscat > paths.txt << 'EOF'/var/log/syslog/home/user/documents/report.pdf/etc/nginx/nginx.conf/usr/local/bin/script.shEOF
# File to clean upcat > messy.txt << 'EOF' Ligne avec espaces au débutLigne normale Autre ligne avec espacesLigne vide après
Encore du texte Espaces partoutEOF
echo "✓ Lab créé dans ~/sed-lab"ls -la ~/sed-labThe eight steps
These eight commands run without -i except the seventh: they print the
result on screen and leave the lab files intact. Get into the habit of comparing
the output with the original file with diff <(sed ...) fichier before moving to
the next step. The progression goes from simple replacement to targeting
by address, which is the real skill to acquire: knowing how to restrict a
transformation to a few lines avoids damage to configuration files.
-
Simple substitution. Replace
localhostwith127.0.0.1inconfig.conf.Terminal window sed 's/localhost/127.0.0.1/' ~/sed-lab/config.confs/ancien/nouveau/replaces the first occurrence on each line. For every occurrence: add thegflag. -
Alternative delimiter. Replace
/var/logwith/opt/logsinpaths.txt.Terminal window sed 's#/var/log#/opt/logs#' ~/sed-lab/paths.txtWhen the text contains
/, use another delimiter (#here). -
Delete lines. Delete the lines containing
404inaccess.log.Terminal window sed '/404/d' ~/sed-lab/access.log/motif/ddeletes the lines that contain the pattern. -
Delete empty lines. Remove the empty lines of
messy.txt.Terminal window sed '/^$/d' ~/sed-lab/messy.txt^$matches an empty line (start immediately followed by end). -
Target a line by number. Replace
userwithadminonly on line 3 ofusers.csv.Terminal window sed '3s/user/admin/' ~/sed-lab/users.csvThe number placed before the command restricts it to that line.
-
Range of lines. Replace
@example.comwith@company.orgon lines 2 to 4 ofusers.csv.Terminal window sed '2,4s/@example.com/@company.org/' ~/sed-lab/users.csvN,Mtargets the lines from N to M inclusive. -
In-place editing with a backup. Modify
config.confby replacingtruewithfalse, keeping a backup.Terminal window sed -i.bak 's/true/false/' ~/sed-lab/config.conf-i.bakmodifies the file and createsconfig.conf.bakbeforehand. -
Remove leading spaces. Remove the spaces at the start of each line of
messy.txt.Terminal window sed 's/^[[:space:]]*//' ~/sed-lab/messy.txt^[[:space:]]*matches zero or more spaces at the start of the line.
Progressive exercises
Eight exercises to validate your mastery. All of them work on the files of
the ~/sed-lab lab. Search on your own first, then unfold the solution
to check yourself.
Foundation level
These first three exercises only use one command at a time: substitute, change the delimiter, delete lines. Write your answer before opening the solution, then run it on the lab: a command that looks right on paper often fails on a delimiter or escaping detail.
Exercise 1, basic substitution. Replace every error with warning in access.log. Hint: do not forget the g flag.
Show the solution
sed 's/error/warning/g' ~/sed-lab/access.logThe g flag replaces every occurrence, not only the first one of each line.
Exercise 2, replace paths. Replace /usr/local with /opt in paths.txt. Hint: the / of the text get in the way of the default delimiter.
Show the solution
sed 's#/usr/local#/opt#g' ~/sed-lab/paths.txt# is used as the delimiter to avoid escaping the /.
Exercise 3, delete comments. Delete every line starting with # in config.conf. Hint: ^ stands for the start of line, d deletes.
Show the solution
sed '/^#/d' ~/sed-lab/config.conf/^#/ matches the lines starting with #; d deletes them.
Composition level
The next three exercises combine a regex and an address. It is the hard
part: you have to describe a position (end of line, line number) and no
longer just a word. If your command returns nothing, first check the pattern
alone with grep before blaming sed.
Exercise 4, clean up spaces. Remove the trailing spaces of messy.txt. Hint: [[:space:]] matches spaces, $ the end of line.
Show the solution
sed 's/[[:space:]]*$//' ~/sed-lab/messy.txt[[:space:]]*$ matches zero or more spaces right before the end of the line.
Exercise 5, modify a specific line. Change the whole content of line 3 of users.csv to MODIFIÉ. Hint: c\ replaces a whole line.
Show the solution
sed '3c\MODIFIÉ' ~/sed-lab/users.csv3c\ targets line 3; c\ replaces its whole content.
Exercise 6, extract lines (like grep). Print only the lines of access.log containing 404. Hint: -n disables automatic printing, p prints the matched lines.
Show the solution
sed -n '/404/p' ~/sed-lab/access.log-n cuts the default printing; /404/p only prints the matching lines.
Advanced level
The last two exercises touch on the moves found in production scripts:
prefixing a stream and modifying a file in place. The second is the only
one of the series that writes to disk; check that the .bak file is there
after running it, it is the proof that the safety net worked.
Exercise 7, add a prefix to each line. Add LOG: at the start of each line of access.log. Hint: ^ matches the start of line.
Show the solution
sed 's/^/LOG: /' ~/sed-lab/access.logs/^/LOG: / replaces the "start of line" with LOG: , so it prefixes each line.
Exercise 8, safe in-place editing. Replace localhost with 127.0.0.1 in config.conf while keeping a backup. Hint: -i.bak creates the backup automatically.
Show the solution
sed -i.bak 's/localhost/127.0.0.1/g' ~/sed-lab/config.conf-i.bak modifies the file in place and creates config.conf.bak as a safety net.
Troubleshooting
When a sed substitution does not work, the problem most often comes from the
pattern or the delimiters. Here is how to diagnose it, then how to
read the error messages.
Diagnostic methods
These four commands run in order: they progressively isolate the origin of the
problem. The third one, sed -n '/pattern/=', prints the matched line
numbers and answers the only question that matters at first: does the pattern
match anything? If it returns nothing, the problem is in the regex, not in
the substitution. If it returns numbers but the replacement does not show up,
the error is in the right-hand part of the s///.
# Always preview before using -ised 's/old/new/g' fichier
# See which lines a pattern matchessed -n '/pattern/p' fichier
# Print the numbers of the matched linessed -n '/pattern/=' fichier
# Validate the pattern on its own with grepgrep 'pattern' fichierFrequent errors
Unlike the absence of a match, these situations produce an explicit error
message and a non-zero return code, so a script with set -e stops on
them. Read the message in full: sed gives the faulty character number in
the expression, which immediately locates the missing delimiter.
| Error message | Probable cause | Fix |
|---|---|---|
unterminated 's' command | Missing closing delimiter | Make sure you have exactly 3 delimiters: s/ancien/nouveau/ |
unknown option to 's' | Unescaped / in the text, read as a delimiter | Change the delimiter: s#ancien#nouveau# |
unknown command: 'X' | Unknown command letter, or misplaced script (-i .bak) | Check the command letter and glue the suffix: -i.bak |
invalid option -- 'Z' | Option that does not exist in this sed | Check sed --version; on macOS, install GNU sed |
no input files | -i without a file: in-place editing does not apply to a pipe | Pass the file as an argument |
invalid command code | Message of macOS's BSD sed, often -i without a suffix | Write sed -i '' on macOS, or install GNU sed |
sed in DevOps workflows
sed is everywhere in automation, but a few reflexes avoid incidents in CI
and in configuration management, starting with portability.
The GNU versus BSD trap in CI
sed -i 's/a/b/' fichier works on Linux (GNU sed) but fails on macOS
(BSD sed), where -i expects a backup suffix right after it. A pipeline
that runs on both Linux and macOS runners must use a portable form:
sed -i.bak 's/a/b/' fichier && rm fichier.bak # portable GNU + BSD# or, even more robust:perl -pi -e 's/a/b/' fichiersed -i is not idempotent: prefer Ansible
In configuration management, a sed -i run through the shell module is
replayed on every run and always reports changed: Ansible cannot know
whether the change is already applied, and a line addition ends up
duplicated. To edit a configuration file durably, the lineinfile and
replace modules are declarative and idempotent, handle
--check/--diff and create a backup. Keep sed for one-off
transformations, not for the state of a server.
Containers and templating
In a Dockerfile, a RUN sed -i freezes the change in an image layer.
Prefer configuring at runtime (ENV, variables, entrypoint script) so that a
single image serves every environment. To inject variables into a file
(${VAR}), envsubst (in French) is
safer than sed (no delimiter problem, no injection risk); as soon as you need
conditions or loops, move to a real template engine such as Jinja2.
sed cheat sheet
The five tables that follow gather what is used daily, sorted by nature: options, addresses, commands, regex, delimiters. The entries marked GNU do not exist on macOS nor on embedded BusyBox systems, to check before slipping them into a shared script.
Syntax and options
Options go before the command. Remember the -n and p pair: it is the
combination that turns sed into an extraction tool. The -E option also
deserves your attention, because it switches to extended regex and removes
tedious escapes: \(...\) becomes (...), and \+ becomes +.
| Element | Role | Example |
|---|---|---|
sed 'cmd' fichier | Applies a command to the file | sed 's/a/b/' f.txt |
sed -e 'cmd1' -e 'cmd2' | Chains several commands | sed -e 's/a/b/' -e 's/c/d/' |
cmd | sed 'cmd' | Processes the input of a pipe | cat f.txt | sed 's/a/b/' |
-i | In-place editing (modifies the file) | sed -i 's/a/b/' f.txt |
-i.bak | In-place editing with a backup | sed -i.bak 's/a/b/' f.txt |
-n | Quiet mode (nothing by default) | sed -n '/err/p' f.txt |
-E (or -r) | Enables extended regex (ERE) | sed -E 's/[0-9]+/X/g' |
-f script.sed | Reads the commands from a file | sed -f cmds.sed fichier |
Addresses (targeting)
The address goes in front of the command and restricts its scope. Only one
entry of the table is a GNU extension absent from POSIX: 1~2, which
selects every other line. Negation ! on the other hand is standard, and it is
the most useful in practice: /DEBUG/!d keeps only the debug lines, which
avoids writing the inverse regex, often impossible to phrase cleanly.
| Address | Role | Example |
|---|---|---|
5 | Line number 5 | 5s/a/b/ |
$ | Last line | $d |
/regex/ | Lines matched by the regex | /error/d |
1,10 | Lines 1 to 10 | 1,10s/a/b/ |
/début/,/fin/ | Range between two patterns | /BEGIN/,/END/d |
1~2 | Line 1 then every other line (GNU) | 1~2d |
! | Inverts the address (NOT) | /DEBUG/!d |
Main commands
The three insertion commands i\, a\ and c\ are the ones that cause the
most portability problems: GNU accepts the text on the same line
(1i\# Header), whereas the strict POSIX form requires a line break after
the backslash. If an insertion script fails on another system, that is almost
always where the problem comes from.
| Command | Role | Example |
|---|---|---|
s/old/new/ | Substitutes (1st occurrence) | s/error/warning/ |
s/old/new/g | Substitutes (every occurrence) | s/foo/bar/g |
s/old/new/gi | Substitutes regardless of case | s/error/warning/gi |
d | Deletes the line | /^#/d |
p | Prints the line | -n '/error/p' |
q | Quits after this line | 10q |
= | Prints the line number | /error/= |
i\texte | Inserts a line before | 1i\# Header |
a\texte | Appends a line after | $a\# Footer |
c\texte | Replaces the whole line | 3c\Nouvelle ligne |
Regex and references
sed uses basic regex (BRE) by default, not the PCRE syntax of programming
languages. Concretely, group parentheses are written \(...\) and not (...).
GNU does accept \w and \s, but they are non-portable extensions, and
\d does not mean "digit" there: prefer the POSIX classes [[:digit:]],
[[:alnum:]] and [[:space:]], which work on every implementation. The -E
option switches to extended regex and brings back parentheses without
backslashes.
| Element | Role | Example |
|---|---|---|
& | The matched text, in the replacement | s/[0-9]*/(&)/ |
\1, \2… | Captured groups | s/\(.*\)/[\1]/ |
\(\) | Capture group (basic regex) | s/\(mot\)/[\1]/ |
^ / $ | Start / end of line | s/^/PREFIX: / |
. | Any character | s/a.c/X/ |
[abc] / [^abc] | Class / negated class | s/[^0-9]//g |
Alternative delimiters
The character that immediately follows the s becomes the delimiter: sed
does not impose the /, it is a mere convention. Pick a character absent from
the pattern and the replacement, otherwise you are back to the original problem.
# suits file paths, @ or , suit URLs containing //. A
well-chosen delimiter removes every backslash and keeps the command readable six
months later.
| Form | Role | Example |
|---|---|---|
s#old#new# | # delimiter (ideal for paths) | s#/var#/opt#g |
s@old@new@ | @ delimiter | s@http://@https://@g |
s|old|new| | | delimiter | s|foo|bar|g |
Mastery checklist
You master sed when you can tick each of these points, from substitution
to in-place editing and diagnosis:
- I can use
s/old/new/to substitute - I can use the
gflag to replace globally - I can use an alternative delimiter (
#,@) - I can delete lines with
d - I can target lines by number or by pattern
- I can use
-nandpto extract lines - I can edit in place with
-iand-i.bak - I can insert or append a line with
i\anda\ - I can use backreferences (
\1,&) - I can diagnose a substitution that does not work
Key points
sededits plain text line by line:s///substitution,ddeletion, addresses to target.- The
gflag replaces every occurrence of a line; without it, only the first one. - Always preview without
-i, then-i.bakto keep a safety net. - For structured data, change tools:
jq(JSON),yq(YAML),awkorperl(multi-line). - In CI, beware of GNU versus BSD (
-i); in configuration management, prefer Ansiblelineinfile(idempotent). - Classic traps: symlink broken by
-i, the\dfalse friend, CRLF line endings.
Knowledge check
Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: xargs: build commands from input
A new full quiz with random questions
Work again only on the questions you missed
FAQ: frequently asked questions
The answers below cover the questions that come up most often about sed, from
substitution to in-place editing and regular expressions.
How it works
Sed reads the text line by line, applies transformations, and prints the result. By default, the original file stays unchanged (except with-i).Main commands
| Command | Action | Syntax | Example |
|---|---|---|---|
s |
Substitution | s/ancien/nouveau/ |
sed 's/foo/bar/' file.txt |
d |
Delete | [address]d |
sed '3d' file.txt |
i |
Insert (before the line) | [address]i\\text |
sed '1i\\Header' file.txt |
a |
Append (after the line) | [address]a\\text |
sed '5a\\Footer' file.txt |
c |
Change (replace the line) | [address]c\\text |
sed '2c\\New' file.txt |
p |
[address]p |
sed -n '/error/p' file.txt |
sed vs classic editors
| Criterion | sed | vim/nano | Advantage |
|---|---|---|---|
| Interaction | Non-interactive | Interactive | sed = automation |
| Processing | Stream (stdin/stdout) | File in memory | sed = pipelines |
| Volume | GB+ files | Limited by RAM | sed = large files |
| Automation | Shell scripts | Limited macros | sed = CI/CD |
Everyday use cases
# Config: replace a parameter
sed 's/DEBUG/INFO/' app.conf
# Logs: delete empty lines
sed '/^$/d' app.log
# CSV: replace the separator
sed 's/;/,/g' data.csv
# Scripts: update paths
sed 's/\/old\/path/\/new\/path/' deploy.sh
# HTML: inject content
sed '/<body>/a\\<script src="analytics.js"></script>' index.html
Why sed is essential
- DevOps: automatic changes in CI/CD
- SysAdmin: log cleanup, config updates
- Data Engineering: CSV/JSON transformation
- Scripting: integration into Unix pipelines
Main uses
sed transforms text automatically without an interactive editor, straight in the terminal or in scripts.DevOps/SysAdmin scenarios
| Context | Operation | sed command |
|---|---|---|
| Configuration | Change a port in a config | sed 's/port: 8080/port: 9090/' app.yaml |
| Logs | Delete empty lines | sed '/^$/d' app.log |
| Deployment | Update paths | sed 's/\/old\/path/\/new\/path/g' script.sh |
| Data | Convert a CSV | sed 's/;/,/g' export.csv |
| Templating | Inject variables | sed 's/{{VERSION}}/1.2.3/' deploy.yaml |
Concrete examples
# Automatic version update in package.json
sed -i 's/"version": "1.0.0"/"version": "1.1.0"/' package.json
# Log cleanup (remove DEBUG)
sed '/DEBUG/d' app.log > clean.log
# Bulk change of Nginx configs
find /etc/nginx -name '*.conf' -exec sed -i 's/80/8080/' {} \;
# Report generation: replace a placeholder
sed 's/{{DATE}}/2026-01-08/' template.html > report.html
Why sed rather than an editor?
- Automation: fits into CI/CD scripts
- Performance: processes multi-GB files
- Pipelines: usable with
|(grep, awk, sort...) - Non-interactive: no need to open the file
General structure
sed [OPTIONS] 'COMMANDE' [FICHIER]
Or with a pipe:commande | sed 'COMMANDE'
Main options
| Option | Effect | Example |
|---|---|---|
-n |
Quiet mode (no automatic printing) | sed -n '/error/p' log.txt |
-e |
Multiple commands | sed -e 's/foo/bar/' -e 's/baz/qux/' |
-i |
In-place editing (modifies the file) | sed -i 's/old/new/' file.txt |
-i.bak |
Editing + backup | sed -i.bak 's/old/new/' file.txt |
-f |
External sed script | sed -f commands.sed file.txt |
Fundamental commands
| Command | Action | Syntax |
|---|---|---|
s |
Substitution | s/ancien/nouveau/[flags] |
d |
Delete (line deletion) | [address]d |
p |
[address]p |
|
i |
Insert (before the line) | [address]i\\text |
a |
Append (after the line) | [address]a\\text |
c |
Change (line replacement) | [address]c\\text |
Syntax examples
# Simple substitution
sed 's/error/warning/' log.txt
# Several commands (-e)
sed -e 's/foo/bar/' -e 's/hello/hi/' file.txt
# With an address (line 3)
sed '3d' file.txt
# With a pattern (lines containing "error")
sed '/error/d' log.txt
# In-place editing + backup
sed -i.backup 's/DEBUG/INFO/' config.ini
Full structure
sed [adresse][commande][options] fichier
# | | |
# | | +-> g (global), i (case-insensitive)...
# | +-----------> s, d, p, i, a, c...
# +---------------------> line number, /.../ pattern, 1,5 range
Substitution syntax
sed 's/ANCIEN/NOUVEAU/' fichier.txt
Important: by default, it replaces only the first occurrence per line.Basic examples
# Replace "error" with "warning"
sed 's/error/warning/' app.log
# Replace a path
sed 's/\/usr\/local/\/opt/' config.sh
# Change a file extension
sed 's/\.txt/.md/' liste.txt
# Update a variable
sed 's/PORT=8080/PORT=9090/' .env
Line-by-line behaviour
# Source file
cat exemple.txt
error: failed error
warning: ok
error: timeout
# Substitution (1st occurrence only)
sed 's/error/ERREUR/' exemple.txt
ERREUR: failed error # Only the 1st "error" replaced
warning: ok
ERREUR: timeout
In memory vs in the file
# Display only (file unchanged)
sed 's/foo/bar/' file.txt
# Redirect to a new file
sed 's/foo/bar/' file.txt > output.txt
# Direct modification (with -i)
sed -i 's/foo/bar/' file.txt
Common use cases
# CI/CD: version update
sed 's/v1.0.0/v1.1.0/' README.md
# Logs: mask an IP
sed 's/192\.168\.1\.10/XXX.XXX.X.XX/' access.log
# Config: change the domain
sed 's/localhost/production.example.com/' app.yaml
Tip: to replace every occurrence, use the g flag: sed 's/error/warning/g'.The g flag (global)
sed 's/ANCIEN/NOUVEAU/g' fichier.txt
# |
# +-> g = EVERY occurrence per line
Without vs with g
# Test file
cat data.txt
foo bar foo baz foo
error error error
# WITHOUT g (1st occurrence only)
sed 's/foo/FOO/' data.txt
FOO bar foo baz foo # Only the 1st foo
error error error
# WITH g (every occurrence)
sed 's/foo/FOO/g' data.txt
FOO bar FOO baz FOO # Every foo
error error error
Practical use cases
# CSV: replace every separator
sed 's/;/,/g' export.csv
# Logs: mask every IP
sed 's/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/XXX.XXX.X.XX/g' access.log
# HTML: replace every occurrence of a class
sed 's/class="old"/class="new"/g' index.html
# Config: update every port
sed 's/:8080/:9090/g' services.yaml
Other combinable flags
| Flag | Effect | Example |
|---|---|---|
g |
Global (every occurrence) | s/foo/bar/g |
i |
Case-insensitive | s/error/warning/gi |
2 |
Only the 2nd occurrence | s/foo/bar/2 |
p |
Print if modified (with -n) | sed -n 's/old/new/gp' |
Combining flags
# Replace every "error" (case-insensitive) and print
sed -n 's/error/WARNING/gip' app.log
# Only the 2nd occurrence of each line
sed 's/foo/bar/2' file.txt
Rule: without g, sed stops at the 1st occurrence. With g, it goes through the whole line.The d command (delete)
sed 'NUMEROd' fichier.txt
NUMERO = number of the line to delete (1 = first line).Line deletion examples
# Delete line 3
sed '3d' file.txt
# Delete line 1 (header)
sed '1d' data.csv
# Delete the last line ($)
sed '$d' file.txt
# Delete lines 5 to 10
sed '5,10d' file.txt
Line ranges
| Syntax | Effect | Example |
|---|---|---|
3d |
Line 3 | sed '3d' file.txt |
1d |
First line | sed '1d' file.txt |
$d |
Last line | sed '$d' file.txt |
2,5d |
Lines 2 to 5 | sed '2,5d' file.txt |
1,3d |
Lines 1 to 3 | sed '1,3d' file.txt |
5,$d |
Line 5 to the end | sed '5,$d' file.txt |
Practical cases
# CSV: delete the header
sed '1d' export.csv > data.csv
# Logs: keep only the first 100 lines
sed '101,$d' app.log
# Config file: delete a comment line (line 2)
sed '2d' app.conf
# Delete the first 10 lines
sed '1,10d' file.txt
Check before deleting
# Print ONLY the line to delete
sed -n '3p' file.txt # Check line 3
# Then delete
sed '3d' file.txt
# Or directly with -i + backup
sed -i.bak '3d' file.txt
Difference: d deletes line(s), p prints line(s), s modifies content.Pattern delete syntax
sed '/PATTERN/d' fichier.txt
Deletes every line containing the pattern.Common examples
# Delete the lines containing "error"
sed '/error/d' app.log
# Delete comments (starting with #)
sed '/^#/d' config.sh
# Delete empty lines
sed '/^$/d' file.txt
# Delete the lines with a specific IP
sed '/192\.168\.1\.10/d' access.log
Useful patterns
| Pattern | Meaning | Example |
|---|---|---|
/error/d |
Contains "error" | sed '/error/d' log.txt |
/^#/d |
Starts with # | sed '/^#/d' script.sh |
/^$/d |
Empty lines | sed '/^$/d' file.txt |
/TODO/d |
Contains TODO | sed '/TODO/d' code.js |
/^\s*$/d |
Empty + spaces | sed '/^\s*$/d' file.txt |
/DEBUG/d |
DEBUG level | sed '/DEBUG/d' app.log |
Production log cleanup
# Delete DEBUG + INFO (keep WARN/ERROR)
sed -e '/DEBUG/d' -e '/INFO/d' app.log > critical.log
# Delete the lines with private IPs
sed '/192\.168/d' access.log
# Clean a config file (comments + empty lines)
sed -e '/^#/d' -e '/^$/d' nginx.conf
Inversion (delete what does NOT contain)
# Keep ONLY the lines with "error"
sed '/error/!d' app.log
# |
# +-> ! = NOT (inverts)
# Delete everything EXCEPT comments
sed '/^#/!d' script.sh
DevOps cases
# CI/CD: filter logs
sed '/^\[INFO\]/d' build.log > errors-only.log
# Monitoring: delete health checks
sed '/healthcheck/d' access.log
# Security: anonymise sensitive data
sed -e '/password/d' -e '/token/d' dump.log
Tip: test with -n '/pattern/p' before deleting with /pattern/d.The i command (insert)
sed 'ADRESSEi\TEXTE' fichier.txt
Inserts TEXTE before the targeted line.Examples by line number
# Insert before line 1 (header)
sed '1i\# Header' file.txt
# Insert before line 5
sed '5i\--- Section ---' doc.md
# Insert before the last line
sed '$i\Footer' file.txt
Alternative syntax (POSIX vs GNU)
# GNU sed (Linux): single backslash
sed '1i\Header' file.txt
# POSIX sed (macOS/BSD): newline required
sed '1i\
Header' file.txt
# Portable (works everywhere)
sed -e '1i\' -e 'Header' file.txt
Insertion by pattern
# Insert before the first line containing "[database]"
sed '/^\[database\]/i\# Configuration database' config.ini
# Insert before each "error" line
sed '/error/i\--- Error detected ---' app.log
# Insert a comment before the main function
sed '/^def main/i\# Entry point' script.py
Practical use cases
# Add a shebang at the start of a script
sed '1i\#!/bin/bash' script.sh
# Insert a CSV header
sed '1i\Name,Email,Age' data.csv
# Add an HTML DOCTYPE
sed '1i\<!DOCTYPE html>' page.html
# Insert an import in Python
sed '1i\import logging' app.py
Multi-line
# Insert several lines (GNU sed)
sed '1i\Line 1\nLine 2\nLine 3' file.txt
# Or with several commands
sed -e '1i\Line 1' -e '1i\Line 2' file.txt
In-place modification
# Insert + save
sed -i '1i\# Auto-generated' config.yaml
# With a backup
sed -i.bak '1i\#!/usr/bin/env python3' script.py
Difference: i inserts before, a inserts after.The a command (append)
sed 'ADRESSEa\TEXTE' fichier.txt
Adds TEXTE after the targeted line.Examples by number
# Append after line 3
sed '3a\New line after 3' file.txt
# Append at the end ($)
sed '$a\Footer line' file.txt
# Append after line 1
sed '1a\Second line' file.txt
By pattern
# After each line containing "[section]"
sed '/^\[section\]/a\key=value' config.ini
# After the <head> tag
sed '/<head>/a\ <meta charset="UTF-8">' index.html
# After import logging
sed '/^import logging/a\import sys' app.py
DevOps use cases
# Inject analytics after <body>
sed '/<body>/a\<script src="analytics.js"></script>' index.html
# Add a route after the routes section
sed '/# Routes/a\app.get("/health", healthCheck)' server.js
# Add a variable after an environment
sed '/^\[production\]/a\DEBUG=False' .env
# Footer in a README
sed '$a\\n## License\nMIT' README.md
Multi-line
# Append several lines (GNU sed)
sed '5a\Line A\nLine B\nLine C' file.txt
# With -e
sed -e '5a\Line A' -e '5a\Line B' file.txt
Conditional insertion
# Append only after the first occurrence
sed '0,/pattern/a\New line' file.txt
# Append if the pattern is found
sed '/TODO/a\# FIXME: urgent' code.js
i vs a
| Command | Position | Example |
|---|---|---|
i |
Before the line | sed '3i\Before 3' file.txt |
a |
After the line | sed '3a\After 3' file.txt |
# Original file
Line 1
Line 2
Line 3
# With i (insert before)
sed '2i\NEW' file
Line 1
NEW
Line 2
Line 3
# With a (append after)
sed '2a\NEW' file
Line 1
Line 2
NEW
Line 3
Tip: use -n and p to test before modifying: sed -n '3p' file.txt.The c command (change)
sed 'ADRESSEc\NOUVEAU_CONTENU' fichier.txt
Replaces the line entirely (deletes the old content).Examples by number
# Replace line 5
sed '5c\New content for line 5' file.txt
# Replace line 1 (header)
sed '1c\# New Title' README.md
# Replace the last line
sed '$c\End of file' file.txt
By pattern
# Replace the line containing "DEBUG"
sed '/DEBUG=true/c\DEBUG=false' config.sh
# Replace the version line
sed '/^version=/c\version=2.0.0' package.conf
# Replace an obsolete import line
sed '/^import old_module/c\import new_module' script.py
s vs c
| Command | Action | Example | Result |
|---|---|---|---|
s |
Partial substitution | sed 's/foo/bar/' |
Keeps the rest of the line |
c |
Full replacement | sed '2c\NEW' |
Line completely replaced |
# File
cat file.txt
Line 1 with foo
Line 2 with foo
Line 3
# With s (substitution)
sed '2s/foo/bar/' file.txt
Line 1 with foo
Line 2 with bar # "Line 2 with" kept
Line 3
# With c (change)
sed '2c\bar' file.txt
Line 1 with foo
bar # Everything replaced
Line 3
Practical cases
# Config: replace the port line
sed '/^port=/c\port=9090' app.conf
# Update an environment variable
sed '/^export PATH=/c\export PATH=/usr/local/bin:$PATH' .bashrc
# Dockerfile: new base image
sed '/^FROM/c\FROM node:18-alpine' Dockerfile
# Makefile: new command
sed '/^build:/c\build: clean compile' Makefile
Line ranges
# Replace lines 2 to 4 with a single one
sed '2,4c\Single replacement line' file.txt
# Replace everything except line 1
sed '2,$c\All replaced' file.txt
In-place modification
# Replace + save
sed -i '/old_value/c\new_value' config.yaml
# With a backup
sed -i.bak '5c\Updated line' file.txt
Summary: c = complete line replacement, s = partial modification.The -i option (in-place)
sed -i 'COMMANDE' fichier.txt
Effect: modifies the file directly (no redirection needed).Default behaviour vs -i
# WITHOUT -i: stdout display (file unchanged)
sed 's/foo/bar/g' file.txt
# Prints the result, file.txt intact
# Manual redirection
sed 's/foo/bar/g' file.txt > output.txt
# file.txt intact, result in output.txt
# WITH -i: direct modification
sed -i 's/foo/bar/g' file.txt
# file.txt modified, no stdout output
Linux (GNU) vs macOS (BSD) differences
| System | Syntax | Required |
|---|---|---|
| Linux | sed -i 's/foo/bar/' file.txt |
Optional extension |
| macOS | sed -i '' 's/foo/bar/' file.txt |
Extension required (empty = '') |
# Linux/GNU sed
sed -i 's/old/new/' file.txt
# macOS/BSD sed (ERROR without '')
sed -i 's/old/new/' file.txt # ❌ ERROR
sed -i '' 's/old/new/' file.txt # ✅ OK
# Portable (works everywhere)
sed -i.bak 's/old/new/' file.txt
DevOps use cases
# CI/CD: version update
sed -i 's/version: 1.0.0/version: 1.1.0/' Chart.yaml
# Deployment: environment change
sed -i 's/NODE_ENV=development/NODE_ENV=production/' .env
# Server config: port update
sed -i 's/listen 80/listen 8080/' nginx.conf
# Ansible: dynamic templating
sed -i 's/{{DB_HOST}}/postgres.local/' config.php
Bulk modification
# Every .conf file
find /etc/app -name '*.conf' -exec sed -i 's/old/new/g' {} \;
# Every .js of the project
find src -name '*.js' -exec sed -i 's/var /let /g' {} +
# With confirmation
find . -name '*.txt' -exec sed -i.bak 's/foo/bar/g' {} \;
Safety
# DANGER: no backup
sed -i 's/important/data/' critical.conf
# ✅ RECOMMENDED: with a backup
sed -i.bak 's/important/data/' critical.conf
# Creates critical.conf.bak
# Test first (-n for a visual dry run)
sed -n 's/old/new/gp' file.txt # See the changes
sed -i 's/old/new/g' file.txt # Apply
Good practice: always use -i.bak in production, or test without -i first.The -i.EXTENSION option
sed -i.EXTENSION 'COMMANDE' fichier.txt
Creates fichier.txt.EXTENSION (backup) before the change.Common extensions
# .bak extension
sed -i.bak 's/old/new/g' config.txt
# Creates config.txt.bak
# .orig extension
sed -i.orig 's/foo/bar/' script.sh
# Creates script.sh.orig
# Dated extension
sed -i.$(date +%Y%m%d) 's/old/new/' file.txt
# Creates file.txt.20260108
# Timestamp extension
sed -i.$(date +%s) 's/old/new/' data.csv
# Creates data.csv.1704729600
Safe workflow
# 1. Check the change (dry run)
sed -n 's/DEBUG/INFO/gp' app.conf
# 2. Apply with a backup
sed -i.backup 's/DEBUG/INFO/g' app.conf
# 3. Check the result
diff app.conf.backup app.conf
# 4. If OK, delete the backup
rm app.conf.backup
# 5. If KO, restore
mv app.conf.backup app.conf
Comparing the options
| Option | Backup | Original file | Use |
|---|---|---|---|
sed 's/...' file |
❌ No | ✅ Intact | Test/display |
sed -i 's/...' file |
❌ No | ❌ Modified | Risky |
sed -i.bak 's/...' file |
✅ Yes | ❌ Modified | Recommended |
Production cases
# Deployment with a timestamped backup
sed -i.$(date +%Y%m%d-%H%M%S) \
's/version=1.0/version=2.0/' /etc/app/config.ini
# Update several configs
for conf in /etc/nginx/sites-enabled/*; do
sed -i.backup 's/listen 80/listen 8080/' "$conf"
done
# Rollback on a problem
find /etc/nginx/sites-enabled -name '*.backup' | while read bak; do
mv "$bak" "${bak%.backup}"
done
Backup cleanup
# Delete every .bak
find . -name '*.bak' -delete
# Keep backups younger than 7 days
find . -name '*.bak' -mtime +7 -delete
# List backups before deleting
find . -name '*.bak' -ls
macOS difference
# macOS: extension MANDATORY (even empty)
sed -i '' 's/old/new/' file.txt # No backup
sed -i '.bak' 's/old/new/' file.txt # With a backup
# Linux: optional extension
sed -i 's/old/new/' file.txt # No backup
sed -i.bak 's/old/new/' file.txt # With a backup
Golden rule: always use -i.bak on critical files (configs, source code).Built-in regex support
Yes, sed uses regular expressions (ERE/BRE) for complex patterns.Available syntaxes
| Mode | Activation | Support |
|---|---|---|
| BRE | Default | Basic Regular Expressions |
| ERE | -E or -r |
Extended Regular Expressions |
# BRE (default)
sed 's/[0-9]\+/NUM/g' file.txt
# ERE (more readable)
sed -E 's/[0-9]+/NUM/g' file.txt
Common patterns
| Pattern | Meaning | Example |
|---|---|---|
^ |
Start of line | sed 's/^/PREFIX/' file.txt |
$ |
End of line | sed 's/$/SUFFIX/' file.txt |
. |
Any character | sed 's/a.c/ABC/' file.txt |
* |
0 or more occurrences | sed 's/a*/X/' file.txt |
[0-9] |
Digit | sed 's/[0-9]/#/g' file.txt |
[a-z] |
Lowercase letter | sed 's/[a-z]/X/g' file.txt |
\w |
Word (ERE) | sed -E 's/\w+/WORD/g' file.txt |
\s |
Space (ERE) | sed -E 's/\s+/ /g' file.txt |
Practical examples
# Comment out the lines starting with a digit
sed 's/^[0-9]/# &/' file.txt
# Remove leading/trailing spaces
sed 's/^[[:space:]]*//; s/[[:space:]]*$//' file.txt
# Replace an IP with XXX
sed -E 's/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/XXX.XXX.XXX.XXX/g' log.txt
# Extract the email domain
sed -E 's/.*@([a-z0-9.-]+).*/\1/' emails.txt
# Validate a phone format
sed -n '/^[0-9]\{10\}$/p' phones.txt
Capture groups
# BRE: \( \) and \1
sed 's/\([a-z]*\)@\([a-z]*\)\.com/User: \1, Domain: \2/' emails.txt
# ERE: ( ) and \1 (more readable)
sed -E 's/([a-z]*)@([a-z]*)\.com/User: \1, Domain: \2/' emails.txt
# Swap first name and last name
sed -E 's/([A-Z][a-z]+) ([A-Z][a-z]+)/\2, \1/' names.txt
# John Doe → Doe, John
POSIX classes
# [:digit:] = [0-9]
sed 's/[[:digit:]]/#/g' file.txt
# [:alpha:] = [a-zA-Z]
sed 's/[[:alpha:]]/X/g' file.txt
# [:space:] = spaces/tabs
sed 's/[[:space:]]/,/g' file.txt
# [:alnum:] = alphanumeric
sed 's/[^[:alnum:]]//g' file.txt
Advanced DevOps cases
# Logs: extract the timestamp
sed -E 's/^([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1/' app.log
# Config: validate the KEY=VALUE format
sed -n '/^[A-Z_]\+=[^[:space:]]\+$/p' .env
# CSV: validate an email
sed -E '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/!d' emails.csv
# Markdown: extract links
sed -E 's/.*\[([^]]+)\]\(([^)]+)\).*/Link: \1 -> \2/' README.md
Note: -E (ERE) avoids escaping +, ?, |, () (more readable).The -f option (script file)
sed -f SCRIPT.sed fichier.txt
Runs every command of the SCRIPT.sed file.Script file structure
# cleanup.sed
# Delete comments
/^#/d
# Delete empty lines
/^$/d
# Replace tabs with spaces
s/\t/ /g
# Trim trailing spaces
s/[[:space:]]*$//
Usage:sed -f cleanup.sed config.txt
Use case: log normalisation
# normalize-logs.sed
# Anonymise IPs
s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/XXX.XXX.XXX.XXX/g
# Mask emails
s/[a-zA-Z0-9._%+-]\+@[a-zA-Z0-9.-]\+\.[a-zA-Z]\{2,\}/***@***.com/g
# Delete DEBUG/INFO
/\[DEBUG\]/d
/\[INFO\]/d
# Keep only WARN/ERROR
/\[WARN\]/!{/\[ERROR\]/!d}
Application:sed -f normalize-logs.sed app.log > clean.log
CSV transformation
# csv-transform.sed
# Delete the header
1d
# Replace the ; separator with ,
s/;/,/g
# Quote the text columns
s/^\([^,]*\),\([^,]*\),/"\1","\2",/
# Delete empty lines
/^$/d
Multi-step configuration
# deploy-config.sed
# Production environment
s/DEBUG=true/DEBUG=false/
s/localhost/production.example.com/g
s/port: 3000/port: 8080/
# Database
s/db.local/db.prod.internal/
s/max_connections=10/max_connections=100/
# Logs
s/log_level=debug/log_level=warn/
Deployment:for conf in *.yaml; do
sed -i.dev -f deploy-config.sed "$conf"
done
Advantages over multiple -e
| Method | Readability | Maintenance | Reusability |
|---|---|---|---|
Repeated -e |
❌ Low | ❌ Hard | ❌ No |
-f script.sed |
✅ High | ✅ Easy | ✅ Yes |
# Inline (hard to read)
sed -e 's/foo/bar/' -e '/^#/d' -e 's/old/new/g' -e '/^$/d' file.txt
# Script (clear)
sed -f transform.sed file.txt
Combining -f and -e
# Script + inline commands
sed -f base-cleanup.sed -e 's/CUSTOM/VALUE/' file.txt
CI/CD: versioned scripts
# scripts/sed/
├── anonymize.sed # Log anonymisation
├── config-prod.sed # Production config
├── csv-normalize.sed # CSV normalisation
└── sanitize.sed # General cleanup
# Usage in a pipeline
sed -f scripts/sed/anonymize.sed logs/*.log
Advantage: versioned sed scripts = reproducible and testable transformations.1. Log cleanup
# Delete DEBUG/INFO
sed -e '/DEBUG/d' -e '/INFO/d' app.log > errors.log
# Anonymise IPs
sed 's/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/XXX.XXX.X.XX/g' access.log
# Delete empty lines + trim
sed -e '/^$/d' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' app.log
# Keep only the last 1000 lines
sed -e :a -e '$q;N;1001,$D;ba' huge.log
2. Automatic configuration
# Port update
sed -i 's/port: 8080/port: 9090/' config.yaml
# Change environment
sed -i 's/NODE_ENV=development/NODE_ENV=production/' .env
# Database connection
sed -i 's/localhost:3306/db.prod.local:3306/' app.conf
# Batch of configs
find /etc/app -name '*.conf' -exec sed -i.bak 's/old_value/new_value/' {} \;
3. CSV/data transformation
# Change the separator
sed 's/;/,/g' export.csv > import.csv
# Delete the header
sed '1d' data.csv
# Add quotes
sed 's/^\|$/"/g; s/,/","/g' data.csv
# Filter columns (keep 1 and 3)
sed -E 's/^([^,]*),([^,]*),([^,]*).*/\1,\3/' data.csv
4. Scripts: path updates
# Change the shebang
sed -i '1s|^#!/usr/bin/python|#!/usr/bin/env python3|' *.py
# Update paths
find . -name '*.sh' -exec sed -i 's|\/old\/path|\/new\/path|g' {} +
# Python module imports
sed -i 's/^from old_module/from new_module/' **/*.py
5. Templating / content injection
# HTML: analytics
sed '/<\/head>/i\<script src="analytics.js"></script>' index.html
# Replace variables
sed 's/{{VERSION}}/1.2.3/g; s/{{ENV}}/production/g' template.yaml
# Generate a config from a template
sed -e "s/{{DB_HOST}}/$DB_HOST/" \
-e "s/{{DB_PORT}}/$DB_PORT/" \
template.conf > app.conf
6. CI/CD: automated deployment
# package.json version update
sed -i "s/\"version\": \".*\"/\"version\": \"$NEW_VERSION\"/" package.json
# Dockerfile: new base image
sed -i "s/^FROM .*/FROM node:${NODE_VERSION}-alpine/" Dockerfile
# Kubernetes: namespace
sed "s/namespace: dev/namespace: $DEPLOY_ENV/" k8s/*.yaml
7. Format conversion
# DOS → Unix (CRLF → LF)
sed -i 's/\r$//' file.txt
# Unix → DOS (LF → CRLF)
sed -i 's/$/\r/' file.txt
# Markdown → plain text (remove links)
sed -E 's/\[([^]]+)\]\([^)]+\)/\1/g' README.md
8. Validation / extraction
# Extract emails
sed -n 's/.*\([a-zA-Z0-9._%+-]\+@[a-zA-Z0-9.-]\+\.[a-zA-Z]\{2,\}\).*/\1/p' file.txt
# Validate an IP format
sed -n '/^[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}$/p' ips.txt
# Extract URLs
sed -n 's/.*https\?:\/\/\([^[:space:]]\+\).*/\1/p' links.txt
Summary by field
| Field | sed use | Frequency |
|---|---|---|
| DevOps | Auto config, templating | 5/5 |
| SysAdmin | Logs, maintenance | 5/5 |
| Data Engineering | CSV, transformations | 4/5 |
| CI/CD | Versioning, deployment | 4/5 |
| Scripting | Automation | 5/5 |