僕はコマンドラインで使うシェルスクリプトを書くことがけっこうあるんだけど、インターフェイスというか呼び出し方はとても大事だと思ってるので、そこにわりと時間をかけて考えるようにしてる。実装はいつでも変更できるけど呼び出し方を変えた時は利用者にも変更を強いるので、できれば最初から良い設計で作りたいと思っている。
そこで、僕がシェルスクリプトのオプションとか引数とかの仕様を決める上で注意していることをまとめてみた。シェルスクリプトや、その他コマンドラインのツールを作るときに参考にしてほしい。
シェルの種類は bash や zsh を想定してるけど、実装によらない話なのでどんなシェルでも使えると思う。
エラーの時に Usage (使い方ヘルプメッセージ)を表示するのはやめる
エラーになった時に Usage (使い方ヘルプメッセージ) を表示するスクリプトがあるけど、やめたほうがいいと思う。例えばこんな感じ。
% hoge file
hoge: file: そのようなファイルやディレクトリはありません
Usage : hoge [OPTIONS] FILE
# ... 以下、hoge の使い方が表示される
このエラーメッセージで一番大事なのは1行目だ。ここを見ることによってエラーの原因が分かり、呼び出した人が修正できるようになる。
その下の使い方の説明は、重要度はぐっと下がる。ノイズと言っても良い。特に Usage が長くなった時に本来のエラーメッセージが埋もれてしまう恐れがある。
なので Usage を表示するのはやめたほうがいいと思っている。ヘルプを見てほしい場合は "Try '-h' option for more information." とでも出力しておけば良いと思う。
- で始まる引数も指定できるようにする
例えば grep
で --help
という単語を検索したいとき、次のようにしてもうまくいかない。
# ダメな例
% grep --help file.txt
こうやっても --help
がオプションとみなされるので、grep
のヘルプメッセージが出るだけになる。
もちろん検索できないと困るので、ちゃんと対策方法は用意されている。
方法1 : -- で区切る
# -- の後はオプションでないものとみなされる。
% grep -- --help file.txt
方法2 : 専用のオプションをつくる
# grep の場合は -e オプションの引数として検索する文字列を指定できる。
# オプションの引数は - で始まっていてもオプションとはみなされない。
% grep -e --help file.txt
自分でシェルスクリプトを作るときでも、少なくともどちらか1つの方法で対策をとっておく。「方法1」については、普通に getopts
でオプションをパースすればその動作になる。「方法2」については、自分でオプションを増やしてそうなるように対応する。
on/off を切り替える設定のオプションは後に現れた方を優先にする
例えば何かを削除するコマンドを作っていて、-i
オプションで削除前に本当に削除するか確認する、-f
で確認なしに強制的に削除する、というオプションを作ったとしよう。
-i
と -f
が両方指定されたときの仕様としては以下が考えられる。
- 先に指定した方を優先する
- 後に指定した方を優先する
- エラーにする
どれでも良いようにも思えるけど、「後に指定した方を優先する」にしたほうが良いと思う。
なぜかというと、alias でデフォルト動作を指定していたとして、明示的にオプションをつければデフォルトと違う動作を指定できるから。
例えば cmd
というスクリプトを作って、それが -i
-f
というオプションを受け付けるようにしたとする。そして、普段は確認してほしいので以下のように alias を設定した人がいたとしよう。
alias cmd='cmd -i'
この状態で cmd -f
とした場合は、後に指定した方が優先としたので、結果として -f
オプションの動作になる。つまり、オプションを明示的に指定した時はデフォルトの動作を上書きする、という形に自然になっている。「先が優先」や「エラー」という仕様の場合はこうはいかない。
入力ファイルを受け取る場合、ファイル名が '-' またはファイル名の指定を省略した場合は標準入力から読み込むようにする
標準入力は「標準」という名前の通り、一番標準的な入力だ。なので普通に自然に使った時は標準入力が使われるようにしたほうが良い。
具体的には cat
コマンドにならって、ファイル名が '-' またはファイル名の指定を省略した場合は標準入力から読み込むようにすればよい。
-
というファイルをサポートして何がうれしいかと言うと、ラッパースクリプトを書くのが楽になるというのがある。
例えば grep
のラッパーを書く場合はこんな感じ。
pattern=
input_file="-"
if [ -n "$1" ]; then
pattern=$1
fi
if [ -n "$2" ]; then
input_file=$2
fi
grep "$pattern" "$input_file"
デフォルトの入力ファイル名を -
にしているところがポイントで、こうすることによって入力ファイルを指定しない場合は標準入力から読み込む、という動作にできる。これは grep
コマンドが -
を標準入力としてあつかうように実装されているおかげだ。
オプションでない引数はオプションでも指定できるようにする
これは絶対必要というほどではないけど、オプション以外の普通に指定する引数があるときは、それをオプションでも指定できるようにしておいたほうが便利なことがある。
例えば cp
コマンドは cp コピー元 コピー先
という形で、オプションでない引数を2つ指定して使う。
ところで、cp
コマンドはコピー先を -t
オプションでも指定できるようになっている。つまり、以下の2行は同じ意味だ。
% copy FROM TO
% copy -t TO FROM
-t
オプションがあって何が便利かというと、例えばこんな alias を作れるようになる。
# 指定したファイルを $HOME/backup にコピーする alias
# 使い方 backup FILE ...
alias backup="cp -t $HOME/backup"
bash や zsh の alias は引数をとれないので、もし -t
オプションがなかったらこういうふうにはできない。
最後に
このあたりのルールは、一言で言うと 「Unix の哲学を理解する」と言える。「哲学」を学ぶには次の本が参考になるので、興味がある人は読んでみると良いと思う(ちょっと古いけど内容としては今でも役に立つ)。