skip to content

A bash script that works on your Linux build server misbehaves on macOS: `sed -i 's/a/b/' notes.txt` fails with a cryptic message about an undefined label, and `date -d '2 days ago'` is rejected outright. Why do identically named commands behave differently, and how would you make the script work on both?

level: seniorimportance: should knowfreq 38%

answer

  1. same name, different implementation
  2. the shell is not the boundary here
  3. POSIX core versus vendor extensions
  4. GNU coreutils versus BSD userland
  5. sed -i takes its argument differently

basics

~20 s

Linux ships GNU versions of sed, date and stat while macOS ships BSD ones. Only the POSIX-specified core is common; extensions like sed -i and date -d differ or do not exist. Stick to the portable subset, or detect which tool you have.

solid answer

~50 s

The shell is the same; the userland is not. Linux distributions ship GNU coreutils, GNU sed and GNU grep, while macOS ships BSD-derived versions inherited from FreeBSD. POSIX standardises a common core of each utility, and everything beyond that is a vendor extension that either differs or is missing. `sed -i` is not in POSIX at all: GNU takes an optional suffix attached to the flag, BSD requires a separate extension argument, so on macOS your `s/a/b/` script is consumed as the backup suffix and the filename is then parsed as the sed program. BSD `date` has no GNU-style `-d` date parser; it uses `-v-2d` for relative adjustment, and `-r` means something different on each. My fixes, in order of preference: restructure to avoid the extension entirely — write to a temp file and `mv` instead of editing in place; feature-detect once at the top and prefer `gsed`/`gdate` when Homebrew has installed them; or pin the userland by running the script in a container.

code

bash · 3 lines
bash
#!/usr/bin/env bash
tmp=$(mktemp)
sed 's/foo/bar/g' config.txt > "$tmp" && mv "$tmp" config.txt

go deeper

for a junior

Know that macOS and Linux ship different implementations of common commands, so a flag you found online may not exist on the machine in front of you. Read the local man page rather than assuming.

for a middle

Be able to name concrete divergences — sed -i's argument shape, date -d versus -v, stat -c versus -f — and to explain that POSIX specifies a core while everything else is a vendor extension.

for a senior

Demonstrate the fix rather than the complaint: restructure to avoid the extension, feature-detect once at the top rather than sniffing uname, and know when pinning the userland in a container is the cheaper answer than making the script bilingual.

for a principal

Decide where portability is even a goal. If dev laptops are a supported runtime you pay this tax on every script forever; declaring a single pinned execution environment removes the class of problem and is usually the better trade.

## Two different programs wearing the same name `sed` on a Debian or RHEL host is GNU sed. `sed` on macOS is a BSD sed descended from the FreeBSD tree. They are separate codebases that both implement the POSIX `sed` specification and then add their own options on top. The same is true of `date`, `stat`, `grep`, `readlink`, `awk` and most of the text-processing toolkit. Nothing about your bash script changed — the programs it calls did. POSIX is the contract you can rely on. Anything outside it is an extension, and extensions are exactly where the two lineages diverge. ## The specific traps worth memorising **`sed -i`** is not in POSIX at all. GNU sed accepts `-i` alone, or a suffix attached with no space (`-i.bak`). BSD sed defines `-i extension`, taking the extension as a *separate argument* that may be an empty string. The consequences are symmetrical and both are confusing: ```bash sed -i 's/a/b/' f # BSD: 's/a/b/' becomes the backup suffix, then f is read as the script sed -i '' 's/a/b/' f # GNU: '' becomes the script, then 's/a/b/' is read as a filename sed -i -e 's/a/b/' f # BSD: -e is taken as the backup suffix ``` **`date`.** GNU has `-d 'STRING'`, a full natural-language-ish date parser: `date -d '2 days ago' +%F`. BSD has no such thing; it has `-v` adjustments: `date -v-2d +%F`. Worse, `-r` exists on both and means different things — GNU's `-r FILE` prints that file's modification time, BSD's `-r SECONDS` interprets an epoch timestamp. **`stat`** has entirely different format syntax: GNU `stat -c '%s' f` versus BSD `stat -f '%z' f`. **`readlink -f`** is a GNU extension; older macOS `readlink` has no `-f`, which is why so many scripts hand-roll a `cd "$(dirname "$0")" && pwd` idiom. **`grep -P`** (PCRE) is a GNU extension and is not available on macOS's grep. `grep -E` is POSIX and safe. **`awk`** is three different programs in practice — gawk, mawk, and the BSD one-true-awk — so gawk-only features such as `gensub()` or `IGNORECASE` are not portable either. ## The strategies, from best to worst **1. Avoid the extension.** Most in-place edits do not need `-i`. Writing to a temp file and moving it into place is portable, and it is also atomic — readers never see a half-written file: ```bash tmp=$(mktemp) sed 's/foo/bar/g' config.txt > "$tmp" && mv "$tmp" config.txt ``` Similarly, prefer `date -u +%s` arithmetic over relative date strings where you can. Be aware this only takes you so far: converting an epoch back to a formatted date is `date -d @EPOCH` on GNU and `date -r EPOCH` on BSD, so some branch is unavoidable. **2. Feature-detect, don't guess the OS.** Testing behaviour beats testing `uname`, because a Mac with Homebrew's `coreutils` and `gnu-sed` installed has both userlands available under `g`-prefixed names: ```bash if date --version >/dev/null 2>&1; then stamp=$(date -d '2 days ago' +%F) # GNU else stamp=$(date -v-2d +%F) # BSD fi ``` Do the detection once at the top and store the answer in a variable; do not sprinkle `uname` checks through the script. **3. Fix the environment instead of the script.** If the script is infrastructure rather than a developer convenience, run it in a container image where the userland is pinned. That converts an open-ended portability problem into a build-time dependency, and it is usually the cheapest answer for anything that runs in CI or production. **4. Move the logic somewhere uniform.** If the script is doing enough text manipulation that every second command has a portability branch, a single POSIX `awk` program — or a real program in another language — replaces a dozen fragile invocations. ## What to say in an interview The insight interviewers want is *the shell is not the portability boundary; the userland is*. A script can be perfectly POSIX shell code and still be completely unportable because of the options it passes to `sed`. Naming one concrete trap precisely — the `sed -i` argument shape is the canonical one — plus the temp-file-and-`mv` fix and feature detection over `uname` sniffing, covers the ground well.

  • Why is feature-detecting the tool better than branching on `uname`?
    Because the platform does not determine the userland. A Mac with Homebrew's coreutils and gnu-sed has GNU tools available as `gsed` and `gdate`, and some Linux containers ship BusyBox applets rather than GNU ones. Testing what the command actually supports — `date --version >/dev/null 2>&1` — matches reality; testing the OS name encodes an assumption that breaks quietly.
  • Is `sed -i.bak 's/a/b/' file` a safe cross-platform in-place edit?
    It happens to run on both, because the suffix is attached to the flag in GNU and read as the attached argument in BSD — but it leaves a `.bak` file behind that you must then delete, and `-i` is still outside POSIX, so a third `sed` need not support it at all. The temp-file-plus-`mv` form has neither problem and is atomic besides.
  • Your script is pure POSIX shell with no bashisms. Does that make it portable?
    No. Shell dialect and userland portability are separate axes. A `#!/bin/sh` script that calls `sed -i`, `grep -P` or `stat -c` is unportable regardless of how clean the shell code is. Both have to be checked, and the utility options are the half people forget.

GNU and BSD utilities are two dialects that share a core vocabulary: the standard words mean the same thing in both, but each has idioms the other simply does not parse.

saying these in an interview costs you the question

  • Assuming sed and date are one program everywhere
  • Branching on uname instead of testing the tool
  • Believing POSIX shell code implies portable commands
  • Adding GNU-only flags because they work locally
  • Thinking macOS is simply an outdated Linux

context