PHPでUNIXコマンドを実装する話

  • 7
    いいね
  • 0
    コメント

めりーくりすます! 

昨日は@oubakiouさんのぼくのかんがえたさいきょうのはいれつ 2016 winterでした。私もいろいろ考察して実装したことがあるので参考にしてみてくださいね。


こんにちは、今年もいろいろ書きたいことはあったんですけどちょっと眠すぎて時間がとれないので、一年ちょっと前にプログラミングのれんしゅうのために作ってたBaguettePHP/UnixCommandの話をします。

実装過程はQiitaにも書いてました ヾ(〃><)ノ゙☆

  1. PHPでコマンドを実装してみる かんたんecho篇 - Qiita
  2. PHPでコマンドを実装してみる cat篇 - Qiita
  3. PHPでコマンドを実装してみる かんたんseq篇 - Qiita
  4. 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と同じ意味になるようにしました。

同時に、コマンド名と同じシンボリックリンクを作成するとコマンドとして機能するようにしました。

bagunix.php
$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に送ってみてくださいね。

この投稿は PHP Advent Calendar 201623日目の記事です。