A shell script that works on a Linux CI runner fails on a developer's Mac at the line `sed -i 's/foo/bar/' config.txt`. Why does macOS behave differently here, and what are the portable ways to fix it?
answer
- Darwin userland descends from BSD
- the flag takes an argument here
- one invocation cannot satisfy both
- g-prefixed twins from Homebrew
- shadowing system tools helps only you
basics
~20 smacOS ships a BSD userland, not GNU coreutils, and BSD sed requires an explicit backup-suffix argument after -i. It reads 's/foo/bar/' as that suffix and then tries to parse the filename as the script, so the command fails.
solid answer
~50 smacOS is Darwin: its userland descends from BSD, so `/usr/bin/sed`, `date`, `stat` and `awk` are BSD tools, not the GNU ones a Linux script was written against. BSD `sed -i` takes a **mandatory** backup-suffix argument as a separate word; GNU `sed -i` takes it optionally and attached (`-i.bak`). So on macOS the command consumes `s/foo/bar/` as the backup suffix and then treats `config.txt` as the script, producing a confusing parse error. The macOS-correct form is `sed -i '' 's/foo/bar/' config.txt`, but that form is wrong on GNU, so there is no single `sed -i` invocation that works on both. Portable options: write to a temporary file and `mv` it over the original; use `perl -i -pe`, which behaves identically on both; install GNU tools with `brew install gnu-sed coreutils` and call `gsed`/`gdate` explicitly; or run the script in a Linux container so the userland matches CI.
code
bash · 7 lines# Not portable: BSD sed needs the suffix, GNU sed must not have it.
# sed -i 's/foo/bar/' config.txt # fails on macOS
# sed -i '' 's/foo/bar/' config.txt # fails on GNU/Linux
# Portable: edit to a temp file and move it into place.
tmp=$(mktemp)
sed 's/foo/bar/' config.txt > "$tmp" && mv "$tmp" config.txtgo deeper
Recall that macOS command-line tools are BSD versions, so flags you learned on Linux may not exist or may need different arguments — sed's in-place flag being the classic example.
Explain the parse: BSD sed's -i requires a separate suffix argument, so the substitution expression is eaten as the suffix and the filename is compiled as the script. Name the same trap in date and stat.
Show the portable instinct — a temp file and mv, or a tool that behaves identically everywhere — and be able to argue why installing GNU tools over the system ones solves one machine's problem while creating the team's.
Decide the policy: whether operational scripts must be POSIX-portable across the fleet or whether the team standardises on running them in Linux containers, and make that a stated convention rather than something each author rediscovers.
## macOS is a BSD, not a GNU system Darwin's userland comes from BSD, and Apple has never adopted GNU coreutils. That means the tools with the familiar names are different programs with different flags: - `/usr/bin/sed` is BSD sed, not GNU sed - `/bin/date` is BSD date - `/usr/bin/stat` is BSD stat - `/usr/bin/awk` is the classic one-true-awk, not gawk They share the POSIX-specified behaviour and diverge everywhere else — and "everywhere else" is exactly where scripts written on Linux live. A second reason the divergence is so wide is licensing: Apple stopped shipping GPLv3 software, which froze or removed the GNU tools that were once bundled. ## The specific sed failure GNU sed's `-i` takes an *optional* suffix, and because it is optional it must be attached: `-i` alone edits in place, `-i.bak` keeps a backup. BSD sed's `-i` takes a **required** suffix as the next argument: `-i ''` means "in place, no backup", `-i .bak` means "keep a backup". So on macOS: ```bash sed -i 's/foo/bar/' config.txt ``` is parsed as: suffix = `s/foo/bar/`, script = `config.txt`. sed then tries to compile `config.txt` as a sed program and fails with a message about an invalid command code — a message that tells you nothing about the real problem, which is why this eats an hour the first time. The macOS form is: ```bash sed -i '' 's/foo/bar/' config.txt ``` and that form is not portable back to GNU sed, where the empty string becomes the *script*. There is genuinely no single `sed -i` line that is correct on both. Other BSD-vs-GNU sed differences that bite: BSD sed does not interpret `\t` in the replacement text (it produces a literal `t`), and BSD basic regular expressions do not support GNU's `\+` and `\?` extensions. `-E` for extended regular expressions is one of the few things both accept. ## The same trap in other tools **date.** Date arithmetic is completely different. GNU: `date -d '1 day ago' +%F`. BSD: `date -v-1d +%F`. And `-r` means different things — on BSD it interprets an epoch value, on GNU it reads a file's modification time. **stat.** GNU uses `-c` with `%s` for size; BSD uses `-f` with `%z`. A script that prints a file size with `stat -c %s` fails outright on macOS. **ls.** GNU's `--color` long option does not exist; BSD uses `-G`. GNU long options in general are sparse across the BSD tools. ## The fixes, ranked **1. Avoid the non-portable feature.** In-place editing is a convenience, not a requirement: ```bash tmp=$(mktemp) sed 's/foo/bar/' config.txt > "$tmp" && mv "$tmp" config.txt ``` This is POSIX, works on every Unix, and has the pleasant side effect of not clobbering the original if sed fails. **2. Use a tool that is the same everywhere.** Perl behaves identically on macOS and Linux: ```bash perl -i -pe 's/foo/bar/' config.txt ``` macOS still bundles perl, though Apple has flagged the bundled scripting runtimes as deprecated for future removal, so a long-lived script should not lean on it forever. **3. Install the GNU tools and call them by their real names.** `brew install coreutils gnu-sed grep findutils` installs GNU versions under `g`-prefixed names — `gsed`, `gdate`, `gstat`, `ggrep`, `gfind`. Each of those formulae also ships an unprefixed directory, `$(brew --prefix <formula>)/libexec/gnubin`, that you can put on PATH to shadow the BSD versions. That last option is the tempting one and the one to think hardest about. Putting `gnubin` on PATH globally makes *your* Mac behave like Linux — and makes it unlike every colleague's Mac and unlike anything the script will run on. Scripts written on that machine acquire silent GNU dependencies that fail for everyone else. Calling `gsed` explicitly is honest; shadowing `sed` is a trap you set for your teammates. **4. Match the runtime instead of the script.** If the script only ever runs in CI on Linux, the real fix may be to run it on Linux locally too — inside a container — rather than making it bilingual. ## What the interviewer is checking Not whether you memorised `-i ''`. They want to hear that you know macOS's userland is BSD-derived, that you reach for a portable construct rather than sprinkling platform checks through a script, and that you understand why installing GNU tools over the top of the system ones is a personal convenience with a team-wide cost.
- What does `brew install coreutils` actually give you, and why are the commands prefixed with g?It installs GNU coreutils as `gls`, `gdate`, `gstat`, `gcp` and so on, so they cannot shadow the BSD tools macOS relies on. The formula also ships `$(brew --prefix coreutils)/libexec/gnubin`, a directory of unprefixed names you can prepend to PATH if you deliberately want GNU behaviour by default. `gnu-sed`, `grep` and `findutils` follow the same pattern.
- Why is putting the gnubin directory at the front of PATH a risky habit on a shared team?It makes one machine behave like Linux while every other Mac and every review of the script still sees BSD tools. Scripts written there quietly acquire GNU-only flags that fail for teammates and in any environment without the same PATH. Calling `gsed` by name keeps the dependency explicit and visible in the script.
- How would you catch this class of bug before a Mac developer hits it?Run the scripts where they will actually run — in CI on Linux — and additionally lint them with a shell checker that flags non-POSIX usage. For scripts that genuinely must run on both, prefer POSIX constructs, and test them on macOS in the pipeline if any Mac path matters; a platform-detection branch is a last resort, not a design.
saying these in an interview costs you the question
- Assuming macOS ships GNU coreutils under the same names
- Believing sed -i '' is the portable form for both systems
- Adding platform-detection branches instead of a portable construct
- Thinking the difference is only about long option names
- Shadowing system tools with gnubin and calling it a fix