tkuchikiの日記

新ブログ https://blog.tkuchiki.net

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.)

n オプションは、BSDssh にもあるようなので、mac でも同様に使用することができそうだ。