はじめに

Bashでのファイルの読み込み、変数への格納、一時的な環境変数について、forよりもwhile readを使うと便利な点を記載する。

例題として、以下のCSVファイルを読み込んで各列を変数に格納する。

id1,sh
id2,bash

forを使う場合

forを使うと以下のようになる。

for i in `cat csv`
do
  key=`echo $i | cut -d, -f1`
  val=`echo $i | cut -d, -f2`
  echo "$key => $val"
done
# 出力結果
#id1 => sh
#id2 => bash

while readを使う場合

while + readに一時的な環境変数を用いると、もっと簡潔に書ける。

while IFS=, read key val
do
  echo "$key => $val"
done < csv

readのうしろに変数を並べると、カラム列を変数に順に格納してくれる。
もしファイルがCSVでなくスペース区切りであれば、区切り文字解釈用のシェル変数IFS(Internal Field Separator)を変更する必要はないのだが、今回はCSVつまりカンマ区切りとしたいので、IFS=,を実行する必要がある。
ここでもしIFSの変更をreadと一緒に書かないと、環境変数が変わってしまうためにこの次に実行するコマンド全てが影響を受ける。

readとIFSの変更を同時に行っており、後続の処理に影響を与えていないパターン

echo 'id1,sh
id2,bash' > csv

while IFS=, read key val
do
  echo "$key => $val"
done < csv

sed -i 's/,/ /g' csv

while read key val
do
  echo "$key => $val"
done < csv
# 出力結果
#id1 => sh
#id2 => bash
#id1 => sh
#id2 => bash

readとIFSの変更を別々に行っており、後続の処理に影響を与えているパターン

echo 'id1,sh
id2,bash' > csv

IFS=, 
while read key val
do
  echo "$key => $val"
done < csv

sed -i 's/,/ /g' csv

while read key val
do
  echo "$key => $val"
done < csv
# 出力結果
#id1 => sh
#id2 => bash
#id1 sh => 
#id2 bash =>

一時的な環境変数

シェルには環境変数を一時的に設定してコマンドを実行する機能があり、環境変数=値 コマンドとすれば、このコマンドやスクリプトを実行する時だけ環境変数を一時的に設定できる。
この機能を使用しないのであれば、IFS_ORG="$IFS"のように元のIFSの値をバックアップしてからIFSを変更し、処理が終わったのちにIFS="$IFS_ORG"としてIFSに元の値を書き戻す必要がある。

while readの落とし穴

項目数

1行を区切り文字で分割して各変数に分けるのは、forを使うよりwhile readの方が簡潔なのは明らかだが、項目数に気をつけないと変な値になってしまう点には注意が必要。

echo 'id1,sh,Unix
id2,bash,Linux' > csv

while IFS=, read key val
do
  echo "$key => $val"
done < csv

上記の実行結果は以下のようになってしまう。

id1 => sh,Unix
id2 => bash,Linux

1項目と2項目のみ取得したければ、readのうしろにkey val1 val2のように項目数分の変数を用意しなければいけない。

echo 'id1,sh,Unix
id2,bash,Linux' > csv

while IFS=, read key val1 val2
do
  echo "$key => $val1"
done < csv
# 出力結果
#id1 => sh
#id2 => bash

ssh + while read

もう一つ注意すべき点として、サーバ管理でよく行うforループとsshで複数台のサーバに同一の作業を行う方法を、そのままwhile readに変えても実行できないという点がある。
shのwhileループでファイルを読み、中でsshを実行すると1回しかループしないに書かれている通り、sshははじめの1回しか実行されない。

SSH を実行すると、標準入力がそちらに振り向けられるため、read で読んだ1行のみならず、ファイル全体が SSH に渡されてしまう。従って、SSH を実行した後はもう読める行がないので while ループは1回で終了してしまう。
これを防ぐには、ssh に -n オプションを付け、/dev/null をリダイレクトし、標準入力をリダイレクトしないようにする。

sshのmanには-nオプションについて以下のように書かれている。

-n Redirects stdin from /dev/null (actually, prevents reading from stdin). This must be used when ssh is run in the background.

例えば、redis.confのslaveofの設定をしたいとする。対象のサーバは複数台あり、各slaveが参照するmasterがバラバラだとして、slaveとmasterの一覧が記載されたファイル(redis_slave_list.txt)を元にwhile readとsshでコマンドを組み立てるとき、sshに-nオプションを付けて、以下のように書く。

while read slave master
do
  echo ======$slave===========
  ssh -n $slave "sed -i -r 's/slaveof .*/slaveof ${master} 6379/' /etc/redis.conf"
done < redis_slave_list.txt