You have a configuration file with a hundred lines and you need to change one IP address. Or a log file with ten thousand lines and you need to find every “ERROR” from the last hour. Opening an editor is the slow way. grep finds what you are looking for; sed changes it. The danger is that sed can rewrite a file in place with no undo, so the real skill is knowing how to verify before you commit. This guide covers both commands, how to combine them safely, and the mistakes that turn a quick edit into data loss.
grep searches text for patterns. sed edits text from the command line. Together they are the fastest way to find and change strings across files without opening an editor, but only if you know how to preview changes before committing them.
I start every sed session the same way: preview first, then commit. sed without -i prints what would change to standard output without touching the file. That preview step is the habit that prevents data loss. Only after I confirm the output looks right do I add -i (or -i.bak for a safety net) to write the result back.
grep: find lines matching a pattern
grep prints every line that matches a pattern. It reads files or standard input. The pattern is a regular expression by default. I use grep when I need to answer a question about what is in a file: “Does this log contain errors?” “Which config files reference localhost?” “How many lines match this pattern?”
# Lines containing "error" in a log file
grep "error" app.log
# Case-insensitive search
grep -i "error" app.log
# Search recursively in a directory
grep -r "error" /var/log/
# Show line numbers
grep -n "error" app.log
# Count matching lines
grep -c "error" app.log
# List only filenames that contain the pattern
grep -l "error" *.log
# Invert match: show lines that do NOT contain "error"
grep -v "error" app.log
-r searches directories recursively. -n adds line numbers. -c counts instead of printing. -l lists filenames only, useful when you want the files, not the lines. -v inverts the match.
For multiple patterns, use -E (extended regex) so you can use | for “or”:
# Lines containing error OR fail OR critical
grep -E "error|fail|critical" app.log
-l is particularly useful as a stepping stone to sed. You can use grep -l to find which files contain a string, then feed those filenames to sed to transform them. That two-step pattern, find first, edit second — is safer than editing blindly because you see the file list before any changes happen.
sed: the stream editor
sed reads text line by line and applies commands to each line. The most common command is s (substitute). By default, sed prints every line to standard output. It does not change the file. That read-only default is important: you can run sed to see what would happen, decide whether the result looks right, and only then commit the change.
# Replace "old" with "new" on every line (first occurrence only)
sed 's/old/new/' file.txt
# Replace every occurrence on each line
sed 's/old/new/g' file.txt
# Replace only on line 5
sed '5s/old/new/g' file.txt
# Replace only on lines matching a pattern
sed '/pattern/s/old/new/g' file.txt
The s command means substitute: s/find/replace/flags. The delimiter does not have to be / — any character works, which helps when your text contains slashes:
# Avoid escaping slashes in paths
sed 's|/usr/local|/opt|g' config.txt
sed 's,http://,https://,g' urls.txt
The g flag controls scope. Without it, sed ‘s/old/new/’ replaces only the first occurrence on each line. Add g to replace every occurrence. I always ask myself: “Do I want the first occurrence only, or every occurrence on the line?” The answer depends on what the text represents: a config key that appears once per line versus a URL that might appear multiple times.
To see what sed would do without changing anything, run it without -i:
# Preview: see what would change
sed 's/localhost/127.0.0.1/g' *.conf
# Commit: write the result back to the file
sed -i 's/localhost/127.0.0.1/g' *.conf
-i edits the file in place. There is no undo. Always preview first. This is the single most important safety rule with sed.
Safe in-place editing
If you want a safety net, use -i.bak. sed writes the result to the original file and keeps a backup with the extension you specify:
# Edit in place, keep backup as config.txt.bak
sed -i.bak 's/old/new/g' config.txt
# Remove the backup when you are satisfied
rm config.txt.bak
This is the habit that prevents data loss: preview, then edit with a backup, then remove the backup when you are sure. I keep the backup until I have verified the change works. For a config file, that means restarting the service or reloading the configuration and confirming it behaves as expected.
Combining grep and sed
grep and sed work together naturally: grep selects the files or lines, sed transforms them. A common pattern is to find files containing a string, then replace that string in those files.
# Find files containing "localhost", then replace it in those files
for f in $(grep -l "localhost" *.conf); do
sed -i 's/localhost/127.0.0.1/g' "$f"
done
Quote "$f" to handle filenames with spaces. Without quotes, a file named my config.conf breaks the loop. I always quote shell variables in loops like this. It is a small habit that prevents a class of bugs that are hard to debug once they happen.
You can also use grep’s exit status in shell scripts. grep -q produces no output; it only sets the exit code:
# Use grep in a conditional
if grep -q "PasswordAuthentication no" /etc/ssh/sshd_config; then
echo "Password authentication is disabled"
else
echo "WARNING: Password authentication may be enabled"
fi
grep -q runs silently and sets the exit code: 0 if it found a match, 1 if it did not. That makes it perfect for shell script conditionals. You do not need to parse output; you just need a boolean you can act on.
Useful sed recipes
| Task | Command |
|---|---|
| Delete all blank lines | sed '/^$/d' file.txt |
| Show lines 10 through 20 | sed -n '10,20p' file.txt |
| Delete lines from pattern START to pattern END | sed '/^BEGIN/,/^END/d' file.txt |
| Strip HTML tags | sed 's/<[^>]*>//g' file.txt |
| Strip trailing whitespace | sed 's/[[:space:]]*$//' file.txt |
| Uncomment a line in a config file | sed -i 's/^#\(PermitRootLogin\)/\1/' /etc/ssh/sshd_config |
-n suppresses automatic printing; p prints only the lines you ask for. That is how sed -n ‘10,20p’ shows a range without printing the rest of the file. I use -n when I only need to inspect a specific section of a large file, it keeps the output focused and readable.
Common mistakes
- Running
sed -iwithout previewing. You cannot undo an in-place edit. Always run without-ifirst to verify the output. - Forgetting the
gflag.sed 's/old/new/'replaces only the first occurrence on each line. Addgto replace every occurrence. - Not quoting variables in loops.
for f in $(grep -l ...)breaks on filenames with spaces. Quote"$f". - Assuming
grepfinds everything.grepsearches line by line. It does not match patterns that span multiple lines.
The questions worth asking
- Did you preview the sed output before adding
-i? - Do you need
gto replace every occurrence, or just the first per line? - Are you quoting filenames in loops to handle spaces?
- Are you using
grep -lfor filenames orgrep -nfor line numbers?
References
- grep(1) — Linux manual page — GNU grep reference
- sed(1) — Linux manual page — GNU sed reference
- 3 must-know Linux commands for text manipulation — Red Hat grep, sed, awk overview
- Tutorials/Sed — Debian Wiki — sed recipes and regular expressions
Related Tools
Try these companion tools on DevStackIO Tools:
- find and replace tool — interactive search-and-replace across text with regex support.
- regex tester with capture groups — test and debug regular expressions live.