PHPでコマンドを実装してみる かんたんecho篇


最近プログラミングに自信がないので、かんたんなものを作って気を紛らしてみるテスト。

目標は、ざっくり本物のUnixコマンドをおなじような動きをするPHPを書くこと。もちろんパイプでデータをやりとりできるように。

## おまじなひ

```php
if (realpath($_SERVER['SCRIPT_FILENAME']) == realpath(__FILE__)) {
    $argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : [];
    array_shift($argv);

    exit(_echo($argv));
}
```

↑ ファイルの先頭にはこんな感じのコードを書いておく。

つまり、ファイルを直接実行したときだけ発火するように。

あと、`$argv`変数は[PHP: $argv - Manual](http://php.net/manual/ja/reserved.variables.argv.php)を直接使ってもよいのだけれど、`php.ini`の設定に依存するので今回は`$_SERVER`からとってくることにする。

## おやくそく

 * コマンド実装の函数はコマンドライン引数の配列を受け取り、Unix終了ステータスを返す

## 実装

超かんたん実装では、これで良いはず…!

```php:echo
/**
 * @return int UNIX status code
 */
function _echo(array $argv)
{
    echo array_shift($argv);
    while ($argv) {
        echo ' ';
        echo array_shift($argv);
    }
    echo PHP_EOL;

    return 0;
}
```

```
% ./bin/echo a b c
a b c
% ./bin/echo a '  b ' c
a   b  c
% ./bin/echo a こんにちは c
a こんにちは c
```

いいね、動いてる。

ただ、これでは微妙なところで差がでる。

```
% echo 'a\tb\nc\\d'
a	b
c\d
% ./bin/echo 'a\tb\nc\\d'
a\tb\nc\\d
```

な、なるほどなー。

そんなわけで、ちゃんとする。

```php
static $tr_table = [
    '\\\\' => '\\',
    '\a' => "\a",
    '\b' => "\b",
    '\c' => "\c",
    '\d' => "\d",
    '\e' => "\e",
    '\f' => "\f",
    '\g' => "\g",
    '\h' => "\h",
    '\i' => "\i",
    '\j' => "\j",
    '\k' => "\k",
    '\l' => "\l",
    '\m' => "\m",
    '\n' => "\n",
    '\o' => "\o",
    '\p' => "\p",
    '\q' => "\q",
    '\r' => "\r",
    '\s' => "\s",
    '\t' => "\t",
    '\u' => "\u",
    '\v' => "\v",
    '\w' => "\w",
    '\x' => "\x",
    '\y' => "\y",
    '\z' => "\z",
];
echo strtr($l, array_shift($argv));
```

あとはなんだろう、`-n`オプションかな。と思って`man`を読んでみる。そうそう、Macを使ってるのでBSDコマンドになるのかな。たぶんGNU CoreutilsよりもBSD系の方がシンプルだろうから、こっちを参照し続けることにしよう。

> ## DESCRIPTION
>
>
> The echo utility writes any specified operands, separated by single blank (` `) characters and followed by a newline (`\n`) character, to the standard output.
>
> The following option is available:
> ### `-n`
> Do not print the trailing newline character.  This may also be achieved by appending `\c` to the end of the string, as is done by iBCS2 compatible systems.  Note that this option as well as the effect of `\c` are implementation-defined in IEEE Std 1003.1-2001 (“POSIX.1”) as amended by Cor. 1-2002.  Applications aiming for maximum portability are strongly encouraged to use printf(1) to suppress the newline character.

*(`man 1 echo` April 12, 2003 BSDより引用、引用者がMarkdownに改変)*

なるほどなるほど、BSD echo(1)のオプションは`-n`しかないのか。こりゃ好都合だ。あと、`\c`があれば、そこが終端になるらしい。

```
% echo 'a b \c d'
a b %
% ./bin/echo 'a b \c d'
a b \c d
```


## 完成

これを`echo`って名前で保存して`chmod +x ./echo`で実行できるはず。

```php:echo
#!/usr/bin/env php
<?php
namespace zonuexe\UnixCommand;

/**
 * ECHO command
 *
 * @author    USAMI KENTA <tadsan@zonu.me>
 * @license   http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
 * @copyright 2015 USAMI Kenta
 */

if (realpath($_SERVER['SCRIPT_FILENAME']) == realpath(__FILE__)) {
    $argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : [];
    array_shift($argv);

    exit(_echo($argv));
}

/**
 * @return int UNIX status code
 */
function _echo(array $argv)
{
    static $tr_table = [
        '\\\\' => '\\',
        '\a' => "\a",
        '\b' => "\b",
        '\c' => "\c",
        '\d' => "\d",
        '\e' => "\e",
        '\f' => "\f",
        '\g' => "\g",
        '\h' => "\h",
        '\i' => "\i",
        '\j' => "\j",
        '\k' => "\k",
        '\l' => "\l",
        '\m' => "\m",
        '\n' => "\n",
        '\o' => "\o",
        '\p' => "\p",
        '\q' => "\q",
        '\r' => "\r",
        '\s' => "\s",
        '\t' => "\t",
        '\u' => "\u",
        '\v' => "\v",
        '\w' => "\w",
        '\x' => "\x",
        '\y' => "\y",
        '\z' => "\z",
    ];

    $last_newline = true;
    $first = array_shift($argv);

    if ($first === '-n') {
        $last_newline = false;
        $first = array_shift($argv);
    }


    $printline = function ($l) use ($tr_table) {
        $terminate = false;
        $line = strtr($l, $tr_table);
        if (strpos($line, "\c") !== false) {
            $terminate = true;
            list($line, $_) = explode("\c", $line, 2);
        }
        echo $line;

        return $terminate;
    };

    if ($printline($first)) {
        return 0;
    }

    while ($argv) {
        echo ' ';

        if ($printline(array_shift($argv))) {
            return 0;
        }
    }

    if ($last_newline) {
        echo PHP_EOL;
    }

    return 0;
}
```

やったね。さすがに`echo`コマンドはシンプルなだけあって100行以内に収まった。

ただ、[GNU Coreutils版の`echo`](http://www.gnu.org/software/coreutils/manual/html_node/echo-invocation.html#echo-invocation)はもうちょっとオプション多いので、めんどくさそう…