web-dev-qa-db-ja.com

bash関数の引数に参照渡しを使用する方法

「変数名」を関数に渡し、そのような「変数名」を含む変数に含まれる値を関数に変換させ、変換されたオブジェクトを元の「変数名」で参照できるようにしています。

たとえば、区切りリストを配列に変換する関数があり、「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
6
mackdoyle

説明

これを理解するには少し努力が必要です。我慢して。ソリューションは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
_

「内部」は「機能の内部」を意味し、「外部」はその反対を意味することを理解してください。

_$VarName_内の値は、文字列としての変数名__animal_list_です。

_${!VarName}_の値はリストであることが示されています:_anaconda, bison, cougar, dingo_

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

_echo a\=\(${!VarName}\)
_

これは、evalを含む次の行の実行内容を示しています。

_a=(anaconda  bison  cougar  dingo)
_

eval uatedになると、変数aは動物のリストを含む配列になります。この例では、変数aを使用して、評価がどのように影響するかを正確に示しています。

次に、aの各要素の値が_<in> val_として出力されます。
そして、関数の外側の部分でも同じことが_<out> val_として実行されます
それはこの2行に示されています:

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

実際の変更は関数の最後の評価で実行されたことに注意してください。
以上です。これで、varに値の配列があります。

実際、関数のコアは1行です:eval $VarName\=\(${!VarName}\)

また、IFSの値は関数に対してローカルに設定されるため、追加の作業なしで関数を実行する前の値に戻ります。元のアイデアに関するコメントを寄せてくれた Peter Cordes に感謝します。

これで説明は終わりです。明確にしてください。


実際の機能

不要な行をすべて削除してコアevalのみを残し、IFSの新しい変数のみを作成する場合、関数を最小限の式に減らします。

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

IFSの値をローカル変数として設定すると、関数の「デフォルト」値も設定できます。 IFSに必要な値が2番目の引数として関数に送信されない場合は常に、ローカルIFSは「デフォルト」値を取ります。デフォルトは space ()(これは常に有用な分割値です)、 colon (:)、 そしてその vertical line (|)。これらの3つはいずれも値を分割します。もちろん、デフォルトは、ニーズに合う他の値に設定できます。

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")><./*><*><*>
_

上記の変数_animal_list_に設定された値のほとんどは、evalで失敗します。
しかし、問題なく読み取りを渡します。

  • 注:関数を呼び出す直前にvarsの値がプレーンテキスト値に設定されているため、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は関数内でのみ設定され、永続的に変更されるわけではないため、古い値に再設定する必要はありません。さらに、関数の2番目の呼び出し "people_list"はIFSのデフォルト値を利用するため、2番目の引数を設定する必要はありません。


"ここにドラゴンがある"¯\ _(ツ)_ /¯


警告01:

(eval)関数が作成されたので、varが引用されずにシェル解析に公開される場所が1つあります。これにより、IFS値を使用して「単語分割」を実行できます。しかし、それはvarsの値を公開します(引用符でそれを妨げない限り)。注文。そして、それをサポートするシステムでのプロセス置換<() >()

それぞれの例(最後を除く)は、この単純なエコーに含まれています(注意してください)。

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

つまり、_{~$`<>_で始まるか、ファイル名と一致するか、または_?*[]_を含む文字列は、潜在的な問題です。

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

警告02:

はい、readには、独自の"dragons"が付属しています。

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

警告や読み取りの問題を含めるつもりはありませんでしたが、一般的なリクエストにより、申し訳ありませんでした。

8
user79743

The Bash FAQは、参照/間接参照による呼び出しに関するエントリ全体を持っています

単純なケースでは、他の回答で提案されているevalのより良い代替手段であり、引用符muchがより簡単になります。

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

Bash-completion (タブを押したときに実行されるコード)は、その内部関数がevalではなくprintf -vに切り替わりました。

配列を返す場合、 Bash 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のmanページの表現は少し混乱していることに注意してください。これは、-n属性を配列変数に適用できないことを示しています。つまり、参照の配列は使用できませんが、配列への参照を使用できます

8
Peter Cordes

関数の内容のみを渡すため、関数内の変数(この場合は配列)を変更することはできません。関数は、渡された変数を認識していません。

回避策として、変数のnameを渡し、関数の内部でfunction evaluateを使用してコンテンツを取得できます。

#!/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
3
jimmij
_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はシェルの現在の_$0_であり、_arr@yname_setSplit()の最初の引数でした。私はそれを呼んだ、そしてそれはそのメッセージを書いた。

これは2つの関数でもあります。したがって、呼び出し側はisName()関数をまったく変更せずに、その裁量でsetSplit()のテストを動的に再定義できます。

また、シェルのファイル名生成グロブを安全に無効化して、分割中の不注意による展開を防ぎます。デフォルトでは、引数に文字_[*?_が含まれている場合にそうなる可能性があります。シェルオプションを返す前に、シェルオプションを復元したときに、それらを見つけた状態に変更した可能性があります。つまり、シェルのファイル名のグロビングを有効または無効にして呼び出すことができ、その設定に影響を与えることはありません。 。

ただし、ここで欠落している重要な点があります。_$IFS_は構成されていません。 isName()関数は、bashパターン内のPOSIXブラケット式の内容に_$IFS_を適用するというかなり警戒すべきcaseバグの回避策を実装します(真剣に:一体何ですか?単一の自己再帰呼び出しでローカル_$IFS_を無効にし、グローバル値が戻っていない場合。しかし、それは配列の分割と完全に直交しています。それ以外の場合、setSplit()は_$IFS_を使用して何も実行しません。そして、それは本来あるべき姿です。そのようにする必要はありません。

callerは次のように設定する必要があります。

_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_値を設定します。

POSIXly:

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

...同じ目的を果たします。違いは、特別な組み込み関数や関数の永続的な環境に関するbashの標準との違いにあります。それ以外の場合は、コマンドラインで設定された変数が現在のシェル環境に影響を与えることを指定します(したがって、どちらの方法でも使用できるという点で推奨されます)

好みが何であれ、重要なのは、発信者がここでショットを呼び出し、着信者がシュートするだけです。

0
mikeserv