web-dev-qa-db-ja.com

シェルスクリプトは、最後の行の欠落を読み取りました

私は、bashシェルスクリプトに関して、奇妙な問題を抱えています。

私のチームは、ファイル内の行を反復処理し、各行のコンテンツをチェックするスクリプトに取り組んでいます。異なるスクリプトを一緒にシーケンス処理する自動化プロセスで実行すると、最後の行が表示されないというバグがありました。

ファイル内の行を反復するために使用されるコード(DATAFILEに保存されている名前は

cat "$DATAFILE" | while read line 

コマンドラインからスクリプトを実行すると、ファイルのすべての行(最後の行を含む)が正常に表示されます。ただし、自動化されたプロセス(問題のスクリプトの直前にDATAFILEを生成するスクリプトを実行するプロセス)によって実行される場合、最後の行は表示されません。

次のコードを使用して行を反復処理するようにコードを更新し、問題を解決しました。

for line in `cat "$DATAFILE"` 

注:DATAFILEには、ファイルの最後に改行が書き込まれていません。

私の質問は2つの部分です...最後の行が元のコードに表示されないのはなぜですか、そしてなぜこれが変わるのでしょうか?

私は最後の行が表示されない理由について思い付くことができると思っただけでした:

  • ファイルを書き込む前のプロセスは、ファイル記述子を閉じるために終了するプロセスに依存していました。
  • 問題のスクリプトは、前のプロセスが「終了」したが、システムがファイル記述子を自動的に閉じるのに十分な「シャットダウン/クリーンアップ」ができなかったほど早くファイルを起動して開いていました。

つまり、シェルスクリプトに2つのコマンドがある場合、スクリプトが2番目のコマンドを実行するまでに最初のコマンドを完全にシャットダウンする必要があるようです。

質問、特に最初の質問に対する洞察は非常にありがたいです。

54
RHSeeger

C標準では、テキストファイルは改行で終了する必要があります。そうしないと、最後の改行の後のデータが正しく読み取れない場合があります。

ISO/IEC 9899:2011§7.21.2ストリーム

テキストストリームは、行に構成された文字の順序付きシーケンスであり、各行は0個以上の文字と終了改行文字で構成されます。最後の行に終了改行文字が必要かどうかは、実装によって定義されます。ホスト環境でテキストを表すためのさまざまな規則に準拠するために、入力および出力で文字を追加、変更、または削除する必要がある場合があります。したがって、ストリーム内の文字と外部表現内の文字の間には1対1の対応関係は必要ありません。テキストストリームから読み込まれたデータは、次の場合にのみ、そのストリームに以前に書き込まれたデータと必ず比較されます。データが印刷文字と制御文字の水平タブと改行のみで構成されている。改行文字のすぐ前にスペース文字はありません。最後の文字は改行文字です。読み込み時に改行文字の直前に書き出されるスペース文字が表示されるかどうかは実装定義です。

bash(または任意のUnixシェル)で問題を引き起こすファイルの終わりに予期しない改行がないことはありませんが、それは再現可能な問題のようです($はこのプロンプトです出力):

$ echo xxx\\c
xxx$ { echo abc; echo def; echo ghi; echo xxx\\c; } > y
$ cat y
abc
def
ghi
xxx$
$ while read line; do echo $line; done < y
abc
def
ghi
$ bash -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ ksh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ zsh -c 'while read line; do echo $line; done < y'
abc
def
ghi
$ for line in $(<y); do echo $line; done      # Preferred notation in bash
abc
def
ghi
xxx
$ for line in $(cat y); do echo $line; done   # UUOC Award pending
abc
def
ghi
xxx
$

また、bashに限定されません— Korn Shell(ksh)およびzshも同様に動作します。私は生き、学びます。問題を提起してくれてありがとう。

上記のコードで示したように、catコマンドはファイル全体を読み取ります。 for line in `cat $DATAFILE`手法は、すべての出力を収集し、空白の任意のシーケンスを単一の空白に置き換えます(ファイルの各行に空白が含まれていないと判断します)。

Mac OS X 10.7.5でテスト済み。


POSIXは何と言っていますか?

POSIX read コマンド仕様には次のように記載されています。

読み取りユーティリティは、標準入力から1行を読み取ります。

デフォルトでは、-rオプションが指定されていない限り、<backslash>はエスケープ文字として機能します。エスケープされていない<バックスラッシュ>は、<改行>を除き、次の文字のリテラル値を保持します。 <改行>が<バックスラッシュ>の後に続く場合、読み取りユーティリティはこれを行の継続と解釈します。 <backslash>と<newline>は、入力をフィールドに分割する前に削除されます。入力をフィールドに分割した後、エスケープされていない他のすべての<バックスラッシュ>文字は削除されます。

標準入力が端末デバイスで、呼び出しシェルが対話型である場合、readは、-rオプションが指定されていない限り、<backslash> <newline>で終わる入力行を読み取るときに継続行を要求します。

終端<newline>(ある場合)は入力から削除され、パラメータの結果のシェルのように結果がフィールドに分割されます。拡張(フィールド分割を参照); [...]

「(もしあれば)」(引用符で強調されている)ことに注意してください!改行がない場合でも、結果を読み取る必要があるように思えます。一方で、それはまた言う:

STDIN

標準入力はテキストファイルです。

そして、改行で終わらないファイルがテキストファイルかどうかについての議論に戻ります。

ただし、同じページに記載されている理論的根拠:

標準入力はテキストファイルである必要があるため、常に<newline>で終了します(空のファイルでない限り)が、-rオプションが使用されていない場合の継続行の処理は、入力が<newline>で終わらない。これは、入力ファイルの最後の行が<バックスラッシュ> <改行>で終わる場合に発生します。このため、説明の「終了する<newline>(存在する場合)を入力から削除する」で「存在する場合」を使用します。標準入力がテキストファイルであるという要件を緩和するものではありません。

その理由は、テキストファイルが改行で終わることになっていることを意味する必要があります。

テキストファイルのPOSIX定義は次のとおりです。

.395 テキストファイル

0個以上の行に編成された文字を含むファイル。行にはNUL文字は含まれず、<newline>文字を含めて、長さが{LINE_MAX}バイトを超えることはできません。 POSIX.1-2008はテキストファイルとバイナリファイルを区別しませんが(ISO C標準を参照)、多くのユーティリティはテキストファイルを操作する場合にのみ予測可能または意味のある出力を生成します。このような制限がある標準ユーティリティは、STDINまたはINPUT FILESセクションで常に「テキストファイル」を指定します。

これは、「<newline>で終わる」を直接規定するものではありませんが、C標準に従います。


「端末改行なし」問題の解決策

Gordon Davissonanswer 。簡単なテストは、彼の観察が正確であることを示しています。

$ while read line; do echo $line; done < y; echo $line
abc
def
ghi
xxx
$

したがって、彼のテクニック:

while read line || [ -n "$line" ]; do echo $line; done < y

または:

cat y | while read line || [ -n "$line" ]; do echo $line; done

最後に改行のないファイルに対して機能します(少なくとも私のマシンでは)。


シェルが入力の最後のセグメント(改行で終わらないので行と呼ぶことはできません)をドロップすることにまだ驚いていますが、そうするためにPOSIXに十分な正当化があるかもしれません。そして、明らかに、テキストファイルが実際に改行で終わるテキストファイルであることを確認するのが最善です。

75

読み取りコマンドのPOSIX仕様 によると、「ファイルの終わりが検出されたか、エラーが発生した」場合、ゼロ以外のステータスを返します。 EOFは最後の「行」を読み取るときに検出されるため、$lineを設定し、エラーステータスを返します。エラーステータスにより、ループはその最後の「行」で実行できません「解決策は簡単です。読み取りコマンドが成功した場合、ループを実行しますOR何かが$lineに読み取られた場合。

while read line || [ -n "$line" ]; do
37
Gordon Davisson

いくつかの追加情報を追加します。

  1. Whileループでcatを使用する必要はありません。 while ...;do something;done<fileで十分です。
  2. forで行を読み込まないでください

Whileループを使用して行を読み取る場合:

  1. IFSを適切に設定します(そうしないと、インデントが失われる可能性があります)。
  2. ほとんどの場合、-rオプションをreadとともに使用する必要があります

上記の要件を満たすと、適切なwhileループは次のようになります。

while IFS= read -r line; do
  ...
done <file

そして、最後に改行なしでファイルで動作させるために(私のソリューションを here から再投稿):

while IFS= read -r line || [ -n "$line" ]; do
  echo "$line"
done <file

または、whileループでgrepを使用します。

while IFS= read -r line; do
  echo "$line"
done < <(grep "" file)
12
Jahid

ファイルの最後の行に改行がないことがこの問題の原因であると思われます。テストのために、スクリプトにわずかな変更を加えて、次のようにDATAFILEを読み取ることができます。

while read line
do
    echo $line # do processing here
done < "$DATAFILE"

そして、これが違いを生むかどうかを確認してください。

1
anubhava

Sedを使用してファイルの最後の行に一致させます。存在しない場合は改行を追加し、ファイルのインライン置換を行います。

sed -i '' -e '$a\' file

コードはこのstackexchangeからのものです link

注:少なくとも[OS X]では、-i ''はバックアップファイルのファイル拡張子として-iを使用していたため、-eに空の一重引用符を追加しました。私は元の投稿に喜んでコメントしただろうが、50点が欠けていた。おそらく、これはこのスレッドで私にいくつかを得るでしょう、ありがとう。

1
Joel Bruner

同様の問題がありました。私はファイルの猫をやって、それを並べ替えてから、結果を「while read var1 var2 var3」にパイプしていました。すなわち:cat $ FILE | sort -k3 | while read Count IP Name do「do」の下の作業は$ Nameフィールドの変更データを識別し、変更または変更なしに基づいて$ Countの合計を行った場合、または合計行をレポートに出力したifステートメント。また、レポートに印刷する最後の行を取得できなかった問題に遭遇しました。 cat/sortを新しいファイルにリダイレクトし、その新しいファイルに改行をエコーするという簡単な方法で行った後、新しいファイルで「while read Count IP Name」を実行し、成功しました。すなわち:cat $ FILE | sort -k3> NEWFILE echo "\ n" >> NEWFILE cat NEWFILE | while read Count IP Name do時には、シンプルでエレガントな方法が最善の方法です。

0
Gulesbaron

回避策として、テキストファイルから読み取る前に、ファイルに改行を追加できます。

echo "\n" >> $file_path

これにより、以前ファイルにあったすべての行が確実に読み取られます。

0
ArunGJ