web-dev-qa-db-ja.com

なぜ「IFS =の代わりにIFS =読み取り」が頻繁に使用されるのですか。読みながら..`?

通常の慣行では、反復ごとに設定を繰り返さないようにするために、whisループの外側にIFSの設定を配置するようです...これは、このサルの場合までそうであったように、これは単なる習慣的な「サル見る、サルする」スタイルですか?私はman readを読みますか、またはここに微妙な(または明らかに明白な)トラップがありませんか?

85
Peter.O

罠は

IFS=; while read..

ループの外側のシェル環境全体にIFSを設定しますが、

while IFS= read

read invocation(Bourne Shellを除く)に対してのみ再定義します。あなたはのようなループをしていることを確認することができます

while IFS= read xxx; ... done

そのようなループの後、echo "blabalbla $IFS ooooooo"

blabalbla
 ooooooo

後に

IFS=; read xxx; ... done

IFSstaysが再定義されました:echo "blabalbla $IFS ooooooo"が出力されます

blabalbla  ooooooo

したがって、2番目の形式を使用する場合は、必ずリセットする必要があります:IFS=$' \t\n'


この質問の2番目の部分 ここにマージされました なので、ここから関連する回答を削除しました。

86
rozcietrzewiacz

入念に作成された入力テキストを使用した例を見てみましょう。

text=' hello  world\
foo\bar'

これは2行です。最初の行はスペースで始まり、バックスラッシュで終わります。最初に、 read の周りに何も注意を払わずに何が起こるかを見てみましょう(ただし、printf '%s\n' "$text"を使用して、$textを拡張のリスクなしに注意深く印刷します)。 (以下、$ ‌はシェルプロンプトです。)

$ printf '%s\n' "$text" |
  while read line; do printf '%s\n' "[$line]"; done
[hello worldfoobar]

readバックスラッシュを削除します:backslash-newlineは改行を無視し、backslash-anythingは最初のバックスラッシュを無視します。バックスラッシュが特別に扱われるのを避けるために、read -rを使用します。

$ printf '%s\n' "$text" |
  while read -r line; do printf '%s\n' "[$line]"; done
[hello  world\]
[foo\bar]

それはましです、予想通り2行あります。 2行にはほぼ目的のコンテンツが含まれています。helloworldの間の二重スペースは、line変数内にあるため保持されています。一方、最初のスペースはすっかり食べ尽くされていました。これは、readが変数を渡すのと同じ数の単語を読み取るためです。ただし、最後の変数には行の残りが含まれますが、最初の単語から始まります。つまり、最初のスペースは破棄されます。

したがって、各行を文字どおりに読み取るには、 Word splitting が実行されていないことを確認する必要があります。 IFS variable を空の値に設定してこれを行います。

$ printf '%s\n' "$text" |
  while IFS= read -r line; do printf '%s\n' "[$line]"; done
[ hello  world\]
[foo\bar]

IFSreadビルトインの期間に具体的に設定する方法に注意してください。 IFS= read -r lineは、IFSの実行専用に環境変数readを(空の値に)設定します。これは、一般的な 単純なコマンド 構文のインスタンスです:変数割り当てのシーケンス(空の可能性があります)の後にコマンド名とその引数が続きます(また、いつでもリダイレクトをスローできます)。 readは組み込みなので、変数が実際に外部プロセスの環境に到達することはありません。それにもかかわらず、readが実行されている限り、$IFSの値はそこで割り当てられますwhat。 read特別な組み込み ではないので、割り当てはその期間だけ持続します。

したがって、IFSの値を変更しないように注意して、それに依存する可能性のある他の命令について説明します。このコードは、周囲のコードがIFSを最初に設定したものに関係なく機能し、ループ内のコードがIFSに依存している場合でも問題は発生しません。

コロンで区切られたパスでファイルを検索するこのコードスニペットとは対照的です。ファイル名のリストは、1行に1つずつ、ファイルから読み取られます。

IFS=":"; set -f
while IFS= read -r name; do
  for dir in $PATH; do
    ## At this point, "$IFS" is still ":"
    if [ -e "$dir/$name" ]; then echo "$dir/$name"; fi
  done
done <filenames.txt

ループがwhile IFS=; read -r name; do …の場合、for dir in $PATH$PATHをコロンで区切られたコンポーネントに分割しません。コードがIFS=; while read …の場合、ループ本体でIFS:に設定されていないことがさらに明白になります。

もちろん、IFSの実行後にreadの値を復元することは可能です。ただし、そのためには以前の値を知る必要があり、これは追加の労力です。 IFS= readは簡単な方法です(便利なことに、最短の方法でもあります)。

¹ そして、トラップが実行されている間に、トラップされた信号によってreadが中断された場合—これはPOSIXでは指定されておらず、実際にはシェルに依存しています。

(すでに明らかにされている)IFSスコープ指定の違いとは別に、while IFS='' readIFS=''; while readおよびwhile IFS=''; readイディオム(コマンドごとvsスクリプト/シェル全体のIFS変数スコープ)、持ち帰りのレッスンは、リーダーを失うことです そして IFS変数がスペースを含む(含む)場合、入力行の末尾のスペース。

ファイルパスが処理されている場合、これはかなり深刻な結果をもたらす可能性があります。

したがって、IFS変数を空の文字列に設定することは、行の先頭と末尾の空白が削除されないことが保証されるため、決して悪い考えではありません。

参照: Bash、IFSを使用してファイルから行ごとに読み取る

(
shopt -s nullglob
touch '  file with spaces   '
IFS=$' \t\n' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
IFS='' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
)
3
jon

ユゼムの答え に触発されました

IFSを実際の文字に設定したい場合、これは私にとってうまくいきました

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
1
Steven Penny