めりーくりすます!
昨日は@oubakiouさんのぼくのかんがえたさいきょうのはいれつ 2016 winterでした。私もいろいろ考察して、実装したことがあるので参考にしてみてくださいね。
こんにちは、今年もいろいろ書きたいことはあったんですけどちょっと眠すぎて時間がとれないので、一年ちょっと前にプログラミングのれんしゅうのために作ってたBaguettePHP/UnixCommandの話をします。
実装過程はQiitaにも書いてました ヾ(〃><)ノ゙☆
- PHPでコマンドを実装してみる かんたんecho篇 - Qiita
- PHPでコマンドを実装してみる cat篇 - Qiita
- PHPでコマンドを実装してみる かんたんseq篇 - Qiita
- PHPでコマンドを実装してみる cp篇 - Qiita
UNIXコマンド?
みなさんGNU/LinuxやmacOSなどのUNIX風のOSを利用されますね。
主にUNIXシェルなどから利用されるコマンドの体系です(UNIXユーティリティの一覧)。「UNIXコマンド」などとOSの名前が付いてますが基本的にはOSの特別なAPIではなくて、基本的には(ユーザーランドで動く)ただのプログラムです。 (内部ではもちろんシステムコールを読んだりしてますが)
といふことは、基本的には 同じ機能のものを 勝手に再実装できるといふことです。実際にLinuxでのコマンドはOS本体(Linuxカーネル)とは完全に独立したもので、GNU Core Utilities(coreutils)と組み合せて利用されることが多い状況です。
もちろんコマンドの実装はcoreutils以外にもいくつかあって、軽量なシステムではBusyBoxが好まれ、BSD系UNIX(macOS含む)ではBSDに端を発する系統のコマンドが利用されます。
UNIX系OSの標準仕様としてPOSIXが存在し、コマンドも定義されますが、今回の記事では詳しく触れないし、実装仕様としても拘りません。 めんどくさいですからね
なぜUNIXコマンドを実装するのか
UNIXコマンドの理解を深めたかったからです。また、文字列処理が基本なのでPHPのコードを書く練習になると思ったからです。
きっかけとしては、数年前に「ふつうのHaskellプログラミング」を読んで、だった気がします。書評(404 Blog Not Found:ふつうくさく、汗臭くないHaskell本)のように賛否のある本ではあるのですが、私は好きな一冊です。 Haskellが好きだとは言ってない
函数実装
ひとつのUNIXコマンドには、ひとつのPHP函数を対応させることにします。ただしPHPの言語機能と同名のもの(予約語)はfunction echo_
のように_
を後置することにします。
設計
すべてのコマンドは以下のような引数と返り値にします。
/**
* @param string[] $argv
* @param resource $stdin
* @param resource $stdout
* @param resource $stderr
* @return int UNIX status code
*/
function hoge(array $argv, $stdin = STDIN, $stdout = STDOUT, $stderr = STDERR)
$argv
はコマンドライン引数、$stdin
は標準入力、$stdout
は標準出力、$stderr
は標準出力、返り値は終了ステータスです。基本的には**return 0
が正常終了です**。
echo 'foo';
とfwrite(STDOUT, 'foo');
は基本的に同じ意味ですが、この仕様に従ったときはfwrite($stdout, ' ')
と書くことになります。なぜこんな回りくどいことをするのかは読者への宿題とします。
コマンド起動
最初はコマンド名と同名のファイルをひとつひとつ用意したのですが、半年ほど前のコミット(24e1b2e
)で単一のbagunix
実行ファイルを用意して、コマンドラインでbagunix echo foo
と起動するとecho foo
と同じ意味になるようにしました。
同時に、コマンド名と同じシンボリックリンクを作成するとコマンドとして機能するようにしました。
$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : [$_SERVER['SCRIPT_FILENAME']];
if (pathinfo($argv[0], PATHINFO_FILENAME) === pathinfo(__FILE__, PATHINFO_FILENAME)) {
array_shift($argv);
}
$command = pathinfo(array_shift($argv), PATHINFO_FILENAME);
if ($command === 'echo') {
$command = 'echo_';
}
$funcion = sprintf('\%s\%s', __NAMESPACE__, $command);
exit($funcion($argv));
この手法は、先述したBusyBoxと同様の手法です。
うっかり composer global require
しても実行パスを汚染しないようにcomposer.json
に定義するのはbin/bagunix
ファイルのみです。
プログラミングスタイル
UNIXコマンドの基本はパイプラインを流れる文字列(バイト列)の処理なので、PHPといふ言語は意外と有利な環境です。また、PHP自体が基本機能を標準函数として提供してくれるので、どちらかと言ったらUNIXコマンドの挙動を再現させるためのコードが大半を占めがちになります。
また、コードを簡潔にするためにarray_shift()
あたりを多用しちゃってるのも、モダンPHPとしては良くない習慣なのかもしれませんねヾ(〃><)ノ゙
Unixコマンドを知る手掛かり
man
コマンドです… man
コマンドを引くのです… シェルでman echo
を実行すると、echo
の説明を読むことができます。Webに転がってる日本語訳のmanマニュアルは古かったりするので、あんまりおすすめしにくい。 差分に何度苦しめられたことか…
読者への宿題
この記事を読んで興味を持った型は、自分でtrue
コマンドとfalse
コマンドを実装しね。何をするコマンドなのか知らないって? man true
で調べてみればいいじゃない! そのほか実装しやすそうなのはyes
とかhead
あたりかな。rmdir
とかtee
もカンタンかも。
もしGitHubでプルリクエストを送ったことがないなら、BaguettePHP/UnixCommandに送ってみてくださいね。