シェルスクリプト(Bash)でwhile readが最初の行だけ読んでbreakするので対処する。

京大スパコンのデータ77TBが消失 バックアップ処理中に不具合 日本ヒューレット・パッカード「100%弊社の責任」 - ITmedia NEWS
シェルスクリプトが話題ですね。このニュースが発表された日にたまたま自分が改修したシェルスクリプト(Bash)でうまく動かない箇所の調査をしていてタイムリーだと思ったのと、むちゃくちゃ苦しんだのでメモとして残しておきます。

やろうとした処理

テキストファイル(host_list.txt)にホスト名が1行ずつ記載されており、while readでtxtを1行ずつ読み込んで 既存のpython製スクリプト(pyscript)のコマンドライン引数に渡す、という簡単な処理です。
最初以下で実装していました。

#!/bin/bash

set -eux

HOST_LIST="host_list.txt"

while read -r HOST; do
  pyscript --target ${HOST}
done < ${HOST_LIST}

発生した事象

想定ではhost_list.txtに記載されたホスト全てに対してpyscriptが実行されると思っていたのですが、txtの最初の行だけ読んで処理が終了してしまいました。
txtの内容がおかしいのかと思いpyscript部分をecho ${HOST}にしましたが、こちらはすべて出力されました。

原因

何が起こっているのかさっぱりわからなかったため、pyscriptの中を全部分解して調査したところ、sshコマンドを呼び出すライブラリを使用した処理が原因であることがわかりました。
Stack Overflowに説明がありました。
shell - While loop stops reading after the first line in Bash - Stack Overflow

The problem is that do_work.sh runs ssh commands and by default ssh reads from stdin which is your input file. As a result, you only see the first line processed, because the command consumes the rest of the file and your while loop terminates.

sshコマンドはデフォルトで標準入力からの読み込みを行うため、標準入力がsshにリダイレクトされ、ファイルの中身をsshが読み取りきってしまうようです。知るわけないだろこんな仕様。今回はsshでしたが、同じ挙動の別のコマンドでも発生しますね。

対処方法

いくつか方法があります。

  1. (原因がsshのみなら)ssh -nで標準入力を使用しない
  2. 対象のコマンドの標準入力に</dev/nullを指定する
  3. while readに標準入力ではなく任意のファイルディスクリプタを指定する

今回はpyscriptが呼び出しているライブラリの処理は変更できないため、ssh -nは使用できません。また、先述したように、標準入力を読んでしまうコマンドがssh以外も存在する場合には無力なので、2. 3. の方法で対処することになります。

対象のコマンドの標準入力に</dev/nullを指定する

修正例は以下です。

while read -r HOST; do
  pyscript --target ${HOST} </dev/null
done < ${HOST_LIST}

while readに標準入力ではなく任意のファイルディスクリプタを指定する

修正例は以下です。

while read -r -u ${FD} HOST; do
  pyscript --target ${HOST}
done {FD}< ${HOST_LIST}

どのコマンドが標準入力を横取りするか考える必要がなくなるので、この方法が良いだろうということになりました。
最初バカ正直にStack Overflowの回答通りファイルディスクリプタに9を指定していたのですが、 自動で10以上の値が使用できることを教えてもらいました。
Redirections (Bash Reference Manual)

Each redirection that may be preceded by a file descriptor number may instead be preceded by a word of the form {varname}. In this case, for each redirection operator except >&- and <&-, the shell will allocate a file descriptor greater than 10 and assign it to {varname}.

知るわけないだろこんな仕様。心の底からBashが嫌いになりました。良かったですね。