98

いくつかの洞察を得たいと思っていたbashシェルスクリプトに...奇妙な問題があります。

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

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

cat "$DATAFILE" | while read line 

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

以下を使用して行を反復処理するようにコードを更新したところ、問題は解消されました。

for line in `cat "$DATAFILE"` 

注: DATAFILE には、ファイルの最後に改行が書き込まれることはありません。

私の質問は 2 つの部分です... 元のコードで最後の行が表示されないのはなぜですか? また、これを変更すると違いが生じるのはなぜですか?

最後の行が表示されない理由は次のとおりです。

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

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

質問、特に最初の質問についての洞察をいただければ幸いです。

4

7 に答える 7

126

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
$

また、bashKorn シェル ( ksh) に限定されずzsh、そのように動作します。私は生き、学びます。問題を提起してくれてありがとう。

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

Mac OS X 10.7.5 でテスト済み。


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

POSIXreadコマンド仕様には次のように書かれています。

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

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

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

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

「(もしあれば)」に注意してください (引用符で強調を追加)! 改行がない場合でも、結果を読み取る必要があるようです。一方で、次のようにも述べています。

標準入力

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

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

ただし、同じページのドキュメントの根拠は次のとおりです。

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

その論理的根拠は、テキスト ファイルが改行で終わるはずであることを意味しているに違いありません。

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

3.395テキストファイル

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

これは「<改行>で終わる」ことを直接規定するものではありませんが、C標準に従い、「ゼロ行以上のに編成された文字を含むファイル」と述べており、「行」のPOSIX定義を見ると」 それは言います:

3.206ライン

0 個以上の非 <newline> 文字と終了 <newline> 文字のシーケンス。

したがって、POSIX の定義によれば、ファイルは行で構成されており、各行は終了改行で終了する必要があるため、ファイルは終了改行で終了する必要があります。


「端末に改行がない」問題の解決策

Gordon Davisson回答に注意してください。簡単なテストは、彼の観察が正確であることを示しています。

$ 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 にはそうする十分な理由があるかもしれません。また、テキスト ファイルが実際に改行で終わるテキスト ファイルであることを確認するのが最善であることは明らかです。

于 2012-10-16T14:18:26.997 に答える
92

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

while read line || [ -n "$line" ]; do
于 2012-10-16T16:57:30.973 に答える
34

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

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

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

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

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

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

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

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

またはgrepwhileループで使用:

while IFS= read -r line; do
  echo "$line"
done < <(grep "" file)
于 2015-07-14T05:43:36.443 に答える
2

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

echo -e "\n" >> $file_path

これにより、以前にファイルに含まれていたすべての行が確実に読み取られます。エスケープ シーケンスの解釈を有効にするには、-e 引数を echo に渡す必要があります。 https://superuser.com/questions/313938/shell-script-echo-new-line-to-file

于 2013-02-04T16:40:07.837 に答える
1

コマンドラインでこれをテストしました

# create dummy file. last line doesn't end with newline
printf "%i\n%i\nNo-newline-here" >testing

最初のフォームでテストします (while-loop へのパイプ)

cat testing | while read line; do echo $line; done

read改行で終わる入力のみを取得するため、これは理にかなっています。


2 番目のフォームでテストします (コマンド置換)

for line in `cat testbed1` ; do echo $line; done

これは最後の行も取得します


read改行で終了した場合にのみ入力を取得するため、最後の行を見逃すのはそのためです。

一方、第二形態では

`cat testing` 

の形に展開します。

line1\nline2\n...lineM 

これはシェルによって IFS を使用して複数のフィールドに分割されるため、次のようになります。

line1 line2 line3 ... lineM 

そのため、まだ最後の行を取得しています。

p/s: 私が理解できないのは、最初のフォームを機能させる方法です...

于 2012-10-16T15:44:31.077 に答える
0

同様の問題がありました。ファイルの cat を実行し、それを並べ替えにパイプしてから、結果を「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 シンプルでエレガントでないことが最善の方法である場合もあります。

于 2014-02-03T18:53:02.987 に答える