結論
\r (CR:キャリッジリターン)と\x08(BS:バックスペース)を使えば良い。
あとは好きな出力を行うだけでOK。
環境
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における制御文字の一種で、プリンターまたは何らかの表示装置にカーソルを同一行の先頭位置に移動させる意味を持つ。
次のページにある動画の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"を処理できないようだ。
実装
むしろプログレスバーを作る処理の方が大変だという・・・
<?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);
};
}