etuts+

シェルスクリプトで一行ずつ読込む while read line 4パターン

  • while read line について

  • Bash でファイル、またはコマンドの実行結果を一行ずつ読込む

  • その1. Here Document 内に直接記述した値を使う場合

  • その2. 変数に格納した値を Here Document で読み込んで使う場合

  • その3. 標準入力リダイレクトでファイルを読み込んで使う場合

  • その4. コマンドを実行すると同時に使う場合

  • 参考. 複数の Here Document を使う場合

  • 行の前後空白が消える問題

  • 行の前後に入っている空白が勝手に消えてしまう

  • 行の前後に入っている空白をそのまま読み込む

  • エスケープ文字が消える問題

  • バックスラッシュが勝手に消えてしまう

  • バックスラッシュを解析せずにそのまま読み込む

  • バックスラッシュをそのまま読み込むと同時に状況に応じてエスケープ文字を解析する

  • 結論. Bash でありのままのデータを一行ずつ読み込む

  • おまけ、その他の使い方 (随時追加)

  • おまけ 1. 配列にセットしたい場合

  • おまけ 2. IFS 応用編 - csv 読み込み、及び フィールドの値取得

  • 終わりに

while read line について

Bash でコマンドの実行結果、またはファイルの中身を一行ずつ読み込むためには、read コマンドを使います。

具体的には、while ループを回しながら、read によって読み込まれたデータを、一行ずつ line という変数に格納する形になります。

コマンドの実行結果、またはファイルの中身が最終行まで完全に読み込まれた時点でループから抜けることになります。

というわけで、while read line セットで覚えると良くて、while read line をより効率的に使うために、Here Document (ヒアドキュメント) と一緒に使うことが多いです。

Unix 系、Linux 系、OSX、BSD 等、シェルが使える環境上でスクリプト書くときにパターン化していつでも参考にして状況に応じて使えるようにまとめてみました。

Bash でファイル、またはコマンドの実行結果を一行ずつ読込む

その1. Here Document 内に直接記述した値を使う場合

まずは、一番基本的な使い方です。

Here Document を使って、ENDEND の間に直接記述した内容を 1行ずつ読み込みます。

Here Document は、<< の後に任意の区切り文字を識別子として付けておいて、その後 文字列を入力。 全ての文字列の入力が終わったところで、もう一回最初と同じ識別子を書いておくと、開始識別子 ~ 終了識別子 間の文字列を (改行を含めて) 入力値として与えることができます。

read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
#!/bin/bash

cnt=0
while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<END
ABC DEF GHI JKL MNO PQR STU VWX YZA
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
CDE FGH IJK LMN OPQ RST UVW XYZ ABC
END
出力結果
1
2
3
4
$ ./read_line_test.sh
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC

その2. 変数に格納した値を Here Document で読み込んで使う場合

変数にコマンドの実行結果を格納し、変数の中身を Here Document を使って1行ずつ読み込みます。

デバッグする際にも、変数に値が入ってたほうが、いろいろ便利なので、私はこのパターンを好んで使っています。

Here Document を使って、ENDEND の間に値が入っている変数を置きます。

/tmp/test.txt
1
2
3
4
$ cat test.txt
ABC DEF GHI JKL MNO PQR STU VWX YZA
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
CDE FGH IJK LMN OPQ RST UVW XYZ ABC
read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
#!/bin/bash

DATA=`cat /tmp/test.txt`

cnt=0
while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<END
$DATA
END
出力結果
1
2
3
4
$ ./read_line_test.sh
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC

その3. 標準入力リダイレクトでファイルを読み込んで使う場合

標準入力リダイレクトでファイルを読み込みながら 1行ずつ処理します。

主に、CSVXML のようなファイルを解析する際によく使っています。

以下の例では、/tmp/test.txt ファイルの中身を読み込んでいます。

/tmp/test.txt
1
2
3
4
$ cat test.txt
ABC DEF GHI JKL MNO PQR STU VWX YZA
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
CDE FGH IJK LMN OPQ RST UVW XYZ ABC
read_line_test.sh
1
2
3
4
5
6
7
8
#!/bin/bash

cnt=0
while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done < /tmp/test.txt
出力結果
1
2
3
4
$ ./read_line_test.sh
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC

その4. コマンドを実行すると同時に使う場合

コマンドを実行すると同時にその結果を 1行ずつ読み込みます。

/tmp/test.txt
1
2
3
4
$ cat test.txt
ABC DEF GHI JKL MNO PQR STU VWX YZA
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
CDE FGH IJK LMN OPQ RST UVW XYZ ABC
read_line_test.sh
1
2
3
4
5
6
7
8
#!/bin/bash

cnt=0
cat /tmp/test.txt | while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done
出力結果
1
2
3
4
$ ./read_line_test.sh
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC

参考. 複数の Here Document を使う場合

状況によっては、一つのスクリプト内で複数の Here Document を使いたい場合もあるかと思います。

当記事内で Here Document を使う際に、END ~ END のような書き方をしていましたが、実際に END というのは、決まり文句ではありません。

基本的には END ~ END を使い回しても大丈夫ですが、他の文字列、例えば、END の代わりに、OH-MY-GOT でも、特殊文字である !!! でも良いです。

しかし、特殊文字に関しては、コメントアウトを表す #、プロセス番号を表す $$、バックグラウンド処理を表す & など、シェルによって勝手に解釈される可能性があるため(エラーで動かない)、使わないほうが良いです。

よって、Here Document の開始・終了文を書く際には、なるべく文字列 (アルファベット) を使ってください

/tmp/test.txt
1
2
3
4
$ cat test.txt
ABC DEF GHI JKL MNO PQR STU VWX YZA
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
CDE FGH IJK LMN OPQ RST UVW XYZ ABC
read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash

DATA=`cat /tmp/test.txt`

echo "----------- OH-MY-GOT -----------"

cnt=0
while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<OH-MY-GOT
$DATA
OH-MY-GOT


echo
echo "----------- !!! -----------"

cnt=0
while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<!!!
$DATA
!!!


echo
echo "----------- %%% -----------"

cnt=0
while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<%%%
$DATA
%%%
出力結果
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
$ ./read_line_test.sh
----------- OH-MY-GOT -----------
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC

----------- !!! -----------
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC

----------- %%% -----------
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC

行の前後空白が消える問題

行の前後に入っている空白が勝手に消えてしまう

読み込む際に、行の前後に空白文字 (スペース、またはタブ) が入っていると消えてしまう問題が発生します。

以下の例では、行頭 / 行末にそれずれスペースが 4つ入っている 状態で一行ずつ読み込んだときの結果ですが、出力してみると前後から空白文字が取り取り除かれた文字列が出力されていることが分かります。

もっと正確に確認するために、出力結果をファイルに保存して viset list コマンドを実行してみると行末付いていた空白も消えていることが分かります。 (改行が入ってる部分 (行末) に $ が付きます)

read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
#!/bin/bash

cnt=0
while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<END
    ABC DEF GHI JKL MNO PQR STU VWX YZA    
    BCD EFG HIJ KLM NOP QRS TUV WXY ZAB    
    CDE FGH IJK LMN OPQ RST UVW XYZ ABC    
END
出力結果
1
2
$ ./read_line_test.sh > ./res.txt
$ vi ./res.txt
1
2
3
4
5
6
7
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA$
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB$
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC$
~
~
~
:set list

行の前後に入っている空白をそのまま読み込む

この問題を解決するためには、read の前に IFS= をセットます。

IFS (Internal Field Separator) は特定区切り文字を指定し、単語を区切るためのシェルの特殊な環境変数です。

普段あまり意識する必要はありませんが、IFS はデフォルトで $' \t\n' (空白、タブ、改行) が区切り文字として認識され、色んなところで使われたりしますが、単純に read しただけだと行の前後に付いていた空白やタブが IFS によって消されてしまいます。

この IFSNULL がセットされると単語分割は行われないため、read で一行ずつ読み込む際に、IFS= で明示的に NULL をセットすることによって、前後空白・タブ含めて正確に読み込むことができます

注意点として、IFS= は必ず read の前に置くこと。 単独で宣言しないでください。 (他の実行結果に影響するので、予期せぬ結果を招いてまたハマります)

read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
#!/bin/bash

cnt=0
while IFS= read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<END
    ABC DEF GHI JKL MNO PQR STU VWX YZA    
    BCD EFG HIJ KLM NOP QRS TUV WXY ZAB    
    CDE FGH IJK LMN OPQ RST UVW XYZ ABC    
END
出力結果
1
2
$ ./read_line_test.sh > ./res.txt
$ vi ./res.txt
1
2
3
4
5
6
7
LINE 1 :     ABC DEF GHI JKL MNO PQR STU VWX YZA    $
LINE 2 :     BCD EFG HIJ KLM NOP QRS TUV WXY ZAB    $
LINE 3 :     CDE FGH IJK LMN OPQ RST UVW XYZ ABC    $
~
~
~
:set list

エスケープ文字が消える問題

バックスラッシュが勝手に消えてしまう

普通は今まで紹介したやり方で十分使えますが、バックスラッシュが付いている文字列を扱う際に、特にエスケープ文字を扱う時に バックスラッシュが消えてしまう 問題が発生します。

read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
#!/bin/bash

cnt=0
while read line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<END
ABC DEF GHI JKL MNO PQR STU VWX YZA\n
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB\n
CDE FGH IJK LMN OPQ RST UVW XYZ ABC\n
END
出力結果
1
2
3
4
$ ./read_line_test.sh
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZAn
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZABn
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABCn

バックスラッシュを解析せずにそのまま読み込む

この問題を解決するためには、read -r を使います。

read のオプションとして -r を付けることによって、バックスラッシュ文字 \ が行内 文字列の一部と見なされます。

read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
#!/bin/bash

cnt=0
while read -r line
do
    cnt=`expr $cnt + 1`
    echo "LINE $cnt : $line"
done <<END
ABC DEF GHI JKL MNO PQR STU VWX YZA\n
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB\n
CDE FGH IJK LMN OPQ RST UVW XYZ ABC\n
END
出力結果
1
2
3
4
$ ./read_line_test.sh
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA\n
LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB\n
LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC\n

バックスラッシュをそのまま読み込むと同時に状況に応じてエスケープ文字を解析する

read -r でエスケープ文字を読み込みつつ、状況に応じて、エスケープ文字を解析したい場合は、echo -e (enable backslash escapes) を使います。

read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
#!/bin/bash

cnt=0
while read -r line
do
    cnt=`expr $cnt + 1`
    echo -e "LINE $cnt : $line"
done <<END
ABC DEF GHI JKL MNO PQR STU VWX YZA\n
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB\n
CDE FGH IJK LMN OPQ RST UVW XYZ ABC\n
END
出力結果
1
2
3
4
5
6
7
$ ./read_line_test.sh
LINE 1 : ABC DEF GHI JKL MNO PQR STU VWX YZA

LINE 2 : BCD EFG HIJ KLM NOP QRS TUV WXY ZAB

LINE 3 : CDE FGH IJK LMN OPQ RST UVW XYZ ABC

結論. Bash でありのままのデータを一行ずつ読み込む

以上の結果を踏まえて、行の前後空白・前後タブ、バックスラッシュが勝手に消されることなく、ありのままのデータを一行ずつ読み込むためには、以下のようにします。

  • while IFS= read -r line で読み込む。 ただし、エスケープ文字は状況に応じて解析する

IFS= は、IFS='' でも構いません。 明確に書きたい場合には後者のほうが良いでしょう。

そもそもエスケープ文字を処理する予定がなければ、-r 抜きでも構いませんが、読込むデータの中にエスケープ文字が含まれている可能性がなければ、-r は入れといた方が良いです。

以下の例では、1行目の行末に改行文字 「\n」2行前の前に半角スペース 4つ3行目の前にタブ文字 1つ が入っている状態で実行します。

read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

cnt=0
while IFS= read -r line    #### ありのままのデータを読み込む
do

    cnt=`expr $cnt + 1`

    if [ $cnt -eq 1 ]; then
        echo -e "$line"    #### エスケープ文字を解析する
    else
        echo    "$line"    #### エスケープ文字を解析しない
    fi

done <<END
ABC DEF GHI JKL MNO PQR STU VWX YZA\n
    BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
    CDE FGH IJK LMN OPQ RST UVW XYZ ABC
END
出力結果 (標準出力)
1
2
3
4
5
$ ./read_line_test.sh
ABC DEF GHI JKL MNO PQR STU VWX YZA

    BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
   CDE FGH IJK LMN OPQ RST UVW XYZ ABC

出力結果を見るだけじゃ分かりにくいかもしれませんが、ファイルに保存して確認して見ると BCD の前にはスペースが 4つ 入っていて、CDE の前にはタブ文字 ^I が 1つ 入っていることが分かります。 (vi エディタ内でタブ文字は ^I で表示されます)

出力結果 (ファイルの中身)
1
2
$ ./read_line_test.sh > ./res.txt
$ vi ./res.txt
1
2
3
4
5
6
7
8
ABC DEF GHI JKL MNO PQR STU VWX YZA$
$
    BCD EFG HIJ KLM NOP QRS TUV WXY ZAB$
^ICDE FGH IJK LMN OPQ RST UVW XYZ ABC$
~
~
~
:set list

後は、状況に合わせて、その1 ~ その4 を参考にし、変数に値をセットして読み込んだり、コマンドを実行すると同時に読み込んだりすると良いです。

おまけ、その他の使い方 (随時追加)

おまけ 1. 配列にセットしたい場合

1行ずつ読み込みながら配列に格納した上で、後から他の処理内で値を取り出して使いたい場合には以下のようにします。

この例では、declare -amy_arr を配列として宣言しています。

read_line_test.sh
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash

declare -a my_arr=()

index=0
while read line
do
    my_arr[${index}]="$line"
    index=`expr $index + 1`
done <<END
ABC DEF GHI JKL MNO PQR STU VWX YZA
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
CDE FGH IJK LMN OPQ RST UVW XYZ ABC
END

echo "配列の要素数 : ${#my_arr[@]}"

echo
echo "----------- ダブルクォーテーション有り -----------"

for i in "${my_arr[@]}"
do
    echo "$i"
done

echo
echo "----------- ダブルクォーテーション無し -----------"

for i in ${my_arr[@]}
do
    echo "$i"
done
出力結果 : ダブルクォーテーション有り
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ./read_line_test.sh
配列の要素数 : 3

----------- ダブルクォーテーション有り -----------
ABC DEF GHI JKL MNO PQR STU VWX YZA
BCD EFG HIJ KLM NOP QRS TUV WXY ZAB
CDE FGH IJK LMN OPQ RST UVW XYZ ABC

----------- ダブルクォーテーション無し -----------
ABC
DEF
GHI
JKL
MNO
PQR
STU
VWX
YZA
BCD
EFG
HIJ
・・・

for ループで配列の中身を取り出す際のポイントとしては、ダブルクォーテーションが有るか無いかによって出力結果が異なるので、注意が必要です。

ダブルクォーテーション付きだと 1行ずつ取り出すことができます。

ダブルクォーテーション無しだとスペース区切りで cut した感じになるので、ダメっていうわけではなく、状況に応じて使い分けできればそれで良いかなと思います。 (連続のスペースが入ってても上記と同じ結果になります)

おまけ 2. IFS 応用編 - csv 読み込み、及び フィールドの値取得

普段 文字列を分割する際には、cutawk をよく使いますが、先ほど紹介した IFS に カンマ 「,」 をセットすれば csv だって簡単に解析することができます。

以下の例では、一行単位で カンマ 「,」 で分割された各フィールドの値を、f1 ~ f9 に格納します。

/tmp/test.csv
1
2
3
4
$ cat test.csv
ABC,DEF,GHI,JKL,MNO,PQR,STU,VWX,YZA
BCD,EFG,HIJ,KLM,NOP,QRS,TUV,WXY,ZAB
CDE,FGH,IJK,LMN,OPQ,RST,UVW,XYZ,ABC
read_line_test.sh
1
2
3
4
while IFS=',' read -r f1 f2 f3 f4 f5 f6 f7 f8 f9
do
    echo "$f1 - $f2 - $f3 - $f4 - $f5 - $f6 - $f7 - $f8 - $f9"
done < /tmp/test.csv
出力結果
1
2
3
4
$ ./read_line_test.sh
ABC - DEF - GHI - JKL - MNO - PQR - STU - VWX - YZA
BCD - EFG - HIJ - KLM - NOP - QRS - TUV - WXY - ZAB
CDE - FGH - IJK - LMN - OPQ - RST - UVW - XYZ - ABC

また、似たような感じで /etc/passwd を読み込みながら、各フィールドの値をとりたい場合には、IFS=':' すれば良いです。

read_line_test.sh
1
2
3
4
while IFS=':' read -r id pw uid gid comment home shell
do
    echo "$id - $pw - $uid - $gid - $comment - $home - $shell"
done < /etc/passwd
出力結果
1
2
3
4
5
6
7
8
9
$ ./read_line_test.sh
root - x - 0 - 0 - root - /root - /bin/bash
bin - x - 1 - 1 - bin - /bin - /sbin/nologin
daemon - x - 2 - 2 - daemon - /sbin - /sbin/nologin
adm - x - 3 - 4 - adm - /var/adm - /sbin/nologin
shutdown - x - 6 - 0 - shutdown - /sbin - /sbin/shutdown
halt - x - 7 - 0 - halt - /sbin - /sbin/halt
mail - x - 8 - 12 - mail - /var/spool/mail - /sbin/nologin
・・・

終わりに

Here Document を使う際に、一つ注意点があって、例えば ENDEND の間に置かれる値終了文字列 END の前に見た目を綺麗にするために、スペースを 4つあけるような処理は入れないでください(空白を入れない)。 誤動作の原因になります。

例えば、if、while、for、case のような制御文内で Here Document を使う際にも以下のように。

制御文内での Here Document
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
#!/bin/bash

if [ 100 -gt 50 ]; then

    DATA=`cat /tmp/test.txt`

    cnt=0
    while IFS= read -r line
    do
        cnt=`expr $cnt + 1`
        echo "LINE $cnt : $line"
    done <<END
$DATA
END

fi

以上、シェルスクリプトで一行ずつ読込む while read line 4パターン でした。

この記事をシェアする

コピー & ペースト

 この記事のタイトルと URL をコピーする
シェルスクリプトで一行ずつ読込む while read line 4パターン
https://server.etutsplus.com/sh-while-read-line-4pattern/
 この記事の HTML リンクをコピーする
<a href="https://server.etutsplus.com/sh-while-read-line-4pattern/" title="シェルスクリプトで一行ずつ読込む while read line 4パターン" target="_blank">シェルスクリプトで一行ずつ読込む while read line 4パターン - eTuts+ Server Tutorials</a>

コメント

コメントをどうぞ

Tutorial 詳細

シェルスクリプト 一行所要時間:30分以内試験環境:CentOS 6.4、bash-4.1.2-15関連カテゴリー: