PHPを使ってWindowsのコマンドプロンプトでインタラクティブシェルを実行する

PHPはreadline付きでビルドしてると
インタラクティブシェルモードで動かす事ができちゃうのは有名。
でもwindows版だと、readlineってなんだよ！ってなっちゃう。
だから、自分用にちょっとでもインタラクティブな事ができたらいいなとおもって書いてみた。

```php:phpa.php
<?php

/**
 * インタラクティブシェルではなく、
 * 単のコマンドラインとして実行された場合
 */
if ($argc > 1) {
    ob_start();
    passthru("php {$argv[1]}");
    $get = ob_get_clean();
    print($get);
    exit(0);
}
/**
 * php.iniが読み込まれているかどうかのチェックおよび
 * PHPエクステンションのロード状況
 */
$iniFlag = php_ini_loaded_file();
if ($iniFlag === false) {
    print("<It was unable to load 'PHP.ini' file!>" . PHP_EOL);
}
if (extension_loaded("mbstring") !== true) {
    print("<none loaded multibyte module!>" . PHP_EOL);
}
print("<This interactive shell used next temporary file.>" . PHP_EOL);
print("=> " . System::getSystemTempDirectory() . PHP_EOL);

/**
 * シェル起動前の事前処理
 * @var int 本スクリプトの最大使用可能バイト数
 * @var int 本スクリプトの最大使用可能メモリ(単位M)
 * @var string PHPの環境パス
 * @var int シングルラインorマルチラインorヒアドキュメントいずれかのフラグ
 * @var int 入力したログ削除のフラグ
 * @var string ヒアドキュメント開始ID
 */
$maxByte = 64000000;
$maxMemory = $maxByte / 100000;
$phpPath = "php";
$multilineFlag = 0;
$memoryResetFlag = 0;
$hereDocumentId = "";

ini_set("date.timezone", "Asia/Tokyo");
ini_set("memory_limit", "{$maxMemory}M");
ini_set("display_errors", "1");
ini_set("engine", "1");
ini_set("log_errors", "1");
error_reporting(-1);

$line = "";
$inputCode = "";
$inputTempCode = "";
$lastOutPutCount = 0;
$obj = null;
$tokenList= array(
    "function",
    "class",
    "foreach",
    "for",
    "if",
    "while",
    "do",
    "switch",
    "trait",
    "interface",
    "abstract"
);
$tempFileName = System::getSystemTempDirectory();
ColorShell::checkSystem();

$errorFunction = '
/**
 * すべてのエラーを表示させる。
 * 実際にバックグランドで実行されるphpソースファイルに
 * あらかじめ記述していおくソースコード
 * (※temp.phpに記述されます。)
 */
ini_set("display_errors", "1");
ini_set("engine", "1");
ini_set("log_errors", "1");
error_reporting(-1);
ini_set("date.timezone", "Asia/Tokyo");
ini_set("memory_limit", "512M");
//エラーハンドリングの設定
    function errorHandler ($errorNumber, $errstr, $errfile, $errline){
        switch ($errorNumber) {
            case 1 :
            trigger_error("E_ERROR " . $errstr . " " . $errfile, E_USER_ERROR);
            break;
            case 2 :
            trigger_error("E_WARNING " . $errstr . " " . $errfile, E_USER_ERROR);
            break;
            case 4 :
            trigger_error("E_PARSE " . $errstr . " " . $errfile, E_USER_ERROR);
            break;
            case 8:
            trigger_error("E_NOTICE " . $errstr . " " . $errfile, E_USER_ERROR);
            break;
            default:
            trigger_error("OTHER_ERROR " . $errstr . " " . $errfile,E_USER_ERROR);
        }
    }
set_error_handler("errorHandler");
//シャットダウン関数の設定
function shutdown(){
}
register_shutdown_function("shutdown");
';



//コンソール上でカラーリングするクラス
class ColorShell
{
    public static $osFlag = '';
    //コード実行直後のみ、OSの種類をチェックする。
    public static function checkSystem()
    {
        if (preg_match('/WIN/', PHP_OS) === 1) {
            //OSがwindows系の場合
            self::$osFlag = 0;
        } else {
            self::$osFlag = 1;
        }
    }
    public static function coloring($shellColor)
    {
        if (self::$osFlag == 1) {
            return pack('c', 0x1B) . $shellColor;
        } else {
            return '';
        }
    }
}


//メインループ
while (true) {
    try {
        if ($memoryResetFlag === 0) {

            //(1)デフォルトのシングルライン入力
            if ($multilineFlag === 0) {
                print(ColorShell::coloring('[0m'));
                $obj = new RunCode ($tempFileName, $errorFunction, $phpPath, $maxByte);
                if (function_exists("readline")) {
                    $line = Clean::cleaning(readline(">>> "));
                    readline_add_history($line);
                } else {
                    print(">>> ");
                    $line = Clean::cleaning(fgets(STDIN));
                }
                //標準入力をトークンに分解
                $token = preg_split("/(\(.*\)| |{|})+/", $line);
                if (($line == PHP_EOL) || ($line == '')) {
                    continue;
                } elseif (preg_match("/<<<[ ]*([a-zA-Z_]+[a-zA-Z_0-9]*)[ ]*$/", $line, $match)) {
                    //ヒアドキュメント入力
                    $hereDocumentId = $match[1];
                    $inputTempCode .= $line . PHP_EOL;
                    $multilineFlag = 2;
                    continue;
                } elseif ( preg_match("/\\\\$/", $line) === 1) {
                    $line = preg_replace("/\\\\$/", "", $line);
                    $inputTempCode .= $line . PHP_EOL;
                    $multilineFlag = 3;
                    continue;
                } elseif (count(array_intersect($tokenList, $token)) > 0) {
                    $inputTempCode .= $line . PHP_EOL;
                    $multilineFlag = 1;
                    continue;
                } elseif (preg_match("/^del/", $line)) {
                    $memoryResetFlag = 1;
                    continue;
                }
                $inputTempCode .= $line . PHP_EOL . " print(PHP_EOL); " . PHP_EOL;
                $obj->writeTempCode($inputTempCode);
                $obj->run($lastOutPutCount);
                $lastOutPutCount = $obj->lastOutPutCount;
                $inputCode = $inputTempCode;
            } elseif ($multilineFlag === 1) {
                //(2)シングルライン入力
                if (function_exists("readline")) {
                    $line = Clean::cleaning(readline("... "));
                    readline_add_history($line);
                } else {
                    print("... ");
                    $line = Clean::cleaning(fgets(STDIN));
                }
                if (($line == PHP_EOL) || ($line == "")) {
                    $inputTempCode .= $line . PHP_EOL . " print(PHP_EOL); " . PHP_EOL;
                    $obj->writeTempCode($inputTempCode);
                    print(ColorShell::coloring('[31m'));
                    $obj->run($lastOutPutCount);
                    $lastOutPutCount = $obj->lastOutPutCount;
                    $inputCode = $inputTempCode;
                    $multilineFlag = 0;
                    continue;
                } elseif (preg_match("/^del/", $line)) {
                    $memoryResetFlag = 1;
                    continue;
                }
                $inputTempCode .= $line;
                continue;
            } elseif ($multilineFlag === 2) {
                //(3)ヒアドキュメント入力
                if (function_exists("readline")) {
                    $line = Clean::cleaning(readline("... "));
                    readline_add_history ($line);
                } else {
                    print("... ");
                    $line = trim(fgets(STDIN));
                }
                if (preg_match("/^{$hereDocumentId}[ ]*;$/", $line)) {
                    $inputTempCode .= PHP_EOL . $line . PHP_EOL . PHP_EOL . " print(PHP_EOL); " . PHP_EOL;
                    $obj->writeTempCode($inputTempCode);
                    print(ColorShell::coloring("[31m"));
                    $obj->run($lastOutPutCount);
                    $lastOutPutCount = $obj->lastOutPutCount;
                    $inputCode = $inputTempCode;
                    $multilineFlag = 0;
                    continue;
                }
                $inputTempCode .= $line;
                continue;
            } elseif ($multilineFlag === 3) {
                //(4)バックスラッシュによる複数行入力
                if (function_exists("readline")) {
                    $line = Clean::cleaning(readline("... "));
                    readline_add_history ($line);
                } else {
                    print("... ");
                    $line = Clean::Cleaning(fgets(STDIN));
                }
                 if(preg_match("/\\\\$/",$line) === 1){
                    $line = preg_replace("/\\\\$/","",$line);
                    $inputTempCode .= $line;
                    $inputTempCode .= PHP_EOL. " print PHP_EOL; " . PHP_EOL;
                    $obj->writeTempCode ($inputTempCode);
                    print(ColorShell::coloring("[31m"));
                    $obj->run($lastOutPutCount);
                    $lastOutPutCount = $obj->lastOutPutCount;
                    $inputCode = $inputTempCode;
                    $multilineFlag = 0;
                    continue;
                } elseif (preg_match("/^del/",$line)) {
                    $memoryResetFlag = 1;
                    continue;
                }
                $inputTempCode .= $line . PHP_EOL;
                continue;
            }
        } elseif ($memoryResetFlag === 1) {
            file_put_contents($tempFileName , "");
            $inputCode = "";
            $inputTempCode = "";
            $lastOutPutCount = 0;
            $memoryResetFlag = 0;
            $multilineFlag = 0;
            $obj = null;
            print("<LOG DELETE>" . PHP_EOL . PHP_EOL);
            continue;
        }
    } catch (Exception $e) {
        print(ColorShell::coloring("[31m"));
        print($e->getMessage() . PHP_EOL . PHP_EOL);
        $multilineFlag = 0;
        $memoryResetFlag = 0;
        $inputTempCode = $inputCode;
        continue;
    }
}

class RunCode
{
    public $tempFileName = "";
    public $tempCode = "";
    public $allCode = "";
    public $errorHandlerCode = "";
    public $phpPath = "";
    public $maxByte = "";
    public $oldOutPutArray = array();
    public $lastOutPutCount = 0;
    /**
     * @param string $fileName
     * @param string $errorHandler
     * @param string $phpPath
     * @param int $maxByte
     * @return object
     */
    public function __construct ($fileName, $errorHandler, $phpPath, $maxByte)
    {
        $this->tempFileName = $fileName;
        $this->errorHandlerCode = $errorHandler;
        $this->tempCode = $this->errorHandlerCode;
        $this->phpPath = $phpPath;
        $this->maxByte = $maxByte;
    }
    
    /**
     * @param string $codeParam
     * @return void
     */
    public function writeTempCode ($codeParam)
    {
        $this->tempCode .= $codeParam . PHP_EOL;
    }
    /**
     * 実行用ソースを、テンポラリファイルに書き出し実行。
     * 実行結果をexec関数で取得し、戻り値が数値型であれば正常実行。
     * 数値型でない場合は、PHPエラー発生と判断する。
     * exec構文に渡す文字列は、windowsのスペース入りパスに対応するために
     * ダブルクオーテーションでラップする。
     * @param string $lastParam
     */
    public function run($lastParam)
    {
        print(ColorShell::coloring("[31m"));
        file_put_contents($this->tempFileName ,"<?php " . PHP_EOL . 'ob_start(); ' .$this->tempCode .' print (PHP_EOL);' . PHP_EOL .'$outPutLength = ob_get_contents(); ob_end_clean();print strlen($outPutLength);');
        $execCommand = $this->phpPath . " " . "\"" . $this->tempFileName ."\"";
        $res = exec($execCommand);
        if (is_numeric($res) === true) {
            $res = (int)$res;
            if ($res > $this->maxByte * 0.75) {
                throw new Exception("<PHP MEMORY OVER ERROR : Background Error {$res}Bytes>");
                return false;
            } else {
                //仮実行ソースを実行ソースへコピー
                $this->allCode = $this->tempCode;
                //実行ソースコードを保存する。
                file_put_contents($this->tempFileName, "<?php ". PHP_EOL .$this->allCode);
                $outPut = array();
                $returnVar = null;
                $res = exec($execCommand, $outPut, $returnVar);
                //配列の全要素数を取得
                $totalOutPutCount = count($outPut);
                print(ColorShell::coloring("[32m"));
                $outPutSlice = array_slice($outPut, $lastParam);
                foreach ($outPutSlice as $key => $value) {
                    print($value);
                    if (strlen($value) > 0) {
                        print(PHP_EOL);
                    }
                }
                print(PHP_EOL);
                $this->lastOutPutCount = count($outPut);
                return true;
            }
        } else {
            throw new Exception ("<PHP FATAL ERROR : Background Error>");
            return false;
        }
    }
}

/**
 * 改行文字を削除する静的メソッドを、持ったクラスを定義する。
 */
class Clean
{
    /**
     * @param string $inputLine 標準入力からの文字列
     * @return string 改行文字を取り除いた第一引数を返す。
     */
    public static function cleaning($inputLine = "")
    {
        $inputLine = trim($inputLine);
        $patternArray = array("/[　 ]*$/","/^[　 ]*/");
        $replaceArray = array("","");
        $inputLine = preg_replace($patternArray, $replaceArray, $inputLine);
        return $inputLine;
    }
}

/**
 * temporaryファイルを作成、およびその絶対パスを返す。
 */
class System
{
    /**
     * @return string テンポラリファイル名
     */
    public static function getSystemTempDirectory()
    {
        $tempFile = tempnam("", "");
        $tempDir = dirname($tempFile) . DIRECTORY_SEPARATOR . "temp.php";
        //一時的に作成されたテンポラリファイルを削除する。
        unlink($tempFile);
        return $tempDir;
    }
}

```

もうそのまんま。
phpa.phpとか適当に保存して
windowsのプロンプトでphp phpa.phpってしやると
コンソールっぽくなる。

ちょっと遊びにつかったり、それこそ関数の動作やメソッドの動作に使ったりしてます。
windows上で作業するときだけね。