MacOS's subtle differences in command line tools

Some tools like sed, readlink, wc, and stat behave different on MacOS. See how to get those Linux commands running.

MacOS's subtle differences in command line tools
Photo by Kenny Eliason / Unsplash

I'm a Linux guy. Nevertheless I've been setting up a development environment on MacOS. As this is still a Unix based system running zsh, I wasn't expecting any issues at all.

I was wrong.

It turns out, that many command line tools, work slightly different on MacOS than they do on Linux. Here's what I found so far.

sed

The stream editor sed is very handy to quickly replace text in files. To edit a file in-place on Linux, you'd run

$ sed -i 's/TEXT/REPLACEMENT/d' *.sh

On MacOS this command fails with

$ sed -i 's/TEXT/REPLACEMENT/d' *.sh
sed 1: "patchUP.sh": command c expects \ followed by text

The reason is that on Mac OS -i expects an argument for a backup extensions. Hence the command to in-place edit files is

$ sed -i '' 's/TEXT/REPLACEMENT/d' *.sh

readlink can print the value of a symbolic link or canonical file name. The MacOS version does not have the -f (canonicalize symlink), and -e (canonicalize existing symlink) options. Thus running the command with such option will result in

$ readlink -e $directory
readlink: illegal option -- e
usage: readlink [-fn] [file ...]

The solution here isn't straightforward. It's either using greadlink from Homebrew's coreutils or involves scripting around pwd -P .  For the latter have a look into this Gist: How to get GNU's readlink -f behavior on OS X.

wc

We've been using a variant of Git's predefined pre-commit hook. On MacOS this hook was constantly failing on each commit. One offending line was

test $(git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0

To see what's going on, let's have a look at the output of following script

# /usr/bin/env bash

set -o xtrace
echo "single line" | wc -l

On Linux this results in

$ ./wcount.sh
+ echo 'single line'
+ wc -l
1

While on MacOS

$ ./wcount.sh
+ echo 'single line'
+ wc -l
       1

So wc -l is prefixing its result with some spaces.

To get rid of those, they are trimmed within the test command

test $(git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0' | wc -c |  LC_ALL=C tr -d ' ') != 0

stat

Checking file sizes is also part of the pre-commit hook. The command run on Linux is

stat -c %s $file

On MacOS this command fails with

$ stat -c %s /etc/hosts
stat: illegal option -- c
usage: stat [-FLnq] [-f format | -l | -r | -s | -x] [-t timefmt] [file ...]

The man page reveals the format to display the file size

stat -f %z /etc/hosts

Summary

Having these subtle but breaking changes in such basic commands is really unexpected. It seems like MacOS is trying to different only for the sake of being different here. So from now on I'm cluttering my scripts with switches like

if [ "$(uname)" = "Darwin" ]; then
   # do Mac stuff
else
   # do Linux stuff
fi