Passando matrizes como parâmetros no bash

188

Como posso passar uma matriz como parâmetro para uma função bash?

Nota: Depois de não encontrar uma resposta aqui no Stack Overflow, publiquei minha solução um tanto grosseira. Ele permite que apenas uma matriz seja passada e seja o último elemento da lista de parâmetros. Na verdade, ele não está transmitindo a matriz, mas uma lista de seus elementos, que são remontados em uma matriz por named_function (), mas funcionou para mim. Se alguém souber uma maneira melhor, fique à vontade para adicioná-lo aqui.

DevSolar
fonte
1
Aqui você tem uma boa referência e vários exemplos.
Artem Barger
16
Errr ... Três votos negativos em uma pergunta de cinco anos de idade no mesmo minuto?
DevSolar

Respostas:

220

Você pode passar várias matrizes como argumentos usando algo como isto:

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

ecoará:

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

Editar / notas: (dos comentários abaixo)

  • descTablee optsTablesão passados ​​como nomes e são expandidos na função Portanto, não $é necessário quando fornecido como parâmetros.
  • Observe que isso ainda funciona mesmo com descTableetc sendo definido com local, porque os locais são visíveis para as funções que chamam.
  • O !in ${!1}expande a variável arg 1.
  • declare -a apenas torna explícita a matriz indexada, não é estritamente necessário.
Ken Bertelson
fonte
14
Uma coisa a observar é que, se a matriz original for esparsa, a matriz na função de recebimento não terá os mesmos índices.
Pausado até novo aviso.
13
Isso é brilhante, mas Ken ou alguém pode explicar algumas coisas que me intrigam sobre o porquê de funcionar: 1 - Eu pensaria que descTable e optsTable teriam que ser prefixadas com $ quando passadas como argumentos de função. 2 - Na primeira linha de "takes ...", por que é necessária uma declaração explícita de array? 3 - E o que faz o! significa na expressão $ {! 1} e por que [@] não é necessário ou mesmo permitido lá? - Isso funciona, e todos esses detalhes parecem necessários com base nos meus testes, mas eu gostaria de entender o porquê!
Jan Hettich
8
1: descTable e optsTable são passadas apenas como nomes; portanto, não há $, elas serão expandidas apenas na função chamada 2: não totalmente certa, mas acho que não é realmente necessário 3: the! é usado porque os parâmetros passados ​​para a função precisam ser expandidos duas vezes: $ 1 se expande para "descTable [@]" e deve ser expandido para "$ {descTable [@]}". A sintaxe $ {! 1} faz exatamente isso.
Elmar Zander
8
Eu não acho que a parte "declare -a" seja necessária. A existência de parênteses já define o LHS da atribuição como uma matriz.
Erik Aronesty
3
Esta resposta me ajudou a resolver um problema agora. No entanto, eu queria ressaltar que na minha máquina (usando o bash 4.3.42) os "$ {! 1}" e "$ {! 2}" precisam remover as aspas. Caso contrário, o valor da matriz original é lido como uma sequência e atribuído a argAry1 [0] e argAry2 [0], respectivamente, basicamente significando que a estrutura da matriz é perdida.
user.friendly
85

Nota: Esta é a solução um tanto grosseira que eu postei, depois de não encontrar uma resposta aqui no Stack Overflow. Ele permite que apenas uma matriz seja passada e seja o último elemento da lista de parâmetros. Na verdade, ele não está transmitindo a matriz, mas uma lista de seus elementos, que são remontados em uma matriz por named_function (), mas funcionou para mim. Um pouco mais tarde, Ken postou sua solução, mas eu mantive a minha aqui para referência "histórica".

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

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

Melhorado pelo TheBonsai, obrigado.

DevSolar
fonte
19
Três anos após o fato, essa resposta - mantida apenas por razões históricas - recebeu duas votações negativas em alguns dias. Como sempre é habitual no SO, sem nenhuma nota sobre o motivo pelo qual as pessoas pensam que isso se justifica. Observe que essa resposta é anterior a todas as outras e que aceitei a resposta de Ken como a melhor solução. Estou perfeitamente ciente de que não é nem de longe perfeito, mas por quatro meses foi o melhor disponível no SO. O motivo pelo qual ele deve ser rebaixado dois anos depois de ter ficado em segundo lugar na solução perfeita de Ken está além de mim.
DevSolar
@geirha: Peço que você verifique quem postou a pergunta, quem postou esta resposta e quem provavelmente aceitou a resposta que você está chamando de "ruim". ;-) Você também pode verificar a Nota na pergunta, que mostra por que essa solução é inferior à de Ken.
precisa saber é o seguinte
2
Eu sei que você fez a pergunta, escreveu esta resposta e aceitou a resposta ruim. Foi por isso que escrevi dessa maneira. A razão pela qual a resposta aceita é ruim é porque está tentando passar a matriz por referência, o que é algo que você realmente deve evitar. Além disso, o exemplo mescla vários argumentos em uma única sequência. Se você realmente precisa passar matrizes por referência, bash é o idioma errado para começar. Mesmo com as novas variáveis ​​nameref do bash 4.3, você não pode evitar com segurança colisões de nomes (referência circular).
Geirha
4
Bem, você pode passar várias matrizes se incluir o número de elementos de cada matriz. called_function "${#array[@]}" "${array[@]}" "${#array2[@]}" "${array2[@]}"etc ... ainda com algumas restrições óbvias, mas realmente melhor resolver o problema da maneira que o idioma suportar, em vez de tentar dobrar o idioma para que ele funcione da maneira que você está acostumado em outros idiomas.
Geirha
1
@geirha: Bem, acho que teremos que concordar que não concordamos, e você terá que me deixar julgar qual resposta responde melhor à minha pergunta. Pessoalmente, eu prefiro passar arrays por referência de qualquer maneira (não importa o idioma, para salvar a cópia dos dados); ainda mais quando a alternativa é dobrar para trás e passar o tamanho da matriz como parâmetro adicional ...
DevSolar
38

Comentando a solução de Ken Bertelson e respondendo a Jan Hettich:

Como funciona

a função takes_ary_as_arg descTable[@] optsTable[@]line in try_with_local_arys()envia:

  1. Na verdade, isso cria uma cópia das matrizes descTablee optsTableque são acessíveis à takes_ary_as_argfunção.
  2. takes_ary_as_arg()função recebe descTable[@]e optsTable[@]como strings, isso significa $1 == descTable[@]e $2 == optsTable[@].
  3. no início da takes_ary_as_arg()função, ele usa a ${!parameter}sintaxe, que é chamada de referência indireta ou, às vezes, dupla referência , isso significa que, em vez de usar $1o valor de, usamos o valor do valor expandido de$1 , por exemplo:

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

    da mesma forma para $2.

  4. colocar isso argAry1=("${!1}")cria argAry1como uma matriz (os colchetes a seguir =) com o expandido descTable[@], assim como escrever argAry1=("${descTable[@]}")diretamente nele . o declarenão é necessário.

NB: Vale ressaltar que a inicialização do array usando este formulário de colchete inicializa o novo array de acordo com o IFSou Internal Field Separator, que é, por padrão , guia , nova linha e espaço . nesse caso, como usava a [@]notação, cada elemento é visto por si só como se tivesse sido citado (ao contrário de [*]).

Minha reserva com ele

Em BASH, escopo de variável local é a função atual e todas as funções filho são chamadas a partir dele, isso se traduz no fato de que a takes_ary_as_arg()função "vê" esses descTable[@]e optsTable[@]arrays, portanto está funcionando (veja a explicação acima).

Sendo esse o caso, por que não olhar diretamente para essas variáveis? É como escrever lá:

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

Veja a explicação acima, que apenas copia descTable[@]os valores da matriz de acordo com a corrente IFS.

Em suma

Isso está passando, em essência, nada por valor - como sempre.

Também quero enfatizar o comentário de Dennis Williamson acima: matrizes esparsas (matrizes sem todas as chaves definidas - com "orifícios" nelas) não funcionarão conforme o esperado - perderíamos as chaves e "condensaríamos" a matriz.

Dito isto, vejo o valor da generalização, pois as funções podem obter as matrizes (ou cópias) sem saber os nomes:

  • para ~ "cópias": esta técnica é boa o suficiente, basta ter consciência de que os índices (chaves) desapareceram.
  • para cópias reais: podemos usar uma avaliação para as chaves, por exemplo:

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

e depois um loop usando-os para criar uma cópia. Nota: aqui !não é usada sua avaliação indireta / dupla anterior, mas, no contexto da matriz, retorna os índices da matriz (chaves).

  • e, é claro, se passássemos descTablee optsTablestrings (sem [@]), poderíamos usar o próprio array (como em referência) com eval. para uma função genérica que aceita matrizes.
O bruxo
fonte
2
Boas explicações sobre o mecanismo por trás da explicação de Ken Bertelson. À pergunta "Sendo assim, por que não olhar diretamente para essas variáveis?", Responderei: simplesmente para reutilizar a função. Digamos que eu precise chamar uma função com Array1, depois com Array2, passar os nomes dos arrays se torne útil.
precisa saber é
Ótima resposta, precisamos de mais explicações como essa!
Édouard Lopez
22

O problema básico aqui é que os desenvolvedores do bash que projetaram / implementaram matrizes realmente estragaram tudo. Eles decidiram que ${array}era apenas uma mão curta ${array[0]}, o que foi um grande erro. Especialmente quando você considera que ${array[0]}não tem significado e avalia a sequência vazia se o tipo de matriz é associativo.

A atribuição de uma matriz assume a forma em array=(value1 ... valueN)que value tem a sintaxe [subscript]=string, atribuindo um valor diretamente a um índice específico na matriz. Isso faz com que haja dois tipos de matrizes, indexadas numericamente e indexadas por hash (chamadas matrizes associativas na linguagem do bash). Ele também permite criar matrizes esparsas indexadas numericamente. Sair da [subscript]=peça é uma abreviação para uma matriz indexada numericamente, começando com o índice ordinal de 0 e incrementando a cada novo valor na instrução de atribuição.

Portanto, ${array}deve avaliar para toda a matriz, índices e tudo. Deve avaliar o inverso da declaração de atribuição. Qualquer aluno do terceiro ano do ensino médio deve saber disso. Nesse caso, esse código funcionaria exatamente como você pode esperar:

declare -A foo bar
foo=${bar}

Em seguida, passar matrizes por valor para funções e atribuir uma matriz a outra funcionaria como o resto da sintaxe do shell exigir. Mas como eles não fizeram isso corretamente, o operador de atribuição =não funciona para matrizes, e matrizes não podem ser passadas por valor para funções ou subcascas ou saída em geral ( echo ${array}) sem código para analisar tudo.

Portanto, se tivesse sido feito corretamente, o exemplo a seguir mostraria como a utilidade de matrizes no bash poderia ser substancialmente melhor:

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

a saída resultante deve ser:

(first=one second=2 third=3)

Em seguida, as matrizes podem usar o operador de atribuição e serem passadas por valor para funções e até outros scripts de shell. Facilmente armazenado com saída para um arquivo e facilmente carregado de um arquivo para um script.

declare -A foo
read foo <file

Infelizmente, fomos decepcionados por uma equipe superlativa de desenvolvimento do bash.

Como tal, para passar um array para uma função, existe realmente apenas uma opção, e é usar o recurso 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

resultará na seguinte saída:

indexes: foo zoom
values: bar fast

Como isso é transmitido por referência, você também pode atribuir à matriz na função. Sim, a matriz que está sendo referenciada deve ter um escopo global, mas isso não deve ser muito importante, considerando que esse é um script de shell. Para passar uma matriz indexada associativa ou esparsa por valor para uma função, é necessário lançar todos os índices e valores na lista de argumentos (não muito útil se for uma matriz grande) como cadeias únicas como esta:

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

e, em seguida, escrevendo um monte de código dentro da função para remontar a matriz.

Tigerand
fonte
1
A solução do uso local -né melhor e mais atualizada do que a resposta aceita. Essa solução também funcionará para uma variável de qualquer tipo. O exemplo listado nesta resposta pode ser abreviado para local -n ARR=${1}. No entanto, a -nopção local/ declareestá disponível apenas no Bash versão 4.3 e superior.
Richardjsimkins
Isso é legal! Pequena pegadinha: se você passar uma variável com o mesmo nome do argumento local da sua função (por exemplo funky ARR), o shell emitirá um aviso circular name reference, porque basicamente a função tentará executar local -n ARR=ARR. Boa discussão sobre este tópico.
Gene Pavlovsky
5

A resposta do DevSolar tem um ponto que eu não entendo (talvez ele tenha um motivo específico para fazê-lo, mas não consigo pensar em um): ele define o array a partir dos parâmetros posicionais elemento por elemento, iterativo.

Uma abordagem mais fácil seria

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

  # now get a copy of the positional parameters
  local_array=("$@")
  ...
}
TheBonsai
fonte
1
Minha razão para não fazer isso é que eu não brinquei com matrizes bash até alguns dias atrás. Anteriormente, eu teria mudado para o Perl se ele se tornasse complexo, uma opção que não tenho no meu trabalho atual. Obrigado pela dica!
DevSolar
3
function aecho {
  set "$1[$2]"
  echo "${!1}"
}

Exemplo

$ foo=(dog cat bird)

$ aecho foo 1
cat
Steven Penny
fonte
3

Uma maneira fácil de passar várias matrizes como parâmetro é usar uma sequência separada por caracteres. Você pode chamar seu script assim:

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

Em seguida, você pode extraí-lo em seu código assim:

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

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

Dessa forma, você pode realmente passar várias matrizes como parâmetros e não precisa ser o último parâmetro.

Remy Cilia
fonte
1

Este funciona mesmo com espaços:

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[@]}"
humbleSapiens
fonte
2
Eu me pergunto qual é o sentido aqui. Isso é apenas uma passagem normal de argumento. O "$ @" sintaxe é feito para trabalhar para espaços: "$ @" é equivalente a "$ 1" "$ 2" ...
Andreas Spindler
Posso passar 2 matrizes para uma função?
pihentagy
1

Com alguns truques, você pode realmente passar parâmetros nomeados para funções, juntamente com matrizes.

O método que desenvolvi permite acessar parâmetros passados ​​para uma função como esta:

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"

Em outras palavras, não apenas você pode chamar seus parâmetros pelos nomes (o que compõe um núcleo mais legível), como também pode passar matrizes (e referências a variáveis ​​- esse recurso funciona apenas no bash 4.3)! Além disso, as variáveis ​​mapeadas estão todas no escopo local, assim como $ 1 (e outras).

O código que faz esse trabalho é bastante leve e funciona tanto no bash 3 quanto no bash 4 (essas são as únicas versões com as quais eu testei). Se você estiver interessado em mais truques como esse que tornam o desenvolvimento com o bash muito mais agradável e fácil, você pode dar uma olhada no meu Bash Infinity Framework , o código abaixo foi desenvolvido para esse fim.

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'
niieani
fonte
1

Apenas para adicionar à resposta aceita, como eu achei que não funcionaria bem se o conteúdo da matriz fosse algo como:

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

Nesse caso, cada membro da matriz é dividido, portanto, a matriz que a função vê é equivalente a:

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

Para que esse caso funcione, a maneira que encontrei é passar o nome da variável para a função e usar eval:

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

function RUN_COMMANDS

Apenas meu 2 ©

AlvaroGMJ
fonte
1

Por mais feia que seja, aqui está uma solução alternativa que funciona desde que você não passe uma matriz explicitamente, mas uma variável correspondente a uma matriz:

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

Tenho certeza de que alguém pode apresentar uma implementação mais clara da ideia, mas achei que essa é uma solução melhor do que passar uma matriz "{array[@]"}e acessá-la internamente usando array_inside=("$@"). Isso se torna complicado quando existem outros getoptsparâmetros posicionais . Nesses casos, tive que primeiro determinar e remover os parâmetros não associados à matriz usando alguma combinação de shiftremoção e elemento da matriz.

Uma perspectiva purista provavelmente vê essa abordagem como uma violação da linguagem, mas, pragmaticamente falando, essa abordagem me salvou bastante. Em um tópico relacionado, eu também uso evalpara atribuir uma matriz construída internamente a uma variável nomeada de acordo com um parâmetro que target_varnameeu passo para a função:

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

Espero que isso ajude alguém.

Blake Schultze
fonte
0

Requisito : Função para encontrar uma sequência em uma matriz.
Essa é uma ligeira simplificação da solução do DevSolar, na medida em que usa os argumentos passados ​​em vez de copiá-los.

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 
Andre
fonte
0

Minha resposta curta é:

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[*]}"
Deve-se notar que o ${test_array[*]}e ${test_array2[*]}deve estar cercado por "", caso contrário você falhará.

tOmMy
fonte
Seu exemplo está incorreto porque incompleto. Por favor, forneça o código completo do script.
Dennis VR