- 技術 💻
シェルスクリプトのfor・whileループ|ファイルを1行ずつ読む書き方
シェルスクリプトの繰り返しは、「決まった一覧を順に処理するなら 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期連続の黒字決算。