How to Use sed in Place on Both macOS and Linux Step by Step
Edit files in place with sed on both macOS and Linux: why -i behaves differently, the portable backup-suffix form, and when to reach for perl or ed instead.

A script that edits config files in place passes on a Linux server and fails on a colleague's Mac, with an error such as "extra characters at the end of d command". The cause is that -i means different things in GNU sed and BSD sed. It is a standard portability trap in replacing text with sed, and the commands here assume GNU sed 4.9 on Linux and the sed that ships with macOS.
Why the two disagree
# GNU sed on Linux
sed -i 's/debug=false/debug=true/' app.conf
# BSD sed on macOS
sed -i '' 's/debug=false/debug=true/' app.confIn GNU sed the suffix is optional and must be glued to the flag: -i.bak. In BSD sed the suffix is a separate, mandatory argument, so -i 's/a/b/' reads the whole expression as the suffix and then treats your file name as the script. Neither form works on the other platform. Passing -i '' to GNU sed makes the empty string a file name and prints "can't read".
The form that works on both
sed -i.bak 's/debug=false/debug=true/' app.conf
rm -f app.conf.bakGlue a suffix to -i and both implementations accept it. The price is a backup file you then delete. In a loop over many files, delete the backups in one find . -name '*.bak' -delete afterwards, and make sure no real file in the directory already ends in .bak, because it would be removed too.
Detect the sed and wrap it
sed_inplace() {
if sed --version >/dev/null 2>&1; then
sed -i "$@"
else
sed -i '' "$@"
fi
}
sed_inplace 's/8080/9090/' server.confGNU sed answers --version; BSD sed rejects it. The wrapper keeps the call sites clean, and the test runs once per call, which is cheap. Another route is to install GNU sed on macOS with Homebrew (brew install gnu-sed) and call it as gsed, but that adds a dependency every contributor must have.
Skip -i entirely
tmp=$(mktemp)
sed 's/debug=false/debug=true/' app.conf > "$tmp" && mv "$tmp" app.conf
perl -pi -e 's/debug=false/debug=true/' app.confWriting to a temporary file and moving it over the original is portable and safe: if sed fails, the original is untouched. The move drops the file's permissions and ownership unless you copy them, so use cp -p first or cat "$tmp" > app.conf to keep the inode. Perl is preinstalled on macOS and most Linux distributions, uses the same flags everywhere, and gives you lookaheads that sed lacks. See editing config files with sed for the safe-edit pattern.
Other differences to expect
The in-place flag is the loudest difference, but BSD sed also lacks \+, \? and \| in basic mode and does not understand \n in the replacement. Use -E (supported by both) for extended expressions and a literal newline escape through a variable. The POSIX specification is in the sed entry of the Open Group standard, and the GNU extensions are listed in the GNU sed manual. For the full picture of what differs between the two platforms in scripts, read date on macOS versus Linux.
Why this trap keeps catching people
The difference between the two sed implementations is a historical accident. The BSD version, which macOS ships, required a backup extension argument from the start, and the GNU version made it optional and attached. Both are valid readings of a flag that POSIX never specified, because in-place editing is not in the standard. Every script that uses it is therefore nonportable by definition, and the failure is delayed until someone runs it on the other kind of system. That delay is why the error shows up in a colleague's terminal weeks after the script was written.
The error messages are unhelpful because the shell command is valid in both worlds, just with a different meaning. On the Mac, sed -i 's/a/b/' file treats s/a/b/ as the backup suffix and file as the script, producing messages about unknown commands or extra characters. On Linux, sed -i '' 's/a/b/' file treats the empty string as the script and s/a/b/ as a file name, and complains about a missing file. Recognising both messages saves time.
Atomic edits and file metadata
In-place editing is not really in place. Both implementations write a new file and rename it over the original, which changes the inode. That has consequences. A symbolic link to the file is replaced by a regular file, hard links are broken, and a process holding the old file open keeps reading the old content. Ownership and permissions are copied by GNU sed, but extended attributes and ACLs may be lost. If the target is a symlink, use --follow-symlinks in GNU sed, or edit the real path. For files read by long-running daemons that watch their inode, such as some log shippers, in-place replacement can confuse the watcher, and appending or truncation is the safer operation.
Making scripts portable by design
The cleanest option is to avoid editing files with sed at all. Generate the file from a template with envsubst, or keep configuration in separate files and include them, so that changes are written wholesale and not patched in. Where editing is unavoidable, use the temporary file approach, which behaves identically everywhere and gives you a natural place to validate the result before it replaces the original: run the service's config check against the new file, and only then move it into place.
If portability is a hard requirement across systems you do not control, test it. A CI job with a macOS runner and a Linux runner running the same script catches the difference before release. Put the sed wrapper in a shared library file and source it, so one corrected definition fixes every script that uses it, and add a short test for the function that runs on both platforms.
Other portability notes for sed and friends
The same families of differences exist in neighbouring tools. date, stat, readlink, xargs and grep all have GNU and BSD variants with different flags. A reasonable policy for a team that mixes systems is to install the GNU versions on the Macs under their usual names, through a package manager that places them first on the path, or to restrict scripts to the intersection documented by POSIX. Choose one policy and write it down, because the mixed middle, where each script guesses, is where bugs accumulate.
Where this does not apply
If the script only ever runs on a Linux server you control, plain sed -i is fine and portable code is a cost without a benefit. If the edit is structural, such as changing a value inside a JSON or YAML file, use jq or yq instead of sed; a regular expression will eventually match the wrong line.


