説明する

説明する

「var name」を関数に渡し、関数が「var name」を使用して変数に含まれる値を変換し、変換されたオブジェクトを元の「var name」として参照できるようにします。

たとえば、区切りリストを配列に変換する関数があり、「animal_list」という区切りリストがあるとします。リスト名を関数に渡し、現在の配列を「animal_list」として参照して、リストを配列に変換したいと思います。

コード例:

function delim_to_array() {
  local list=$1
  local delim=$2
  local oifs=$IFS;

  IFS="$delim";
  temp_array=($list);
  IFS=$oifs;

  # Now I have the list converted to an array but it's 
  # named temp_array. I want to reference it by its 
  # original name.
}

# ----------------------------------------------------

animal_list="anaconda, bison, cougar, dingo"
delim_to_array ${animal_list} ","

# After this point I want to be able to deal with animal_name as an array.
for animal in "${animal_list[@]}"; do 
  echo "NAME: $animal"
done

# And reuse this in several places to converted lists to arrays
people_list="alvin|baron|caleb|doug"
delim_to_array ${people_list} "|"

# Now I want to treat animal_name as an array
for person in "${people_list[@]}"; do 
  echo "NAME: $person"
done

答え1

説明する

これを理解するには少し努力が必要です。忍耐を持ってください。このソリューションはbashでうまく機能します。 「バシム」が必要です。

まず、変数への「間接」アクセスを使用する必要があります${!variable}$variable文字列が含まれている場合は、animal_name「パラメータ拡張」:${!variable}に展開されます$animal_name

このアイデアが実際に実行されている様子を見てみましょう。わかりやすくするために、使用した名前と値を最大限に保ちました。

#!/bin/bash

function delim_to_array() {
    local VarName=$1

    local IFS="$2";
    printf "inside  IFS=<%s>\n" "$IFS"

    echo "inside  var    $VarName"
    echo "inside  list = ${!VarName}"

    echo a\=\(${!VarName}\)
    eval a\=\(${!VarName}\)
    printf "in  <%s> " "${a[@]}"; echo

    eval $VarName\=\(${!VarName}\)
}

animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","

printf "out <%s> " "${animal_list[@]}"; echo
printf "outside IFS=<%s>\n" "$IFS"

# Now we can use animal_name as an array
for animal in "${animal_list[@]}"; do
    echo "NAME: $animal"
done

フルスクリプトを実行すると(so-setvar.shと仮定)、次のようになります。

$ ./so-setvar.sh
inside  IFS=<,>
inside  var    animal_list
inside  list = anaconda, bison, cougar, dingo
a=(anaconda  bison  cougar  dingo)
in  <anaconda> in  <bison> in  <cougar> in  <dingo> 
out <anaconda> out <bison> out <cougar> out <dingo> 
outside IFS=< 
>
NAME: anaconda
NAME: bison
NAME: cougar
NAME: dingo

「内部」は「関数の内部」を意味し、「外部」はその逆を意味することを理解してください。

内部値は文字列の$VarNamevar:の名前です。animal_list

値は${!VarName}リストとして表示されます。anaconda, bison, cougar, dingo

これで、ソリューションがどのように構築されたかを示すために、echoを含む行があります。

echo a\=\(${!VarName}\)

eval次の行が何をするかを示します。

a=(anaconda  bison  cougar  dingo)

一度評価するはい、変数はa動物のリストを含む配列です。この場合、var a は、eval がどのように影響するかを正確に示すために使用されます。

その後、各要素の値がa印刷されます<in> val。そして、次の2行に示す
ように、関数の外側の部分でも同じことを行います。<out> val

in  <anaconda> in  <bison> in  <cougar> in  <dingo>
out <anaconda> out <bison> out <cougar> out <dingo>

実際の変更は、関数の最後の評価で行われます。
それはすべてです。 varには現在値配列があります。

実際、この関数の中心は1行です。eval $VarName\=\(${!VarName}\)

さらに、IFSの値は関数のローカル値に設定されているため、追加の操作なしで関数が実行される前の値に戻ることができます。ありがとうピーター・コルデス独創的なアイデアについての意見です。

これで説明が終わりました。明確になりますように。


実際の機能

不要な行をすべて削除し、コア評価のみを残し、IFSの新しい変数のみを作成すると、関数を最小限の表現に減らします。

delim_to_array() {
    local IFS="${2:-$' :|'}"
    eval $1\=\(${!1}\);
}

IFS値をローカル変数に設定すると、関数の「デフォルト」値を設定することもできます。 IFSが要求する値が2番目のパラメーターとして関数に送信されない限り、ローカルIFSは「デフォルト」値を想定します。デフォルトはspace()(常に有用な分割値です)、colon(:)、vertical line(|)でなければならないと思います。これら3つのうちの1つは値を分割します。もちろん、デフォルト値は必要に応じて異なる値に設定できます。

使用するように編集read:

evalで参照されていない値のリスクを減らすために、次のものを使用できます。

delim_to_array() {
    local IFS="${2:-$' :|'}"
    # eval $1\=\(${!1}\);
    read -ra "$1" <<<"${!1}"
}

test="fail-test"; a="fail-test"

animal_list='bison, a space, {1..3},~/,${a},$a,$((2+2)),$(echo "fail"),./*,*,*'

delim_to_array "animal_list" ","
printf "<%s>" "${animal_list[@]}"; echo

$ so-setvar.sh
<bison>< a space>< {1..3}><~/><${a}><$a><$((2+2))><$(echo "fail")><./*><*><*>

上記のvarに設定されたほとんどの値はanimal_listevalで失敗します。
しかし、問題なく読んでください。

  • 注:evalオプションを試してみるのは完全に安全です。このコードではこれは、関数を呼び出す前に変数値がプレーンテキスト値に設定されているためです。実際に実行されても、それは単なるテキストです。パス名拡張が最後の拡張であるため、ファイル名が間違っても問題は発生しないため、パス名拡張では変数拡張は再実行されません。また、現状のコードこれは決して汎用的な検証ではありませんeval

はい

この関数の機能と仕組みを実際に理解するために、この関数を使用して公開したコードを書き直しました。

#!/bin/bash

delim_to_array() {
        local IFS="${2:-$' :|'}"
        # printf "inside  IFS=<%s>\n" "$IFS"
        # eval $1\=\(${!1}\);
        read -ra "$1" <<<"${!1}";
}

animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","
printf "NAME: %s\t " "${animal_list[@]}"; echo

people_list="alvin|baron|caleb|doug"
delim_to_array "people_list"
printf "NAME: %s\t " "${people_list[@]}"; echo

$ ./so-setvar.sh
NAME: anaconda   NAME:  bison    NAME:  cougar   NAME:  dingo    
NAME: alvin      NAME: baron     NAME: caleb     NAME: doug      

ご覧のとおり、IFSは関数内でのみ設定され、永久に変更されないため、以前の値にリセットする必要はありません。さらに、「people_list」関数への2番目の呼び出しは、2番目のパラメータを設定せずにIFSのデフォルト値を利用します。


«ここに龍がある» ́\_(ツ)_/́


警告01:

(eval)関数を設定すると、varが引用符なしでシェル解析に公開される点があります。これにより、IFS値を使用して「単語の分割」を完了できます。しかし、これはまた、varの値を「中括弧拡張」、「チルダ拡張」、「引数、変数および算術拡張」、「コマンド置換」、および「パス名拡張」に公開します。ここでコマンドは次のようになります。<() >()これをサポートするシステムでプロセス交換を実行します。

すべての例(最後の例を除く)は、次の単純なエコーでラップされます(注意してください)。

 a=failed; echo {1..3} ~/ ${a} $a $((2+2)) $(ls) ./*

つまり、ファイル{~$`<>名で始まる、ファイル名と一致する、またはファイル名を含めることができる文字列は、潜在的な問題になる?*[]可能性があります。

変数にそのような問題のある値が含まれていないと確信している場合は安全です。そのような値を持つことが可能であれば、あなたの質問に答える方法はより複雑で、より多くの(またはより長い)説明と説明が必要です。使用はread代替品です。

警告02:

はい、独自のread「ドラゴン」があります。

  • 常に-rオプションを使用してください。このオプションが不要な状況は考えられません。
  • このreadコマンドは1行だけインポートできます。オプションを設定しても複数行を使用するには-d特別な注意が必要です。または、入力全体を変数に割り当てます。
  • 値にスペースが含まれている場合は、先行スペースと末尾のIFSスペースが削除されます。まあ、完全な説明にはそれに関するいくつかの詳細を含める必要がありますtabが、省略します。
  • パイプを介して|データを読み取らないでください。これにより、サブシェルから読み取りが行われます。子シェルに設定されたすべての変数は、親シェルに戻っても保持されません。まあ、いくつかの回避策がありますが、再び詳細はスキップします。

もともと読書に関する警告や問題を含めたくはありませんでしたが、一般のニーズに合わせて含める必要がありました。申し訳ありません。

答え2

Bash FAQには、参照/間接呼び出しの完全なエントリがあります。

簡単な場合、これはeval引用を作成する他の答えで提案するより良い選択肢です。たくさん簡単になりました。

func() {  # set the caller's simple non-array variable
    local retvar=$1
    printf -v "$retvar"  '%s ' "${@:2}"  # concat all the remaining args
}

バッシュ完成(Tabを押したときに実行されるコード)は、読みやすく、潜在的に高速なため、内部機能の代わりにprintf -v切り替えられました。eval

配列を返すにはバッシュFAQread -a配列変数を読み取るには、順次配列インデックスを使用することをお勧めします。

# Bash
aref=realarray
IFS=' ' read -d '' -ra "$aref" <<<'words go into array elements'

Bash 4.3には、参照で呼び出すことをより便利にする機能が導入されました。 Bash 4.3はまだ非常に新しいバージョンです(2014)。

func () { # return an array in a var named by the caller
    typeset -n ref1=$1   # ref1 is a nameref variable.
    shift   # remove the var name from the positional parameters
    echo "${!ref1} = $ref1"  # prints the name and contents of the real variable
    ref1=( "foo" "bar" "$@" )  # sets the caller's variable.
}

Bashのマニュアルページの表現は少し混乱しています。-n配列変数には属性を適用できないと出てきます。これは、参照配列を持つことはできませんが、参照は持つことができることを意味します。到着ソート。

答え3

内容だけを渡すため、関数内で変数(またはこの場合は配列)を変更することはできません。関数はどの変数が渡されたかを知ることができません。

回避策として、以下を通過できます。名前変数の値を関数内で使用してeval内容を取得します。

#!/bin/bash 

function delim_to_array() {
  local list=$1
  local delim=$2
  local oifs=$IFS;

  IFS="$delim"
  temp_array=($(eval echo '"${'"$list"'}"'))
  IFS=$oifs;

  eval "$list=("${temp_array[@]}")"            
}                                             

animal_list="anaconda, bison, cougar, dingo"
delim_to_array "animal_list" ","
printf "NAME: %s\n" "${animal_list[@]}"

people_list="alvin|baron|caleb|doug"
delim_to_array "people_list" "|"
printf "NAME: %s\n" "${people_list[@]}"

eval使用された行内の引用符に細心の注意を払ってください。式の一部は一重引用符で囲み、他の部分は二重引用符で囲む必要があります。また、最終印刷では、ループをforより簡単なコマンドに置き換えました。printf

出力:

NAME: anaconda
NAME: bison
NAME: cougar
NAME: dingo
NAME: alvin
NAME: baron
NAME: caleb
NAME: doug

答え4

function delim_to_array() {
  local list=$1
  local delim=$2
  local oifs=$IFS;

  IFS="$delim";
  temp_array=($list);
  IFS=$oifs;
}

したがって、この機能を使用すると、非常に簡単な詳細をスキップしているようです。呼び出し受信者が繰り返し処理を実行し、呼び出し側がショットを呼び出すと、常に簡単になります。この関数では、呼び出し受信者にすべての呼び出しを実行させました。名前をこのように処理しないでください。

isName()
    case   "${1##[0-9]*}"   in
    (${IFS:+*}|*[!_[:alnum:]]*)
    IFS= "${IFS:+isName}" "$1"|| ! :
    esac  2>/dev/null

setSplit(){
   isName "$1" ||
   set "" "setSplit(): bad name: '$1'"
   eval   "shift; set -f
           ${1:?"$2"}=(\$*)
           set +f -$-"
}

これは配列名の妥当性を安全にチェックし、stderrから意味のあるエラー出力を生成し、無効な引数で呼び出されたときに適切に終了を停止します。エラー出力は次のとおりです。

bash: 1: setSplit(): bad name: 'arr@yname'

...どこbashはシェルの現在の値です$0arr@yname私がそれを呼び出してメッセージを書くとき、それはsetSplit()最初のパラメータです。

また、2つの関数なので、呼び出し側はisName()関数を変更することなく独自の判断でテストを動的にオーバーライドできますsetSplit()

また、分割時に誤って拡張されるのを防ぐために、シェルファイル名の生成globを安全に無効にします。これは、引数に文字が含まれている場合にデフォルトで発生する可能性があります[*?。返す前に、すべてのシェルオプションを復元して、そのオプションを検出時の状態に変更できます。つまり、シェルファイル名のグロービングを有効または無効にして呼び出すことができ、返すだけでなく、この設定にも影響します。

しかし、ここには1つの重要な事項がありません。つまり、$IFS構成はありません。この関数は、パターンのPOSIX角括弧式の内容に適用されるisName()かなり懸念されるbashバグの回避策を実装します。$IFScase(真剣に:一体何ですか?)$IFS返す前にグローバル値が存在しない場合は、個別の自己再帰呼び出しでローカル値をキャンセルします。しかし、これは配列分割と完全に直交しています。それ以外の場合はsetSplit()何もしません$IFS。これがまさにその方法です。そうする必要はありません。

これ訪問者次のように設定する必要があります。

IFS=aBc setSplit arrayname 'xyzam*oBabc' x y z
printf '<%q>\n' "$IFS" "${arrayname[@]}"

<$' \t\n'>
<xyz>
<m\*o>
<''>
<b>
<''>
<x>
<y>
<z>

上記のコードは、呼び出された関数のローカル値をbash設定して$IFSシェルで機能します。

POSIX的に:

IFS=aBc command eval "setSplit arrayname 'xyzam*oBabc' x y z"

...同じ目的を達成します。違いは、bash特殊な組み込み機能と関数の永続環境に関する標準に違反していることです。それ以外の場合は、コマンドラインに設定されている変数が現在のシェル環境に影響を与える必要があることを指定します。(どちらでも入手できるのでこれが望ましいでしょう)

何が望ましいのかは、発信者がここでショットを呼び出し、受信者が単にショットを実行することです。

関連情報