はじめに
シェルスクリプトから呼び出すコマンドは環境依存があります。Linux と macOS、そして BSD 系 Unix と System V 系 Unix で同じ名前のコマンドでも機能が異なります。
POSIX で標準化されている機能だけ使えばいいのでは?というのは机上の空論でしかありません。POSIX で標準化されている機能は少なくそれだけでは不便でできないこともあります。また POSIX で標準化されていてもなお違いがあります。違いがある理由は、一部の機能は後方互換性を維持するために違いがあっても良いと POSIX が認めているからです。したがって「昔のやり方」は POSIX 規格書を読んで各環境の違いに気をつけてシェルスクリプト書くという非効率なやり方が求められていました。
令和の今、そのようなことをする必要はありません。なぜなら POSIX に準拠して作られた GNU コマンドがあるからです。POSIX に準拠して作られたものはどの環境でも動きます。だから GNU コマンドはどこでも動いています。OS に標準でインストールされているコマンドに依存せずに GNU コマンドに統一すれば、環境依存の問題はありません。まあ現実にはそれだけでは足りない部分はありますが、簡単に移植性を高める方法の第一歩が、GNU bash と GNU コマンドを使うことです。
ただし GNU bash や GNU コマンドを使うと言っても、正しくそれを実践できている人はほとんどいないようです。なぜならシェル言語を使う「シェルプログラミング」をしっかりと学んでいないからです。シェルスクリプトとっての基礎はシェル言語です。シェルスクリプトから呼び出すコマンドは取り替え可能なただの外部ライブラリにすぎません。シェルスクリプトを書くにはシェル言語の機能を学ぶ必要があります。
この記事は、移植性を高める正しいシェルプログラミングなので、シェル言語の話までは踏み込みません。今すぐ使える移植性を高める書き方について解説します。
bashとGNUコマンドをHomebrewからインストールせよ!
Homebrew については解説しませんが、bash と GNU コマンドは Homebrew から簡単にインストールできます。
$ brew install bash coreutils
これを実行すると最新の bash と GNU コマンドがインストールされます。GNU コマンドは頭に g
を付けて実行することができます。例えば sed
コマンドは gsed
です。
シバンには /usr/bin/env bash
と書け!
macOS 標準の bash はバージョンが古いです。最新の bash を使いましょう。そのためにはシバンに /usr/bin/env bash
と書きます。
#!/usr/bin/env bash
# ↑一行目のシバンの書き方
echo ok
コマンド名を変数に入れるな!
まずよく見かける書き方がこれです。
if macOS なら; then
SED=gsed
else
SED=sed
fi
こんなことをやってはいけません。こんなことをすると修正が面倒ではないですか?だって sed
コマンドを使うときに今まで sed
と書けばよかったのに $SED
と書き換えなければいけないのでしょう?
echo foo | "$SED" 's/foo/FOO/g'
正しい書き方はこうです。
# 移植性を高める部分
if macOS なら; then
sed() { gsed "$@"; }
fi
# 本質的なコードを書く部分(今まで通りの書き方で良い)
echo foo | sed 's/foo/FOO/g'
移植性を高める正しい方法というのは「面倒なことがない」ことです。macOS 対応のために今までの Linux 用コードを書き換える必要はありません。「移植性を高める部分」と「本質的なコードを使う部分」は分離するのが鉄則です。そのためにシェル関数を定義します。
コマンドがたくさんあるならループしてevalせよ!
ループはシェル言語で必要とされた機能です。eval
もそうです。正しく使えば eval
が危険なことはありません。正しく使う方法を学びましょう。正しく使えないからという理由で禁止するのは悪手です。
for i in sed grep tr awk; do
eval "$i() { g${i} \"\$@\"; }"
done
eval
は信用できない外部からデータが渡されたときに気をつけるものです。ソースコードの直接書かれているのであれば、危険なことは何もありません。
というかですね。eval
が危険というのなら以下のコードも同じように危険なんですよ。これらは本質的に eval
と同じです。
# varに不正な文字が入っていれば脆弱性になる
awk '
長いコード
長いコード
'"$var"'
長いコード
長いコード
'
# varに不正な文字が入っていれば脆弱性になる
echo "$var" | bash
eval
は適切に使えば良いだけです。
複数のオプションや引数は配列に入れろ!
シェル言語に配列はいらないなどという人がいますが、本来シェル言語に配列は必須なものです。UNIX に開発者は次世代の OS として Plan9 を開発していましたが、新しいシェルである rc シェルは全てが配列でした。もし Plan9 が成功していたら、今頃はあたり前のものとしてシェルで配列を使っていたことでしょう。bash は UNIX 開発者たちが望んだ新しいシェルを別の形(シンプルでない代わりに互換性を保った形)で実装したものなのです。
なぜシェル言語に配列が必要なのか? それはオプションや引数を入れるためです。
sed_opts=('-E' '-i')
sed "${sed_opts[@]}}"
配列じゃなくてもただの変数でいいじゃんと思う人は、ただの変数ではスペースが含まれる引数を正しく扱えないという事実を知りません。
ary=('-E' '-i' 'foo bar baz') # スペースが含まれていても正しく扱える
var='-E -i foo bar baz' # スペースが含まれていると正しく扱えない
配列が必須な理由は、位置パラメータ($1
, $2
, ...) を一つの配列変数に代入しておくためです。次のような当たり前の処理を当たり前に書くには配列がなければ実現できません。
func() {
# 一つの配列に位置パラメータすべてを正しく保存
org_args=("$@")
# 以下はバグ(正しくない)
# org_args=$@
}
func "a b" "c d" "e f"
一応補足しておきますが、一つの普通の変数にスペースが含まれる複数の引数を代入するということは、私はすでに実現しています。ヒントは eval
を使います。とても面倒であることを知っているから、配列が必要だと言っています。
よく使うなら関数を定義しろ!
複数の引数を配列に入れると言っても、こんな面倒なコードは書きたくないわけですよ。
sed_opts=('-E')
sed "${sed_opts[@]}}"
それがデフォルトでよく使うなら関数を定義するべきです。
# 移植性を高める部分
esed() { sed -E "$@"; }
# 本質的なコードを書く部分
esed 's/foo/FOO/'
既存のコマンドを置き換えたいなら command コマンドを使え!
sed
はもう拡張正規表現以外は使わない!っていうなら sed
を置き換えればいいです。その時に command
コマンドを使えば、シェル関数を無視して(再帰呼び出しを防いで)元のコマンドを呼び出せます。
# 移植性を高める部分
if macOSなら; then
sed() { gsed -E "$@"; }
else
sed() { command sed -E "$@"; }
fi
# 本質的なコードを書く部分
sed 's/foo/FOO/'
さいごに ~ 移植性を高めるには関数を定義して解決しろ!
思いつきで書いた記事なので雑で申し訳ありません。
他にも移植性を高めるためのシェルプログラミング技術はありますが、どれも基本は「移植性を高める部分で関数を定義」し、「本質的なコードを書く部分」のコードを最小化し、Linux 用と macOS 用の違いがある場所を局所化してメンテナンスしやすくすることです。
決してコードの途中で条件分岐してはいけません
コードの流れは上から下へとまっすぐにです。「本質的なコードを書く部分」で分岐させてはいけないのです。
# 本質的なコードを書く部分
case $(uname) in
*Linux*)
Linux 用の処理を長々と書く
;;
*Darwin*)
macOS 用の処理を長々と書く
;;
esac
このようなコードをあちこちに書いていると、いざ他の OS にも対応としようと考えると、あちこちを修正して回る羽目になります。ではどうするか?
違いを吸収する関数を定義するのが正しいシェルプログラミングです。このようなシェルプログラミング技術は、ただコマンドの使い方を勉強しているだけでは身につきません。