while read でファイルを行読み込みして ssh をするシェルスクリプトの注意点
$ cat /tmp/hosts 192.168.0.10 192.168.0.11 192.168.0.12
のようなファイルがあったときに、
cat /tmp/hosts | while read _HOST; do ssh $_HOST "touch /tmp/testfile" done
などして、複数のホストに ssh でつないでコマンドを実行しようとする。
期待している処理としては、ループが回って 3 回処理を行ってほしいところだが、
実際は一度しか処理が実行されない。
実験と調査の結果、解決策は 2 つある(他にもあるかもしれない)。
解決策1. while の代わりに for を使う
私がこの問題に直面したときに解決した方法はこちらである。
原因はよくわからないが、read が標準入力を受けて処理するということだったので、
おそらく ssh でつなぐと 標準入力がなくなって、それ以降 read で読めなくなるのではないかと考えた(実際それは当たっていたようで、詳しくは解決策2 に記す)。
それなら cat をパイプして while read しなければうまくいくだろうと思い、
以下のように修正した。
for in _HOST $(cat /tmp/hosts); do ssh $_HOST "rm -rf /tmp/*" done
これで標準入力から読まなくなったので、無事に実行された。
for の場合、空白を含む行を処理しようとすると、スペースで区切って変数に代入されるので、
行ごとに変数に入れたい場合は、
IFS=$'\n'
として、区切り文字を変更すると良い。
解決策2. ssh の n オプションを使う
while だとだめな理由はなんとなくしかわからなかったので、調べてみた。
shのwhileループでファイルを読み、中でsshを実行すると1回しかループしない | b.l0g.jp に説明が書いてあった。
大変勉強になりました。ありがとうございます。
引用させていただくと、
SSH を実行すると、標準入力がそちらに振り向けられるため、read で読んだ1行のみならず、ファイル全体が SSH に渡されてしまう。従って、SSH を実行した後はもう読める行がないので while ループは1回で終了してしまう。
これを防ぐには、ssh に -n オプションを付け、/dev/null をリダイレクトし、標準入力をリダイレクトしないようにする
とのこと。
man にも書いてあった。
-n Redirects stdin from /dev/null (actually, prevents reading from stdin). This must be used when ssh is run in the background. A common
trick is to use this to run X11 programs on a remote machine. For example, ssh -n shadows.cs.hut.fi emacs & will start an emacs on shad-
ows.cs.hut.fi, and the X11 connection will be automatically forwarded over an encrypted channel. The ssh program will be put in the back-
ground. (This does not work if ssh needs to ask for a password or passphrase; see also the -f option.)