シェルスクリプトで指定回数 (N回) ループ
シェルスクリプトで指定回数ループする方法を記す。
最終的に,awk
を使った実装が高速で汎用的なので,ベストだと結論づけた。
N=10
for i in $(awk "BEGIN{for(i=1; i<=$N; ++i) print i}"); do
echo $i
done
ただし,ループ変数が不要な場合は,for-printf
を使う方法が最速で手短だ。
N=10
for i in $(printf "%0${N}d\n" | fold -w 1); do
echo $i
done
また,速度が重要でない場合や,記述の簡潔さを重視する場合は,while
による方法も検討に値する。
i=0 N=10
while [ $((i+=1)) -le $N ]; do
echo $i
done
背景
シェルスクリプトで,指定回数だけ処理を実行したいときがある。Linux系OSであればseq
コマンドやyes
コマンド,Mac含むBSD系OSだとjot
,その他シェル独自拡張 (bash/ksh93/zsh) のブレース展開や算術for文を使って実現できる。
N=10
## seq is not POSIX
for i in $(seq $N); do
echo $i
done
N=10
## yes is not POSIX
yes | head -n $N | while read -r i; do
echo $i
done
N=10
## jot is not POSIX
for i in $(jot $N); do
echo $i
done
## shell specific syntax
for i in {1..$N}; do
echo $i
done
for ((i=1; i<=10; ++i)); do
echo $i
done
しかし,どの方法もPOSIX非準拠となってしまう。seq
やyes
は機能の単純さや利用頻度からPOSIXで定義されているかと勘違いしがちだが,実はPOSIXで未定義だ。
回数を指定した反復は頻出事項であり,POSIX互換な方法を確立する必要があると感じたので検討した。
アプローチとして以下の2種類がある。
ループ方法の工夫
while
まず,最も簡単な方法はwhile
を使うことだ。
i=0 N=10
while [ $((i+=1)) -le $N ]; do
echo $i
done
while
を使えば,通常のループに自然に組み込めるので,違和感は少ない。また,連番に必要なコードはループ変数を増加させるだけなので,最小となる。
printf
次の方法は,printfで指定回数文だけ空白区切りの文字列を用意することだ。
N=10
# for i in $(printf "%0${N}d\n" | sed 's/0/0 /g'); do
for i in $(printf "%0${N}d\n" | fold -w 1); do
echo $i # 0
done
この方法は少しトリッキーなので仕組みを解説する。
printf
コマンドで%0
による0パティングにより任意の数の0を一度に出力できることを利用している。最初のprintf "%0${N}d\n"
で任意の個数の0を出力して,これを1文字ずつ改行することで実現する。fold
の他にsedで0
を0
に置換することでも実現できる。
処理の内容が単純で,ループ変数のインクリメントが不要なので,速い実行速度を期待できる。
seqコマンドの代替
もう片方のアプローチとしては,seq
コマンドそのものを代替することだ。seq
コマンドのように連番の数字を別の方法で出力して,それをfor
文に使う。bc
コマンドとawk
コマンドを使う2通りの方法がある。
bc
まず,bc
コマンドを使う方法は以下の通りだ。
N=10
for i in $(echo "for (i=1; i<=$N; ++i) i" | bc); do
echo $i
done
この方法では,bcコマンドが計算式と認識できる式をパイプで渡してbcコマンドで式を実行している。
bc
コマンドはPOSIXで定義されているが,標準で付属されていない環境がいくつかある。試しに,Ubuntu16.04で以下のコマンドを実行すると,外部パッケージとしてインストールされていることがわかる。
apt search bc 2>&- | grep -B 1 "GNU bc"
bc/xenial,now 1.06.95-9build1 amd64 [installed,automatic] GNU bc arbitrary precision calculator language
その他,seq
コマンドをPOSIX互換で本気で実装する場合,bc
コマンドは小数の出力形式がまちまちであるなどいくつかの欠点がある。
awk
続いて,awk
で実装する場合以下の通りとなる。
N=10
for i in $(awk "BEGIN{for(i=1; i<=$N; ++i) print i}"); do
echo $i
done
bc
とほぼ同様で,C系言語のfor
文の書式で連番を出力している。awk
だと出力形式などをカスタマイズしやすいという利点がある。
portability – Portable POSIX shell alternative to GNU seq(1)? – Unix & Linux Stack Exchange
速度比較
ここまでで,POSIX互換な合計5通りのループの実装方法を説明した。最後にこれらの実装の速度を比較して,どれが最良であるかの判断材料とする。
以下のコードで示すように,10万回のループを実行して速度を測る。
さらに,上記のコードを5回実行して平均をとったものを以下の表にまとめた。
方法 | 5回平均user時間[s] | 欠点 |
---|---|---|
while | 0.312 | ループ毎に評価が必要なので遅い。 |
for-printf-sed | 0.168 | ループ変数が0固定。 |
for-printf-fold | 0.160 | ループ変数が0固定。 |
for-bc | 0.266 | whileに比べると複雑。インストールされていないことがある。 |
for-awk | 0.180 | whileに比べると複雑。 |
この結果から,最速だったのはprintf
を使ったものだった。この方法では,ループ変数のインクリメントなどが不要であり,処理が単純なので速かったのだと思われる。また,sedとfoldでは,foldのほうがわずかに速かった。foldのほうが記述が簡単なので,for-printfでのループではfoldを使うのが良いだろう。
次点はawk
によるものだった。最も遅かったのはやはりwhile
によるものだった。while
ではループの度にインクリメントや評価が行われるので,速度が遅くなるだろうという予想通りの結果となった。
printf
とawk
によるものはwhile
の方法の約2倍の実行速度であり有力だと感じた。
結論
5通りのループの実装方法を紹介し速度を計測した。この結果から,awkによる実装がベストだろうと思った。
N=10
for i in $(awk "BEGIN{for(i=1; i<=$N; ++i) print i}"); do
echo $i
done
理由は以下2点だ。
- 速度が速い
- 応用が効く
短いループを何回も行う場合でも,実行速度は重要になる。awk
を使っておけば,ループ変数の出力の形式を調整したり,デクリメントなど複雑な連番処理にも対応でき,seq
自体をawk
を使って自分で実装して代替することもできそうだ。
しかし,awk
で行う場合は記述が長くなってしまう。ループ変数がそもそも不要であったり,デクリメントなどループ変数の処理が不要な場合は,記述の手軽さからprintf
コマンドを使うのも悪くない。
N=10
for i in $(printf "%0${N}d\n" | fold -w 1); do
echo $i # 0
done
また,速度は劣るものの,記述が簡単であり,ループ変数から番号も取得できるので,素直にwhile文を使うのもありだろう。whileだとデクリメントや2個飛ばしなども,ループ変数の加減算により簡単にできる。
i=0 N=10
while [ $((i+=1)) -le $N ]; do
echo $i
done
用途に応じて,awk
,printf
,while
を使い分けるのがよいだろう。個人的には,以下の優先順位で利用を検討するのがよいと思った。
方法 | 利用場面 |
---|---|
for-printf | ループ変数が不要な場合。 |
while | ループ回数が少なく,速度が重要でない場合。 |
for-awk | 実行速度が重要な場合,汎用性を高める場合。 |
今後,シェルスクリプトで指定回数のループを行うときは,seq
を使わずにfor-printf
,while
,for-awk
を使いPOSIX互換なシェルスクリプトを心がけていこう。
オプションのないseqのawkでの実装
最後に,参考までにawk
によるseq
の実装コードを記す。オプションのパースが複雑になるので,ひとまずオプションは使わないという前提をおいている。
引数の処理を行い,デクリメントなども対応している。その内勉強も兼ねて,POSIX互換なseq
の実装にも挑戦してみたい。