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 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 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 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 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 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 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 coreutilsgives youg-prefixed twins —gsed,gdate,gstat. Addinggnubinto your PATH makes a Mac behave like Linux, which fixes your laptop and hides the bug from everyone else on the team.readlink -fis not safe to assume. Older macOS doesn't have it. Userealpathwhere available, orgreadlink -f.base64 -dvs-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
- Same workflow on Windows: PowerShell and cmd parity
- Network triage in plain English: connections, ports, DNS
- From one-liner to checked-in script: when to graduate
CliAI detects the OS and shell you're on and generates the flags that machine actually accepts. Install it in one line.