One evening I asked an agent to draft a procedure for rewriting a handful of config values in bulk. It ran cleanly in a Linux environment, so I pasted the same steps into the terminal on my iMac at home.
Nothing was replaced. The terminal printed a single error I didn't recognize, and that was it. The cause was a small difference in how sed -i is written.
As an indie developer, I move between a Linux server and my iMac all the time, so steps that cross that line are routine. Procedures that come back from an agent often lean on Linux conventions. Before you run one on your own Mac, here is what I look at, along with a small reproduction.
Where "it worked" actually lives
That evening I treated "it ran on Linux" as proof the procedure was correct. What had actually run was the procedure together with that particular environment.
sed is the classic case of one name with two behaviors. The sed that ships with macOS and the GNU sed common on Linux disagree about what follows -i.
Reproducing the difference
I ran this on a Linux machine with GNU sed 4.8. I did not test macOS directly, so the macOS column below reflects the documented behavior rather than my own measurement.
Take a small config file and replace old with new.
printf 'API_HOST=old.example.com\nMODE=old\n' > app.env
sed -i 's/old/new/' app.env
cat app.envWith GNU sed, both lines change to new, and no extra file appears.
Next I tried the form usually recommended for macOS, an empty string right after -i, on the same Linux machine.
sed -i '' 's/old/new/' app.envThe result:
sed: can't read s/old/new/: No such file or directory
Exit code 2, and the file still said old. GNU sed read '' as an empty script and then tried to open s/old/new/ as a filename.
So the argument after -i means different things in different places. A Linux-style short form stalls on a Mac, and a Mac-style form stalls on Linux.
| Form | GNU sed (measured here) | macOS sed (documented behavior) |
|---|---|---|
sed -i 's/old/new/' f | Replaces (exit 0) | Reads the expression as the backup suffix and then misreads the next argument, so it errors |
sed -i '' 's/old/new/' f | Fails (exit 2, file unchanged) | Replaces |
sed -i.bak 's/old/new/' f | Replaces and leaves f.bak | Replaces and leaves f.bak |
Only the third row means the same thing on both. That is the way out.
Three forms that behave the same
The first is -i.bak, with the suffix attached directly. GNU sed replaces in place and keeps f.bak, and macOS does the same.
The second is perl -pi -e. On the same config file it replaced the values and left no stray files.
perl -pi -e 's/old/new/' app.envPerl ships with macOS, which makes it convenient for procedures you want to reuse. Regular expression details differ slightly from sed, so test anything complicated before trusting it.
The third is to detect which sed you have before using it.
if sed --version >/dev/null 2>&1; then
SED_INPLACE=(sed -i) # GNU sed
else
SED_INPLACE=(sed -i '') # sed bundled with macOS
fi
"${SED_INPLACE[@]}" 's/old/new/' app.envGNU sed answers --version; the macOS one does not. Because the script checks first, it no longer depends on which environment the author had in mind.
If you keep backups, clean up in the same step
-i.bak is safe, but it leaves files behind. Running it over three files turned a directory of 5 entries into 8, and a cleanup rm brought it back to 5.
In a Git repository the leftovers showed up as untracked files next to the modified ones.
M g1.txt
M g2.txt
M g3.txt
?? g1.txt.bak
?? g2.txt.bak
?? g3.txt.bak
I've nearly done exactly that in my own site's repository; I noticed just before the commit. A careless git add . would commit the backups too. I now put the cleanup on the same line as the replacement.
sed -i.bak 's/old/new/' g*.txt && rm -f g*.txt.bakOne more trap people mention on the macOS side: writing -i -e makes -e the backup suffix, which leaves files named like file-e. GNU sed produced no extra files for the same form. Since the outcome differs by environment, I avoid the -i -e shape altogether.
Tell the agent where the steps will run
Doing all of this checking in my head every time is tiring, so I added one line about the environment to my requests.
These steps will run on both macOS (my iMac) and Linux.
Use the -i.bak form of sed -i so it means the same thing on both,
and add a line that removes the backup files right after the replacement.That alone makes the returned steps noticeably more consistent. Even so, I try the replacement on a single file before running it across everything.
What worked was never the procedure alone; it was the procedure plus its environment. I try to remember that sentence whenever I receive a set of steps.
The next time an agent hands you a sed -i, look at what follows -i first. If it isn't -i.bak or a perl -pi, try it on one file before you let it loose on the rest.