コンソールによるプログレス表示

作成:

ソフトウェアの処理には往々にして、完了までに利用者を待たせざるを得ない場面というものがある。 純粋にCPU処理に時間がかかるものであったり、ダウンロード等の通信に時間がかかるものなど、様々な状況があるだろう。 その場合、何もユーザに知らせないと、異常が発生し止まっているのか、正常な待ち時間なのかが分からないし、 同じ時間待つにしても、待つ必要があることが、更にはどのぐらい待つ必要があるのか、 が分かったほうが、その待ち時間は短く感じるものだ。

ユーザビリティ上プログレス表示の重要度は高い。 GUIではプログレスバーという表現方法が一般的だが、 コンソール上でも類似した表示を実現する方法が幾つかある。 その方法について紹介する。

前準備

プログレスバーの表現方法を紹介していくが、 本来であれば「何らかの処理」をしながら表示するということになる。 ここでは、「何らかの処理」をスリープで表現している。

スリープは以下の関数を利用している。 この関数は既にPOSIXの規定から削除されているため、本来は使うべきではない。 しかし、ここではスリープの実現方法は本質ではないため記述量の少なさから採用している。

#include <unistd.h>

int usleep(useconds_t usec);

引数はマイクロ秒で、その時間呼び出し元スレッドの実行を延期する関数である。

垂れ流し

まず最初に紹介する方法は、 おそらく、わざわざ解説する必要もないほど簡単な方法だ。 処理の進捗に合わせて文字を出力していく方法である。 改行を出力しないことで「バー」として表現することができる。 標準出力の場合は出力バッファがあるため、 改行コードを出力するまでは画面に反映されない可能性がある。 出力の意味的にも標準エラー出力を使ったほうが良い。

#include <stdio.h>
#include <unistd.h>

int main(int argc, char**argv) {
  int i;
  for (i = 0; i < 20; i++) {
    fprintf(stderr, "#");
    usleep(500000);
  }
  fprintf(stderr, "\nfinish!\n");
  return 0;
}

実行すると以下のような表示ができる。 ※JavaScriptで表現。



この方法のメリットとしては実装が非常に簡単かつ負荷も少なく、 コンソールが使用できる箇所であれば、おそらくどこでも動作するという点だろう。

キャリッジリターンの利用

次に紹介するのは、エスケープシーケンスのところで例示した方法だ。 キャリッジリターン(\r)を出力すると、 多くのコンソールではカーソルを(次の行ではなく)その行の先頭に移動させる制御となる。 そして、コンソールでの出力は上書きの動作となるのが一般的である。 すなわち、1行の範囲内であれば、出力後、先頭から出力し直すことで一度出力した内容を更新することができる。 これを繰り返し行うことでアニメーションを行うことができる。

#include <stdio.h>
#include <unistd.h>

int main(int argc, char**argv) {
  int i;
  for (i = 0; i <= 100; i++) {
    fprintf(stderr, "\r[%3d / 100]", i);
    usleep(100000);
  }
  fprintf(stderr, "\nfinish!\n");
  return 0;
}

実行すると以下のような表示ができる。 ※JavaScriptで表現。



この方法も、おそらく殆どの環境で使用することができるだろう。 ただし、キャリッジリターンが改行として解釈されたり、 先頭へのカーソル移動ではなく、不明な文字コードとして解釈される可能性もある。 また、ファイルへリダイレクトし、後で見るような場合にも意図した表示にならないため注意が必要だ。 そういった意味でも少なくとも標準出力ではなく、標準エラー出力を使うようにしたほうが良い。

標準エラー出力もログとしてファイルにダンプされる可能性があるので、より高度には出力先がファイルかコンソールかを判定してだし分けを行うようにする。 この方法については、Linuxプログラミング解説のlsっぽいコマンドを作る-ファイル種別に基づく色付き表示で触れているので興味があれば見て欲しい。

プログレスを表現するにあたって、 具体的な進捗率を表現できればよいが、中には具体的な進捗がわからないというケースもある。 その場合は例えば以下のような表現方法がある。

#include <stdio.h>
#include <unistd.h>

void progress() {
  static int pos = 0;
  static char mark[] = {'-','\\','|','/'};
  fprintf(stderr, "\r%c", mark[pos]);
  pos = (pos + 1) % sizeof(mark);
}

int main(int argc, char**argv) {
  int i;
  for (i = 0; i < 100; i++) {
    progress();
    usleep(100000);
  }
  fprintf(stderr, "\nfinish!\n");
  return 0;
}

実行すると以下のような表示ができる。 ※JavaScriptで表現。



棒が回っているように見えるはずだが、 日本語環境ではバックスラッシュ(\)が円マーク(¥)で表示される場合がある。 その場合は別の表現方法を考えたほうが良いだろう。 例えば...の点の数を変化させる、点の位置を変化させるなど、いろいろと表現方法があるだろう。

エスケープシーケンスの利用

もう少しこった表現をしようと思うと、 エスケープシーケンス(ANSIエスケープコード)を使う方法がある。 詳しくは、説明ページを参照して欲しいが、以下のコードでポイントは\033[2Aの出力だ。 これはカーソルを2行上に移動させるという意味になる。

#include <stdio.h>
#include <unistd.h>

int main(int argc, char**argv) {
  int i, j;
  fprintf(stderr, "0%%       50%%       100%%\n");
  fprintf(stderr, "+---------+---------+\n");
  for (i = 0; i <= 100; i++) {
    for (j = 0; j < i / 5 + 1; j++) {
      fprintf(stderr, "#");
    }
    fprintf(stderr, "\n");
    fprintf(stderr, "%3d%%\n", i);
    usleep(100000);
    fprintf(stderr, "\033[2A");
  }
  fprintf(stderr, "\n\nfinish!\n");
  return 0;
}

実行すると以下のような表示ができる。 ※JavaScriptで表現。





エスケープシーケンスを使うと色を付けることもできるので、 テキストで表示できるものであればほぼ何でもありになる。 しかし、この方法は利用できる環境が限られるし、 そもそも、処理の進捗を表示するためにそれほどリッチな表現を行う必要はなく、 この出力のためにリソースを使って待ち時間が伸びるようなことがあれば本末転倒だ。 遊び以外ではあまりやり過ぎないようにしよう。

以上、コンソールでのプログレス表示の方法について紹介した。 ユーザビリティという意味での重要性もあるが、プログラミング入門者視点で見ると、 お手軽に「それっぽい」プログラムが作れるということで、モチベーションアップにもなるのではないだろうか。 他にもいろいろと表現方法があると思うので、いろいろとためしてみると面白いだろう。