CLI AI

sed -i not working on Mac? BSD vs GNU flags, fixed

2026-05-15

If you got sed: 1: "s/foo/bar/g": invalid command code, nothing is broken. macOS ships BSD userland tools; your script was written for GNU ones. Same command names, different flags. Here are the six that actually bite.

1. sed -i wants an argument on macOS

clai
$ clai replace foo with bar in config.txt in place, works on both mac and linux→ sed -i.bak 's/foo/bar/g' config.txt   # portable: writes config.txt.bak

GNU treats the backup suffix as optional; BSD requires it. sed -i '' 's/…' works on macOS and fails on Linux. sed -i.bak works on both — you just get a backup file to delete.

2. Extended regex: use -E, never -r

clai
$ clai replace any run of digits with N using extended regex→ echo "foo123" | sed -E 's/[0-9]+/N/'fooN

-r is GNU-only. -E works on both — GNU has accepted it for years. Make -E your default and one whole class of bug disappears.

3. date: -d on Linux, -r on macOS

clai
$ clai convert unix timestamp 1735689600 to a readable date in UTC→ date -u -d @1735689600 '+%Y-%m-%d %H:%M:%S UTC'    # GNU2025-01-01 00:00:00 UTC→ date -u -r 1735689600 '+%Y-%m-%d %H:%M:%S UTC'     # BSD / macOS

There is no portable spelling here. Branch on uname, or install GNU coreutils and call gdate.

4. stat: -c on Linux, -f on macOS

clai
$ clai print the size and name of this file→ stat -c '%s %n' t.txt      # GNU4 t.txt→ stat -f '%z %N' t.txt      # BSD / macOS

Different flag and different format specifiers. If a script does anything non-trivial with stat, reach for find -printf on Linux or wc -c for the portable case.

5. grep -P doesn't exist on macOS

clai
$ clai extract just the digits from this string, portably→ echo "abc123" | grep -Eo '[0-9]+'123

grep -P (Perl regex) is a GNU build option and macOS doesn't ship it. -E covers most real patterns; if you genuinely need lookahead, you need perl or ripgrep, not grep.

6. xargs -r is GNU-only — and you need it

clai
$ clai find log files older than 30 days and delete them, don't run rm on an empty list→ find . -name '*.log' -mtime +30 -print0 | xargs -0 -r rm

Without -r, GNU xargs runs the command once with no arguments on empty input — which is how xargs rm in an empty directory turns into an error, and how xargs docker rm becomes a surprise. BSD xargs already refuses to run on empty input, so -r is unnecessary there and unrecognised on older versions.

Gotchas

  • brew install coreutils gives you g-prefixed twins — gsed, gdate, gstat. Adding gnubin to your PATH makes a Mac behave like Linux, which fixes your laptop and hides the bug from everyone else on the team.
  • readlink -f is not safe to assume. Older macOS doesn't have it. Use realpath where available, or greadlink -f.
  • base64 -d vs -D. GNU decodes with -d; macOS historically wants -D.

Related questions

What does "invalid command code" actually mean? BSD sed consumed the next word as the backup suffix, then read the rest of your script as a command it didn't recognise. It's a parsing casualty, not a regex problem.

Is macOS BSD or Linux? Neither. Its userland descends from FreeBSD, which is why the tools share names with Linux but not flags.

How do I write a script that works on both? Stick to POSIX flags, prefer -E, always pass sed -i.bak, and test on both before it goes near CI.

See also

CliAI detects the OS and shell you're on and generates the flags that machine actually accepts. Install it in one line.