Pasar matrices como parámetros en bash

188

¿Cómo puedo pasar una matriz como parámetro a una función bash?

Nota: Después de no encontrar una respuesta aquí en Stack Overflow, publiqué mi solución algo cruda yo mismo. Solo permite pasar una matriz y es el último elemento de la lista de parámetros. En realidad, no está pasando la matriz en absoluto, sino una lista de sus elementos, que se vuelven a ensamblar en una matriz mediante called_function (), pero funcionó para mí. Si alguien conoce una mejor manera, siéntase libre de agregarla aquí.

DevSolar
fuente
1
Aquí tienes una buena referencia y toneladas de ejemplos.
Artem Barger
16
Errr ... ¿Tres votos negativos sobre una pregunta de cinco años en el mismo minuto?
DevSolar

Respuestas:

220

Puede pasar múltiples matrices como argumentos usando algo como esto:

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

hará eco:

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

Edición / notas: (de los comentarios a continuación)

  • descTabley optsTablese pasan como nombres y se expanden en la función. Por lo tanto, no $se necesita cuando se dan como parámetros.
  • Tenga en cuenta que esto todavía funciona incluso con descTable etc., ya que se define a los locallocales, ya que los locales son visibles para las funciones que llaman.
  • El !en ${!1}expande la variable arg 1.
  • declare -a solo hace explícita la matriz indexada, no es estrictamente necesaria.
Ken Bertelson
fuente
14
Una cosa a tener en cuenta es que si la matriz original es escasa, la matriz en la función de recepción no tendrá los mismos índices.
Pausado hasta nuevo aviso.
13
Esto es brillante, pero ¿pueden Ken o alguien explicar un par de cosas que me desconciertan acerca de por qué funciona: 1 - Pensé que descTable y optsTable debían tener el prefijo $ cuando se pasaban como argumentos de función. 2 - En la primera línea de "toma ...", ¿por qué se necesita una declaración de matriz explícita? 3 - ¿Y qué hace el! significa en la expresión $ {! 1}, y ¿por qué [@] no se requiere o incluso se permite allí? - Esto funciona, y todos estos detalles parecen ser necesarios según mis pruebas, ¡pero me gustaría entender por qué!
Jan Hettich
8
1: descTable y optsTable solo se pasan como nombres, por lo tanto no hay $, se expandirán solo en la función llamada 2: no totalmente seguro, pero creo que no es realmente necesario 3: the! se usa porque los parámetros pasados ​​a la función deben expandirse dos veces: $ 1 se expande a "descTable [@]", y eso debe expandirse a "$ {descTable [@]}". La sintaxis $ {! 1} hace exactamente esto.
Elmar Zander
8
No creo que la parte "declarar -a" sea necesaria. La existencia de paréntesis ya define el LHS de la asignación como una matriz.
Erik Aronesty
3
Esta respuesta me ayudó a resolver un problema en este momento. Sin embargo, quería señalar que en mi máquina (usando bash 4.3.42) los "$ {! 1}" y "$ {! 2}" necesitan que se eliminen las comillas. Si no lo hace, el valor de la matriz original se lee como una cadena y se asigna a argAry1 [0] y argAry2 [0] respectivamente, lo que básicamente significa que la estructura de la matriz se pierde.
user.friendly
85

Nota: Esta es la solución un tanto cruda que publiqué yo mismo, después de no encontrar una respuesta aquí en Stack Overflow. Solo permite pasar una matriz y es el último elemento de la lista de parámetros. En realidad, no está pasando la matriz en absoluto, sino una lista de sus elementos, que se vuelven a ensamblar en una matriz mediante called_function (), pero funcionó para mí. Un poco más tarde, Ken publicó su solución, pero yo guardé la mía aquí como referencia "histórica".

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

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

Mejorado por TheBonsai, gracias.

DevSolar
fuente
19
Tres años después del hecho, esta respuesta, mantenida solo por razones históricas, recibió dos votos negativos en un par de días. Como es tristemente habitual en SO, sin ninguna nota de por qué la gente piensa que esto está justificado. Tenga en cuenta que esta respuesta es anterior a todas las demás, y que acepté la respuesta de Ken como la mejor solución. Soy perfectamente consciente de que no es perfecto, pero durante cuatro meses fue el mejor disponible en SO. Por qué debería ser rechazado dos años después de tomar el segundo lugar para la solución perfecta de Ken está más allá de mí.
DevSolar
@geirha: Le pediría que verifique quién publicó la pregunta, quién publicó esta respuesta y quién probablemente aceptó la respuesta que está llamando "mala". ;-) También es posible que desee comprobar la Nota en la pregunta, que señala por qué esta solución es inferior a la de Ken.
DevSolar
2
Sé que hiciste la pregunta, escribiste esta respuesta y que aceptaste la mala respuesta. Por eso lo expresé de esa manera. La razón por la que la respuesta aceptada es mala es porque está tratando de pasar la matriz por referencia, que es algo que realmente debe evitar. Además, el ejemplo combina múltiples argumentos en una sola cadena. Si realmente necesita pasar matrices por referencia, bash es el idioma incorrecto para empezar. Incluso con las nuevas variables nameref de bash 4.3, no puede evitar con seguridad las colisiones de nombres (referencia circular).
geirha
44
Bueno, puede pasar múltiples matrices si incluye el número de elementos de cada matriz. called_function "${#array[@]}" "${array[@]}" "${#array2[@]}" "${array2[@]}"etc ... aún con algunas restricciones obvias, pero en realidad, es mejor resolver el problema de una manera que el idioma admita, en lugar de tratar de doblar el idioma para que funcione como está acostumbrado en otros idiomas.
geirha
1
@geirha: Bueno, supongo que tendremos que estar de acuerdo en que no estamos de acuerdo, y tendrás que dejarme ser el juez de la respuesta que responde mejor a mi pregunta. Personalmente, prefiero pasar matrices por referencia de todos modos (sin importar el idioma, para guardar la copia de datos); aún más cuando la alternativa es inclinarse hacia atrás y pasar el tamaño de la matriz como parámetro adicional ...
DevSolar
38

Comentando sobre la solución de Ken Bertelson y respondiendo a Jan Hettich:

Cómo funciona

la takes_ary_as_arg descTable[@] optsTable[@]línea en la try_with_local_arys()función envía:

  1. Esto realmente crea una copia de las matrices descTabley optsTableque son accesibles para la takes_ary_as_argfunción.
  2. takes_ary_as_arg()función recibe descTable[@]y optsTable[@]como cadenas, eso significa $1 == descTable[@]y$2 == optsTable[@] .
  3. Al comienzo de la takes_ary_as_arg()función, utiliza la ${!parameter}sintaxis, que se denomina referencia indirecta o, a veces, doble referencia , lo que significa que, en lugar de usar $1el valor de, usamos el valor del valor expandido de$1 , por ejemplo:

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

    igualmente para $2 .

  4. poner esto en argAry1=("${!1}")crea argAry1como una matriz (los corchetes siguientes =) con el expandido descTable[@], al igual que escribir allí argAry1=("${descTable[@]}")directamente. El declareno es necesario.

NB: Vale la pena mencionar que la inicialización de la matriz utilizando este formulario de paréntesis inicializa la nueva matriz de acuerdo con el separador de campo internoIFS o que es, por defecto , la pestaña , la nueva línea y el espacio . en ese caso, dado que usaba notación, cada elemento es visto por sí mismo como si fuera citado (contrario a ).[@][*]

Mi reserva con el

En BASH, el alcance de la variable local es la función actual y cada función secundaria llamada desde ella, esto se traduce en el hecho de que la takes_ary_as_arg()función "ve" esas descTable[@]y optsTable[@]matrices, por lo que está funcionando (ver explicación anterior).

Siendo ese el caso, ¿por qué no mirar directamente esas variables? Es como escribir allí:

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

Consulte la explicación anterior, que solo copia descTable[@]los valores de la matriz de acuerdo con la corriente IFS.

En resumen

Esto es, en esencia, pasar nada por valor, como de costumbre.

También quiero enfatizar el comentario anterior de Dennis Williamson: las matrices dispersas (las matrices sin todas las claves definidas - con "agujeros" en ellas) no funcionarán como se esperaba - perderíamos las claves y "condensaríamos" la matriz.

Dicho esto, veo el valor para la generalización, las funciones pueden obtener las matrices (o copias) sin conocer los nombres:

  • para ~ "copias": esta técnica es lo suficientemente buena, solo hay que tener en cuenta que los índices (claves) se han ido.
  • para copias reales: podemos usar una evaluación para las claves, por ejemplo:

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

y luego un bucle que los usa para crear una copia. Nota: aquí !no se utiliza su evaluación previa indirecta / doble, sino que en el contexto de matriz devuelve los índices de matriz (claves).

  • y, por supuesto, si tuviéramos que pasar descTabley optsTablecadenas (sin [@]), podríamos usar la matriz en sí (como en referencia) con eval. para una función genérica que acepta matrices.
El mago
fuente
2
Buenas explicaciones del mecanismo detrás de la explicación de Ken Bertelson. A la pregunta "Siendo ese caso, ¿por qué no mirar directamente esas variables en sí mismas?", Responderé: simplemente para reutilizar la función. Digamos que necesito llamar a una función con Array1, luego con Array2, pasar los nombres de la matriz se vuelve útil.
gfrigon
Gran respuesta, ¡necesitamos más explicaciones como esta!
Édouard Lopez
22

El problema básico aquí es que los desarrolladores de bash que diseñaron / implementaron matrices realmente atornillaron al chucho. Decidieron que ${array}era solo una mano corta ${array[0]}, lo cual fue un grave error. Especialmente cuando consideras eso${array[0]} no tiene significado y evalúa la cadena vacía si el tipo de matriz es asociativo.

La asignación de una matriz toma la forma array=(value1 ... valueN)donde el valor tiene la sintaxis [subscript]=string, asignando así un valor directamente a un índice particular en la matriz. Esto hace que pueda haber dos tipos de matrices, indexadas numéricamente e indexadas hash (llamadas matrices asociativas en lenguaje bash). También lo hace para que pueda crear dispersas matrices indexadas numéricamente. Dejar la [subscript]=parte es una abreviatura para una matriz indexada numéricamente, comenzando con el índice ordinal de 0 e incrementándose con cada nuevo valor en la instrucción de asignación.

Por lo tanto, ${array}debe evaluar a toda la matriz, índices y todo. Debe evaluar al inverso de la declaración de asignación. Cualquier estudiante de tercer año de CS debería saber eso. En ese caso, este código funcionaría exactamente como podría esperar:

declare -A foo bar
foo=${bar}

Luego, pasar matrices por valor a funciones y asignar una matriz a otra funcionaría como lo dicta el resto de la sintaxis de shell. Pero debido a que no hicieron esto correctamente, el operador de asignación =no funciona para las matrices, y las matrices no se pueden pasar por valor a funciones o subcapas o salida en general ( echo ${array}) sin código para analizarlo todo.

Entonces, si se hubiera hecho bien, el siguiente ejemplo mostraría cómo la utilidad de las matrices en bash podría ser sustancialmente mejor:

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

El resultado resultante debe ser:

(first=one second=2 third=3)

Luego, las matrices podrían usar el operador de asignación y pasarlas por valor a las funciones e incluso a otros scripts de shell. Se almacena fácilmente mediante la salida a un archivo y se carga fácilmente desde un archivo a un script.

declare -A foo
read foo <file

Por desgracia, nos ha decepcionado un equipo de desarrollo de bash de otro modo superlativo.

Como tal, para pasar una matriz a una función, en realidad solo hay una opción, y es usar la función 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

dará como resultado la siguiente salida:

indexes: foo zoom
values: bar fast

Como esto pasa por referencia, también puede asignar a la matriz en la función. Sí, la matriz a la que se hace referencia debe tener un alcance global, pero eso no debería ser un gran problema, teniendo en cuenta que se trata de scripts de shell. Para pasar una matriz indexada asociativa o dispersa por valor a una función, es necesario incluir todos los índices y los valores en la lista de argumentos (no demasiado útil si se trata de una matriz grande) como cadenas individuales como esta:

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

y luego escribir un montón de código dentro de la función para volver a ensamblar la matriz.

tigerand
fuente
1
La solución del uso local -nes mejor y más actualizada que la respuesta aceptada. Esta solución también funcionará para una variable de cualquier tipo. El ejemplo que figura en esta respuesta se puede acortar a local -n ARR=${1}. Sin embargo, la -nopción para local/ declaresolo está disponible en Bash versión 4.3 y superior.
richardjsimkins
¡Esto es bonito! Pequeño problema: si pasa una variable con el mismo nombre que el argumento local de su función (por ejemplo funky ARR), el shell dará una advertencia circular name reference, porque básicamente la función intentará hacerlo local -n ARR=ARR. Buena discusión sobre este tema.
Gene Pavlovsky
5

La respuesta de DevSolar tiene un punto que no entiendo (tal vez tiene una razón específica para hacerlo, pero no se me ocurre ninguno): establece la matriz de los parámetros posicionales elemento por elemento, iterativa.

Un acercamiento más fácil sería

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

  # now get a copy of the positional parameters
  local_array=("$@")
  ...
}
TheBonsai
fuente
1
Mi razón para no hacerlo es que no he jugado con los arrays bash hasta hace unos días. Anteriormente me hubiera cambiado a Perl si se volviera complejo, una opción que no tengo en mi trabajo actual. ¡Gracias por la pista!
DevSolar
3
function aecho {
  set "$1[$2]"
  echo "${!1}"
}

Ejemplo

$ foo=(dog cat bird)

$ aecho foo 1
cat
Steven Penny
fuente
3

Una manera fácil de pasar varias matrices como parámetro es usar una cadena separada por caracteres. Puedes llamar a tu script de esta manera:

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

Luego, puede extraerlo en su código de esta manera:

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

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

De esta manera, puede pasar múltiples matrices como parámetros y no tiene que ser los últimos parámetros.

Remy Cilia
fuente
1

Este funciona incluso con espacios:

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[@]}"
humildesapiens
fuente
2
Me pregunto cuál es el punto aquí. Esto es simplemente pasar un argumento normal. La sintaxis "$ @" está hecha para trabajar para espacios: "$ @" es equivalente a "$ 1" "$ 2" ...
Andreas Spindler
¿Puedo pasar 2 matrices a una función?
pihentagy
1

Con algunos trucos, puede pasar parámetros con nombre a funciones, junto con matrices.

El método que desarrollé le permite acceder a los parámetros pasados ​​a una función 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"

En otras palabras, no solo puedes llamar a tus parámetros por sus nombres (lo que compensa un núcleo más legible), sino que también puedes pasar matrices (y referencias a variables; ¡esta función solo funciona en bash 4.3!). Además, las variables asignadas están todas en el ámbito local, igual que $ 1 (y otras).

El código que hace que esto funcione es bastante ligero y funciona tanto en bash 3 como en bash 4 (estas son las únicas versiones con las que lo he probado). Si está interesado en más trucos como este que hacen que el desarrollo con bash sea mucho más fácil y fácil, puede echar un vistazo a mi Bash Infinity Framework , el código a continuación fue desarrollado para ese propósito.

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
fuente
1

Solo para agregar a la respuesta aceptada, ya que descubrí que no funciona bien si el contenido de la matriz es algo así como:

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

En este caso, cada miembro de la matriz se divide, por lo que la matriz que ve la función es equivalente a:

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

Para que este caso funcione, la forma en que encontré es pasar el nombre de la variable a la función, luego usar eval:

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

function RUN_COMMANDS

Solo mi 2 ©

AlvaroGMJ
fuente
1

Tan feo como es, aquí hay una solución alternativa que funciona siempre y cuando no pase una matriz explícitamente, sino una variable correspondiente a una 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

Estoy seguro de que alguien puede llegar a una implementación más clara de la idea, pero he encontrado que esta es una mejor solución que pasar una matriz como "{array[@]"}y luego acceder internamente usando array_inside=("$@"). Esto se complica cuando hay otros posicionales / getoptsparámetros. En estos casos, primero tuve que determinar y luego eliminar los parámetros no asociados con la matriz usando alguna combinación de shifty eliminación de elementos de la matriz.

Una perspectiva purista probablemente ve este enfoque como una violación del lenguaje, pero hablando pragmáticamente, este enfoque me ha ahorrado mucho dolor. En un tema relacionado, también uso evalpara asignar una matriz construida internamente a una variable nombrada de acuerdo con un parámetro target_varnameque paso a la función:

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

Espero que esto ayude a alguien.

Blake Schultze
fuente
0

Requisito : Función para encontrar una cadena en una matriz.
Esta es una ligera simplificación de la solución de DevSolar, ya que utiliza los argumentos pasados ​​en lugar de copiarlos.

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
fuente
0

Mi respuesta corta es:

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[*]}"
Debe notarse que ${test_array[*]}y ${test_array2[*]}debe estar rodeado por "", de lo contrario, fallará.

TOMMY
fuente
Su ejemplo es incorrecto porque está incompleto. Por favor proporcione el código completo de la secuencia de comandos.
Dennis VR