PHPでコマンドを実装してみる cp篇


最近仕事してない気がする… と怯えはじめたtadsan氏は
飽きるまで
プログラミングの練習に励むことにしたのだった…

## これまでのあらすじ

 1. [PHPでコマンドを実装してみる かんたんecho篇](http://qiita.com/tadsan/items/675bd3b37a8689090417)
 2. [PHPでコマンドを実装してみる cat篇](http://qiita.com/tadsan/items/27882550005efbeab35f)
 3. [PHPでコマンドを実装してみる かんたんseq篇](http://qiita.com/tadsan/items/e7dc090a0a873205d9fe)

## cpコマンドってなんだっけ

コマンドライン引数のパースはめんどくさいので、例によって省略することにする。

```sh
# ファイルaをファイルbにコピーする
# (ファイルaは存在しないとエラー、bはなくても良いが、bがディレクトリだったらエラー)
% cp a b

# ファイルaとファイルbをディレクトリDにコピーする
# (ファイルaとbが存在しないとエラー、Dがディレクトリじゃないとエラー)
% cp a b D
```

ちょっぴりめんどくさいぞー?

## 実装

ファイルをコピーするための[copy](http://php.net/copy)函数が失敗したときの保険のためにエラーハンドラーをセットしておく。個人的には禁じ手に近いのだけれど…

```php:bootstrap.php
<?php
namespace zonuexe;

if (!function_exists('\zonuexe\exception_error_handler')) {
    /**
     * @link http://php.net/manual/functions.user-defined.php
     */
    function exception_error_handler($severity, $message, $file, $line)
    {
        if (!(error_reporting() & $severity)) {
            return;
        }
        throw new \ErrorException($message, 0, $severity, $file, $line);
    }
    set_error_handler('\zonuexe\exception_error_handler');
}
```

あとは、ディレクトリを指定したときに`./hoge/`と書いても`./hoge`と書いても問題ないように正規化するやつ。

```php
if (is_dir($dest)) {
    $pathinfo = pathinfo($dest);
    $dest = $pathinfo['dirname'] . '/' . $pathinfo['basename'];
}
```

## 完成

```php:cp
#!/usr/bin/env php
<?php
namespace zonuexe\UnixCommand;
require_once dirname(__DIR__) . '/bootstrap.php';

/**
 * CP 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(cp($argv));
}

/**
 * @return int UNIX status code
 */
function cp(array $argv)
{
    $failed = false;

    $len = count($argv);
    if ($len === 0) {
        fwrite(STDERR, 'cp: missing file operand' . PHP_EOL);
        return 1;
    }

    $dest = array_pop($argv);
    $is_dir = is_dir($dest);

    if ($len === 1) {
        $message = "cp: missing destination file operand after '{$dest}'";
        fwrite(STDERR, $message . PHP_EOL);
        return 1;
    }

    if (count($argv) > 1 && !$is_dir) {
        $message = "cp: target '{$dest}' is not a directory";
        fwrite(STDERR, $message . PHP_EOL);
        return 1;
    }

    if ($is_dir) {
        $pathinfo = pathinfo($dest);
        $dest = $pathinfo['dirname'] . '/' . $pathinfo['basename'];
    }

    foreach ($argv as $f) {
        if (is_dir($f)) {
            $message = "cp: omitting directory '{$f}'";
        } elseif (!is_file($f)) {
            $message = "cp: cannot stat '{$f}': No such file or directory";
        } elseif ($dest == $f) {
            $message = "cp: '{$f}' and '{$f}' are the same file";
        }

        if ($is_dir) {
            $new_file = $dest . '/' . basename($f);
        } else {
            $new_file = $dest;
        }

        if (!is_writable($dest)) {
            $message = "cp: cannot create regular file '{$new_file}': Permission denied";
        }

        if (isset($message)) {
            fwrite(STDERR, $message . PHP_EOL);
            $failed = true;
            continue;
        }

        try {
            copy($f, $new_file);
        } catch (\ErrorException $e) {
            $message = preg_replace('@\Acopy@', 'cp', $e->getMessage());
            fwrite(STDERR, $message . PHP_EOL);
            $failed = true;
            continue;
        }
    }

    return $failed ? 1 : 0;
}
```