29
25

More than 5 years have passed since last update.

PHP CLIでプログレスバーを実現する

Last updated at Posted at 2016-02-06

結論

\r (CR:キャリッジリターン)と\x08(BS:バックスペース)を使えば良い。
あとは好きな出力を行うだけでOK。

progress.gif

環境

PHP 7.0.2 + bash on CentOS 7
PHP 7.0.2 + Git Bash on Windows 10 Pro
PHP 7.0.2 + Windows PowerShell on Windows 10 Pro
※Windows 10におけるcmd.exeは制御コード類が丸っと動作しない仕様なのでもう諦めた。

そもそもCRとはなんなのか。

コンピューティングでは、キャリッジ・リターン (CR) はASCII、Unicode、EBCDICにおける制御文字の一種で、プリンターまたは何らかの表示装置にカーソルを同一行の先頭位置に移動させる意味を持つ。

キャリッジ・リターン - Wikipedia

次のページにある動画の5秒当たりの動きが判りやすいかも。
ヘッドが左側に戻っている。
Electrical Typewriter Machine In Use 映像素材 | ロイヤリティフリーのハイビジョン映像・ビデオ・動画素材ライブラリー | 6625814

実際の使い出

跡が残る

echo 'ここで一旦書いても';
echo "\r";
echo 'なかった事になる。';
echo PHP_EOL;

なお、あくまで行頭に戻すだけなので、次のような処理を行うと不思議な表示になる。

echo '987654321';
echo "\r";
echo '1234';
echo PHP_EOL;

この場合、"123454321"と表示される。
最終行で改行が行われた結果、先行して書いていた"54321"の部分の表示が更新されないためである。
最終行の改行を削除すると、"1234"として表示される。

ではどうやって意図通りの見ためにするか?

差分のある分を空白文字で上書きしてやればいい。

echo '987654321';
echo "\r";
echo '1234';
echo '     ';
echo PHP_EOL;

こうすると見事に"54321"が消える。が。

echo '987654321';
echo "\r";
echo '1234';
echo '     ';

改行を無くし、リアルな表示にしてやると"1234 "となってしまう。

ではどうやって意図通りの見ためにするか?ツヴァイ

制御コードを更に使う。

"\x08"(バックスペース)を使う事で表示した文字を無かった事にできる。

なので・・・

echo '987654321';
echo "\r";
echo '1234';
echo "     \x08\x08\x08\x08\x08";
echo PHP_EOL;

あわせて・・・

echo '987654321';
echo "\r";
echo '1234';
echo "     \x08\x08\x08\x08\x08";

とする事で、見た目もリアルな文字も綺麗に除去する事ができた。

なお、次の通りとして近い結果を得られる。

echo '987654321';
echo "\r";
echo "\e[2K";
echo '1234';

残念ながら、Windows Power Shellは"\e[2K"を処理できないようだ。

実装

むしろプログレスバーを作る処理の方が大変だという・・・

test.php
<?php
/**
 * usage:
 *
 * コマンドラインで次のコマンドを入力し、Enterを押下してください。
 * (php binaryへのパスは通っているものとします)
 * php test.php
 *
 * その後"please input count:"と表示されるので、任意の数字を入力し、Enterを押下してください。
 */
//======================================================
//初期設定
//======================================================
//おやくそく
date_default_timezone_set('Asia/Tokyo');

//繰り返し中の待ち時間 単位はマイクロ秒 50000の場合は50ミリ秒
define('WAIT_TIME', 50000);

//======================================================
//実処理
//======================================================
//ここで繰り返し用数値を取得
echo 'please input count:';
$all_count = trim(fgets(STDIN));

//プログレスバー用の関数生成
$progress = create_progress($all_count);

//繰り返し部
for ($i = 1;$i <= $all_count;$i++) {
    //プログレスバーの表示
    echo $progress();

    //ちょっとだけスリープする。
    usleep(WAIT_TIME);
}

//最後に改行いれないと見づらくなる。
echo "\n";

//======================================================
//以下関数
//======================================================
/**
 * プログレスバーを生成する関数を生成します。
 *
 * usage:
 * $all_count = count($array);
 * $progress create_progress($all_count);
 * foreach ($array as $row) {
 *     // $rowを使った何らかの処理
 *     $progress();
 * }
 *
 * 引数無しで$progress();を利用し、進捗が100%まで到達した場合、内部で初期化が入る為、そのまま再利用できます。
 *
 * @param   int     $all_count      対象とする処理の全数
 * @param   array   $progress_chars プログレスバーでバーとして利用する文字設定
 *  [
 *      'finished'      => string 終了済みを示す文字 キー名を省略した場合、0番目の要素を使う
 *      'current'       => string 現在位置を示す文字 キー名を省略した場合、1番目の要素を使う
 *      'unfinished'    => string 完了済みを示す文字 キー名を省略した場合、2番目の要素を使う
 *  ]
 * @param   string  $format         プログレスバー表示フォーマット sprintf互換
 *  位置指定子に対する内容
 *      1$:現在の進捗率
 *      2$:プログレスバーの終了分
 *      3$:プログレスバーの現在位置の文字
 *      4$:プログレスバーの未了分
 *      5$:予想残り時間
 *      6$:全数に対する作業完了数
 * @return  callable    プログレスバーを表示する関数
 *  @param          int $current    対象とする処理の現在位置 開始値は1 省略すると現在の値に+1した値で実行する
 *  @throws     \Exception      $currentが1未満の数字を与えると例外が発生する
 */
function create_progress ($all_count, $progress_chars = [], $format = null) {
    //======================================================
    //関数作成前の初期化
    //======================================================
    //プログレスバー文字の確定
    $finished_str   = isset($progress_chars['finished']) ? $progress_chars['finished'] : (isset($progress_chars[0]) ? $progress_chars[0] : '|');
    $current_str    = isset($progress_chars['current']) ? $progress_chars['current'] : (isset($progress_chars[1]) ? $progress_chars[1] : '|');
    $unfinished_str = isset($progress_chars['unfinished']) ? $progress_chars['unfinished'] : (isset($progress_chars[2]) ? $progress_chars[2] : ' ');

    //プログレスバーフォーマットの確定
    if ($format === null) {
        $format = sprintf(' %% 3s%%%% [%%s%%s%%s] ETA %%s %% %ss/%s', strlen($all_count), $all_count);
    }

    //======================================================
    //関数構築
    //======================================================
    return function ($current = null) use ($all_count, $finished_str, $current_str, $unfinished_str, $format) {
        //======================================================
        //初期処理
        //======================================================
        //プログレスバー実行開始時点の時間保持用変数
        static $start_ts;

        //直前に表示されたプログレスバーの文字列長
        static $before_width;

        //最後に実行した位置
        static $position;

        //現在の時間を取得
        $current_ts = microtime(true);

        //======================================================
        //検証
        //======================================================
        if ($current !== null && $current < 1) {
            throw new \Exception(sprintf('現在位置は1以上の数値のみ使用できます。$current:%s', $current));
        }

        //======================================================
        //クロージャ―変数の初期化
        //======================================================
        if ($current === null) {
            if ($position === null) {
                $position = 0;
            }
            $position++;
        } else {
            $position = $current;
        }

        //プログレスバー初期化処理
        if ($position === 1) {
            $start_ts = $current_ts;
        }

        //======================================================
        //実処理
        //======================================================
        //プログレスバー実行開始からの経過時間
        $elapsed_ts = ($current_ts - $start_ts) / $position * ($all_count - $position);

        //予想終了時間の算出
        $eta = sprintf('%02.2s:%02s:%02s.%0-3.3s', $elapsed_ts / 60 / 60 % 60, $elapsed_ts / 60 % 60, $elapsed_ts % 60, round(($elapsed_ts - floor($elapsed_ts)) * 1000));

        //進捗状況の算出
        $percent = $position / $all_count * 100;
        $progress = round($percent / 2);

        //プログレスバーの構築
        $progress_bar = sprintf($format, round($percent), str_repeat($finished_str, $progress), $current_str, str_repeat($unfinished_str, 50 - $progress), $eta, $position);

        //直前に表示したプログレスバーの文字列長が現在よりも長い場合、消しこみ処理を追加する。
        $current_width = mb_strwidth($progress_bar);
        $sol = '';
        if ($before_width > $current_width) {
            $diff_width = $before_width - $current_width;
            $sol = sprintf('%s%s', str_repeat(' ', $diff_width), str_repeat("\x08", $diff_width));
        }
        $before_width = $current_width;

        //現在位置が$all_countと同一になった場合、初期化して終わる。
        if ($all_count == $position) {
            $start_ts       = null;
            $before_width   = null;
            $position       = null;
            $eol            = '';
        } else {
            //末尾につける "\r" が全ての答えだった。"\n"に変えると良く判る。
            $eol            = "\r";
        }

        //処理の終了
        return sprintf('%s%s%s', $sol, $progress_bar, $eol);
    };
}

29
25
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
25