$IFS変数を「バックアップ」する合理的な方法ですか?

$IFS変数を「バックアップ」する合理的な方法ですか?

$IFS私は大きな絵を台無しにするので、いつも混乱を経験することを躊躇します。

しかし、通常、bash配列に文字列をロードするのは素晴らしく簡潔ですが、bashスクリプトでは簡潔さを達成するのは難しいです。

$IFSだから私はスタートアップを他の変数に「保存」してから、何かを使い終わったらすぐに復元する$IFSことが何もしないよりも良いと思いました。

これは実用的ですか?それとも本質的に無意味であり、その後の使用IFSに必要に応じてリセットする必要がありますか?

答え1

通常、条件をデフォルト値に復元することをお勧めします。

しかし、この場合はそうではありません。

なぜ? :

また、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.

答え2

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

あなたの質問に対するコメントで@lluaが述べたように、IFSの設定を解除すると、空白のタブの改行を割り当てるのと同じデフォルトの動作が復元されます。

追加の問題がどのように発生するかを検討する価値があります。いいえIFSを明示的にオン/オフに設定することは、これを行うよりも重要です。

POSIXバージョン2013以降、2.5.3 シェル変数:

実装は、シェルを呼び出すときに環境のIFS値を上書きするか、IFSが環境に存在しない可能性があります。この場合、シェルは呼び出すときにIFSを<space> <tab> <newline>に設定する必要があります。

POSIX準拠の呼び出しシェルは、その環境からIFSを継承することも、継承しないこともあります。以下からわかります。

  • 移植可能なスクリプトは、環境を介してIFSを確実に継承できません。
  • デフォルトの分割動作(または場合は結合"$*")のみを使用するように意図されていますが、環境でIFSを初期化するシェルで実行できるスクリプトは、環境侵入から保護するためにIFSを明示的に設定/設定解除する必要があります。

注:この議論では、「呼び出し」という言葉には特定の意味があることを理解することが重要です。#!/path/to/shellシェルは、その名前(shebangを含む)を使用して明示的に呼び出された場合にのみ呼び出されます。サブシェル($(...)またはによって作成できるサブシェル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=:dashと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にその値を明示的に割り当てる必要があります。

答え3

大きな絵を台無しにすることを躊躇するのは正しいです。心配しないでください。実際のグローバルを修正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! 
    

    コマンドは変数setでecho呼び出されますが、気にしないか使用しません。文字列に拡張する魔法は、呼び出し前(サブ)シェル自体によって実行されます。IFS,echoIFS"${array[*]}"echo

  • フルファイル(NULLバイトを除く)を単一の変数として読み取るには、次の手順を実行しますVAR

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

    注:IFS=IFSを空の文字列に設定するのと同じように、これは非常に異なります。設定しないと、内部で使用されるすべての bash 関数はデフォルト値と同じように動作します。IFS=""IFS=''unset IFSIFSIFSIFS$' \t\n'

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

    または、通常の改行ではなく、1バイトでのみ現在の呼び出しを停止するように-d ''readに指示します。-d ""NULL

  • $PATH区切り記号による分割::

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

    この例は単なる説明のためのものです。区切り記号に沿って分割する一般的な場合、個々のフィールドには対応する区切り記号(エスケープバージョン)を含めることができます。.csv列自体にカンマ(何らかの方法でエスケープまたは引用符)が含まれる可能性があるファイルの1行を読むことを想像してください。この場合、上記のコードスニペットは期待どおりに機能しません。

    つまり、UNIX / Linuxパス名はインクルードを許可しますが、その:パスをパスに追加して実行可能ファイルを保存しようとすると、bashはとにかくそのようなパスを処理できないようです。コードはエスケープ/引用符付きコロンを解析できます。$PATH:$PATHBash 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はコードの読みやすさとメンテナンス性を減らします。

ライブラリスクリプトに関するその他の考慮事項

IFSIFSこれは、呼び出し側が課したグローバル状態(、シェルオプションなど)に関係なく、コードが強く機能し、その状態をまったく妨げないことを確認する必要があるシェルライブラリの作成者にとって特に重要です(呼び出し側はこれに依存します)。常に静的なままにするには)。

ライブラリコードを書くとき、IFS特定の値(デフォルトではない)に依存したり、まったく設定されないことがあります。代わりに明示IFS的にIFS

IFSローカライズ効果についてこの回答で説明されている2つのメカニズムのうちの1つを使用して、その値が重要なすべてのコード行で必要な値(デフォルトでも)に明示的に設定されている場合、コードは両方とも独立しています。グローバル状態を完全に破壊するのを防ぎます。このアプローチは、(最も基本的な保存/復元と比較して)最小限のテキストコストで行うことができるという追加の利点を提供するため、IFSスクリプトを読んでいる人にとって非常に明確であり、これはこのコマンド/拡張に非常に重要です。

正確にどのコードが影響を受けますかIFS

IFS幸い重要なシーンはそれほど多くありません。常に拡張子を引用してください):

  • "$*""${array[*]}"拡張
  • read組み込みターゲットのマルチ変数(read VAR1 VAR2 VAR3)または配列変数(read -a ARRAY_VAR_NAME)を呼び出します。
  • read先行/末尾の空白または空白以外の文字がある場合は、単一変数へのIFS呼び出しです。
  • 単語スプレー(例:引用されていない拡張、感染症のように避けたいかもしれません。)
  • その他のあまり一般的ではない状況(参照:IFS @グレッグウィキ)

答え4

これは実用的ですか?それとも本質的に意味がなく、将来の使用に必要な値にIFSをリセットする必要がありますか?

$' \t\n'あなたがしなければならないことはそれだけです。

OIFS=$IFS
do_your_thing
IFS=$OIFS

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

( IFS=:; do_your_thing; )

関連情報