はじめに
シェルスクリプトの read
を使ってファイルを読み込む場合、対象のファイルの最後の行は末尾に改行がなければ読み込めないというのはある程度シェルスクリプトを書いてる人なら一度はハマったことがあるかと思います。一般的には改行で終わらせましょうという話ですが、対応せざるを得ない場合もあるかと思います。その場合の対応のさせかたです。なお(いつもどおり)POSIX 準拠かつ外部コマンド呼び出しなしでシェルスクリプトの機能だけで実装します。
ファイルの末尾に改行が必要なのは「POSIXの仕様」だとする説がありますが、これは間違いで POSIX は標準規格で使用している「テキストファイル」の用語を定義しているだけです。
POSIX では「テキストファイル」という用語を、0行以上の行で構成されたファイルと定義しており、行とはヌル文字がを含まず末尾に改行がある LINE_MAX
バイト(一般的に2048バイト)以内の文字列のことです。
詳細は以下を参照してください。
実装
末尾改行なしに対応させる
しばしば勘違いされていると思いますが read
は最後の行は末尾に改行がなければ読み込めないというのは正確ではなく、読み込んではいます。ただ read
関数が正常終了になっていないだけです。なので判定方法を工夫すれば読み込めます。
# これだと最後の行は改行がなければならない
while IFS= read -r line; do
echo "$line"
done
# これならOK。最後の行は実は読み込まれてるので $line に何かしら入っている
while IFS= read -r line || [ "$line" ]; do
echo "$line"
done
# 最後の行が改行で終わってない場合を区別したい場合
while IFS= read -r line; do
echo "$line"
done
if [ "$line" ]; then
# 最後の行が改行で終わってない場合
echo "$line"
fi
Windows改行コードに対応させる
Linux / Unix / macOS の改行コードは LF
ですが、Windowsの改行コードは CR
LF
です。余計な CR
があるのでこれを削除するだけです。
CR=$(printf '\r')
while IFS= read -r line && line=${line%"$CR"} || [ "$line" ]; do
echo "$line ${#line}"
done
関数化
ここまでの話ならただの雑学。ここからが本題です。上記のコードを何度も書くのは面倒ですよね?そこで関数化したいと考えます。read
関数が少し特徴的なのもあってぱっと思い浮かばないのではないでしょうか?まあ引っ張る話でもないのでさくっと実装です。
readline() {
IFS= read -r "$1" || eval "[ \"\${$1}\" ]"
}
line='' # shellcheck に未定義の変数を使用していると怒られないようにするため
while readline line; do
echo "$line"
done
次はWindowsの改行コードに対応です。先程の応用です。
CR=$(printf '\r')
readline() {
# shellcheck が SC2015 の警告を出すので { } でくくる
{ IFS= read -r "$1" && eval "$1=\${$1%\"\$CR\"}"; } || eval "[ \"\${$1}\" ]"
}
line='' # shellcheck に未定義の変数を使用していると怒られないようにするため
while readline line; do
echo "$line"
done
pdksh, loksh ワークアラウンド
基本的には上記のコードで良いのですが、pdksh と lokshでバグがあります。(loksh は Alpine Linux にパッケージがあります。loksh には loksh is a Linux port of OpenBSD's ksh と書いてあり、OpenBSD の ksh は public domain Korn shell と書いてあるので同じバグなのだと思います。)
pdksh, loksh は set -e
した状態だと途中でスクリプトが終了してしまいます。以下はそれを再現する簡単なコードです。本来ならば FALSE と END が表示されなければいけないはずですが eval
の中で [ ]
を使用すると途中で中断してしまうのです。(TRUE も表示されません。)
set -e
eval "[ ]" && echo TRUE || echo FALSE
echo END
ワークアラウンドは簡単で eval
の中の [ ]
の後ろに &&:
(&& true
) をつけるだけです。
set -e
eval "[ ] &&:" && echo TRUE || echo FALSE
echo END
ということで関数化したコードにも追加します。
# 末尾改行なし対応
readline() {
IFS= read -r "$1" || eval "[ \"\${$1}\" ] &&:"
}
# 末尾改行なし + Windows改行コード対応
readline() {
{ IFS= read -r "$1" && eval "$1=\${$1%\"\$CR\"}"; } || eval "[ \"\${$1}\" ] &&:"
}
以上で完成です。