単一のステートメントにIFSを設定する


42

単一のコマンド/ビルトインのスコープに対してカスタムIFS値を設定できることを知っています。単一のステートメントにカスタムIFS値を設定する方法はありますか?明らかにそうではありません、これに基づいてグローバルIFS値は以下に基づいて影響を受けるためです

#check environment IFS value, it is space-tab-newline
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003
#invoke built-in with custom IFS
IFS=$'\n' read -r -d '' -a arr <<< "$str"
#environment IFS value remains unchanged as seen below
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003

#now attempt to set IFS for a single statement
IFS=$'\n' a=($str)
#BUT environment IFS value is overwritten as seen below
printf "%s" "$IFS" | od -bc
0000000 012
         \n
     0000001

回答:


39

いくつかのシェル(を含むbash):

IFS=: command eval 'p=($PATH)'

(でbashcommandsh / POSIXエミュレーションにない場合は省略できます)。ただし、引用符で囲まれていない変数を使用する場合はset -f、一般的にも必要であり、ほとんどのシェルではそのためのローカルスコープがないことに注意してください。

zshを使用すると、次のことができます。

(){ local IFS=:; p=($=PATH); }

$=PATHデフォルトでは行われない単語分割を強制することですzsh(変数展開時のグロビングも行われないためset -f、shエミュレーション以外では必要ありません)。

(){...}(またはfunction {...})は匿名関数と呼ばれ、通常はローカルスコープを設定するために使用されます。関数でローカルスコープをサポートする他のシェルでは、次のようなことを行うことができます。

e() { eval "$@"; }
e 'local IFS=:; p=($PATH)'

POSIXシェルで変数とオプションのローカルスコープを実装するには、https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.shで提供されている関数を使用することもできます。その後、次のように使用できます。

. /path/to/locvar.sh
var=3,2,2
call eval 'locvar IFS; locopt -f; IFS=,; set -- $var; a=$1 b=$2 c=$3'

(ところで、他のシェルの$PATH場合を除き、上記の方法で分割することは無効ですzsh。IFSはフィールド区切り文字ではなくフィールド区切り文字です)。

IFS=$'\n' a=($str)

ちょうど2つの割り当てで、次のように次々に割り当てられa=1 b=2ます。

以下に関する説明のメモvar=value cmd

に:

var=value cmd arg

シェルの実行/path/to/cmd、新しいプロセスやパスでcmdargargv[]var=valueenvp[]。これは実際には変数の割り当てではなく、実行されたコマンドに環境変数を渡すことです。BourneシェルまたはKornシェルではset -k、と書くこともできますcmd var=value arg

現在、それは実行されない組み込み関数または関数には適用されません。Bourneシェルでは、でvar=value some-builtin、単独でのvar場合と同様に、後で設定されvar=valueます。これは、たとえば、var=value echo foo(役に立たない)の動作echoが組み込みかどうかによって異なることを意味します。

POSIXおよび/またはそれをksh変更しました。Bourneの動作は、特別なbuiltinsと呼ばれるビルトインのカテゴリでのみ発生します。eval特別なビルトインであり、そうでreadはありません。特別なビルトインでvar=value builtinvarない場合は、ビルトインの実行のみを設定します。これにより、外部コマンドが実行されているときと同様に動作します。

このcommandコマンドを使用して、これらの特別なビルトインの特別な属性を削除できます。POSIXが見落としているのは、とビルトインの場合、シェルは変数スタックを実装する必要があることを意味します(コマンドまたはスコープ制限コマンドを指定していなくても)。eval.localtypeset

a=0; a=1 command eval 'a=2 command eval echo \$a; echo $a'; echo $a

あるいは:

a=1 command eval myfunction

myfunction使用または設定し$a、潜在的にを呼び出す関数であるcommand eval

ksh(仕様の大部分が基づいている)それを実装していなかった(そしてAT&T kshzshまだ実装していない)ので、それは本当に見過ごされましたが、今日、これら2つを除いて、ほとんどのシェルが実装しています。動作はシェルによって異なりますが、次のようなものです。

a=0; a=1 command eval a=2; echo "$a"

しかし。localそれをサポートするシェルで使用することは、ローカルスコープを実装するより信頼性の高い方法です。


奇妙なことに、POSIXで義務付けられているように、ksh 93uではなく、ダッシュ、pdksh、およびbash の期間だけIFS=: command eval …設定します。kshが奇妙な非準拠のone-outであるのは珍しいことです。IFSeval
ジル 'SO-悪であるのをやめる'

12

KernighanとPikeによる「The Unix Programming Environment」から取った標準の保存と復元:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}

2
ありがとう、+ 1。はい、私はこのオプションを知っていますが、私が意味することを知っているなら、「クリーナー」オプションがあるかどうか知りたいです
-iruvar

セミコロンで1行にジャムすることもできますが、それはきれいだとは思いません。表現したいものすべてに特別な構文サポートがあればいいかもしれませんが、おそらくコーディングの代わりに大工仕事またはsumptinを学ぶ必要があります;)
msw

9
$IFS以前に設定解除されていた場合、正しく復元できません。
ステファンシャゼラス

2
それは、設定を解除して、bashの扱いだ場合$'\t\n'' ':として、ここで説明し、wiki.bash-hackers.org/syntax/expansion/...
ダヴィデ

2
@davideになります$' \t\n'。スペースが最初に使用される必要があり"$*"ます。すべてのBourneのようなシェルで同じであることに注意してください。
ステファンシャゼル

8

スクリプトを関数に入れ、その関数を呼び出してコマンドライン引数を渡します。IFSはローカルに定義されているため、これを変更してもグローバルIFSには影響しません。

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"

6

このコマンドの場合:

IFS=$'\n' a=($str)

別の解決策があります:最初の割り当て(IFS=$'\n')に実行するコマンド(関数)を与えるには:

$ split(){ a=( $str ); }
$ IFS=$'\n' split

これにより、IFSが環境にスプリットを呼び出すようになりますが、現在の環境では保持されません。

これにより、常に危険を伴うevalの使用も回避されます。


ksh93とmksh、およびPOSIXモードの場合のbashとzshでは、POSIXの必要に応じて$IFS設定が$'\n'その後のままになります。
ステファンシャゼル

4

@helpermethodから提案された答えは、確かに興味深いアプローチです。しかし、BASHではローカル変数のスコープが呼び出し側から呼び出された関数にまで及ぶため、これもちょっとした落とし穴です。したがって、main()でIFSを設定すると、その値はmain()から呼び出された関数に保持されます。以下に例を示します。

#!/usr/bin/env bash
#
func() {
  # local IFS='\'

  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local f_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#f_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${f_args[$i]}  "
  done
  echo
}

main() {
  local IFS='/'

  # the rest goes here
  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local m_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#m_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${m_args[$i]}  "
  done
  echo

  func "${m_args[*]}"
}

main "$@"

そして出力...

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick  [1]: blick  [2]: flick

main()で宣言されたIFSがfunc()のスコープ内になかった場合、配列はfunc()で適切に解析されなかったでしょう。

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick/blick/flick

IFSが範囲外になった場合は、これを取得する必要があります。

私見よりもはるかに優れたソリューションは、グローバル/ローカルレベルでIFSの変更や依存を控えることです。代わりに、新しいシェルを作成し、そこでIFSをいじります。たとえば、次のようにmain()でfunc()を呼び出す場合、バックスラッシュのフィールド区切り文字を含む文字列として配列を渡します。

func $(IFS='\'; echo "${m_args[*]}")

... IFSへの変更はfunc()には反映されません。配列は文字列として渡されます:

ick\blick\flick

...しかし、func()の内部では、func()でローカルに変更されない限り、IFSは「/」(main()で設定)のままです。

IFSへの変更の分離に関する詳細については、次のリンクをご覧ください。

bash配列変数を改行で区切られた文字列に変換するにはどうすればよいですか?

IFSで配列にbash文字列

一般的なシェルスクリプトプログラミングのヒント-「サブシェルの使用に注意してください...」


...確かに面白い
iruvar

「IFSを使用して配列にバッシュ文字列を追加」IFS=$'\n' declare -a astr=(...)完璧な感謝!
アクエリアスパワー14年

1

質問からのこのスニペット:

IFS=$'\n' a=($str)

は、左から右に評価される2つの個別のグローバル変数割り当てとして解釈され、次と同等です。

IFS=$'\n'; a=($str)

または

IFS=$'\n'
a=($str)

これは、グローバルIFSが変更された理由と、$str配列要素への単語分割がの新しい値を使用して実行された理由の両方を説明していますIFS

サブシェルを使用して、次のIFSように変更の効果を制限したい場合があります。

str="value 0:value 1"
a=( old values )
( # Following code runs in a subshell
 IFS=":"
 a=($str)
 printf 'Subshell IFS: %q\n' "${IFS}"
 echo "Subshell: a[0]='${a[0]}' a[1]='${a[1]}'"
)
printf 'Parent IFS: %q\n' "${IFS}"
echo "Parent: a[0]='${a[0]}' a[1]='${a[1]}'"

しかし、変更はaサブシェルに限定されていることにすぐに気付くでしょう。

Subshell IFS: :
Subshell: a[0]='value 0' a[1]='value 1'
Parent IFS: $' \t\n'
Parent: a[0]='old' a[1]='values'

次に、@ mswによるこの前の回答からのソリューションを使用してIFSを保存または復元するか、@ helpermethodによって提案さlocal IFSた関数の内部を使用してみてください。しかし、すぐに、あなたはあらゆる種類のトラブルに直面していることに気づきます。特に、あなたがライブラリーの作者であり、呼び出しスクリプトの誤動作に対して頑健である必要がある場合:

  • IFS最初に設定が解除された場合はどうなりますか?
  • set -u(別名set -o nounset)で実行している場合はどうなりますか?
  • IFS経由で読み取り専用にされた場合はどうなりdeclare -r IFSますか?
  • 再帰や非同期実行(trapハンドラーなど)で動作するために保存/復元メカニズムが必要な場合はどうなりますか?

IFSを保存/復元しないでください。代わりに、一時的な変更に固執します。

  • 変数の変更を単一のコマンド、組み込みまたは関数呼び出しに制限するには、を使用しますIFS="value" command

    • 特定の文字で分割して複数の変数を読み込むには(:以下の例として使用)、次を使用します。

      IFS=":" read -r var1 var2 <<< "$str"
    • 配列に読み込むには(の代わりにこれを行いますarray_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • 変数を変更する効果をサブシェルに制限します。

    • コンマで区切られた配列の要素を出力するには:

      (IFS=","; echo "${array[*]}")
    • それを文字列にキャプチャするには:

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

0

最も簡単な解決策は$IFS、たとえばmswの答えのように、オリジナルのコピーを取ることです。ただし、このソリューションでは、多くのアプリケーションで重要な、空の文字列に等しいセットと未セットIFSを区別しませんIFS。この違いを捉えるより一般的なソリューションを次に示します。

# Functions taking care of IFS
set_IFS(){
    if [ -z "${IFS+x}" ]; then
        IFS_ori="__unset__"
    else
        IFS_ori="$IFS"
    fi
    IFS="$1"
}
reset_IFS(){
    if [ "${IFS_ori}" == "__unset__" ]; then
        unset IFS
    else
        IFS="${IFS_ori}"
    fi
}

# Example of use
set_IFS "something_new"
some_program_or_builtin
reset_IFS
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.