$ IFS変数を「バックアップ」するのは正しいアプローチですか?


19

それは$IFSグローバルを破壊しているので、私はいつも混乱するのを本当にheしています。

しかし、多くの場合、文字列をbash配列にロードすると、簡潔かつ簡潔になります。また、bashスクリプトの場合、簡潔さを実現するのは困難です。

だから、開始内容$IFSを別の変数に「保存」して$IFS、何かに使い終わったらすぐにそれを復元しようとすると、何よりも良いかもしれません。

これは実用的ですか?それとも本質的に無意味IFSですか、それ以降の使用に必要なものに直接戻す必要がありますか?


なぜ実用的ではないのですか?
ブラッチリー

IFSの設定を解除することで問題が解決するためです。
llua

1
未設定IFSは罰金を動作するという方のために、それが状況であることに留意してください:stackoverflow.com/questions/39545837/...を。私の経験では、シェルインタープリターのデフォルトにIFSを手動で設定$' \t\n'することをお勧めします。つまり、bashを使用している場合です。unset $IFS単にデフォルトになると予想されるものに常に復元するとは限りません。
ダレルホルト

回答:


9

必要に応じてIFSに保存して割り当てることができます。そうすることには何の問題もありません。配列割り当ての例のように、一時的かつ迅速な変更後の復元のためにその値を保存することは珍しくありません。

@lluaがあなたの質問へのコメントで言及しているように、単にIFSを設定解除すると、space-tab-newlineを割り当てるのと同等のデフォルトの動作に戻ります。

IFSを明示的に設定/設定解除しないことが、それを行うよりも問題になる可能性があることを検討する価値があります。

POSIX 2013エディション、2.5.3シェル変数から

実装は、シェルの起動時に、環境内のIFSの値、または環境からのIFSの欠如を無視する場合があります。この場合、シェルは、起動時にIFSを<space> <tab> <newline>に設定します。 。

POSIX準拠の呼び出されたシェルは、その環境からIFSを継承する場合としない場合があります。これから次のとおりです。

  • ポータブルスクリプトは、環境を介してIFSを確実に継承できません。
  • デフォルトの分割動作のみを使用する(またはの場合は参加する"$*")が、環境からIFSを初期化するシェルの下で実行される可能性のあるスクリプトは、IFSを明示的に設定/設定解除して、環境の侵入から身を守る必要があります。

注意この議論では、「呼び出された」という言葉には特定の意味があることを理解することが重要です。シェルは、その名前(#!/path/to/shellシェバンを含む)を使用して明示的に呼び出された場合にのみ呼び出されます。$(...)またはによって作成される可能性のあるサブシェルcmd1 || cmd2 &は、呼び出されたシェルではなく、そのIFS(およびその実行環境のほとんど)はその親のものと同一です。呼び出されたシェルは値を$pidに設定し、サブシェルはそれを継承します。


これは単なる教訓的な論争ではありません。この領域には実際の分岐があります。これは、いくつかの異なるシェルを使用してシナリオをテストする簡単なスクリプトです。変更されたIFS(に設定:)を起動されたシェルにエクスポートし、シェルはデフォルトのIFSを出力します。

$ cat export-IFS.sh
export IFS=:
for sh in bash ksh93 mksh dash busybox:sh; do
    printf '\n%s\n' "$sh"
    $sh -c 'printf %s "$IFS"' | hexdump -C
done

IFSは通常、エクスポート用にマークされていませんが、bash、ksh93、およびmkshが環境のを無視しIFS=:、ダッシュとbusyboxがそれを尊重することに注意してください。

$ sh export-IFS.sh

bash
00000000  20 09 0a                                          | ..|
00000003

ksh93
00000000  20 09 0a                                          | ..|
00000003

mksh
00000000  20 09 0a                                          | ..|
00000003

dash
00000000  3a                                                |:|
00000001

busybox:sh
00000000  3a                                                |:|
00000001

いくつかのバージョン情報:

bash: GNU bash, version 4.3.11(1)-release
ksh93: sh (AT&T Research) 93u+ 2012-08-01
mksh: KSH_VERSION='@(#)MIRBSD KSH R46 2013/05/02'
dash: 0.5.7
busybox: BusyBox v1.21.1

bash、ksh93、およびmkshは環境からIFSを初期化しませんが、変更されたIFSを再エクスポートします。

何らかの理由で環境を介してIFSを移植可能に渡す必要がある場合、IFS自体を使用して渡すことはできません。値を別の変数に割り当て、その変数をエクスポート用にマークする必要があります。その後、子供はその値をIFSに明示的に割り当てる必要があります。


つまり、言い換えると、使用するほとんどの状況で明示的に値を指定するが間違いなく移植性が高いためIFS、元の値を「保存」しようとするのはあまり生産的ではありません。
スティーブンルー

1
最も重要な問題は、スクリプトでIFSを使用する場合、IFSを明示的に設定/設定解除して、その値が意図したとおりになるようにする必要があることです。通常、引用符で囲まれていないパラメーター展開、引用符で囲まれていないコマンド置換、引用符で囲まれていない算術展開、reads、または二重引用符で囲まれた参照がある場合、スクリプトの動作はIFSに依存します$*。そのリストは私の頭の一番上にあるので、包括的ではないかもしれません(特に現代のシェルのPOSIX拡張を検討するとき)。
裸足IO

10

一般に、条件をデフォルトに戻すことをお勧めします。

ただし、この場合はそれほどではありません。

なぜ?:

また、IFS値の保存にも問題があります。
元のIFSが設定解除されていた場合、コードIFS="$OldIFS"はIFSを""設定解除せずにに設定します。

IFSの値を(設定されていなくても)実際に保持するには、これを使用します。

${IFS+"false"} && unset oldifs || oldifs="$IFS"    # correctly store IFS.

IFS="error"                 ### change and use IFS as needed.

${oldifs+"false"} && unset IFS || IFS="$oldifs"    # restore IFS.

IFSを実際に設定解除することはできません。設定を解除すると、シェルはデフォルト値に戻します。そのため、保存するときに実際に確認する必要はありません。
フィルブランデン

注意してくださいという点でbashunset IFSそれは親コンテキスト(関数コンテキスト)内のローカルではなく、現在のコンテキスト内で宣言されていた場合には未設定のIFSに失敗しました。
ステファンシャゼラス

5

グローバルを破壊することをためらうことは正しいことです。恐れる必要はありません。実際のglobalを変更しIFSたり、面倒でエラーが発生しやすい保存/復元ダンスを実行したりすることなく、きれいな作業コードを書くことができます。

あなたはできる:

  • 単一の呼び出しにIFSを設定します。

    IFS=value command_or_function

    または

  • サブシェル内でIFSを設定します。

    (IFS=value; statement)
    $(IFS=value; statement)

  • 配列からコンマ区切りの文字列を取得するには:

    str="$(IFS=, ; echo "${array[*]-}")"

    注:これ-、設定解除時にデフォルト値をset -u提供することによって空の配列を保護することのみです(この場合、その値は空の文字列です)

    IFS変更はによって生成されたサブシェルの内部にのみ適用され$() 、コマンド置換。これは、サブシェルには呼び出し元のシェル変数のコピーがあり、その値を読み取ることができるためですが、サブシェルによって行われた変更はサブシェルのコピーにのみ影響し、親の変数には影響しないためです。

    また、考えているかもしれません:サブシェルをスキップして、これを行うだけではありません:

    IFS=, str="${array[*]-}"  # Don't do this!

    ここにはコマンド呼び出しはありません。代わりに、この行は、次の2つの独立した後続の変数割り当てとして解釈されます。

    IFS=,                     # Oops, global IFS was modified
    str="${array[*]-}"

    最後に、このバリアントが機能しない理由を説明しましょう。

    # Notice missing ';' before echo
    str="$(IFS=, echo "${array[*]-}")" # Don't do this! 

    echoコマンドは、確かにそのと呼ばれるIFSに設定された変数,が、echo気にしたり、使用しませんIFS"${array[*]}"文字列に展開する魔法echoは、呼び出される前に(サブ)シェル自体によって行われます。

  • NULLバイトを含まない)ファイル全体を、という名前の単一の変数に読み込むにはVAR

    IFS= read -r -d '' VAR < "${filepath}"

    注: IFS=と同じであるIFS=""IFS=''、非常に異なっている空の文字列にすべての設定IFS unset IFS:場合はIFS、内部での使用は、すべてのbashの機能の動作設定されていないIFS場合と全く同じであるIFSのデフォルト値を持っていたし$' \t\n'

    IFS空の文字列に設定すると、先頭と末尾の空白が保持されます。

    -d ''または-d ""だけで、現在の呼び出しを停止するために読んで指示しますNULL代わりに通常の改行を、バイト。

  • 区切り文字に$PATH沿って分割するには:

    IFS=":" read -r -d '' -a paths <<< "$PATH"

    この例は純粋に例示です。区切り文字に沿って分割する一般的なケースでは、個々のフィールドにその区切り文字(のエスケープバージョン)を含めることができます。.csv列自体にカンマが含まれている可能性があるファイルの行を読み取ろうと考えてください(何らかの方法でエスケープまたは引用符で囲まれています)。上記のスニペットは、このような場合に意図したとおりに機能しません。

    とはいえ、:内でそのような-を含むパスに遭遇する可能性は低い$PATHです。UNIX / Linuxのパス名にはaを含めることが許可され:ていますが$PATH、エスケープ/引用符で囲まれたコロンを解析するコードがないため、パスを追加して実行可能ファイルを保存しようとすると、bashはそのようなパスを処理できません:bash 4.4のソースコード

    最後に、スニペットは結果の配列の最後の要素に末尾の改行を追加することに注意してください(@StéphaneChazelasによって削除されたコメントで呼び出されます)。入力が空の文字列の場合、出力は単一要素になります配列。要素は改行($'\n')で構成されます。

動機

old_IFS="${IFS}"; command; IFS="${old_IFS}"グローバルに触れる基本的なアプローチIFSは、最も単純なスクリプトに対して期待どおりに機能します。ただし、複雑さを追加するとすぐに簡単にバラバラになり、微妙な問題が発生する可能性があります。

  • 場合はcommand、グローバル変更することbashの機能であるIFS(直接、または、それが呼び出す内部さらに別の機能、視界から隠され)、そのため誤ってやっている間は、同じグローバル使用してold_IFS保存/復元を行うに変数を、あなたはバグを取得します。
  • @Gillesによるこのコメントで指摘されているように、の元の状態IFSが設定されていない場合、単純な保存と復元は機能せず、一般的に(誤って)使用されたset -u(別名set -o nounset)シェルオプション有効です。
  • 一部のシェルコードは、シグナルハンドラーなどを使用して、メイン実行フローと非同期に実行することができます(を参照help trap)。そのコードがグローバルも変更するIFSか、特定の値を持っていると仮定すると、微妙なバグが発生する可能性があります。

あなたは(そのように提案されているものとしてシーケンスを保存/復元より堅牢な工夫ができ、この他の答えこれらの問題の一部または全部を避けるために。しかし、あなたは一時的にカスタムを必要な場所騒々しい定型コードのその部分を繰り返さなければならないIFS。これをコードの可読性と保守性が低下します。

ライブラリのようなスクリプトに関する追加の考慮事項

IFS特に、IFS呼び出し側によって課されるグローバルな状態(、シェルオプションなど)に関係なく、またその状態をまったく邪魔することなく(呼び出し側が依存する可能性がある)その上で常に静的なままにします)。

ライブラリコードを記述する場合IFS、特定の値(デフォルト値でさえない)に依存することも、まったく設定されていることにも依存できません。代わりに、IFS動作がに依存するスニペットを明示的に設定する必要がありますIFS

場合はIFS明示的に値がこの回答で説明する2つのメカニズムのいずれか使用して重要なコードのライン毎に(つまり、デフォルトのものであることを起こる場合であっても)必要な値に設定されている効果を局在化することが適切である場合、コードは、両方でありますグローバル状態に依存せず、それを完全に破壊することを回避します。このアプローチには、IFS最小のテキストコストで(最も基本的な保存/復元と比較しても)正確にこの1つのコマンド/拡張に重要なスクリプトを読む人に非常に明確にするという追加の利点があります。

IFSとにかくどのコードが影響を受けますか?

幸いなことに、IFS重要なシナリオはそれほど多くありません(常に拡張を引用すると仮定します)。

  • "$*"および"${array[*]}"拡張
  • read複数の変数(read VAR1 VAR2 VAR3)または配列変数(read -a ARRAY_VAR_NAME)をターゲットとする組み込みの呼び出し
  • read現れる先頭または末尾の空白または非空白文字に関して、単一の変数を対象とする呼び出しIFS
  • 単語分割(引用符のない拡張など、ペストのように避けたい場合があります
  • 他のあまり一般的でないシナリオ(IFS @ Greg's Wikiを参照)

Toが$ PATHを:区切り文字に沿って分割すること理解できているとは言えません。区切り文字は:いつコンポーネントに含ま:れますか?
ステファンシャゼラス

@StéphaneChazelasまあ、:ほとんどのUNIX / Linuxファイルシステムでファイル名に使用する有効な文字なので、を含む名前のディレクトリを持つことは完全に可能:です。おそらく、いくつかのシェルが脱出する準備持つ:ようなものを使用してPATHには\:(、そして、あなたはその出現列は、実際の区切り文字ではありません見るであろうことは、bashは、このようなエスケープを許可していないようです。繰り返し処理を行う場合、低レベルの機能を利用$PATHするために、単に検索:でC文字列:git.savannah.gnu.org/cgit/bash.git/tree/general.c#n891)。
sls

答えを修正して、分割の$PATH例を:より明確にすることを願っています。
sls

1
SOへようこそ!そのような詳細な回答をありがとう:)
スティーブンルー

1

これは実用的ですか?それとも本質的に無意味であり、IFSをその後の使用に必要なものに直接戻す必要がありますか?

$' \t\n'あなたがしなければならないときにIFSを設定するタイプミスのリスクがあるのはなぜですか

OIFS=$IFS
do_your_thing
IFS=$OIFS

または、変数を内部で設定/変更する必要がない場合は、サブシェルを呼び出すことができます。

( IFS=:; do_your_thing; )

IFS最初に設定解除された場合は機能しないため、これは危険です。
ジル 'SO-悪である停止
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.