シェルスクリプトの繰り返しは、「決まった一覧を順に処理するなら for」「条件が成り立つ間くり返すなら while」「ファイルを1行ずつ読むなら while IFS= read -r line; do ... done < file」と使い分けます。 until は while の条件を逆にしたもので、「〜になるまで待つ」処理に向いています。

この記事では、for の6つの書き方と while・until の構文を bash 5.2.21 で動かした実行結果つき で一覧にします。そのうえで、ファイルを1行ずつ読むときに起きやすい4つの現象(空白で分割される、パイプの先で変数が消える、最終行を読み落とす、ループ内のコマンドが標準入力を読んでしまう)を 実際に再現し、直す前と後を並べて 説明します。コードはそのままコピーして試せる形にしました。

for・while・untilの早見表

構文の一覧

やりたいこと 書き方 実行結果(確認済み)
決まった値を順に処理 for name in apple banana cherry; do ...; done 3回くり返す
1から5まで for i in {1..5}; do ...; done 1 2 3 4 5
5刻み for i in {0..10..5}; do ...; done 0 5 10
ゼロ埋め for i in {01..03}; do ...; done 01 02 03
変数で回数を決める for ((i = 1; i <= n; i++)); do ...; done n=3 なら 1 2 3
seq を使う(sh でも動く) for i in $(seq 1 "$n"); do ...; done 1 2 3
配列の全要素 for f in "${files[@]}"; do ...; done 空白入りの要素も1件ずつ
配列の添字 for i in "${!files[@]}"; do ...; done 0 1 2
ファイル名(glob) for f in logs/*.log; do ...; done 空白入りのファイル名も1件ずつ
スクリプトの引数 for arg; do ...; done "$@" を順に処理
条件が真の間 while [ "$i" -le 3 ]; do ...; done 3回
条件が真になるまで until [ "$i" -gt 3 ]; do ...; done 3回
ファイルを1行ずつ while IFS= read -r line; do ...; done < file 行をそのまま読む
無限ループ while true; do ...; sleep 1; done break で抜ける

1行ずつ読むときの対照表

つまずきやすい書き方 起きること 直した書き方
for line in $(cat file) 空白で分割され、* はファイル名に展開される while IFS= read -r line; do ...; done < file
while read line 行頭の空白が消え、\ が取り除かれる IFS= と -r を付ける
cat file | while read ... ループ内で変えた変数が、ループの外で元に戻る done < file または done < <(コマンド)
最終行に改行が無いファイル 最後の1行が処理されない while IFS= read -r line || [ -n "$line" ]
ループ内で ssh・ffmpeg を実行 残りの行を読まれて、1回で終わる・行が欠ける < /dev/null、ssh -n、ffmpeg -nostdin
for f in *.log(0件) *.log という文字列で1回まわる shopt -s nullglob
for i in {1..$n} {1..3} という文字列で1回まわる for ((i = 1; i <= n; i++))

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「シェルスクリプトのfor・whileループ|ファイルを1行ずつ読む書き方」

検証環境

項目 内容
bash GNU bash 5.2.21
sh dash 0.5.12(Ubuntu の /bin/sh)
ffmpeg 6.1.1
確認方法 掲載したスクリプトを bash ファイル名.sh で実行し、出力をそのまま掲載

スクリプトの1行目は #!/usr/bin/env bash にしています。この記事で使う {1..5}、(( ))、配列、< <( ) は bash の機能で、sh で実行すると動きが変わります(後半で実際に試します)。基本構文の全体像は既存記事「シェルスクリプトの書き方|基本構文の早見表と安全なテンプレート」、条件の書き方は「シェルスクリプトif条件式一覧」をあわせてご覧ください。

forループの書き方6種類

次のスクリプトは、for の書き方をまとめて確認するものです。for_basic.sh として保存し、bash for_basic.sh で実行します。

#!/usr/bin/env bash
echo "[1] リスト"
for name in apple banana cherry; do echo "fruit: $name"; done

echo "[2] ブレース展開"
for i in {1..5}; do printf '%s ' "$i"; done; echo
for i in {0..10..5}; do printf '%s ' "$i"; done; echo
for i in {01..03}; do printf '%s ' "$i"; done; echo
for c in {a..e}; do printf '%s ' "$c"; done; echo

echo "[3] 変数はブレース展開できない"
n=3
for i in {1..$n}; do echo "i=$i"; done

echo "[4] C言語風"
for ((i = 1; i <= n; i++)); do printf '%s ' "$i"; done; echo

echo "[5] seq"
for i in $(seq 1 "$n"); do printf '%s ' "$i"; done; echo
for i in $(seq 10 -5 0); do printf '%s ' "$i"; done; echo

echo "[6] 配列"
files=("report 2026.txt" "memo.txt" "todo list.md")
for f in "${files[@]}"; do echo "<$f>"; done

echo "[6b] クォートなし"
for f in ${files[@]}; do echo "<$f>"; done

echo "[7] 添字つき"
for i in "${!files[@]}"; do echo "$i: ${files[$i]}"; done

echo "[8] 引数"
set -- "a b" c
for arg; do echo "arg=<$arg>"; done

実行結果です。

[1] リスト
fruit: apple
fruit: banana
fruit: cherry
[2] ブレース展開
1 2 3 4 5 
0 5 10 
01 02 03 
a b c d e 
[3] 変数はブレース展開できない
i={1..3}
[4] C言語風
1 2 3 
[5] seq
1 2 3 
10 5 0 
[6] 配列
<report 2026.txt>
<memo.txt>
<todo list.md>
[6b] クォートなし
<report>
<2026.txt>
<memo.txt>
<todo>
<list.md>
[7] 添字つき
0: report 2026.txt
1: memo.txt
2: todo list.md
[8] 引数
arg=<a b>
arg=<c>

ブレース展開に変数は使えない

[3] の結果に注目してください。{1..$n} は 1 2 3 にならず、{1..3} という1つの文字列のまま ループが1回だけ実行されました。bash はブレース展開を変数の展開より先に行うため、{1..$n} の時点ではまだ $n が数字になっていないのが理由です。

回数の決まり方 おすすめの書き方
数字を直接書ける for i in {1..10}
変数で決まる(bash) for ((i = 1; i <= n; i++))
変数で決まる(sh でも動かしたい) for i in $(seq 1 "$n")

配列は "${配列[@]}" とクォートする

[6] と [6b] の違いは、ダブルクォートの有無だけです。クォートを付けると要素ごとに1回、付けないと空白で分割されて5回 まわりました。ファイル名や人名のように空白を含みうる値を扱うときは、"${files[@]}" の形で書きます。

C言語風のforは複数の変数も扱える

for ((i = 0, j = 10; i < j; i += 4, j -= 4)); do echo "$i $j"; done
0 10
4 6

(( )) の中では変数に $ を付けなくても参照でき、<= や ++ などの算術演算子がそのまま使えます。

whileとuntilの書き方

#!/usr/bin/env bash
i=1
while [ "$i" -le 3 ]; do echo "while $i"; i=$((i + 1)); done

i=1
until [ "$i" -gt 3 ]; do echo "until $i"; ((i++)); done

i=0
while ((i < 3)); do printf '%s ' "$i"; ((i++)); done; echo
while 1
while 2
while 3
until 1
until 2
until 3
0 1 2 
構文 くり返す条件 向いている処理
while 条件; do ...; done 条件が 真の間 入力がある間読む、カウンタが上限に届くまで
until 条件; do ...; done 条件が 真になるまで(偽の間) ファイルができるまで待つ、接続できるまで再試行

while と until は条件の向きが逆なだけ で、どちらで書いても同じ処理が作れます。「〜するまで待つ」と読める場面では until のほうが意図が伝わりやすくなります。

set -e と ((i++)) の組み合わせ

エラーで止める設定(set -e)を使うスクリプトでは、カウンタの書き方に1つだけ注意点があります。

bash -c 'set -e; i=0; ((i++)); echo "ここには来ない"'; echo "exit=$?"
bash -c 'set -e; i=0; i=$((i + 1)); echo "i=$i"'
exit=1
i=1

((i++)) は「式の値が 0 のとき終了コード 1」を返します。i が 0 のときの i++ は値が 0 なので、set -e によってそこでスクリプトが終わりました。set -e を使うなら i=$((i + 1)) と書く と、値に関係なく続行できます。

ファイルを1行ずつ読む:3つの書き方を比べる

ここからが本題です。次の3行のファイル members.txt を用意します。3行目は、行頭に空白2つ、途中に \t という2文字、末尾に * を含めています。

printf 'Tanaka Taro\nSato Hanako\n  indented\\tline *\n' > members.txt
mkdir d && touch d/a.txt d/b.txt   # * の展開を見るためのファイル
Tanaka Taro
Sato Hanako
  indented\tline *

d ディレクトリに移動してから、3つの書き方で読んでみます。

#!/usr/bin/env bash
cd d
echo "[A] for line in \$(cat file)"
for line in $(cat ../members.txt); do echo "<$line>"; done

echo "[B] while read line (IFS, -r なし)"
while read line; do echo "<$line>"; done < ../members.txt

echo "[C] while IFS= read -r line"
while IFS= read -r line; do echo "<$line>"; done < ../members.txt
[A] for line in $(cat file)
<Tanaka>
<Taro>
<Sato>
<Hanako>
<indented\tline>
<a.txt>
<b.txt>
[B] while read line (IFS, -r なし)
<Tanaka Taro>
<Sato Hanako>
<indentedtline *>
[C] while IFS= read -r line
<Tanaka Taro>
<Sato Hanako>
<  indented\tline *>
書き方 結果 理由
[A] for line in $(cat file) 3行が7個に分かれ、* が a.txt b.txt に変わった $( ) の結果は空白・タブ・改行で単語に分割され、さらに glob(ファイル名展開)が行われる
[B] while read line 行単位にはなったが、行頭の空白と \ が消えた read は既定で前後の空白を取り除き、\ をエスケープ文字として扱う
[C] while IFS= read -r line 元の行と完全に同じ IFS= で空白の除去を止め、-r で \ をそのまま読む

for は「単語」を回す構文、read は「行」を読むコマンド です。行単位で処理したいときは [C] の形を使います。

IFS= と -r の意味

while IFS= read -r line; do
  printf '%s\n' "$line"
done < members.txt
部分 役割
IFS= 区切り文字(IFS)を read の間だけ空にする。行頭・行末の空白が保たれる
read -r \ を特別扱いしない。Windows のパスや正規表現を含む行もそのまま読める
line 読んだ1行が入る変数。名前は自由
done < members.txt ループ全体の標準入力をファイルにつなぐ(リダイレクト)

IFS= を read の直前に置くと、その read コマンドにだけ 効きます。スクリプト全体の IFS は変わらないので、ほかの処理に影響しません。リダイレクトの読み方は既存記事「Linuxリダイレクト 2>&1の意味と順序」で詳しく扱っています。

bash の help read で確認できる主なオプションは次のとおりです。

オプション 意味
-r バックスラッシュをエスケープとして扱わない
-a 配列名 分割した単語を配列に入れる
-d 区切り 改行の代わりに、指定した文字までを1件として読む
-u 番号 標準入力の代わりに、指定したファイル記述子から読む
-t 秒 指定秒数で入力が無ければ終了する
-p 文字列 入力を待つ前にプロンプトを表示する

パイプでwhileに渡すと変数が消える

行数を数えるつもりで、次のように書いたとします。

#!/usr/bin/env bash
count=0
cat members.txt | while IFS= read -r line; do count=$((count + 1)); done
echo "パイプ: count=$count"

count=0
while IFS= read -r line; do count=$((count + 1)); done < members.txt
echo "リダイレクト: count=$count"

count=0
while IFS= read -r line; do count=$((count + 1)); done < <(grep -v '^ ' members.txt)
echo "プロセス置換: count=$count"

count=0
shopt -s lastpipe
cat members.txt | while IFS= read -r line; do count=$((count + 1)); done
echo "lastpipe: count=$count"
パイプ: count=0
リダイレクト: count=3
プロセス置換: count=2
lastpipe: count=3

1つ目だけ count=0 のままです。bash はパイプ(|)でつないだ各コマンドを、サブシェル(別のプロセス)で実行します。 ループの中で増やした count はサブシェルの中の変数で、ループが終わるとサブシェルごと消えるため、外側の count は 0 のまま残ります。

flowchart LR
    A["cat members.txt"] -->|パイプ| B["while ループ<br>(サブシェル内)<br>count=3"]
    B -.->|終了時に破棄| C["外側のシェル<br>count=0 のまま"]
入力元 書き方 変数は残るか
ファイル while ...; done < file 残る
コマンドの出力 while ...; done < <(コマンド)(プロセス置換) 残る
文字列 while ...; done <<< "$text"(ヒアストリング) 残る
パイプ コマンド | while ...; done 残らない
パイプ+shopt -s lastpipe 同上(スクリプト内でのみ有効) 残る

コマンドの出力を読むなら done < <(コマンド) が使いやすい形 です。< と <( の間には空白が必要です。lastpipe は、パイプの最後のコマンドを今のシェルで実行する設定で、ジョブ制御が無効なとき(通常のスクリプト実行時)に働きます。対話シェルでは既定でジョブ制御が有効なので、スクリプト向きの設定と考えるとよいでしょう。

最終行に改行が無いと読み落とす

エディタの設定や、ほかのプログラムが出力したファイルでは、最後の行の末尾に改行が付いていないことがあります。

printf 'line1\nline2\nline3' > nonl.txt   # 最後に \n が無い
#!/usr/bin/env bash
od -c nonl.txt | head -2
echo "[直す前]"
while IFS= read -r line; do echo "<$line>"; done < nonl.txt
echo "[直した後]"
while IFS= read -r line || [ -n "$line" ]; do echo "<$line>"; done < nonl.txt
echo "wc -l: $(wc -l < nonl.txt)"
0000000   l   i   n   e   1  \n   l   i   n   e   2  \n   l   i   n   e
0000020   3
[直す前]
<line1>
<line2>
[直した後]
<line1>
<line2>
<line3>
wc -l: 2

直す前は line3 が出力されていません。read は、改行に出会う前にファイルの終わりに達すると、変数には値を入れたうえで終了コード 1(偽)を返します。 while は条件が偽になったのでループ本体を実行せずに終わり、最後の行だけが処理されないまま残ります。

|| [ -n "$line" ] を足すと、「read が偽でも、変数に中身があれば本体を実行する」という条件になり、最後の行も処理されます。wc -l が 2 を返しているのも同じ理由で、wc -l は改行の数を数えています。外部から受け取るファイルを読むスクリプトでは、最初からこの形にしておく と安心です。

# コピーして使える形
while IFS= read -r line || [ -n "$line" ]; do
  printf '%s\n' "$line"
done < "$file"

ループ内のコマンドが標準入力を読んでしまう

現象を再現する

ホスト名の一覧を読みながら ssh でコマンドを実行する、というスクリプトでよく起きる現象です。検証環境には接続先の SSH サーバーが無いため、まず 「標準入力を最後まで読むコマンド」を ssh の代役にして 仕組みを再現します。

printf 'web01\nweb02\nweb03\n' > hosts.txt
#!/usr/bin/env bash
# ssh の代わりに、標準入力を読むコマンド(cat > /dev/null)で再現
fake_ssh() { cat > /dev/null; echo "connected: $1"; }

echo "[直す前]"
while IFS= read -r host; do fake_ssh "$host"; done < hosts.txt

echo "[対処1] < /dev/null"
while IFS= read -r host; do fake_ssh "$host" < /dev/null; done < hosts.txt

echo "[対処2] 別のファイル記述子 3 で読む"
while IFS= read -r host <&3; do fake_ssh "$host"; done 3< hosts.txt
[直す前]
connected: web01
[対処1] < /dev/null
connected: web01
connected: web02
connected: web03
[対処2] 別のファイル記述子 3 で読む
connected: web01
connected: web02
connected: web03

直す前は 1台目だけ処理してループが終わりました。 done < hosts.txt はループ全体の標準入力をファイルにつなぐので、ループの中で動くコマンドも同じ標準入力を共有します。1回目に実行されたコマンドが残りの2行を読み切ってしまい、2回目の read には読む行が残っていない、という流れです。

ffmpegで実際に再現する

ffmpeg は実行中のキー操作を受け付けるために標準入力を読みます。1秒の音声ファイルを3つ用意して、実物で確認しました。

for n in clip_a clip_b clip_c; do
  ffmpeg -loglevel error -f lavfi -i "sine=frequency=440:duration=1" "$n.wav" < /dev/null
done
printf 'clip_a\nclip_b\nclip_c\n' > clips.txt
#!/usr/bin/env bash
echo "[直す前]"
while IFS= read -r name; do
  echo "convert: <$name>"
  ffmpeg -loglevel error -y -i "$name.wav" "$name.flac"
done < clips.txt
ls *.flac
rm -f *.flac

echo "[直した後] -nostdin"
while IFS= read -r name; do
  echo "convert: <$name>"
  ffmpeg -nostdin -loglevel error -y -i "$name.wav" "$name.flac"
done < clips.txt
ls *.flac
[直す前]
convert: <clip_a>

Enter command: <target>|all <time>|-1 <command>[ <argument>]

Parse error, at least 3 arguments were expected, only 1 given in string 'lip_b'
convert: <clip_c>
clip_a.flac
clip_c.flac
[直した後] -nostdin
convert: <clip_a>
convert: <clip_b>
convert: <clip_c>
clip_a.flac
clip_b.flac
clip_c.flac

直す前は、ffmpeg が2行目の clip_b を自分へのキー入力として読み取り(先頭の c をコマンドとして解釈し、残りの lip_b を引数として読もうとしています)、clip_b の変換が行われませんでした。 エラーで止まらずに続くため、件数を数えないと気づきにくい現象です。

対処方法の一覧

方法 書き方 向いている場面
そのコマンドの標準入力を空にする コマンド < /dev/null どのコマンドにも使える
コマンド側のオプション ffmpeg -nostdin ... ffmpeg(上で確認済み)
コマンド側のオプション ssh -n ホスト コマンド ssh(標準入力を /dev/null につなぐオプション。一般に知られた仕様で、この記事では未実行)
一覧を別のファイル記述子で読む while read -r x <&3; do ...; done 3< file ループ内で対話入力も使いたいとき
先に配列へ読み込む mapfile -t hosts < file → for h in "${hosts[@]}" 件数が多すぎないとき

最後の方法は、読み込みとループを分けてしまう書き方です。

mapfile -t lines < members.txt
echo "行数=${#lines[@]} 2行目=<${lines[1]}>"
行数=3 2行目=<Sato Hanako>

mapfile -t(別名 readarray)はファイル全体を配列に入れる bash の組み込みコマンド です。for で回せるので、標準入力の取り合いが起きません。

CSVをIFS=,で読む

区切り文字をカンマにすると、1行を複数の変数に分けて読めます。

printf 'id,name,qty\n1,ボールペン,12\n2,"ノート, A4",5\n3,付箋,\n' > items.csv
#!/usr/bin/env bash
{
  read -r header            # 1行目(見出し)を読み飛ばす
  while IFS=, read -r id name qty; do
    echo "id=$id name=$name qty=${qty:-0}"
  done
} < items.csv

echo "[4列目以降は最後の変数にまとまる]"
while IFS=, read -r a b; do echo "a=<$a> b=<$b>"; done <<< "1,2,3,4"

echo "[配列に読む]"
while IFS=, read -r -a cols; do echo "列数=${#cols[@]} 2列目=${cols[1]}"; done <<< "x,y,z"
id=1 name=ボールペン qty=12
id=2 name="ノート qty= A4",5
id=3 name=付箋 qty=0
[4列目以降は最後の変数にまとまる]
a=<1> b=<2,3,4>
[配列に読む]
列数=3 2列目=y
確認できたこと 説明
見出し行の読み飛ばし { read -r header; while ...; done; } < file と、ループの前に1回 read する
空の列 ${qty:-0} で既定値を補える(3行目)
変数より列が多い 余った分は最後の変数にまとめて入る
列数が決まっていない read -r -a cols で配列に入れる
引用符つきの列 2行目の "ノート, A4" は、カンマで分かれてしまう

IFS=, は単純にカンマで区切るだけで、ダブルクォートで囲んだ列の中のカンマは区別しません。 列の中にカンマや改行が入りうる CSV は、Python の csv モジュールなど専用のパーサーで処理するのが確実です。

Windowsで作ったCSV(CRLF)を読む

改行が CRLF のファイルでは、最後の列の末尾に \r(復帰文字)が残ります。

printf '1,pen,12\r\n2,note,5\r\n' > crlf.csv
while IFS=, read -r id name qty; do echo "qty=<$qty>" | od -c | head -1; done < crlf.csv
echo "[直した後]"
while IFS=, read -r id name qty; do qty=${qty%$'\r'}; echo "qty=<$qty>"; done < crlf.csv
0000000   q   t   y   =   <   1   2  \r   >  \n
0000000   q   t   y   =   <   5  \r   >  \n
[直した後]
qty=<12>
qty=<5>

画面では見えませんが、od -c で見ると 12 の後ろに \r が付いています。このまま数値の比較に使うとエラーになるので、${qty%$'\r'} で末尾の \r を取り除く と扱いやすくなります。

globでファイルを順に処理する

mkdir -p logs empty
touch "logs/app 1.log" logs/app2.log
#!/usr/bin/env bash
echo "[1] glob はスペース入りでも1件ずつ"
for f in logs/*.log; do echo "<$f>"; done

echo "[2] ls の結果を回すと分割される"
for f in $(ls logs/*.log); do echo "<$f>"; done

echo "[3] 0件のとき"
for f in empty/*.log; do echo "<$f>"; done

echo "[4] nullglob"
shopt -s nullglob
for f in empty/*.log; do echo "<$f>"; done
echo "(0回)"
shopt -u nullglob

echo "[5] 存在確認で読み飛ばす"
for f in empty/*.log; do [ -e "$f" ] || continue; echo "<$f>"; done
echo "(0回)"
[1] glob はスペース入りでも1件ずつ
<logs/app 1.log>
<logs/app2.log>
[2] ls の結果を回すと分割される
<logs/app>
<1.log>
<logs/app2.log>
[3] 0件のとき
<empty/*.log>
[4] nullglob
(0回)
[5] 存在確認で読み飛ばす
(0回)

ファイルを順に処理するときは、ls の結果ではなく glob(*.log)を for に直接書きます。 glob の展開結果は単語分割されないので、空白を含むファイル名も1件として扱われます([1] と [2] の違い)。

0件のときに備える(nullglob)

[3] のとおり、当てはまるファイルが1つも無いと、bash はパターンの文字列 empty/*.log をそのまま残します。 ループは1回まわり、存在しないファイル名で処理が走ります。

方法 書き方 0件のときの動き
nullglob shopt -s nullglob パターンが消えて、ループは0回
存在確認 [ -e "$f" ] || continue 1回まわるが、すぐ次へ進む(sh でも動く)
failglob shopt -s failglob no match: empty/*.log と表示し、その for 文は実行されない

nullglob を有効にしたままにすると、ls *.log のようなコマンドが「引数なしの ls」として動くなど、ほかの箇所の挙動も変わります。ループの直前で有効にし、終わったら shopt -u nullglob で戻す 使い方が扱いやすいです。

サブディレクトリも含めて処理する(find -print0)

階層をたどる場合は、find の結果を NUL 文字区切りで受け取ります。

while IFS= read -r -d '' f; do echo "<$f>"; done < <(find logs -name '*.log' -print0 | sort -z)
<logs/app 1.log>
<logs/app2.log>

-print0 と read -d '' の組み合わせは、ファイル名に空白や改行が含まれていても1件ずつ正確に受け取れる書き方です。

break・continueと多重ループ

#!/usr/bin/env bash
for i in 1 2 3 4 5; do
  [ "$i" -eq 2 ] && continue
  [ "$i" -eq 4 ] && break
  echo "i=$i"
done

echo "[多重ループ]"
for x in 1 2 3; do
  for y in a b c; do
    [ "$y" = b ] && continue
    echo "$x$y"
  done
done | tr '\n' ' '; echo

echo "[break 2]"
for x in 1 2 3; do
  for y in a b c; do
    [ "$x$y" = 2b ] && break 2
    echo "$x$y"
  done
done | tr '\n' ' '; echo

echo "[continue 2]"
for x in 1 2 3; do
  for y in a b c; do
    [ "$y" = b ] && continue 2
    echo "$x$y"
  done
done | tr '\n' ' '; echo
i=1
i=3
[多重ループ]
1a 1c 2a 2c 3a 3c 
[break 2]
1a 1b 1c 2a 
[continue 2]
1a 2a 3a 
命令 動き
continue 今の回の残りを飛ばして、次の回へ進む
break いちばん内側のループを抜ける
break 2 内側と外側、2つのループをまとめて抜ける
continue 2 内側を抜けて、外側のループの次の回へ進む

数字は「何重ぶんのループに作用するか」 を表します。3重以上で数字が増えてきたら、内側の処理を関数に分けて return で戻る形にすると読みやすくなります。

空行とコメント行を読み飛ばす

設定ファイルや一覧ファイルを読むときの定番の形です。

printf 'a\n\n# comment\nb\n' | while IFS= read -r line; do
  [[ -z $line || $line == \#* ]] && continue
  echo "<$line>"
done
<a>
<b>

ループ全体の出力をまとめてファイルに書く

done の後ろにリダイレクトを書くと、ループ全体の出力を1回のオープンで書き出せます。ループの中で毎回 >> するよりも簡潔です。

for i in 1 2 3; do echo "row $i"; done > out.txt
wc -l < out.txt    # 3

無限ループとsleep

#!/usr/bin/env bash
n=0
while true; do
  n=$((n + 1))
  echo "$(date +%T) try $n"
  [ -e ready.flag ] && { echo "ready"; break; }
  [ "$n" -ge 3 ] && touch ready.flag   # 動作確認用:3回目でファイルを作る
  sleep 1
done
rm -f ready.flag

echo "[リトライ]"
attempt=1; max=3
until [ -e nothing.flag ]; do
  if [ "$attempt" -ge "$max" ]; then echo "give up after $attempt tries"; break; fi
  echo "retry $attempt"; attempt=$((attempt + 1)); sleep 0.2
done
11:41:48 try 1
11:41:49 try 2
11:41:50 try 3
11:41:51 try 4
ready
[リトライ]
retry 1
retry 2
give up after 3 tries
ポイント 理由
while true(または while :) 条件が常に真なので、break するまで続く
ループ内に sleep を入れる 待ち時間が無いと CPU を使い続ける。1秒ごとの出力で間隔を確認できる
回数の上限を決める 条件が成り立たないままでも、決めた回数で終わる
手で止めるときは Ctrl + C 端末から実行中のループを中断できる

「〜になるまで待つ」処理には、上限回数と sleep を必ず組み合わせる と、待ち続ける状態を避けられます。外側から時間で区切る方法もあります。

timeout 1 bash -c 'while :; do sleep 0.3; done'; echo "exit=$?"
exit=124

timeout で打ち切られたときの終了コードは 124 です。

shで実行すると動きが変わる書き方

#!/bin/sh や sh script.sh で実行すると、bash 固有の書き方は別の動きになります。Ubuntu の sh(dash 0.5.12)で確認しました。

sh -c 'for i in {1..3}; do echo "$i"; done'
sh -c 'for ((i=0;i<2;i++)); do echo $i; done'
{1..3}
sh: 1: Syntax error: Bad for loop variable
書き方 bash sh(dash) sh でも動く書き方
{1..3} 1 2 3 {1..3} の文字列のまま $(seq 1 3)
for ((...)) 動く Syntax error: Bad for loop variable while [ "$i" -le 3 ]
配列 "${a[@]}" 動く 使えない set -- a b c; for x in "$@"
done < <(cmd) 動く 使えない 一時ファイルに書き出して done < file

この記事のコードを使うときは、1行目を #!/usr/bin/env bash にして、bash script.sh または実行権限を付けて ./script.sh で実行 してください。

つまずきやすい書き方と直した書き方の対照表

No. つまずきやすい書き方 実際に起きたこと 直した書き方
1 for line in $(cat members.txt) 3行が7個に分割、* がファイル名に while IFS= read -r line; do ...; done < members.txt
2 while read line 行頭の空白と \ が消えた while IFS= read -r line
3 cat f | while read ...; do count=...; done count=0 のまま done < f / done < <(cmd)
4 改行で終わらないファイルを read 最終行 line3 が処理されない read -r line || [ -n "$line" ]
5 ループ内で ffmpeg -i ... clip_b が変換されない ffmpeg -nostdin / < /dev/null
6 for f in $(ls logs/*.log) app 1.log が2つに分割 for f in logs/*.log
7 for f in empty/*.log(0件) <empty/*.log> で1回実行 shopt -s nullglob
8 for i in {1..$n} i={1..3} で1回実行 for ((i = 1; i <= n; i++))
9 for f in ${files[@]} 3要素が5個に分割 for f in "${files[@]}"
10 set -e と ((i++))(i=0) 終了コード1でスクリプトが終了 i=$((i + 1))
11 CRLF の CSV を IFS=, で読む 最後の列に \r が残る qty=${qty%$'\r'}
12 sh script.sh で for ((...)) Syntax error: Bad for loop variable bash script.sh

よくある質問

Q. forとwhileはどう使い分けますか? A. 回す対象が先に決まっているなら for(値の一覧、配列、ファイル名、回数)、終わりが条件で決まるなら while(入力が続く間、状態が変わるまで)です。ファイルの行は「空白を含む1行」が単位なので、単語単位で回す for ではなく while IFS= read -r line を使います。

Q. while read line と while IFS= read -r line は何が違いますか? A. 前者は行頭・行末の空白を取り除き、\ をエスケープ文字として処理します。後者は行をそのまま変数に入れます。この記事の検証では、indented\tline * という行が、前者では indentedtline *、後者では元のまま読めました。迷ったら後者を使うのがおすすめです。

Q. ループの途中で止めたいときはどうしますか? A. 端末で実行中なら Ctrl + C で中断できます。スクリプトの中では break でループを抜け、exit 1 でスクリプト全体を終了します。待ち合わせのループには、回数の上限か timeout コマンドを組み合わせておくと、条件が成り立たない場合も決まった時間で終わります。

Q. 1行ずつ読む処理が遅いときはどうすればよいですか? A. ループの中で毎回 grep や sed などの外部コマンドを起動すると、行数に比例して時間がかかります。行の加工だけなら bash の変数展開(${line%%,*} など)で済ませる、集計や抽出はファイル全体を awk や grep に1回で渡す、という形にすると速くなります。

まとめ

  • for は「決まった一覧」、while は「条件が真の間」、until は「条件が真になるまで」 をくり返す
  • 変数で回数を決めるなら for ((i = 1; i <= n; i++))。{1..$n} は展開されない
  • ファイルを1行ずつ読む基本形は while IFS= read -r line; do ...; done < file。for line in $(cat file) は空白で分割される
  • パイプで while に渡すと、ループ内の変数はサブシェルと一緒に消える。done < file か done < <(コマンド) を使う
  • 最終行に改行が無いファイルに備えて || [ -n "$line" ] を付ける
  • ループ内の ssh・ffmpeg には < /dev/null(または -n、-nostdin) を付けて、標準入力を分ける
  • glob が0件のときは shopt -s nullglob、配列は "${files[@]}" とクォートする

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「シェルスクリプトのfor・whileループ|ファイルを1行ずつ読む書き方」

株式会社RJCでは、シェルスクリプトのループや1行ずつ読む書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?