Unixツールを作成するためのヒント

現代のプログラマを取り巻く世界には無数の方法で組み合わされた、たくさんのUnixツールがあふれています。優れたツールは開発環境とシームレスに統合されますが、そうでないツールは使うたびに不満がたまっていきます。また、優れたツールはあなたの想像力次第でどんなものにも適用できますが、そうでないツールはあなたの開発環境で動かすためだけでも、あの手この手の対策を講じなければならないことがよくあります。

“一つのことだけうまくやればいい”という考えでは目標に到達しない。”うまくいったものを、うまく組み合わせる”ことまで考えるべきだ

良い設計に必要なものとは何かについて論じるつもりはありません。それについては他でも話題に挙がっています。代わりに、私は新しいツールを作成するうえで気を付けるべき慣習についていくつか述べたいと思います。優れたツールを作成するというのは漠然とした目標ですが、悪いツールを作成しないようにすることは難しくありません。Unixがそのツールに求めているのは良識です。つまり、円滑に動作し、(さらに重要なのは)構成するための一連の慣習に従う必要があるのです。これからその慣習の中でも、特に私たちが忘れがちなものについて、いくつか挙げていきます。これらは絶対的要件ではありませんが、できるだけ守るように重視すべきものです。

stdinで入力を読み込み、stdoutで出力を書き出す。つまり、あなたのプログラムをフィルタにするということです。フィルタはシェルのパイプラインと簡単に統合できる、Unixツールの構成でも特に重要なユーティリティであることは間違いありません。

出力にヘッダやその他の装飾が含まれないようにする。無関係な出力はツールの出力を解析しようとするユーザの妨げになります。ヘッダや装飾は、あなたが得ようとしている構造化データに比べると一般的でなく、むしろ異質な存在なので、使わない方がいいでしょう。

解析や構成のため、出力は単純なものにする。通常は、各列を空白で区切った、単一のプレーンテキスト形式の出力行としてレコードを表すことになります(とはいえ、JSONとは違います)。grep、sort、sedなどの特に優れたUnixツールはこれについて考慮されています。簡単な例として、下記にあるベンチマークの出力について見ていきましょう。これはそれぞれのレコードにベンチマークの名前とそれから発生した一組のキーバリューを順に出力するような形式になっています。この構造は順応性が高く、出力形式を乱すことなくキーを加えたり外したりすることが可能です。

$ ./runbenchmarks
Benchmark: fizzbuzz
Time: 10 ns/op
Alloc: 32 bytes/op
Benchmark: fibonnacci
Time: 13 ns/op
Alloc: 40 bytes/op
...
$

便利そうではありますが、これではUnixにおいてあまりに不恰好です。我々が一般的によくやる方法として、まず1つのベンチマークの結果のみに着目して考えていくことにします。以下のようになります。

$ ./runbenchmarks | awk '/^Benchmark:/ { bench = $2}  bench=="fizzbuzz"'
Benchmark: fizzbuzz
Time: 10 ns/op
lloc: 32 bytes/op
$

これを各行ごとに1つのベンチマークのレコードのみを表示するようにし、縦の列は空白で区切るようにします。そうすることで結果が非常にシンプルで見やすくなります。

$ ./runbenchmarks 
fizzbuzz    10  32
fibonnaci   13  40
...
$ ./runbenchmarks | grep '^fizzbuzz'
fizzbuzz    10  32
$

入力を並び替えたり統合する際に、このやり方がいかに有益かよく分かります。例えば、行ごとに記録レコードを表示するようにすると、結果を所要時間順に並び替えることが容易にできます。

$ ./runbenchmarks | sort -n -r -k2,2
fibonnaci   13  40
fizzbuzz    10  32
...
$

ツールをAPIとして扱う。あなたが作成したツールは思いもよらぬコンテキストで使われる可能性があります。もしツールの出力形式が変更されると、構成もしくは別の形でその出力を利用した他のツールが必ず崩壊してしまいます。それではAPIとして扱えていません。

診断出力をstderrに置く。診断出力にはツールからの重要ではないデータ出力も含まれています。進行状況のインジケータ、デバッグの出力、ログメッセージ、エラーメッセージ、そして利用情報などがそれに当たります。もし診断出力をデータと混同させると、構文解析が非常に困難になり、それゆえにツールの出力を構成することも難しくなります。一方、stderrを利用すれば診断出力はより便利なものにもなります。たとえstdoutにフィルタが掛けられたりリダイレクトされたりしても、stderrはユーザが求める診断出力を端末に出力し続けます。

エラーを終了ステータスで確認する。ツールがうまく動かなかった場合、終了時のステータスは0以外になります。これによりシェルの単純結合が可能になり、スクリプト内のエラーハンドリングも単純になります。以下のバイナリを作成する2つのツールについて見ていきましょう。バイナリの作成に成功した場合のみ実行するようなツールを構築したいと思います。Badbuildの場合、ツールがうまく働かなかった時に”FAILED”を最後の行に出力します。

$ ./badbuild binary
...
FAILED
$ echo $?
0
$ # Run binary on successful build.
$ test "$(./badbuild binary | tail -1)" != "FAILED" && ./binary
$

Goodbuildの場合、終了ステータスは適切な値を示します。

$ ./goodbuild
$ echo $?
1
$ # Run binary on successful build.
$ ./goodbuild binary && ./binary
$

ツールの出力に可搬性を持たせる。言い換えれば、解析と解釈にできるだけ小さなコンテキストを要求するように、ツールの出力はそれ自身で完結すべきです。たとえば、ファイルを示すためには絶対パスを使い、インターネットのホストを指定するためには完全装飾ホスト名を使うべきです。可搬的な出力では追加のコンテキストのない他のツールを直に使えます。これにしばしば違反するのはビルドツールです。たとえば、GCCとClangのいずれも、ワーキングディレクトリとの相対的なパスを報告することで賢くあろうとします。この例では、ソースファイルへのパスはコンパイラが起動されたときのカレントワーキングディレクトリとの相対パスを表します。

$ cc tmp/bad/x.c
tmp/bad/x.c:1:1: error: unknown type name 'INVALID_C'
INVALID_C
^
tmp/bad/x.c:1:10: error: expected identifier or '('
INVALID_C
         ^
2 errors generated.
$

この賢さはすぐに破綻します。たとえばmake(1)を-Cフラグと共に使うとこのようになります。

$ cat tmp/bad/Makefile
all:
    cc x.c
$ make -C tmp/bad
cc x.c
x.c:1:1: error: unknown type name 'INVALID_C'
INVALID_C
^
x.c:1:10: error: expected identifier or '('
INVALID_C
         ^
2 errors generated.
make: *** [all] Error 1
$

この出力はあまり有益ではありません。”x.c”が指すのはどのファイルでしょうか? これをビルドする他のツールは、コンパイラの出力を解釈するために、-C引数のような更なるコンテキストを必要とします。この出力はそれ自身で完結していません。

必要のない診断を除外する。実行されている全てをユーザに知らせたい誘惑をぐっとこらえましょう(しかし必要なら、stderrで出力)。良いツールは全てがうまくいっているときに寡黙ですが、うまくいっていないときに有益な診断を提示します。診断が必要以上に多いと、ユーザは診断自体を無視するようになります。有益な診断出力は、何がどこでうまくいかなかったのか見つけるために、際限のないログファイルをくまなく探すことをユーザに要求しません。開発とデバッグを助けるための冗長モード(通常は典型的には”-v”フラグで有効になる)があることになんら間違いはありませんが、これをデフォルトとしないようにしましょう。

対話的なプログラムにしない。ユーザのシェルが提供する対話的な処理を行わなくても、ツールは有益であるべきです。Unixプログラムはユーザの入力なしでの実行が期待されます。プログラムはcronによって非対話的に実行でき、リモートマシンによる実行を容易に分散できます。たった1つの対話処理でも、この極めて有益な能力は失われます。また対話処理は構成をより難しくします。Unixのプログラム構成モデルは出力したプログラムを区別しないので、ユーザがどのプログラムと対話しているかすら常には明らかではありません。対話的なプログラムの一般的な利用法は、ある種の危険な行為についてユーザに確認を求めることです。これはユーザに尋ねる代わりに、コマンドラインのフラグを適切なツールに提供することで容易に回避されます。

この記事を書いたのは、私自身が悪いツールを使ったり作成したりすることで絶えず不満を感じていたからです。悪いツールは時間を浪費してそれ自身の有益さを制限します。これらのツールのほとんどは以上の助言に従うことでずっと良くできるでしょう。

Unixツールの設計に関するより一般的な議論について、私は皆さんにKernighan とPikeの『Unixプログラミング環境』を読むようお勧めします。

Hacker Newsでの議論より。