スペースを含む行をループしたい。 ShellScript (bash)

ちょっと記事が長くなっちゃったので、
スペースを含む、文字列を行毎にループ処理したい場合、最終的にこう書けば良いよ。
というのを先に載せておこうと思います。

#!/bin/bash
set -u

# サンプルデータ (空白を含む文字列)
data=$(cat <<EOT
hoge 123
piyo 456
foo  789
EOT
)

# read -a で、一行毎に配列に格納
while read -a arr; do
    echo "-----------" # ループがわかりやすいよう罫線引く
    echo "   line: ${arr[*]}"     # line

    echo "column1: ${arr[0]}"     # カラム1
    echo "column2: ${arr[1]}"     # カラム2

done < <(echo "$data") # プロセス置換で入力テキストとして扱う

出力結果

-----------
   line: hoge 123
column1: hoge
column2: 123
-----------
   line: piyo 456
column1: piyo
column2: 456
-----------
   line: foo 789
column1: foo
column2: 789

スペースを含む文字列を行単位で処理するのは難しい

例えば、こんなスペースを含むデータがあったとして、行ごとにループして処理したい事が多々ある。

hoge 123
piyo 456
foo 789

バカ正直に書くとこうなんですが、これではうまくいきません。

set -u

data=$(cat <<EOT
hoge 123
piyo 456
foo  789
EOT
)

for line in $data; do
    echo "-----------" # ループがわかりやすいよう罫線引く
    echo "$line"
done
-----------
hoge
-----------
123
-----------
piyo
-----------
456
-----------
foo
-----------
789

for in では、改行だけでなくスペースやタグも区切り文字として扱われてしまうため、
行毎ではなく、単語毎に処理されていきます。

IFSを変更してつかう

その為によくやるのが、
IFSという特殊変数を一時的に改行のみに変更してfor文を書く必要があります。
for in は、IFSに含まれる文字に従って文字列を分割しているのです。

PREV_IFS=$IFS
IFS="
"
for line in $data; do
    IFS=$PREV_IFS

    echo "-----------"
    echo $line
done
-----------
hoge 123
-----------
piyo 456
-----------
foo 789

正常に行毎にループが処理されています。
ただ、IFSは共通の変数なので、変更したらちゃんと元に戻す必要があるので、
PREV_IFSなどの変数に一時的に退避しておいて、設定し直すというのがちょっと面倒です。

while read とプロセス置換という選択肢

IFSを設定する面倒さがあるので、
for in 文ではなく、while read を使う事で行毎に処理する事が出来るようになります。

しかし、while read では、ファイルからテキストを流し込む必要があるため、
いちいち一時ファイルを作成する手間があるのですが、
bashであればプロセス置換という構文を使うことができ、簡単に記述する事ができます。

<(...) の部分が、プロセス置換というものです。
この部分が、一時的に出力されたテキストファイルとして扱う事ができ、とても短く記述する事ができます。

while read line; do
    echo "-----------"
    echo $line
done < <(echo "$data")
-----------
hoge 123
-----------
piyo 456
-----------
foo 789

プロセス置換でなくてもヒアドキュメントでいける

一応、プロセス置換を使わなくても、
ヒアドキュメント記述で、同じように動作させる事ができます。

while read line; do
    echo "-----------"
    echo $line
done <<EOT
hoge 123
piyo 456
foo  789
EOT

もちろん、ヒアドキュメントなので、先に宣言しておいた$dataを、展開してこんな書き方でもOKです。

while read line; do
    echo "-----------"
    echo $line
done <<EOT
$data
EOT

カラム毎に変数にいれる。

更に行毎に処理しつつ、カラム毎に変数に入っていると扱いやすいでしょう。
こんな感じで配列に代入する事ができます。 arr=(...) がIFSに従って配列に展開して代入するという構文です。

while read line; do
    echo "-----------"
    echo $line

    arr=($line)
    echo "arr[0]: ${arr[0]}"
    echo "arr[1]: ${arr[1]}"
done < <(echo "$data")
-----------
hoge 123
arr[0]: hoge
arr[1]: 123
-----------
piyo 456
arr[0]: piyo
arr[1]: 456
-----------
foo 789
arr[0]: foo
arr[1]: 789

カラム毎に変数にいれる。(readコマンド)

readコマンドは、変数を複数指定すると、IFSに従って分割したものを順に変数に代入してくれます。
なので、全体のline変数が不要であれば、このように記述する事ができます。

ただ、指定変数よりもカラム数が多いと、readコマンドに指定した最後の変数に残りカラムが全部入る事になるので注意が必要です。
(3カラム目があると、a2には、2カラム目と3カラム目以降が代入される)

while read a1 a2; do
    echo "-----------"
    echo "a1: $a1"
    echo "a2: $a2"
done < <(echo "$data")
-----------
a1: hoge
a2: 123
-----------
a1: piyo
a2: 456
-----------
a1: foo
a2: 789

カラム毎に変数にいれる。(readコマンドの -aオプション)

最後に、個人的にはこれが一番イケてる書き方じゃないかと思います!!
read コマンドに -a オプションで指定した変数に、IFSに従って分割された配列が代入されます。

arr[*] で、lineとして一行として参照する事もできますし、一番直感的に書けている気がします。

while read -a arr; do
    echo "-----------"
    echo "arr[*]: ${arr[*]}"
    echo "arr[0]: ${arr[0]}"
    echo "arr[1]: ${arr[1]}"
done < <(echo "$data")
-----------
arr[*]: hoge 123
arr[0]: hoge
arr[1]: 123
-----------
arr[*]: piyo 456
arr[0]: piyo
arr[1]: 456
-----------
arr[*]: foo 789
arr[0]: foo
arr[1]: 789

おしまい

いろんな言語ありますが、
僕はプログラマーが一番覚えておいた方がいい言語は、ShellScript だと思っています。

言語ごとにそれぞれ得意な部分がありますし、
特性を活かして、各言語毎に機能実装して、ShellScriptでグルーして使う。
というのが最終的には一番汎用性が高いと思うからです。

ShellScriptはとても癖のある書き方なので、とっつきにくい感じがしますが
覚えておいて損はない言語でしょう。

コメントを残す