sed Part III · Module 7 · Safe File Editing and Debugging
Dry Runs, Backups, and Diff Validation
Chapter 7.1 showed what -i does to a file. This chapter is about whether you should let it. The difference between a mass edit that is routine and one that becomes an incident is almost never the sed command — it is whether anyone looked at the change before it happened. That sounds like advice and it is actually a short, concrete procedure: count what will be touched, read the diff, make the change, then verify nothing stale survived. The chapter also settles an argument with a measurement, because the backup file most people rely on turns out to destroy itself at exactly the moment you need it.
Intermediate19 min readsedPart IIIData SafetyFilelistsReview
Part III · Module 7 · Chapter 7.2 · Dry Runs, Backups, and Diff Validation
1. The Procedure
Four steps. None takes more than a few seconds, and together they turn a mass edit into something you can defend in review:
1. COUNT how many lines will change, and does that number look right?
2. LOOK read the diff — not the output, the diff
3. PROTECT commit first; the backup file is the fallback, not the plan
4. VERIFY grep for the old string afterwards and expect nothingThe rest of the chapter is each step, on a filelist mid-migration:
cat -n filelists/top.f 1 +incdir+/proj/old/rtl
2 /proj/old/rtl/alu.sv
3 /proj/old/rtl/fifo.sv
4 /proj/new/tb/env.svNote line 4 — it is already migrated. A good procedure has to leave it alone, and a bad one will tell you it did when it did not.
2. Step 1 — Count First
Before running anything that writes, find out how big the change is:
grep -c '/proj/old/' filelists/top.f3Three lines. If you expected three, carry on. If you expected forty, your pattern is wrong and you have learned that for the cost of one command rather than one afternoon.
This number is the single most useful thing in the procedure, because it is a prediction you can check against afterwards. An edit with no expected count is an edit you cannot verify.
3. Step 2 — Look at the Change
Two ways, and they answer different questions.
The lines that would change, as they would become. This is the -n plus p-flag idiom from Chapter 3.1, and it shows the result:
sed -n 's|/proj/old/|/proj/new/|gp' filelists/top.f+incdir+/proj/new/rtl
/proj/new/rtl/alu.sv
/proj/new/rtl/fifo.svThree lines out — agreeing with the count from step 1 — and line 4 is correctly absent, because the substitution did not change it.
sed -n 's|/proj/old/|/proj/new/|gp' filelists/top.f | wc -l 3The change itself, as a diff. This is the better review artifact, because it shows what is being replaced as well as what with:
sed 's|/proj/old/|/proj/new/|g' filelists/top.f | diff -u filelists/top.f ---- filelists/top.f 2026-10-12 18:24:02
+++ - 2026-10-12 18:24:02
@@ -1,4 +1,4 @@
-+incdir+/proj/old/rtl
-/proj/old/rtl/alu.sv
-/proj/old/rtl/fifo.sv
++incdir+/proj/new/rtl
+/proj/new/rtl/alu.sv
+/proj/new/rtl/fifo.sv
/proj/new/tb/env.svThree removals, three additions, and line 4 shown with a leading space — present, considered, unchanged. That last detail is what a plain listing cannot give you: evidence that the already-correct line was looked at and left.
The - operand means "read the other side from standard input", which Chapter 2.4 covered.
4. Step 3 — Protect, and Why -i.bak Is the Weak Option
The obvious safety net is sed's own backup suffix. On a glob:
sed -i.bak 's|/proj/old/|/proj/new/|g' filelists/*.f
ls -1 filelists/tb.f
tb.f.bak
top.f
top.f.bakOne .bak per file, each holding the original:
cat filelists/top.f.bak+incdir+/proj/old/rtl
/proj/old/rtl/alu.sv
/proj/old/rtl/fifo.sv
/proj/new/tb/env.svThat looks like safety. Here is why it is not.
5. The Backup Destroys Itself on the Second Run
The first attempt at a mass edit is often slightly wrong. So you fix the pattern and run it again — and that is the moment the backup stops being a backup:
sed -i.bak 's|/proj/new/|/proj/FINAL/|g' filelists/top.f
cat filelists/top.f.bak+incdir+/proj/new/rtl
/proj/new/rtl/alu.sv
/proj/new/rtl/fifo.sv
/proj/new/tb/env.svThe .bak now holds the output of the first run, not the original. The true original — with /proj/old/ — no longer exists anywhere on disk. Exit status 0; nothing warned you.
6. The Fully Portable Alternative
Chapter 7.1 noted that -i is spelled differently on GNU and BSD sed. For a one-off edit you can sidestep the whole question:
sed 's|/proj/old/|/proj/new/|g' top.f > top.f.new && mv top.f.new top.fPortable everywhere, and the && guarantees a failed sed is not followed by a move. The real advantage for this chapter is that it is two steps with an inspectable artifact in between — you can diff top.f.new against top.f before the mv, which is the dry run and the edit sharing one piece of work.
And never > top.f. Chapter 2.4 measured that emptying the file to zero bytes with exit status 0.
7. Step 4 — Verify Afterwards
The step everyone skips. The sweep ran; now prove it did what you predicted:
grep -rn '/proj/old/' filelists/ --include='*.f' || echo 'no stale references in .f files'no stale references in .f filesgrep exits non-zero when it finds nothing, which is why the || reads the way it does — "if the search found nothing, say so". That is the inverted-status idea again, and here it is exactly what you want: finding nothing is the success condition.
Three things to check after a sweep, in order of how often they catch something:
- The old string is gone — the command above.
- The count matches your prediction from step 1. Three lines predicted, three lines in
git diff --stat. - Nothing else changed.
git status --shortshould list only files you expected. This is the one that catches a glob that was wider than you thought.
8. What This Chapter Does Not Do
This is a procedure for seeing a change, not for diagnosing a wrong one. If the diff shows more lines changed than you predicted, the procedure has done its job — it told you to stop — but it does not tell you why.
The usual answer is that the pattern matched more than you meant: an unanchored identifier, a greedy .*, a missing word boundary. Chapter 7.3 is that diagnosis, and it is the natural next step because every technique here produces a number, and 7.3 is about what to do when the number is wrong.
9. Think It Through
1. Your dry run shows 40 changed lines and you expected 12. What do you do?
Stop, and do not run it with -i. Look at the 28 extra: sed -n 's|old|new|gp' file shows them as they would become, and reading five of them usually identifies the over-match immediately. Chapter 7.3 is the systematic version. The point of step 1 is that this costs you a command instead of a rollback.
2. Why is git diff after the sweep better than reading sed's output before it?
Because it covers every file the sweep actually touched, including ones you did not predict. A dry run only shows you the files you thought to run it on. git status --short is the check that catches a glob matching a directory you forgot about — and that is the failure the backup file cannot help with either.
3. Your dry run pipeline exits 0 and prints nothing. Good or bad?
Bad, almost always. diff exits 0 when the two sides are identical, so your edit changes nothing — a pattern typo, or an already-migrated file. The only time it is good news is when you are confirming idempotency: running the sweep a second time should produce no diff, and an exit 0 there means the edit is complete.
4. When is -i.bak genuinely the right tool?
When the files are not in version control and you will run the command exactly once — a generated tree, a scratch area, a directory you do not own. The moment you expect to iterate, section 5 applies: the second run eats the backup. And the moment the files are in a repository, a commit is better in every respect.
5. You migrate paths and the build still fails on one file. Your verification grep found nothing. What did the grep miss?
Most likely you grepped the files you edited rather than the tree. Widen it: grep -rn '/proj/old/' . over the whole area, not just filelists/. The other candidate is a file that was not in your glob at all — *.f does not match *.list, and a sweep cannot fix a file it never read. Chapter 8.3 is about building the file list deliberately for exactly this reason.
6. Write the three commands you would run, in order, before letting a sweep touch a real tree.
git status --short to confirm the tree is clean; grep -rc 'pattern' dir/ | grep -v ':0$' to get the per-file counts you expect; and the dry run piped into diff -u on one representative file. Then the sweep, then git diff --stat against the prediction. None of that is sed expertise — it is the difference between a reviewable change and a hopeful one.
10. Summary
- Four steps: count, look, protect, verify. Each is one command.
- Count before you write.
grep -cgives a prediction you can check afterwards; an edit with no expected count cannot be verified. sed -n 's/old/new/gp'shows the changed lines as they would become.… | diff -u file -shows the change itself, including the unchanged lines it considered.diffexits 1 on differences and 0 on none, so a dry run that exits 0 means your edit does nothing.-i.bakprotects you against the first mistake only. Measured: a second run overwrote the.bakwith the first run's output, and the true original was gone — exit 0, no warning.- Commit first. A commit covers every file, survives every attempt, and
git checkout -- .is the rollback. Keep.bakfor files outside version control. - The portable alternative is
sed … > file.new && mv file.new file, which also leaves an artifact you can diff before committing to it. Never> file. - Verify afterwards: the old string is gone, the count matches, and
git status --shortlists only the files you expected. grepexits non-zero when it finds nothing, which is what makesgrep … || echo 'clean'read correctly.
The procedure tells you that an edit is wrong. It does not tell you why, and the answer is almost always the same: the pattern matched text you were not thinking about. Chapter 7.3 — Regex Overmatching and Unexpected Replacements is that diagnosis — the unanchored identifier, the greedy .*, and the word boundary that is spelled differently on the two seds.
Where this fits
Part of the sed curriculum.
