「IFS = read -r line」を理解する


60

内部フィールド区切り変数に値を追加できることは明らかです。例えば:

$ IFS=blah
$ echo "$IFS"
blah
$ 

また、次の名前の変数にread -r lineデータを保存することも理解しstdinていますline

$ read -r line <<< blah
$ echo "$line"
blah
$ 

しかし、コマンドはどのようにして変数値を割り当てることができますか?そして、それは最初からデータを格納しstdin、変数にline、その後の値与えるlineにはIFS


回答:


104

一部の人々はread、行を読むコマンドであるという誤った概念を持っています。そうではありません。

read(おそらくバックスラッシュに続く)行から単語を読み取ります。単語は$IFS区切られ、バックスラッシュを使用して区切り文字をエスケープ(または行を継続)できます。

一般的な構文は次のとおりです。

read word1 word2... remaining_words

readエスケープされていない改行文字(または入力の終わり)が見つかるまでstdinを一度に1バイトずつ読み取り、それを複雑な規則に従って分割し$word1、その分割結果を$word2...に格納します$remaining_words

たとえば、次のような入力の場合:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

そして、デフォルト値で$IFS、以下read a b cを割り当てます:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

1つの引数のみが渡された場合、それはになりませんread line。まだread remaining_wordsです。バックスラッシュ処理は引き続き行われ、IFSの空白文字は最初と最後から削除されます。

この-rオプションは、バックスラッシュ処理を削除します。したがって、上記の同じコマンド-rは代わりに割り当てます

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

さて、分割部分については$IFS、IFSの空白文字(つまりスペースとタブ(および改行、ただし、-dを使用しない限り重要ではありませんが))の2つのクラスの文字があることを認識することが重要です。$IFS)およびその他のデフォルト値になります。これら2つのクラスのキャラクターの扱いは異なります。

IFS=::IFS空白文字でないという)、のような入力が:foo::bar::に分割されるだろう"""foo"""barおよび""(と余分な""ものは除い重要ではありませんが、いくつかの実装でread -a)。一方、それ:をスペースで置き換えると、分割はとのみにfooなりbarます。つまり、先頭と末尾のものは無視され、それらのシーケンスは1つのように扱われます。で空白文字と非空白文字を組み合わせる場合、追加の規則があります$IFS。一部の実装では、IFS(IFS=::またはIFS=' ')の文字を2倍にすることで、特別な処理を追加/削除できます。

したがって、ここで、先頭および末尾のエスケープされていない空白文字を削除したくない場合は、IFSからそれらのIFS空白文字を削除する必要があります。

IFSの非空白文字であっても、入力行にこれらの文字が1つ(1つだけ)含まれ、POSIXシェル(一部のバージョンでもない)の行の最後の文字(などIFS=: read -r wordの入力の場合foo:)である場合、その入力一つとして考えられているものをシェルに、文字があるため単語として考えられているターミネータ、そう含まれています、ではありません。zshpdkshfoo$IFSwordfoofoo:

したがって、read組み込みで1行の入力を読み取る標準的な方法は次のとおりです。

IFS= read -r line

readNUL文字はを除いてサポートされていないため、ほとんどの実装ではテキスト行でのみ機能することに注意してくださいzsh)。

var=value cmd構文を使用するとIFS、そのcmdコマンドの実行中にのみ異なる方法で設定されます。

履歴メモ

read組み込みは、Bourneシェルによって導入し、読むためにすでにあった言葉ではなく、ラインを。最近のPOSIXシェルにはいくつかの重要な違いがあります。

Bourneシェルread-r(Kornシェルによって導入された)オプションをサポートしていなかったため、そのようなもので入力を前処理する以外にバックスラッシュ処理を無効にする方法はありませんsed 's/\\/&&/g'

Bourneシェルには、2つのクラスの文字という概念がありませんでした(これもkshによって導入されました)。ボーンのすべての文字をシェルのIFS空白文字がkshの中でやるのと同じ治療を受け、それがあるIFS=: read a b cように、入力にfoo::bar割り当てるでしょうbar$b、空の文字列ではありません、。

Bourneシェルで:

var=value cmd

cmdが(のようなread)組み込みである場合、終了後にvar設定されvalueたままcmdになります。$IFSBourneシェルで$IFSは、展開だけでなくすべてを分割するために使用されるため、これは特に重要です。また、$IFSBourneシェルでスペース文字を削除すると、"$@"機能しなくなります。

Bourneシェルでは、複合コマンドをリダイレクトすると、サブシェルで実行されます(初期バージョンでは、機能しなかっread var < fileたり機能exec 3< file; read var <&3しなかったりします)。そのため、Bourneシェルreadでは、端末上のユーザー入力以外に使用することはまれでした。(その行の継続処理が意味をなす場所)

一部のUnices(HP / UXなどutil-linux)には、line1行の入力を読み取るコマンドがあります(以前は、Single UNIX Specificationバージョン2まで標準のUNIXコマンドでし)。

これは基本的に同じですが、head -n 1一度に1バイトずつ読み取り、複数行を読み取らないようにします。これらのシステムでは、次のことができます。

line=`line`

もちろん、それは新しいプロセスを生成し、コマンドを実行し、パイプを介してその出力を読み取ることを意味するため、kshの場合よりもはるかに効率が低くなりますIFS= read -r lineが、それでもはるかに直感的です。


3
+1 bashのIFSでのスペース/タブと「その他」のさまざまな処理に関する有益な洞察に感謝します。それらが異なる方法で処理されることは知っていましたが、この説明はすべてを単純化します。(そして、bash(および他のposixシェル)と通常のsh違いとの間の洞察は、ポータブルスクリプトを記述するのにも役立ちます!)
オリビエデュラック

少なくとも、bash-4.4.19としてwhile read -r; do echo "'$REPLY'"; done機能しwhile IFS= read -r line; do echo "'$line'"; doneます。
x-yuri

これ:「... readは行を読むコマンドであるという誤った概念...」はread、行を読むために使用するのが間違っている場合、何か他のものがあるに違いないと思うようになります。その間違っていない概念は何でしょうか?「あなたが実行して、ファイルから行を読み取るためにそれを使用することができ、それは非常に強力なので読みラインからの単語を読むためのコマンドです::。か、その最初の文では、技術的に正しいですが、真実で誤りのない概念はありますIFS= read -r line
マイクS

8

理論

ここで関係している概念は2つあります。

  • IFSは、入力フィールド区切り文字ですIFS。つまり、読み込まれた文字列はの文字に基づいて分割されます。コマンドラインでIFSは、通常は空白文字であるため、コマンドラインはスペースで分割されます。
  • 何かをVAR=value commandするということは、「コマンドの環境を変更しVARて、値を持たせるvalue」ということです。基本的に、コマンドcommandVARvalueを持っていると見なしますがvalue、その後実行されたコマンドVARは以前の値を持っていると見なします。つまり、その変数はそのステートメントに対してのみ変更されます。

この場合

ですから、やっているときIFS= read -r line、あなたがしていることはIFS空の文字列に設定することです(文字は分割に使用されないので、分割は発生しません)ので、read行全体を読んで、line変数に割り当てられる1つの単語としてそれを見るでしょう。変更IFSはそのステートメントにのみ影響するため、後続のコマンドは変更の影響を受けません。

サイドノートとして

コマンドが正しく、設定、意図したとおりに動作しますがIFS、この場合ではない のかもしれない1ではない必要。builtinセクションのbashmanページに書かれているようにread

標準入力[...]から1行が読み取られ、最初の単語が名に、2番目の単語が2番目の名前に、というように割り当てられます。残りの単語とその間にある区切り文字は姓に割り当てられます。入力ストリームから読み取られる単語が名前よりも少ない場合、残りの名前には空の値が割り当てられます。の文字はIFS、行を単語に分割するために使用されます。[...]

line変数しか持っていないのでとにかくすべての単語が変数に割り当てられます。そのため、前後の空白文字1のいずれかが必要ない場合は、それを書いread -r lineてそれを行うことができます。

[1] unsetまたはデフォルト$IFS値がread先行/末尾IFS空白をどのように考慮するかの一例として、以下を試すことができます。

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

実行すると、IFS設定されていない場合、前後の文字は存続しないことがわかります。さらに、$IFSスクリプトの前の方で変更すると、奇妙なことが起こる可能性があります。


5

このステートメントは2つの部分で読む必要があります。最初の部分はIFS変数の値をクリアします。つまり、より読みやすいものと同等ですIFS=""。2番目の部分はlinestdinから変数を読み取りますread -r line

この構文で具体的なのは、IFSの影響が一時的であり、readコマンドに対してのみ有効であることです。

何かが足りない場合を除いて、その特定のケースではクリアIFSは効果がありませんが、何IFSが設定されていても、行全体がline変数に読み込まれます。複数の変数がパラメーターとしてread命令に渡された場合にのみ、動作に変更があったでしょう。

編集:

これ-r\、特別な処理を行わないで終わる入力を許可するためのものです。つまり、line複数行入力を許可するための継続文字としてではなく、変数にバックスラッシュを含めます。

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

IFSをクリアすると、読み取りを防止して潜在的な先頭および末尾のスペースまたはタブ文字をトリミングするという副作用があります。

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

その違いを指摘してくれたriciに感謝します。


不足しているのは、IFSが変更されていない場合read -r lineline変数に入力を割り当てる前に先頭と末尾の空白を削除することです。
リチ

@rici私はそのような何かを疑っていましたが、単語間のIFS文字のみをチェックし、先頭/末尾の文字はチェックしませんでした。その事実を指摘してくれてありがとう!
jlliagre

IFSをクリアすると、複数の変数の割り当ても防止されます(副作用)。 IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"ショー-aa bb--
-kyodev
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.