ループ中の進捗状況の表示


0. Contents

1.Concept
2.Run Test
3.Summary


1. Concept

for や while ループ中に処理がどこまで進んだかを表示するには
適当な変数をインクリメントして表示するのがお手軽である。

ここでは、あるテキストファイルを読み込んで一行ずつ何らかの処理を
するスクリプトの進捗状況を printf とキャリッジリターン (\r) と bash の
算術展開を使用して少々凝った進捗状況を表示する方法を考えてみる。


2. Run Test

英数字のみが書かれたテキストファイルに対して、大文字と小文字を
入れ替えて別のテキストファイルに出力する処理の例として、tr コマンドを
使うと以下のようになる。

$ tr '[a-z]' '[A-Z]' < source.txt > result.txt

この処理の進捗状況を出力するために while を使って書いてみる。
進捗状況を標準エラー出力に表示する事によって手元に表示したい情報と
ファイルに出力したい情報とを分けることが出来る。

script1.sh
#! /usr/bin/env bash
StartNum=1
EndNum=$( grep -c '' source.txt )

while read ; do
  echo "$REPLY" | tr '[a-z]' '[A-Z]'

  echo -en "${StartNum}/${EndNum}\r" 1>&2
  (( StartNum++ ))
done < source.txt > result.txt
exit

$ ./script1.sh
5/1000

表示桁数を揃えるために printf を使い、ついでにインクリメントも同時に行ってしまう。

script2.sh
#! /usr/bin/env bash
StartNum=0
EndNum=$( grep -c '' source.txt )

while read ; do
  echo "$REPLY" | tr '[a-z]' '[A-Z]'

  printf "%0${#EndNum}d/${EndNum}\r" "$[++StartNum]" 1>&2
done < source.txt > result.txt
exit

$ ./script2.sh
0015/1000

算術展開を使ってパーセンテージを表示してみる。
一行が長くなるので変数名を i と j に変更する。

script3.sh
#! /usr/bin/env bash
i=0
j=$( grep -c '' source.txt )

while read ; do
  echo "$REPLY" | tr '[a-z]' '[A-Z]'

  printf "%0${#j}d/${j} (%03d%%)\r" "$[++i]" "$[ i * 100 / j ]" 1>&2
done < source.txt > result.txt
exit

$ ./script3.sh
0050/1000 (005%)

シェル変数 $SECONDS はシェルが起動してからの経過秒数が格納されていて、
値を代入するとその値を初期値とした経過秒数が格納される。
これを使って処理にかかった時間を "分:秒" というフォーマットで表示してみる。

script4.sh
#! /usr/bin/env bash
i=0
j=$( grep -c '' source.txt )
SECONDS=0

while read ; do
  echo "$REPLY" | tr '[a-z]' '[A-Z]'

  k=$( printf '%02d:%02d' "$[ SECONDS / 60 ]" "$[ SECONDS % 60 ]" )
  printf "%0${#j}d/${j} (%03d%%) $k\r" "$[++i]" "$[ i * 100 / j ]" 1>&2
done < source.txt > result.txt
exit

$ ./script4.sh
0200/1000 (020%) 00:20

もう一工夫すると処理の残り時間を概算する事もできる。
0 で割る事にならないよう注意しながら 10 行おきに
計算させた結果を表示してみる。

script5.sh
#! /usr/bin/env bash
i=0
j=$( grep -c '' source.txt )
SECONDS=0

while read ; do
  echo "$REPLY" | tr '[a-z]' '[A-Z]'

  k=$( printf '%02d:%02d' "$[ SECONDS / 60 ]" "$[ SECONDS % 60 ]" )
  (( i % 10 == 5 )) && {
    l=$[ ( j * SECONDS / i ) - SECONDS ]
    l=$( printf '/%02d:%02d' "$[ l / 60 ]" "$[ l % 60 ]" )
  }

  printf "%0${#j}d/${j} (%03d%%) ${k}${l} \r" "$[++i]" "$[ i * 100 / j ]" 1>&2
done < source.txt > result.txt
exit

$ ./script5.sh
0400/1000 (040%) 00:40/01:00


3. Summary

当然の事だが、凝った進捗状況を表示する為に複雑な処理を行うと
その分だけ全体の処理によけいな時間がかかる事になる。
何事も程ほどに。