web-dev-qa-db-ja.com

bashでパラメーターとして配列を渡す

配列をパラメータとしてbash関数に渡すにはどうすればよいですか?

注: Stack Overflowで答えが見つからなかった後、私はやや粗雑なソリューションを自分で投稿しました。渡される配列は1つのみで、パラメーターリストの最後の要素になります。実際には、配列を渡すのではなく、その要素のリストを呼び出しており、これらの要素はcalled_function()によって配列に再アセンブルされていますが、私にはうまくいきました。誰かがより良い方法を知っているなら、ここに自由に追加してください。

177
DevSolar

次のようなものを使用して引数として複数の配列を渡すことができます:

takes_ary_as_arg()
{
    declare -a argAry1=("${!1}")
    echo "${argAry1[@]}"

    declare -a argAry2=("${!2}")
    echo "${argAry2[@]}"
}
try_with_local_arys()
{
    # array variables could have local scope
    local descTable=(
        "sli4-iread"
        "sli4-iwrite"
        "sli3-iread"
        "sli3-iwrite"
    )
    local optsTable=(
        "--msix  --iread"
        "--msix  --iwrite"
        "--msi   --iread"
        "--msi   --iwrite"
    )
    takes_ary_as_arg descTable[@] optsTable[@]
}
try_with_local_arys

エコーします:

sli4-iread sli4-iwrite sli3-iread sli3-iwrite  
--msix  --iread --msix  --iwrite --msi   --iread --msi   --iwrite
206
Ken Bertelson

注:これは、Stack Overflowで答えを見つけられなかった後、私が投稿したやや粗雑なソリューションです。渡される配列は1つのみで、パラメーターリストの最後の要素になります。実際には、配列を渡すのではなく、その要素のリストを呼び出します。これらの要素は、called_function()によって配列に再アセンブルされますが、うまくいきました。やがてケンは彼の解決策を投稿しましたが、私はここに「歴史的」参照のために私のものを保管しました。

calling_function()
{
    variable="a"
    array=( "x", "y", "z" )
    called_function "${variable}" "${array[@]}"
}

called_function()
{
    local_variable="${1}"
    shift
    local_array=("${@}")
}

TheBonsaiによる改善、ありがとう。

83
DevSolar

Ken Bertelsonソリューションにコメントし、Jan Hettichに回答します。

使い方

try_with_local_arys()関数のtakes_ary_as_arg descTable[@] optsTable[@]行は以下を送信します。

  1. これは、実際にtakes_ary_as_arg関数にアクセス可能なdescTableおよびoptsTable配列のコピーを作成します。
  2. takes_ary_as_arg()関数はdescTable[@]optsTable[@]を文字列として受け取ります。つまり、$1 == descTable[@]$2 == optsTable[@]を意味します。
  3. takes_ary_as_arg()関数の先頭では、${!parameter}構文を使用します。この構文は indirect referenceまたは時々doublereference と呼ばれ、$1の値を使用する代わりに、$1expanded値の値を使用します。例:

    baba=booba
    variable=baba
    echo ${variable} # baba
    echo ${!variable} # booba
    

    $2も同様です。

  4. これをargAry1=("${!1}")に入れると、argAry1を配列(=に続く括弧)として、argAry1=("${descTable[@]}")に直接書き込むように、展開されたdescTable[@]で作成します。 declareは必要ありません。

NB:このブラケット形式を使用した配列の初期化は、IFSまたはInternal Field Separatorに従って新しい配列を初期化することに言及する価値があります。デフォルトではtabnewlineおよびspaceです。その場合、[@]表記を使用しているため、各要素は、引用されているかのように([*]とは反対に)それ自体で認識されます。

私の予約

BASHでは、ローカル変数スコープは現在の関数であり、そこから呼び出されるすべての子関数です。これは、takes_ary_as_arg()関数がdescTable[@]およびoptsTable[@]配列を「見る」という事実に変換されるため、動作します(上記の説明を参照)。

その場合、これらの変数自体を直接見てみませんか?そこに書くようなものです。

argAry1=("${descTable[@]}")

現在のIFSに従ってdescTable[@]配列の値をコピーする上記の説明を参照してください。

要約すれば

これは、本質的に、値では何も渡しません-通常どおり。

また、上記のデニス・ウィリアムソンのコメントを強調したい:sparse配列(すべてのキーが定義されていない配列-それらに「穴」がある)は期待どおりに動作しません-キーを失い、「凝縮する」 "配列。

そうは言っても、一般化の値はわかります。したがって、関数は名前を知らなくても配列(またはコピー)を取得できます。

  • 〜 "copys"の場合:この手法は十分に優れているため、インデックス(キー)がなくなったことに注意してください。
  • 実際のコピーの場合:キーにevalを使用できます。例:

    eval local keys=(\${!$1})
    

そして、それらを使用してコピーを作成するループ。注:ここでは!は以前の間接/二重評価ではなく、配列コンテキストでは配列インデックス(キー)を返します。

  • そして、もちろん、descTableおよびoptsTable文字列([@]なし)を渡す場合、evalで配列自体を(参照により)使用できます。配列を受け入れる汎用関数の場合。
37
TheWizard

ここでの基本的な問題は、配列を設計/実装したbash開発者が本当に失敗したということです。彼らは${array}${array[0]}の単なるショートハンドであると判断しましたが、これは悪い間違いでした。特に、${array[0]}には意味がなく、配列型が連想配列の場合は空の文字列と評価されると考える場合。

配列の割り当てはarray=(value1 ... valueN)という形式を取ります。値の構文は[subscript]=stringです。これにより、配列内の特定のインデックスに値を直接割り当てます。これにより、数値インデックスとハッシュインデックスの2種類の配列(bashの用語では連想配列と呼ばれる)が存在できるようになります。また、スパース数値インデックス配列を作成できるようにします。 [subscript]=部分を省くことは、数値インデックス配列の省略形であり、序数のインデックス0から始まり、割り当てステートメントの新しい値ごとに増加します。

したがって、${array}は、全体配列、インデックス、およびすべてに評価される必要があります。評価は、割り当てステートメントの逆になります。 3年生のCSメジャーはそれを知っているはずです。その場合、このコードは期待どおりに機能します。

declare -A foo bar
foo=${bar}

次に、値に配列を関数に渡し、ある配列を別の配列に割り当てると、シェルの残りの構文の指示どおりに機能します。しかし、彼らはこれを正しくしなかったので、代入演算子=は配列に対して機能せず、配列は値によって関数またはサブシェルに渡されたり、一般的に出力されたりすることはできません(echo ${array})すべてをかみ砕くコードなし。

したがって、正しく行われていれば、次の例は、bashの配列の有用性が大幅に向上することを示しています。

simple=(first=one second=2 third=3)
echo ${simple}

結果の出力は次のようになります。

(first=one second=2 third=3)

次に、配列は代入演算子を使用し、値によって関数や他のシェルスクリプトに渡すこともできます。ファイルに出力することで簡単に保存でき、ファイルからスクリプトに簡単にロードできます。

declare -A foo
read foo <file

残念ながら、私たちは他の最上級のbash開発チームに失望させられました。

そのため、関数に配列を渡すには、実際には1つのオプションしかありません。それは、nameref機能を使用することです。

function funky() {
    local -n ARR

    ARR=$1
    echo "indexes: ${!ARR[@]}"
    echo "values: ${ARR[@]}"
}

declare -A HASH

HASH=([foo]=bar [zoom]=fast)
funky HASH # notice that I'm just passing the Word 'HASH' to the function

次の出力になります。

indexes: foo zoom
values: bar fast

これは参照渡しであるため、関数内の配列に割り当てることもできます。はい、参照される配列はグローバルスコープを持っている必要がありますが、これはシェルスクリプトであることを考慮すると、それほど大きな問題ではありません。連想配列または疎なインデックス配列を値で関数に渡すには、すべてのインデックスと値を引数リスト(大きな配列の場合は役に立たない)に次のような単一の文字列としてスローする必要があります。

funky "${!array[*]}" "${array[*]}"

その後、関数内に一連のコードを記述して、配列を再構築します。

20
tigerand

DevSolarの答えには、私が理解できない1つの点があります(おそらく、そうする特別な理由があるのですが、考えられません):彼は、要素ごとに位置パラメータから反復配列を設定します。

より簡単な承認は

called_function()
{
  ...
  # do everything like shown by DevSolar
  ...

  # now get a copy of the positional parameters
  local_array=("$@")
  ...
}
5
TheBonsai
function aecho {
  set "$1[$2]"
  echo "${!1}"
}

$ foo=(dog cat bird)

$ aecho foo 1
cat
3
Steven Penny

パラメータとして複数の配列を渡す簡単な方法は、文字で区切られた文字列を使用することです。次のようにスクリプトを呼び出すことができます。

./myScript.sh "value1;value2;value3" "somethingElse" "value4;value5" "anotherOne"

その後、次のようにコードで抽出できます。

myArray=$1
IFS=';' read -a myArray <<< "$myArray"

myOtherArray=$3
IFS=';' read -a myOtherArray <<< "$myOtherArray"

この方法では、実際に複数の配列をパラメーターとして渡すことができ、最後のパラメーターである必要はありません。

2
Remy Cilia

いくつかのトリックを使用すると、実際に名前付きパラメーターを配列とともに関数に渡すことができます。

私が開発したメソッドを使用すると、次のような関数に渡されるパラメーターにアクセスできます。

testPassingParams() {

    @var hello
    l=4 @array anArrayWithFourElements
    l=2 @array anotherArrayWithTwo
    @var anotherSingle
    @reference table   # references only work in bash >=4.3
    @params anArrayOfVariedSize

    test "$hello" = "$1" && echo correct
    #
    test "${anArrayWithFourElements[0]}" = "$2" && echo correct
    test "${anArrayWithFourElements[1]}" = "$3" && echo correct
    test "${anArrayWithFourElements[2]}" = "$4" && echo correct
    # etc...
    #
    test "${anotherArrayWithTwo[0]}" = "$6" && echo correct
    test "${anotherArrayWithTwo[1]}" = "$7" && echo correct
    #
    test "$anotherSingle" = "$8" && echo correct
    #
    test "${table[test]}" = "works"
    table[inside]="adding a new value"
    #
    # I'm using * just in this example:
    test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct
}

fourElements=( a1 a2 "a3 with spaces" a4 )
twoElements=( b1 b2 )
declare -A assocArray
assocArray[test]="works"

testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..."

test "${assocArray[inside]}" = "adding a new value"

つまり、パラメーターを名前で呼び出すことができるだけでなく(より読みやすいコアを構成します)、実際に配列(および変数への参照-この機能はbash 4.3でのみ機能します)を渡すことができます!さらに、マッピングされた変数はすべてローカルスコープ内にあり、$ 1(およびその他)と同じです。

この機能を実現するコードは非常に軽く、bash 3とbash 4の両方で機能します(これらは私がテストした唯一のバージョンです)。 bashを使用した開発をより便利で簡単にするこのようなトリックに興味がある場合は、私の Bash Infinity Framework をご覧ください。以下のコードはその目的のために開発されたものです。

Function.AssignParamLocally() {
    local commandWithArgs=( $1 )
    local command="${commandWithArgs[0]}"

    shift

    if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]]
    then
        paramNo+=-1
        return 0
    fi

    if [[ "$command" != "local" ]]
    then
        assignNormalCodeStarted=true
    fi

    local varDeclaration="${commandWithArgs[1]}"
    if [[ $varDeclaration == '-n' ]]
    then
        varDeclaration="${commandWithArgs[2]}"
    fi
    local varName="${varDeclaration%%=*}"

    # var value is only important if making an object later on from it
    local varValue="${varDeclaration#*=}"

    if [[ ! -z $assignVarType ]]
    then
        local previousParamNo=$(expr $paramNo - 1)

        if [[ "$assignVarType" == "array" ]]
        then
            # passing array:
            execute="$assignVarName=( \"\${@:$previousParamNo:$assignArrLength}\" )"
            eval "$execute"
            paramNo+=$(expr $assignArrLength - 1)

            unset assignArrLength
        Elif [[ "$assignVarType" == "params" ]]
        then
            execute="$assignVarName=( \"\${@:$previousParamNo}\" )"
            eval "$execute"
        Elif [[ "$assignVarType" == "reference" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        Elif [[ ! -z "${!previousParamNo}" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        fi
    fi

    assignVarType="$__capture_type"
    assignVarName="$varName"
    assignArrLength="$__capture_arrLength"
}

Function.CaptureParams() {
    __capture_type="$_type"
    __capture_arrLength="$l"
}

alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\$@\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; '
alias @param='@trapAssign local'
alias @reference='_type=reference @trapAssign local -n'
alias @var='_type=var @param'
alias @params='_type=params @param'
alias @array='_type=array @param'
1
niieani

いですが、明示的に配列を渡すのではなく、配列に対応する変数を渡す限り機能する回避策があります。

function passarray()
{
    eval array_internally=("$(echo '${'$1'[@]}')")
    # access array now via array_internally
    echo "${array_internally[@]}"
    #...
}

array=(0 1 2 3 4 5)
passarray array # echo's (0 1 2 3 4 5) as expected

誰かがアイデアをより明確に実装できると確信していますが、これは配列を"{array[@]"}として渡し、array_inside=("$@")を使用して内部的にアクセスするよりも優れたソリューションであることがわかりました。他の位置/ getoptsname__パラメーターがある場合、これは複雑になります。これらの場合、最初にshiftname__と配列要素の削除の組み合わせを使用して、配列に関連付けられていないパラメーターを決定してから削除する必要がありました。

純粋主義者の観点では、このアプローチは言語の違反と見なされる可能性がありますが、実際的に言えば、このアプローチは私に多くの悲しみを救いました。関連するトピックでは、evalname__を使用して、内部的に構築された配列を、パラメーターに応じて名前が付けられた変数に割り当てますtarget_varname関数に渡します:

eval $target_varname=$"(${array_inside[@]})"

これが誰かを助けることを願っています。

1
Blake Schultze

受け入れられた答えに追加するだけですが、配列の内容が次のようになっているとうまく機能しないことがわかりました:

RUN_COMMANDS=(
  "command1 param1... paramN"
  "command2 param1... paramN"
)

この場合、配列の各メンバーは分割されるため、関数が認識する配列は次と同等です。

RUN_COMMANDS=(
    "command1"
    "param1"
     ...
    "command2"
    ...
)

このケースを機能させるには、変数名を関数に渡し、evalを使用する方法を見つけました。

function () {
    eval 'COMMANDS=( "${'"$1"'[@]}" )'
    for COMMAND in "${COMMANDS[@]}"; do
        echo $COMMAND
    done
}

function RUN_COMMANDS

ちょうど私の2©

1
AlvaroGMJ

これはスペースでも機能します:

format="\t%2s - %s\n"

function doAction
{
  local_array=("$@")
  for (( i = 0 ; i < ${#local_array[@]} ; i++ ))
    do
      printf "${format}" $i "${local_array[$i]}"
  done
  echo -n "Choose: "
  option=""
  read -n1 option
  echo ${local_array[option]}
  return
}

#the call:
doAction "${tools[@]}"
1
humbleSapiens

要件:配列内の文字列を検索する関数。
これは、DevSolarのソリューションをわずかに簡略化したもので、コピーではなく、渡された引数を使用します。

myarray=('foobar' 'foxbat')

function isInArray() {
  local item=$1
  shift
  for one in $@; do
    if [ $one = $item ]; then
      return 0   # found
    fi
  done
  return 1       # not found
}

var='foobar'
if isInArray $var ${myarray[@]}; then
  echo "$var found in array"
else
  echo "$var not found in array"
fi 
0
Andre