LinuxとMacではベースコマンドが別物

Linuxのベースコマンド群の一つである「GNU Core Utilities(Coreutils)」は、ほかのUNIX系OSではそのシステムが独自に持っているベースコマンドに対応している。ただし、Coreutilsとそれぞれのベースコマンドは、POSIXで規定されている動作についてはほぼ同じだが、POSIXで規定されていない部分の動作には互換性がない。そのため、Linuxのコマンドと同じ要領で使おうとするとうまくいかないことがある。

一方、MacでCoreutilsに相当するベースコマンドはOSの一部として取り込まれており、基本的にはFreeBSDのベースコマンドが移植されている。こちらも基本動作はCoreutilsのコマンド群と同じだが、細かい挙動は異なっているほか、POSIXに規定されていない動作はCoreutilsとは互換性がほとんどない。

つまり、LinuxでCoreutilsに用意されているコマンドを利用してシェルスクリプトを使う場合、そのままではMacでは動かない可能性がある。今回は簡単なシェルスクリプトを例に、Macに対応させる方法を説明する。いくつかのアプローチがあるので、それぞれのポイントを把握していただきたい。

向こう7日間の日付を一覧表示するシェルスクリプト

まず、Linuxで次のようなシェルスクリプトを使っているとしよう。これは向こう7日間(明日から7日間分)の日付を一覧表示するシェルスクリプトだ。

next7days.sh - 向こう7日間の日付を一覧表示するシェルスクリプト

#!/bin/sh

for i in 1 2 3 4 5 6 7
do
    date --date "$i day" "+%Y-%m-%d"
done

実行すると次のようになる。

実行サンプル - Linux

% ./next7days.sh
2021-12-22
2021-12-23
2021-12-24
2021-12-25
2021-12-26
2021-12-27
2021-12-28
% 

これは期待通りの動作だ。これを、何も修正することなくMacで実行すると次のようになる。

実行サンプル - Mac

% ./next7days.sh
/bin/date: illegal option -- -
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
/bin/date: illegal option -- -
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
/bin/date: illegal option -- -
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
/bin/date: illegal option -- -
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
/bin/date: illegal option -- -
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
/bin/date: illegal option -- -
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
/bin/date: illegal option -- -
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ...
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
% 

Macのdateコマンドには「--date」というオプションは存在しない。そのため、上記実行結果のように全てのdateコマンドの実行がエラーになっている。

では、どうすればこれをMacに対応させることができるのか、その方法を見ていこう。

Mac版へ書き換える

まず、シンプルに考えられる方法は、シェルスクリプトをMac版へ書き換えることだ。Mac dateは「-v」というオプションを提供しており、このオプションを使うことで細かく時間を前後させることができる。先ほどの「--date」の処理を「-v」に書き換えることで、Macでも同じ動作をさせることができる。

Mac用に書き換えると次のようになる。

next7days_mac.sh - 向こう7日間の日付を一覧表示するシェルスクリプト(Mac版)

#!/bin/sh

for i in 1 2 3 4 5 6 7
do
        date -v-${i}d "+%Y-%m-%d"
done

実行すると次のようになる。

実行サンプル

% ./next7days_mac.sh
2021-12-20
2021-12-19
2021-12-18
2021-12-17
2021-12-16
2021-12-15
2021-12-14
% 
  • 実行サンプル - M1 Mac

    実行サンプル - M1 Mac

このように、Macでも動作していることを確認できる。

普段、Mac中心で作業しているのであれば、こういった感じでMac版のシェルスクリプトに書き換えるのがロジカルな選択肢の一つではないだろうか。Macのコマンドにすでに詳しい場合にもちろんのこと、これから詳しくなっていくことでほかの恩恵も受けやすくなる。

Coreutilsを使用するように書き換える

次は、MacでもCoreutilsを使うようにスクリプトを書き換える方法だ。この方法であればオプションなどは書き換えることなく、コマンド名だけを変えるだけで対応できる。

前回説明したように、「brew install coreutils」でCoreutilsをインストールした場合、次のようなコマンドが使えるようになる。

HomebrewでインストールされるCoreutilsコマンド
b2sum, base32, basenc, chcon, factor, g[, gb2sum, gbase32, gbase64, gbasename, gbasenc, gcat, gchcon, gchgrp, gchmod, gchown, gchroot, gcksum, gcomm, gcp, gcsplit, gcut, gdate, gdd, gdf, gdir, gdircolors, gdirname, gdu, gecho, genv, gexpand, gexpr, gfactor, gfalse, gfmt, gfold, ggroups, ghead, ghostid, gid, ginstall, gjoin, gkill, glink, gln, glogname, gls, gmd5sum, gmkdir, gmkfifo, gmknod, gmktemp, gmv, gnice, gnl, gnohup, gnproc, gnumfmt, god, gpaste, gpathchk, gpinky, gpr, gprintenv, gprintf, gptx, gpwd, greadlink, grealpath, grm, grmdir, gruncon, gseq, gsha1sum, gsha224sum, gsha256sum, gsha384sum, gsha512sum, gshred, gshuf, gsleep, gsort, gsplit, gstat, gstdbuf, gstty, gsum, gsync, gtac, gtail, gtee, gtest, gtimeout, gtouch, gtr, gtrue, gtruncate, gtsort, gtty, guname, gunexpand, guniq, gunlink, guptime, gusers, gvdir, gwc, gwho, gwhoami, gyes, hostid, md5sum, nproc, numfmt, pinky, ptx, realpath, runcon, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, shred, shuf, stdbuf, tac, timeout, truncate

dateコマンドはすでにMacに存在するため、Coreutilsでインストールされるdateコマンドは「gdate」というコマンド名になっている。つまり、先ほどのシェルスクリプトで使うコマンドを「date」から「gdate」へ書き換えればよいということになる。次のような感じだ。

next7days_coreutils.sh - 向こう7日間の日付を一覧表示するシェルスクリプト(Mac Coreutils版)

#!/bin/sh

for i in 1 2 3 4 5 6 7
do
        gdate --date "$i day" "+%Y-%m-%d"
done

実行すると次のようになる。

実行サンプル

% ./next7days_coreutils.sh
2021-12-22
2021-12-23
2021-12-24
2021-12-25
2021-12-26
2021-12-27
2021-12-28
% 

Linuxで実行していたときと同じように動作していることがおわかりいただけるだろう。この書き方をする場合、Mac dateとLinux dateを同じシェルスクリプトで同時に使うといったこともできる。MacのコマンドとCoreutilsのコマンドを使い分けたい場合には、こんな感じでCoreutilsのコマンドを明示的に使えばよい。

CoreutilsをMacコマンドよりも優先的に使うようにする

逆に、同じコマンド名でも、MacのコマンドではなくCoreutilsでインストールされるコマンドを優先的に使うという方法もある。例えば、M1 MacのHomebrewでCoreutilsをインストールすると、/opt/homebrew/opt/coreutils/libexec/gnubin/に次のようなコマンドがインストールされる。

/opt/homebrew/opt/coreutils/libexec/gnubin にインストールされるコマンド
[, b2sum, base32, base64, basename, basenc, cat, chcon, chgrp, chmod, chown, chroot, cksum, comm, cp, csplit, cut, date, dd, df, dir, dircolors, dirname, du, echo, env, expand, expr, factor, false, fmt, fold, groups, head, hostid, id, install, join, kill, link, ln, logname, ls, md5sum, mkdir, mkfifo, mknod, mktemp, mv, nice, nl, nohup, nproc, numfmt, od, paste, pathchk, pinky, pr, printenv, printf, ptx, pwd, readlink, realpath, rm, rmdir, runcon, seq, sha1sum, sha224sum, sha256sum, sha384sum, sha512sum, shred, shuf, sleep, sort, split, stat, stdbuf, stty, sum, sync, tac, tail, tee, test, timeout, touch, tr, true, truncate, tsort, tty, uname, unexpand, uniq, unlink, uptime, users, vdir, wc, who, whoami, yes

コマンド名はMacのコマンド名とかぶっている。環境変数PATHで/opt/homebrew/opt/coreutils/libexec/gnubin/を優先的に検索対象になるようにすれば、MacのコマンドではなくCoreutilsのコマンドが使われるようになる。

まず、シェルスクリプトで動作を確認してみよう。次のようにシェルスクリプトで環境変数PATHの先頭に/opt/homebrew/opt/coreutils/libexec/gnubin/を追加するようにする。

next7days_coreutils.sh - 向こう7日間の日付を一覧表示するシェルスクリプト(Mac Coreutils版その2)

#!/bin/sh

PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"

for i in 1 2 3 4 5 6 7
do
        date --date "$i day" "+%Y-%m-%d"
done

実行すると次のようになる。

実行サンプル

% ./next7days_coreutils2.sh
2021-12-22
2021-12-23
2021-12-24
2021-12-25
2021-12-26
2021-12-27
2021-12-28
%

ご覧の通り、きちんと動作している。この方法の場合、1行新しく設定を追加するだけでよい。実行されるのはMacのdateコマンドではなくCoreutilsのdateコマンドだ。

もしくは、次のようにシェルスクリプトを実行する段階で環境変数PATHを設定するようにすれば、シェルスクリプトを書き換えることなくLinux向けのシェルスクリプトをMacで実行することができる。

実行サンプル

% env PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH" ./next7days.sh
sh
2021-12-22
2021-12-23
2021-12-24
2021-12-25
2021-12-26
2021-12-27
2021-12-28
% 

こちらの方法であれば、シェルスクリプトを書き換えることなく、LinuxでもMacでも動作させることができる。シェルスクリプトをGithub.comなどでホストしておき、LinuxでもMacでも使うといった使い方をしている場合には、この方法はとても便利だ。「Linux中心で作業していて、時々Macを使う」といった場合にも、この方法は便利ではないかと思う。

/opt/homebrew/opt/coreutils/libexec/gnubin/をデフォルト化する方法もあり

また、インタラクティブシェルの設定ファイルで/opt/homebrew/opt/coreutils/libexec/gnubin/を環境変数PATHに追加してしまうという方法もある。例えば、最近のMacを使っていてシェルを変更していないなら「zsh」が動作しているはずなので、~/.zprofileに次のような設定を追加すればよい。

/opt/homebrew/opt/coreutils/libexec/gnubin/をデフォルト化する設定 - ~/.zprofile

export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"

この方法ならば、インタラクティブシェルで使うコマンドもCoreutilsに関してはMacではなくCoreutilsのコマンドが使われるようになる。普段使っているOSがLinuxなら、これはそれほど悪くない方法だ。

しかし、逆にMacを想定したコマンドやシェルスクリプトは、この設定が有効になっていると不具合を起こす。その点を考慮して活用していただきたい。