sed -i command for in-place editing to work with both GNU sed and BSD/OSX

I've got a makefile (developed for gmake on Linux) that I'm attempting to port to MacOS, but it seems like sed doesn't want to cooperate. What I do is use GCC to autogenerate dependency files, and then tweak them a bit using sed. The relevant portion of the makefile:

$(OBJ_DIR)/%.d: $(SRC_DIR)/%.cpp
  $(CPPC) -MM -MD $< -o $@
  sed -i 's|\(.*\)\.o:|$(OBJ_DIR)/\1.o $(OBJ_DIR)/\1.d $(TEST_OBJ_DIR)/\1_utest.o:|' $@

While this runs with no trouble under GNU/Linux, I get errors like the following when attempting to build on MacOS:

sed: 1: "test/obj/equipmentConta ...": undefined label 'est/obj/equipmentContainer_utest.d'
sed: 1: "test/obj/dice_utest.d": undefined label 'est/obj/dice_utest.d'
sed: 1: "test/obj/color-string_u ...": undefined label 'est/obj/color-string_utest.d'

It would seem like sed is chopping off a character, but I can't see the solution.


Solution 1:

OS X sed handles the -i argument differently to the Linux version.

You can generate a command that might "work" for both by adding -e in this way:

#      vv
sed -i -e 's|\(.*\)\.o:|$(OBJ_DIR)/\1.o $(OBJ_DIR)/\1.d $(TEST_OBJ_DIR)/\1_utest.o:|' $@

OS X sed -i interprets the next thing after the -i as a file extension for a backup copy of the in-place edit. (The Linux version only does this if there is no space between the -i and the extension.) Obviously a side affect of using this is that you will get a backup file with -e as an extension, which you may not want. Please refer to other answers to this question for more details, and cleaner approaches that can be used instead.

The behaviour you see is because OS X sed consumes the s||| as the extension (!) then interprets the next argument as a command - in this case it begins with t, which sed recognizes as a branch-to-label command expecting the target label as an argument - hence the error you see.

If you create a file test you can reproduce the error:

$ sed -i 's|x|y|' test
sed: 1: "test": undefined label 'est'

Solution 2:

Actually, doing

sed -i -e "s/blah/blah/" files

doesn't do what you expect in MacOS either. Instead it creates backup files with -e extension.

The proper command for MacOS is

sed -i "" -e "s/blah/blah/" files

On Linux, remove the space between -i and "" (see related answer)

sed -i"" -e "s/blah/blah/" files

Solution 3:

The currently accepted answer is flawed in two very important ways.

  1. With BSD sed (the OSX version), the -e option is interpreted as a file extension and therefore creates a backup file with a -e extension.

  2. Testing for the darwin kernel as suggested is not a reliable approach to a cross platform solution since GNU or BSD sed could be present on any number of systems.

A much more reliable test would be to simply test for the --version option which is only found in the GNU version of sed.

sed --version >/dev/null 2>&1

Once the correct version of sed is determined, we can then execute the command in its proper syntax.

GNU sed syntax for -i option:

sed -i -- "$@"

BSD sed syntax for -i option:

sed -i "" "$@"

Finally put it all together in a cross platform function to execute an in place edit sed commend:

sedi () {
    sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@"
}

Example usage:

sedi 's/old/new/g' 'some_file.txt'

This solution has been tested on OSX, Ubuntu, Freebsd, Cygwin, CentOS, Red Hat Enterprise, & Msys.