Passaggio di matrici come parametri in bash


188

Come posso passare un array come parametro a una funzione bash?

Nota: dopo non aver trovato una risposta qui su Stack Overflow, ho pubblicato la mia soluzione un po 'rozza. Consente il passaggio di un solo array ed è l'ultimo elemento dell'elenco dei parametri. In realtà, non sta affatto passando l'array, ma un elenco dei suoi elementi, che vengono riassemblati in un array da called_function (), ma ha funzionato per me. Se qualcuno conosce un modo migliore, sentiti libero di aggiungerlo qui.


1
Qui hai un bel riferimento e tonnellate di esempi.
Artem Barger,

16
Errr ... Tre voti negativi su una domanda di cinque anni nello stesso minuto?
DevSolar,

Risposte:


220

È possibile passare più array come argomenti usando qualcosa del genere:

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

farà eco:

sli4-iread sli4-iwrite sli3-iread sli3-iwrite  
--msix  --iread --msix  --iwrite --msi   --iread --msi   --iwrite

Modifica / note: (dai commenti sotto)

  • descTablee optsTablevengono passati come nomi e vengono espansi nella funzione. Pertanto non $è necessario se indicato come parametro.
  • Si noti che funziona ancora anche con descTableetc definito conlocal , Poiché i locali sono visibili alle funzioni che chiamano.
  • In !in ${!1}espande la variabile arg 1.
  • declare -a rende esplicito l'array indicizzato, non è strettamente necessario.

14
Una cosa da notare è che se l'array originale è scarso, l'array nella funzione di ricezione non avrà gli stessi indici.
In pausa fino a nuovo avviso.

13
Questo è geniale, ma Ken o qualcuno possono spiegare un paio di cose che mi sconcertano sul perché funziona: 1 - Avrei pensato che descTable e optsTable avrebbero dovuto essere preceduti da $ quando passati come argomenti di funzione. 2 - Nella prima riga di "take ...", perché è necessaria una dichiarazione esplicita di array? 3 - E cosa fa il! significa nell'espressione $ {! 1} e perché [@] non è richiesto o addirittura consentito lì? - Funziona e tutti questi dettagli sembrano necessari sulla base dei miei test, ma vorrei capire perché!
Jan Hettich,

8
1: descTable e optsTable sono appena passati come nomi, quindi non ci sono $, devono essere espansi solo nella funzione chiamata 2: non del tutto sicuro, ma penso che non sia davvero necessario 3: il! viene utilizzato perché i parametri passati alla funzione devono essere espansi due volte: $ 1 si espande in "descTable [@]" e deve essere espanso in "$ {descTable [@]}". La sintassi $ {! 1} fa proprio questo.
Elmar Zander,

8
Non penso che la parte "dichiarare -a" sia necessaria. L'esistenza di parentesi definisce già l'LHS dell'assegnazione come una matrice.
Erik Aronesty,

3
Questa risposta mi ha aiutato a risolvere un problema proprio ora. Tuttavia, ho voluto sottolineare che sulla mia macchina (usando bash 4.3.42) "$ {! 1}" e "$ {! 2}" devono avere le virgolette rimosse. In caso contrario, il valore dell'array originale viene letto come una stringa e assegnato rispettivamente a argAry1 [0] e argAry2 [0], il che significa sostanzialmente che la struttura dell'array viene persa.
user.friendly

85

Nota: questa è la soluzione un po 'rozza che mi sono postato, dopo aver trovato una risposta qui su StackTranslate.it. Consente il passaggio di un solo array ed è l'ultimo elemento dell'elenco dei parametri. In realtà, non sta affatto passando l'array, ma un elenco dei suoi elementi, che vengono riassemblati in un array da called_function (), ma ha funzionato per me. Poco dopo Ken pubblicò la sua soluzione, ma io la tenni qui per riferimento "storico".

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

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

Migliorato da TheBonsai, grazie.


19
Tre anni dopo, questa risposta, mantenuta solo per ragioni storiche, ha ricevuto due downvotes entro un paio di giorni. Come al solito triste su SO, senza alcuna nota sul perché la gente pensa che questo sia giustificato. Nota che questa risposta precede tutte le altre e che ho accettato la risposta di Ken come la migliore soluzione. Sono perfettamente consapevole che non è affatto perfetto, ma per quattro mesi è stato il migliore disponibile su SO. Il motivo per cui dovrebbe essere sottovalutato due anni dopo aver ottenuto il secondo posto alla soluzione perfetta di Ken è oltre me.
DevSolar,

@geirha: ti chiedo di verificare chi ha pubblicato la domanda, chi ha pubblicato questa risposta e chi probabilmente ha accettato la risposta che stai chiamando "cattiva". ;-) Potresti anche voler controllare la Nota nella domanda, che indica perché questa soluzione è inferiore a quella di Ken.
DevSolar,

2
So che hai posto la domanda, hai scritto questa risposta e che hai accettato la risposta negativa. Ecco perché l'ho scritto in questo modo. Il motivo per cui la risposta accettata è errata è perché sta provando a passare l'array per riferimento, cosa che dovresti davvero evitare. Inoltre, l'esempio combina più argomenti in una singola stringa. Se hai davvero bisogno di passare array per riferimento, bash è la lingua sbagliata per cominciare. Anche con le nuove variabili nameref di bash 4.3, non è possibile evitare in modo sicuro collisioni di nomi (riferimento circolare).
geirha,

4
Bene, puoi passare più array se includi il numero di elementi di ciascun array. called_function "${#array[@]}" "${array[@]}" "${#array2[@]}" "${array2[@]}"ecc ... ancora con alcune ovvie restrizioni, ma in realtà è meglio risolvere il problema in un modo supportato dalla lingua, piuttosto che cercare di piegare la lingua nel modo in cui sei abituato in altre lingue.
geirha,

1
@geirha: Beh, immagino che dovremo essere d'accordo sul fatto che non siamo d'accordo, e dovrai lasciarmi essere il giudice di quale risposta risponde meglio alla mia domanda. Personalmente, preferisco di gran lunga passaggio di array per riferimento in ogni caso (non importa la lingua, per salvare la copia dei dati); ancora di più quando l'alternativa è piegarsi all'indietro e passare la dimensione dell'array come parametro aggiuntivo ...
DevSolar

38

Commentando la soluzione di Ken Bertelson e rispondendo a Jan Hettich:

Come funziona

la takes_ary_as_arg descTable[@] optsTable[@]linea in try_with_local_arys()funzione invia:

  1. Questo in realtà crea una copia degli array descTablee optsTableche sono accessibili altakes_ary_as_arg funzione.
  2. takes_ary_as_arg()la funzione riceve descTable[@]e optsTable[@]come stringhe, ciò significa $1 == descTable[@]e $2 == optsTable[@].
  3. all'inizio della takes_ary_as_arg()funzione utilizza la ${!parameter}sintassi, che si chiama riferimento indiretto o talvolta doppio riferimento , ciò significa che invece di utilizzare $1il valore, utilizziamo il valore del valore espanso di$1 , ad esempio:

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

    allo stesso modo per $2.

  4. inserendo questo argAry1=("${!1}")crea argAry1come un array (le parentesi che seguono =) con l'espanso descTable[@], proprio come scrivere lì argAry1=("${descTable[@]}")direttamente. il declarenon è richiesto.

NB: Vale la pena ricordare che l'inizializzazione dell'array utilizzando questo modulo parentesi inizializza il nuovo array in base al separatore di campo internoIFS o che è per impostazione predefinita tab , newline e spazio . in quel caso, poiché utilizzava la notazione ogni elemento è visto da solo come se fosse citato (contrariamente a ).[@][*]

La mia prenotazione con esso

In BASH, l'ambito della variabile locale è la funzione corrente e ogni funzione figlio chiamata da essa, questo si traduce nel fatto che la takes_ary_as_arg()funzione "vede" quelli descTable[@]e gli optsTable[@]array, quindi funziona (vedi la spiegazione sopra).

Stando così le cose, perché non guardare direttamente quelle variabili? È proprio come scrivere lì:

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

Vedi la spiegazione sopra, che copia semplicemente descTable[@]i valori dell'array in base alla correnteIFS .

In sintesi

Questo, in sostanza, non passa per valore, come al solito.

Voglio anche sottolineare il commento di Dennis Williamson sopra: scarso matrici (le matrici senza tutte le chiavi definiscono - con "buchi" in esse) non funzioneranno come previsto - perderemmo le chiavi e "condenseremmo" l'array.

Detto questo, vedo il valore per la generalizzazione, quindi le funzioni possono ottenere gli array (o le copie) senza conoscere i nomi:

  • per ~ "copie": questa tecnica è abbastanza buona, è sufficiente tenere presente che gli indici (chiavi) sono spariti.
  • per copie reali: possiamo usare un eval per le chiavi, ad esempio:

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

e poi un loop che li utilizza per creare una copia. Nota: qui !non viene utilizzata la precedente valutazione indiretta / doppia, ma piuttosto nel contesto di matrice restituisce gli indici di matrice (chiavi).

  • e, naturalmente, se dovessimo passare descTablee optsTablestringhe (senza [@]), potremmo usare l'array stesso (come in riferimento) con eval. per una funzione generica che accetta array.

2
Buone spiegazioni del meccanismo alla base della spiegazione di Ken Bertelson. Alla domanda "Stando così le cose, perché non guardare direttamente quelle stesse variabili?", Risponderò: semplicemente per il riutilizzo della funzione. Diciamo che ho bisogno di chiamare una funzione con Array1, quindi con Array2, passare i nomi degli array diventa utile.
Gfrigon,

Ottima risposta, abbiamo bisogno di più spiegazioni come questa!
Édouard Lopez,

22

Il problema di base qui è che gli sviluppatori bash che hanno progettato / implementato array hanno davvero fregato il cane. Decisero che ${array}era solo una scorciatoia per ${array[0]}, che fu un brutto errore. Soprattutto se si considera che ${array[0]}non ha significato e viene valutata la stringa vuota se il tipo di array è associativo.

L'assegnazione di un array assume la forma in array=(value1 ... valueN)cui value ha la sintassi [subscript]=string, assegnando così un valore direttamente a un indice specifico dell'array. Questo lo rende così ci possono essere due tipi di array, indicizzati numericamente e hash indicizzati (chiamati array associativi nel linguaggio bash). Inoltre, consente di creare array sparsi indicizzati numericamente. Lasciare la [subscript]=parte è una scorciatoia per un array indicizzato numericamente, a partire dall'indice ordinale di 0 e incrementando con ogni nuovo valore nell'istruzione di assegnazione.

Pertanto, ${array}dovrebbe valutare l' intero array, gli indici e tutti. Dovrebbe valutare l'inverso della dichiarazione di assegnazione. Qualsiasi maggiore CS del terzo anno dovrebbe saperlo. In tal caso, questo codice funzionerebbe esattamente come ci si aspetterebbe che:

declare -A foo bar
foo=${bar}

Quindi, il passaggio di matrici per valore alle funzioni e l'assegnazione di un array a un altro funzionerebbe come impone il resto della sintassi della shell. Ma poiché non hanno fatto questo bene, l'operatore di assegnazione =non funziona per le matrici e le matrici non possono essere passate in base al valore alle funzioni o alle subshells o all'output in generale ( echo ${array}) senza codice per masticarle.

Quindi, se fosse stato fatto bene, il seguente esempio mostrerebbe come l'utilità degli array in bash potrebbe essere sostanzialmente migliore:

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

l'output risultante dovrebbe essere:

(first=one second=2 third=3)

Quindi, le matrici potrebbero utilizzare l'operatore di assegnazione e passare per valore alle funzioni e persino ad altri script di shell. Facilmente memorizzabile in uscita su un file e facilmente caricato da un file in uno script.

declare -A foo
read foo <file

Purtroppo, siamo stati delusi da un team di sviluppo bash altrimenti superlativo.

Pertanto, per passare un array a una funzione, esiste davvero solo un'opzione, ovvero utilizzare la funzione 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

produrrà il seguente output:

indexes: foo zoom
values: bar fast

Poiché questo passa per riferimento, è anche possibile assegnare alla matrice nella funzione. Sì, l'array a cui si fa riferimento deve avere un ambito globale, ma questo non dovrebbe essere un affare troppo grande, considerando che si tratta di shell scripting. Per passare una matrice indicizzata associativa o sparsa per valore a una funzione è necessario lanciare tutti gli indici e i valori nell'elenco degli argomenti (non troppo utile se si tratta di una matrice di grandi dimensioni) come stringhe singole come questa:

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

e quindi scrivere un sacco di codice all'interno della funzione per riassemblare l'array.


1
La soluzione di utilizzo local -nè migliore e più aggiornata della risposta accettata. Questa soluzione funzionerà anche per una variabile di qualsiasi tipo. L'esempio elencato in questa risposta può essere abbreviato in local -n ARR=${1}. Tuttavia, l' -nopzione per local/ declareè disponibile solo in Bash versione 4.3 e successive.
richardjsimkins,

Questo è carino! Piccolo gotcha: se passi una variabile con lo stesso nome dell'argomento locale della tua funzione (es. funky ARR), La shell darà un avvertimento circular name reference, perché sostanzialmente la funzione proverà a fare local -n ARR=ARR. Buona discussione su questo argomento.
Gene Pavlovsky,

5

La risposta di DevSolar ha un punto che non capisco (forse ha un motivo specifico per farlo, ma non riesco a pensarlo): imposta l'array dai parametri posizionali elemento per elemento, iterativo.

Un approccio più semplice sarebbe

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

  # now get a copy of the positional parameters
  local_array=("$@")
  ...
}

1
La mia ragione per non farlo è che non ho giocato con gli array bash fino a pochi giorni fa. In precedenza sarei passato a Perl se fosse diventato complesso, un'opzione che non ho nel mio attuale lavoro. Grazie per il suggerimento!
DevSolar,

3
function aecho {
  set "$1[$2]"
  echo "${!1}"
}

Esempio

$ foo=(dog cat bird)

$ aecho foo 1
cat

3

Un modo semplice per passare più matrici come parametro consiste nell'utilizzare una stringa separata da caratteri. Puoi chiamare il tuo script in questo modo:

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

Quindi, puoi estrarlo nel tuo codice in questo modo:

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

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

In questo modo, puoi effettivamente passare più array come parametri e non devono essere gli ultimi parametri.


1

Questo funziona anche con gli spazi:

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[@]}"

2
Mi chiedo quale sia il punto qui. Questo è solo un normale passaggio di argomenti. La sintassi "$ @" funziona per gli spazi: "$ @" equivale a "$ 1" "$ 2" ...
Andreas Spindler,

Posso passare 2 matrici a una funzione?
Pihentagy,

1

Con alcuni trucchi puoi effettivamente passare parametri nominati alle funzioni, insieme ad array.

Il metodo che ho sviluppato ti consente di accedere ai parametri passati a una funzione come questa:

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"

In altre parole, non solo puoi chiamare i tuoi parametri con i loro nomi (che compensano un core più leggibile), ma puoi anche passare array (e riferimenti a variabili - questa funzione funziona solo in bash 4.3 però)! Inoltre, le variabili mappate sono tutte nell'ambito locale, proprio come $ 1 (e altre).

Il codice che rende questo lavoro abbastanza leggero e funziona sia in bash 3 che bash 4 (queste sono le uniche versioni con cui l'ho provato). Se sei interessato a più trucchi come questo che rendono lo sviluppo con bash molto più bello e semplice, puoi dare un'occhiata al mio Bash Infinity Framework , il codice qui sotto è stato sviluppato a tale scopo.

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

Solo per aggiungere alla risposta accettata, poiché ho scoperto che non funziona bene se il contenuto dell'array è simile a:

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

In questo caso, ogni membro dell'array viene diviso, quindi l'array visualizzato dalla funzione è equivalente a:

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

Per far funzionare questo caso, il modo in cui ho trovato è quello di passare il nome della variabile alla funzione, quindi utilizzare eval:

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

function RUN_COMMANDS

Solo il mio 2 ©


1

Per quanto brutta sia, ecco una soluzione alternativa che funziona finché non si passa esplicitamente un array, ma una variabile corrispondente a un array:

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

Sono sicuro che qualcuno può trovare un'implementazione più chiara dell'idea, ma ho trovato che questa è una soluzione migliore rispetto al passaggio di un array "{array[@]"}e al suo accesso interno mediante array_inside=("$@"). Questo diventa complicato quando ci sono altri getoptsparametri / posizionali . In questi casi, ho dovuto prima determinare e quindi rimuovere i parametri non associati all'array usando una combinazione dishift e rimozione dell'elemento dell'array.

Una prospettiva purista probabilmente vede questo approccio come una violazione della lingua, ma parlando pragmaticamente, questo approccio mi ha risparmiato molto dolore. Su un argomento correlato, utilizzo anche evalper assegnare una matrice costruita internamente a una variabile denominata in base a un parametro target_varnameche passo alla funzione:

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

Spero che questo aiuti qualcuno.


0

Requisito : funzione per trovare una stringa in un array.
Questa è una leggera semplificazione della soluzione DevSolar in quanto utilizza gli argomenti passati anziché copiarli.

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

La mia breve risposta è:

function display_two_array {
    local arr1=$1
    local arr2=$2
    for i in $arr1
    do
       "arrary1: $i"
    done
    
    for i in $arr2
    do
       "arrary2: $i"
    done
}

test_array=(1 2 3 4 5)
test_array2=(7 8 9 10 11)

display_two_array "${test_array[*]}" "${test_array2[*]}"
Va notato che ${test_array[*]}e ${test_array2[*]}dovrebbe essere racchiuso tra "", altrimenti fallirai.


Il tuo esempio non è corretto perché incompleto. Fornisci il codice completo dello script.
Dennis VR,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.