web-dev-qa-db-fra.com

Existe-t-il un moyen d'éviter les arguments de position dans bash?

Je dois écrire une fonction en bash. La fonction prendra environ 7 arguments. Je sais que je peux appeler une fonction comme celle-ci:

Pour appeler une fonction avec des paramètres:

function_name $arg1 $arg2

Et je peux référencer mes paramètres comme celui-ci dans la fonction:

function_name () {
   echo "Parameter #1 is $1"
}

Ma question est, existe-t-il une meilleure façon de se référer aux paramètres à l'intérieur de la fonction? Puis-je éviter les choses $ 1, $ 2, $ 3, .... et utiliser simplement $ arg1, $ arg2, ...?

Existe-t-il une méthode appropriée pour cela ou dois-je réaffecter ces paramètres à d'autres variables à l'intérieur de la fonction? Par exemple.:

function_name () {
   $ARG1=$1
   echo "Parameter #1 is $ARG1"
}

Tout exemple serait très apprécié.

50
Bhushan

La façon la plus courante de procéder consiste à affecter les arguments aux variables locales de la fonction, à savoir:

copy() {
    local from=${1}
    local to=${2}

    # ...
}

Une autre solution peut être getopt - analyse des options de style.

copy() {
    local arg from to
    while getopts 'f:t:' arg
    do
        case ${arg} in
            f) from=${OPTARG};;
            t) to=${OPTARG};;
            *) return 1 # illegal option
        esac
    done
}

copy -f /tmp/a -t /tmp/b

Malheureusement, bash ne peut pas gérer les options longues qui seraient plus lisibles, à savoir:

copy --from /tmp/a --to /tmp/b

Pour cela, vous devez utiliser le programme externe getopt (qui, je pense, ne prend en charge les options longues que sur les systèmes GNU)) ou implémenter l'analyseur d'options longues à la main, à savoir:

copy() {
    local from to

    while [[ ${1} ]]; do
        case "${1}" in
            --from)
                from=${2}
                shift
                ;;
            --to)
                to=${2}
                shift
                ;;
            *)
                echo "Unknown parameter: ${1}" >&2
                return 1
        esac

        if ! shift; then
            echo 'Missing parameter argument.' >&2
            return 1
        fi
    done
}

copy --from /tmp/a --to /tmp/b

Voir aussi: en utilisant getopts dans le script shell bash pour obtenir des options de ligne de commande longues et courtes


Vous pouvez également être paresseux et simplement passer les `` variables '' comme arguments à la fonction, c'est-à-dire:

copy() {
    local "${@}"

    # ...
}

copy from=/tmp/a to=/tmp/b

et vous aurez ${from} et ${to} dans la fonction en tant que variables locales.

Notez simplement que le même problème que ci-dessous s'applique - si une variable particulière n'est pas transmise, elle sera héritée de l'environnement parent. Vous voudrez peut-être ajouter une "ligne de sécurité" comme:

copy() {
    local from to    # reset first
    local "${@}"

    # ...
}

pour vous assurer que ${from} et ${to} seront désactivés lorsqu'ils ne seront pas transmis.


Et si quelque chose très mauvais vous intéresse, vous pouvez également affecter les arguments comme variables globales lors de l'appel de la fonction, à savoir:

from=/tmp/a to=/tmp/b copy

Ensuite, vous pouvez simplement utiliser ${from} Et ${to} Dans la fonction copy(). Notez simplement que vous devez alors toujours passer tous les paramètres. Sinon, une variable aléatoire peut s'infiltrer dans la fonction.

from= to=/tmp/b copy   # safe
to=/tmp/b copy         # unsafe: ${from} may be declared elsewhere

Si vous avez bash 4.1 (je pense), vous pouvez également essayer d'utiliser des tableaux associatifs. Il vous permettra de passer des arguments nommés mais il sera laid. Quelque chose comme:

args=( [from]=/tmp/a [to]=/tmp/b )
copy args

Et puis dans copy(), vous devez saisir le tablea .

67
Michał Górny

Vous pouvez toujours faire passer des choses dans l'environnement:

#!/bin/sh
foo() {
  echo arg1 = $arg1
  echo arg2 = $arg2
}

arg1=banana arg2=Apple foo
8
William Pursell

Les fonctions shell ont un accès complet à toutes les variables disponibles dans leur portée d'appel, sauf pour les noms de variables qui sont utilisés comme variables locales à l'intérieur de la fonction elle-même. De plus, toute variable non locale définie dans une fonction est disponible à l'extérieur après l'appel de la fonction. Prenons l'exemple suivant:

A=aaa
B=bbb

echo "A=$A B=$B C=$C"

example() {
    echo "example(): A=$A B=$B C=$C"

    A=AAA
    local B=BBB
    C=CCC

    echo "example(): A=$A B=$B C=$C"
}

example

echo "A=$A B=$B C=$C"

Cet extrait a la sortie suivante:

A=aaa B=bbb C=
example(): A=aaa B=bbb C=
example(): A=AAA B=BBB C=CCC
A=AAA B=bbb C=CCC

L'inconvénient évident de cette approche est que les fonctions ne sont plus autonomes et que la définition d'une variable en dehors d'une fonction peut avoir des effets secondaires inattendus. Cela rendrait également les choses plus difficiles si vous vouliez passer des données à une fonction sans les affecter d'abord à une variable, car cette fonction n'utilise plus de paramètres de position.

La façon la plus courante de gérer cela est d'utiliser variables locales pour les arguments et toute variable temporaire dans une fonction:

example() {
   local A="$1" B="$2" C="$3" TMP="/tmp"

   ...
}

Cela évite de polluer l'espace de noms Shell avec des variables fonction-locales.

4
thkala

Tout ce que vous avez à faire est de nommer les variables en chemin vers l'appel de fonction.

function test() {
    echo $a
}

a='hello world' test
#prove variable didnt leak
echo $a .

enter image description here

Ce n'est pas seulement une fonction des fonctions, vous pouvez avoir cette fonction dans son propre script et appeler a='hello world' test.sh et cela fonctionnerait tout de même


Pour vous amuser un peu, vous pouvez combiner cette méthode avec des arguments de position (par exemple, vous créez un script et certains utilisateurs ne connaissent peut-être pas les noms des variables).
Heck, pourquoi ne pas laisser les valeurs par défaut pour ces arguments aussi? Bien sûr, petit pois facile!

function test2() {
    [[ -n "$1" ]] && local a="$1"; [[ -z "$a" ]] && local a='hi'
    [[ -n "$2" ]] && local b="$2"; [[ -z "$b" ]] && local b='bye'
    echo $a $b
}

#see the defaults
test2

#use positional as usual
test2 '' there
#use named parameter
a=well test2
#mix it up
b=one test2 Nice

#prove variables didnt leak
echo $a $b .

enter image description here

Notez que si test était son propre script, vous n'avez pas besoin d'utiliser le mot clé local.

2
Hashbrown

Je pense que j'ai une solution pour vous. Avec quelques astuces, vous pouvez réellement transmettre des paramètres nommés aux 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 que dans bash 4.3 cependant)! De plus, les variables mappées sont toutes dans la portée locale, tout comme $ 1 (et autres).

Le code qui fait ce travail est assez léger et fonctionne à la fois en bash 3 et bash 4 (ce sont les seules versions avec lesquelles je l'ai testé). Si vous êtes intéressé par plus de trucs comme celui-ci qui rendent le développement avec bash beaucoup plus agréable et plus facile, vous pouvez jeter un œil à 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

J'espérais personnellement voir une sorte de syntaxe comme

func(a b){
    echo $a
    echo $b
}

Mais comme ce n'est pas une chose, et que je vois pas mal de références à des variables globales (non sans la mise en garde des conflits de portée et de nommage), je partagerai mon approche.

Utiliser la fonction copy de réponse de Michal :

copy(){
    cp $from $to
}
from=/tmp/a
to=/tmp/b
copy

C'est mauvais, parce que from et to sont des mots si larges que n'importe quel nombre de fonctions pourraient l'utiliser. Vous pourriez rapidement vous retrouver avec un conflit de dénomination ou une "fuite" sur vos mains.

letter(){
    echo "From: $from"
    echo "To:   $to"
    echo
    echo "$1"
}

to=Emily
letter "Hello Emily, you're fired for missing two days of work."

# Result:
#   From: /tmp/a
#   To:   Emily

#   Hello Emily, you're fired for missing two days of work.

Mon approche consiste donc à les "nommer". Je nomme la variable après la fonction et la supprime une fois la fonction terminée. Bien sûr, je ne l'utilise que pour les valeurs facultatives qui ont des valeurs par défaut. Sinon, j'utilise simplement des arguments positionnels.

copy(){
    if [[ $copy_from ]] && [[ $copy_to ]]; then
        cp $copy_from $copy_to
        unset copy_from copy_to
    fi
}
copy_from=/tmp/a
copy_to=/tmp/b
copy # Copies /tmp/a to /tmp/b
copy # Does nothing, as it ought to
letter "Emily, you're 'not' re-hired for the 'not' bribe ;)"
# From: (no /tmp/a here!)
# To:

# Emily, you're 'not' re-hired for the 'not' bribe ;)

Je ferais un terrible patron ...


En pratique, mes noms de fonction sont plus élaborés que "copie" ou "lettre".

L'exemple le plus récent dans ma mémoire est get_input(), qui a gi_no_sort Et gi_Prompt.

  • gi_no_sort Est une valeur vraie/fausse qui détermine si les suggestions de fin sont triées ou non. Par défaut à true
  • gi_Prompt Est une chaîne qui est ... eh bien, cela va de soi. La valeur par défaut est "".

Les arguments réels que la fonction prend sont la source des "suggestions d'achèvement" susmentionnées pour l'invite d'entrée, et comme cette liste est tirée de $@ Dans la fonction, les "arguments nommés" sont facultatifs[1], et il n'y a aucun moyen évident de faire la distinction entre une chaîne signifiée comme un achèvement et un message booléen/d'invite, ou vraiment quoi que ce soit séparé par un espace dans bash, d'ailleurs[2]; la solution ci-dessus a fini par me sauver beaucoup de problèmes .

remarques:

  1. Ainsi, un shift et $1, $2, Etc. codés en dur sont hors de question.

  2. Par exemple. "0 Enter a command: {1..9} $(ls)" une valeur de 0, "Enter a command:" et un ensemble de 1 2 3 4 5 6 7 8 9 <directory contents>? Ou "0", "Enter", "a" Et "command:" Font également partie de cet ensemble? Bash assumera ce dernier, que cela vous plaise ou non.

1
Braden Best

Les arguments sont envoyés aux fonctions en tant que Tuple d'éléments individuels, ils n'ont donc pas de nom en tant que tel, juste des positions. cela permet des possibilités intéressantes comme ci-dessous, mais cela signifie que vous êtes coincé avec 1 $. $ 2, etc. quant à l'opportunité de les mapper à de meilleurs noms, la question se résume à la taille de la fonction et à la clarté de la lecture du code. si c'est complexe, alors mapper des noms significatifs ($ BatchID, $ FirstName, $ SourceFilePath) est une bonne idée. pour les trucs simples cependant, ce n'est probablement pas nécessaire. Je ne dérangerais certainement pas si vous utilisez des noms comme $ arg1.

maintenant, si vous voulez simplement renvoyer les paramètres en écho, vous pouvez les parcourir:

for $arg in "$@"
do
  echo "$arg"
done

juste un fait amusant; sauf si vous traitez une liste, vous êtes probablement intéressé par quelque chose de plus utile

0
Frank Thomas

c'est un sujet plus ancien, mais je voudrais quand même partager la fonction ci-dessous (nécessite bash 4). Il analyse les arguments nommés et définit les variables dans l'environnement de scripts. Assurez-vous simplement d'avoir des valeurs par défaut raisonnables pour tous les paramètres dont vous avez besoin. La déclaration d'exportation à la fin pourrait également être juste un eval. C'est génial en combinaison avec shift pour étendre les scripts existants qui prennent déjà quelques paramètres de position et vous ne voulez pas changer la syntaxe, mais ajoutez encore une certaine flexibilité.

parseOptions()
{
  args=("$@")
  for opt in "${args[@]}"; do
    if [[ ! "${opt}" =~ .*=.* ]]; then
      echo "badly formatted option \"${opt}\" should be: option=value, stopping..."
      return 1
    fi
    local var="${opt%%=*}"
    local value="${opt#*=}"
    export ${var}="${value}"
  done
  return 0
}
0
mkrzewic