Short answer: sed 's/old/new/' file prints the file with the first match on each line replaced, without touching the file; add g for every match, not just the first, and -i to edit the file in place. Every command below was tested with GNU sed 4.9 (Debian 12 and Ubuntu); macOS ships an older BSD sed with a different -i syntax, noted where it differs.
| Task | Command |
|---|---|
| Replace first match per line | sed 's/old/new/' file |
| Replace every match on every line | sed 's/old/new/g' file |
| Edit the file in place | sed -i 's/old/new/g' file |
| Edit in place, keep a .bak backup | sed -i.bak 's/old/new/g' file |
| Case-insensitive match | sed 's/old/new/gi' file |
| Only on lines matching a pattern | sed '/pattern/s/old/new/' file |
| Print only lines 10-20 | sed -n '10,20p' file |
| Delete lines matching a pattern | sed '/pattern/d' file |
| Delete a specific line number | sed '3d' file |
| Several edits in one pass | sed -e 's/a/b/' -e 's/c/d/' file |
| Use a different delimiter (for paths) | sed 's#/old/path#/new/path#' file |
| Insert a line before a match | sed '/pattern/i\\new line' file |
| Append a line after a match | sed '/pattern/a\\new line' file |
The substitute command: s/old/new/
printf "foo bar\nbaz foo\n" > test.txt
sed 's/foo/XXX/' test.txtXXX bar
baz fooOnly the first match on each line changes by default; the second foo on the second line is a different line, so it changes too, but a second foo on the same line would not, without the g flag:
echo "foo foo foo" | sed 's/foo/XXX/' # XXX foo foo — only the first
echo "foo foo foo" | sed 's/foo/XXX/g' # XXX XXX XXX — every match
echo "foo foo foo" | sed 's/foo/XXX/2' # foo XXX foo — only the second match
echo "foo foo foo" | sed 's/foo/XXX/2g' # foo XXX XXX — from the second match onwardUsing another delimiter for paths
The slash after s is a delimiter, not fixed punctuation — sed accepts almost any character there, which matters the moment the text itself contains slashes, as a file path does:
sed 's/\/var\/www\/old/\/var\/www\/new/' nginx.confsed 's#/var/www/old#/var/www/new#' nginx.confCase-insensitive replacement
echo "Hello World" | sed 's/hello/hi/i'GNU sed’s I flag (also lowercase i) is case-insensitive only for that one substitution, not the whole command line.
Editing the file itself: -i
sed -i 's/old/new/g' config.conf # edits in place, no backup
sed -i.bak 's/old/new/g' config.conf # edits in place, keeps config.conf.bakWithout -i, sed only ever prints to standard output and never touches the file — the safe way to try an expression before committing to it. GNU sed’s -i takes the backup suffix glued directly to the flag with no space (-i.bak, not -i .bak); with no suffix at all it edits silently with no backup, which is fine for a throwaway file and risky for a server config you did not back up some other way first.
Addressing: which lines a command touches
Every sed command can be limited to specific lines by putting an address in front of it — a line number, a range, or a pattern that has to match first.
| Address | Means |
|---|---|
3 | Line 3 only |
10,20 | Lines 10 through 20 |
5,$ | Line 5 to the end of the file |
/pattern/ | Every line matching the pattern |
/start/,/end/ | From the first line matching start to the next matching end |
1~2 | Every other line, starting at 1 (GNU sed only) |
sed '/^\[database\]/,/^\[/{s/host=old/host=new/}' config.iniThat line only touches the [database] section of an INI-style config, leaving an identically named key elsewhere in the file alone.
-n with p, and d: printing and deleting lines
sed -n '10,20p' file.txt-n turns off sed’s default behaviour of printing every line; without it, 10,20p would print those ten lines twice. This combination is a common, if old-fashioned, alternative to head and tail piped together.
sed '2d' file.txt # delete line 2
sed '/^#/d' file.txt # delete every comment line
sed '/^$/d' file.txt # delete every blank line
sed '$d' file.txt # delete the last lineSeveral edits in one pass: -e and semicolons
sed -e 's/foo/bar/' -e 's/baz/qux/' file.txt
sed 's/foo/bar/; s/baz/qux/' file.txtBoth run the two substitutions on every line in one read of the file, which matters for a large file — piping sed into another sed reads the data twice for no reason.
Inserting and appending lines
sed '/^\[Service\]/a Restart=always' myapp.service
sed '/^server {/i # managed by deploy script' nginx.confa (append) adds the new line after the matched one; i (insert) adds it before. GNU sed accepts the text directly after a/i with just a space, as above; the older, more portable form needs a backslash and a literal newline (sed '/pattern/a\ on one line, the new text on the next) — worth knowing if a script has to run on both GNU and BSD sed.
Everyday sed one-liners
| Task | Command |
|---|---|
| Remove Windows line endings (CRLF to LF) | sed -i 's/\\r$//' file.sh |
| Remove trailing whitespace | sed -i 's/[ \\t]*$//' file.txt |
| Comment out a config line | sed -i 's/^PermitRootLogin yes/#PermitRootLogin yes/' sshd_config |
| Uncomment a config line | sed -i 's/^#PasswordAuthentication/PasswordAuthentication/' sshd_config |
| Number every line | sed = file.txt | sed 'N;s/\\n/\\t/' |
| Double-space a file | sed G file.txt |
| Print everything between two markers | sed -n '/START/,/END/p' file.txt |
The CRLF fix is the one that shows up most on a server: a script edited on Windows and uploaded fails with /bin/bash^M: bad interpreter, which that troubleshooting guide covers in full, including why file script.sh is the way to confirm it before reaching for sed at all.
Basic vs extended regular expressions
By default sed uses POSIX Basic Regular Expressions, where (, ), {"{"}, {"}"}, + and | are literal characters unless escaped with a backslash — the opposite of what most other regex tools do. -E (or -r on GNU sed, an older alias) switches to Extended Regular Expressions, where those characters work unescaped:
echo "v1.2.3" | sed 's/[0-9]\+/X/g' # need \+ to mean "one or more"echo "v1.2.3" | sed -E 's/[0-9]+/X/g' # + works directly
echo "cat or dog" | sed -E 's/(cat|dog)/pet/'sed vs grep vs awk
These three get reached for in similar moments and do different jobs: grep finds and prints lines, sed transforms lines (substitute, insert, delete) and passes the rest through unchanged, awk processes structured, column-like data and can compute things grep and sed cannot. Finding a line is grep; changing text inside a file, in place, from a script, is sed’s job specifically — the one the examples above cover throughout this site’s own deploy snippets, such as fixing a config value inside the SSH config file without opening an editor.
Frequently asked
How do I replace text in a file with sed?
sed 's/old/new/' file replaces only the first match on each line and prints the result without touching the file; add the g flag, sed 's/old/new/g' file, to replace every match on every line. Add -i to edit the file itself: sed -i 's/old/new/g' file.
How do I edit a file in place and keep a backup with sed?
sed -i.bak 's/old/new/g' file edits the file and saves the original as file.bak. GNU sed (Linux) takes the suffix glued directly to -i with no space; BSD sed on macOS requires the suffix as its own separate argument, even an empty one (sed -i '' '...' file) for no backup, which is the single most common sed mistake when a script moves from Linux to a Mac.
How do I delete specific lines with sed?
sed '3d' file deletes line 3; sed '/pattern/d' file deletes every line matching a pattern; sed '/^$/d' file deletes every blank line. None of these change the file unless you add -i.
What is the difference between sed and grep?
grep finds and prints matching lines; it cannot change a file. sed transforms lines — substitute, insert, delete — and passes every other line through unchanged, which is the tool for editing a file's contents from a script rather than only searching it.