Sed – Achieving Portability with sed -i (In-Place Editing)

freebsdgnuportabilitysedshell

I'm writing shell scripts for my server, which is a shared hosting running FreeBSD. I also want to be able to test them locally, on my PC running Linux. Hence, I'm trying to write them in a portable way, but with sed I see no way to do that.

Part of my website uses generated static HTML files, and this sed line inserts correct DOCTYPE after each regeneration:

sed -i '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

It works with GNU sed on Linux, but FreeBSD sed expects the first argument after -i option to be extension for backup copy. This is how it would look like:

sed -i '' '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

However, GNU sed in turn expects the expression to follow immediately after -i.
(It also requires fixes with newline handling, but that's already answered in here)

Of course I can include this change in my server copy of the script, but that would mess i.e. my use of VCS for versioning. Is there a way to achieve this with sed in a fully portable way?

Best Answer

GNU sed accepts an optional extension after -i. The extension must be in the same argument with no intervening space. This syntax also works on BSD sed.

sed -i.bak -e '…' SOMEFILE

Note that on BSD, -i also changes the behavior when there are multiple input files: they are processed independently (so e.g. $ matches the last line of each file). Also this won't work on BusyBox.

If you don't want to use backup files, you could check which version of sed is available.

case $(sed --help 2>&1) in
  *GNU*) set sed -i;;
  *) set sed -i '';;
esac
"$@" -e '…' "$file"

Or alternatively, to avoid clobbering the positional parameters, define a function.

case $(sed --help 2>&1) in
  *GNU*) sed_i () { sed -i "$@"; };;
  *) sed_i () { sed -i '' "$@"; };;
esac
sed_i -e '…' "$file"

If you don't want to bother, use Perl.

perl -i -pe '…' "$file"

If you want to write a portable script, don't use -i — it isn't in POSIX. Do manually what sed does under the hood — it's only one more line of code.

sed -e '…' "$file" >"$file.new"
mv -- "$file.new" "$file"
Related Question