web-dev-qa-db-fra.com

Passer des tableaux en tant que paramètres dans bash

Comment puis-je passer un tableau en tant que paramètre à une fonction bash?

Note: Après ne pas avoir trouvé de réponse ici sur Stack Overflow, j'ai posté moi-même ma solution un peu rudimentaire. Cela permet de ne transmettre qu'un seul tableau, qui est le dernier élément de la liste de paramètres. En fait, il ne s'agit pas du tout de passer le tableau, mais une liste de ses éléments, qui sont réassemblés dans un tableau par la fonction call_function (), mais cela a fonctionné pour moi. Si quelqu'un connaît un meilleur moyen, n'hésitez pas à l'ajouter ici.

177
DevSolar

Vous pouvez passer plusieurs tableaux comme arguments en utilisant quelque chose comme ceci:

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

va faire écho:

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

Note: C'est la solution un peu grossière que j'ai moi-même postée, après ne pas avoir trouvé de réponse ici sur Stack Overflow. Cela permet de ne transmettre qu'un seul tableau, qui est le dernier élément de la liste de paramètres. En fait, il ne s'agit pas du tout de passer le tableau, mais une liste de ses éléments, qui sont réassemblés dans un tableau par la fonction call_function (), mais cela a fonctionné pour moi. Un peu plus tard, Ken a posté sa solution, mais j'ai gardé la mienne ici pour référence "historique".

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

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

Amélioré par TheBonsai, merci.

83
DevSolar

Le problème de base ici est que le ou les développeurs bash qui ont conçu/implémenté les tableaux ont vraiment foutu en l'air. Ils ont décidé que ${array} était juste une main courte pour ${array[0]}, ce qui était une grave erreur. Surtout quand on considère que ${array[0]} n'a aucune signification et est évalué à la chaîne vide si le type de tableau est associatif.

L'affectation d'un tableau prend la forme array=(value1 ... valueN) où valeur a la syntaxe [subscript]=string, affectant ainsi une valeur directement à un index particulier du tableau. Cela fait en sorte qu'il peut y avoir deux types de tableaux, indexés numériquement et indexés par hachage (appelés tableaux associatifs dans le langage bash). Cela permet également de créer des tableaux peu nombreux indexés numériquement. L'abandon de la partie [subscript]= est un raccourci pour un tableau indexé numériquement, commençant par l'index ordinal de 0 et incrémenté à chaque nouvelle valeur de l'instruction d'affectation.

Par conséquent, ${array} devrait correspondre au tableau entier, aux index et à tous. Il convient d'évaluer à l'inverse de la déclaration d'affectation. Toute troisième année majeure en CS devrait le savoir. Dans ce cas, ce code fonctionnerait exactement comme vous le souhaiteriez:

declare -A foo bar
foo=${bar}

Ensuite, passer des tableaux par valeur à des fonctions et affecter un tableau à un autre fonctionnerait comme le reste de la syntaxe du shell. Mais comme ils ne l'ont pas bien fait, l'opérateur d'affectation = ne fonctionne pas pour les tableaux, et les tableaux ne peuvent pas être passés par valeur à des fonctions ou à des sous-shell ni à une sortie en général (echo ${array}) sans code pour passer à travers tout cela.

Ainsi, si cela avait été bien fait, l'exemple suivant montrerait en quoi l'utilité des tableaux en bash pourrait être considérablement meilleure:

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

le résultat devrait être:

(first=one second=2 third=3)

Ensuite, les tableaux pourraient utiliser l'opérateur d'affectation et être transmis, par valeur, à des fonctions et même à d'autres scripts Shell. Facilement stocké en sortie dans un fichier, et facilement chargé à partir d'un fichier dans un script.

declare -A foo
read foo <file

Hélas, nous avons été abandonnés par une équipe de développement bash, par ailleurs superlative.

En tant que tel, pour passer un tableau à une fonction, il n'y a vraiment qu'une seule option, utiliser la fonctionnalité 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

donnera le résultat suivant:

indexes: foo zoom
values: bar fast

Comme cela passe par référence, vous pouvez également affecter le tableau dans la fonction. Oui, le tableau référencé doit avoir une portée globale, mais cela ne devrait pas être un gros problème, étant donné qu’il s’agit de scripts Shell. Pour passer un tableau indexé associatif ou fragmenté valeur par valeur à une fonction, il faut jeter tous les index et toutes les valeurs dans la liste d'arguments (ce qui n'est pas très utile s'il s'agit d'un grand tableau) sous forme de chaînes uniques comme celle-ci:

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

puis en écrivant un tas de code dans la fonction pour réassembler le tableau.

20
tigerand

La réponse de DevSolar comporte un point que je ne comprends pas (peut-être a-t-il une raison spécifique de le faire, mais je ne peux pas en penser à un): il définit le tableau à partir des paramètres positionnels, élément par élément, itératif.

Un approuch plus facile serait

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}"
}

Exemple

$ foo=(dog cat bird)

$ aecho foo 1
cat
3
Steven Penny

Un moyen simple de passer plusieurs tableaux en paramètre consiste à utiliser une chaîne séparée par des caractères. Vous pouvez appeler votre script comme ceci:

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

Ensuite, vous pouvez l'extraire dans votre code comme ceci:

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

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

De cette façon, vous pouvez réellement passer plusieurs tableaux en tant que paramètres sans que cela ne soit obligatoirement les derniers paramètres.

2
Remy Cilia

Avec quelques astuces, vous pouvez réellement transmettre des paramètres nommés à des fonctions, ainsi que des tableaux.

La méthode que j'ai développée vous permet d'accéder aux paramètres passés à une fonction comme celle-ci:

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 d'autres termes, non seulement vous pouvez appeler vos paramètres par leurs noms (ce qui constitue un noyau plus lisible), vous pouvez en fait passer des tableaux (et des références à des variables - cette fonctionnalité ne fonctionne toutefois que dans le bash 4.3)! De plus, les variables mappées sont toutes dans la portée locale, tout comme $ 1 (et autres).

Le code qui fait que ce travail est assez léger et fonctionne à la fois dans bash 3 et bash 4 (ce sont les seules versions avec lesquelles je l’ai testé). Si vous êtes intéressé par d'autres astuces comme celle-ci qui rendent le développement avec bash beaucoup plus simple et agréable, vous pouvez jeter un oeil à mon Bash Infinity Framework , le code ci-dessous a été développé à cet effet.

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

Aussi laid que cela puisse paraître, voici une solution de contournement qui fonctionne tant que vous ne passez pas explicitement un tableau, mais une variable correspondant à un tableau:

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

Je suis sûr que quelqu'un peut proposer une implémentation plus claire de l'idée, mais j'ai trouvé que c'était une meilleure solution que de passer un tableau sous la forme "{array[@]"} puis d'y accéder en interne à l'aide de array_inside=("$@"). Cela devient compliqué quand il y a d'autres paramètres positionnels/getopts. Dans ces cas, j'ai d'abord dû déterminer, puis supprimer les paramètres non associés au tableau en utilisant une combinaison de shift et de la suppression d'éléments du tableau.

Une perspective puriste considère probablement cette approche comme une violation de la langue, mais, d'une manière pragmatique, cette approche m'a évité beaucoup de chagrin. Sur un sujet connexe, j’utilise également eval pour affecter un tableau construit en interne à une variable nommée en fonction d’un paramètre target_varname que je passe à la fonction:

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

J'espère que ça aide quelqu'un.

1
Blake Schultze

Juste pour ajouter à la réponse acceptée, car j’ai trouvé que cela ne fonctionnait pas bien si le contenu du tableau ressemblait à quelque chose comme:

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

Dans ce cas, chaque membre du tableau est divisé, le tableau que la fonction voit équivaut à:

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

Pour que ce cas fonctionne, la méthode que j'ai trouvée consiste à passer le nom de la variable à la fonction, puis à utiliser eval:

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

function RUN_COMMANDS

Juste mon 2 ©

1
AlvaroGMJ

Celui-ci fonctionne même avec des espaces:

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

Condition requise: Fonction permettant de rechercher une chaîne dans un tableau.
Ceci est une légère simplification de la solution de DevSolar dans la mesure où elle utilise les arguments passés plutôt que de les copier.

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