Une fonction bash qui prend des arguments comme les autres langages?

17

J'ai une fonction bash pour définir $PATHcomme ça -

assign-path()
{
    str=$1
    # if the $PATH is empty, assign it directly.
    if [ -z $PATH ]; then
        PATH=$str;
    # if the $PATH does not contain the substring, append it with ':'.
    elif [[ $PATH != *$str* ]]; then
        PATH=$PATH:$str;
    fi
}

Mais le problème est que je dois écrire une fonction différente pour différentes variables (par exemple, une autre fonction pour le $CLASSPATHmême, assign-classpath()etc.). Je n'ai pas trouvé de moyen de passer l'argument à la fonction bash pour pouvoir y accéder par référence.

Ce serait mieux si j'avais quelque chose comme -

assign( bigstr, substr )
{
    if [ -z bigstr ]; then
        bigstr=substr;
    elif [[ bigstr != *str* ]]; then
        bigstr=bigstr:substr;
    fi
}

Une idée, comment réaliser quelque chose comme ci-dessus en bash?

ramgorur
la source
Quelles "autres langues"?
choroba
eh bien, j'ai essayé de dire si bash permet "passer par référence" comme dans c / java etc.
ramgorur
1
assign-path /abcne sera pas ajouter /abcà PATH si $ PATH contient déjà /abc/def, /abcd, /def/abcetc. Surtout vous ne pouvez pas ajouter /binsi PATH contient déjà /usr/bin.
miracle173
@ miracle173 - cela est vrai, ce que vous devez faire est divisé $PATHet un test negate contre vos arguments tels que : add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". Dans ma réponse, je le fais de cette façon avec autant d'arguments que vous le souhaitez IFS=:.
mikeserv
Lié au problème (d'instance) spécifique de l'ajout de valeurs aux listes séparées par deux-points: comment puis-je ajouter proprement  $PATH?  et Ajouter un répertoire $PATHs'il n'est pas déjà là (sur Super User ).
Scott

Réponses:

17

Dans bashvous pouvez utiliser ${!varname}pour développer la variable référencée par le contenu d'une autre. Par exemple:

$ var=hello
$ foo () { echo "${!1}"; }
$ foo var
hello

Depuis la page de manuel:

${!prefix*}
${!prefix@}
       Names matching prefix.  Expands to the names of variables whose names
       begin with prefix, separated by the first character of the IFS special
       variable.  When @ is used  and the expansion appears within double quotes,
       each variable name expands to a separate word.

De plus, pour définir une variable référencée par le contenu (sans les dangers de eval), vous pouvez utiliser declare. Par exemple:

$ var=target
$ declare "$var=hello"
$ echo "$target"
hello

Ainsi, vous pourriez écrire votre fonction comme ceci (attention car si vous utilisez declaredans une fonction, vous devez donner -gou la variable sera locale):

shopt -s extglob

assign()
{
  target=$1
  bigstr=${!1}
  substr=$2

  if [ -z "$bigstr" ]; then
    declare -g -- "$target=$substr"
  elif [[ $bigstr != @(|*:)$substr@(|:*) ]]; then
    declare -g -- "$target=$bigstr:$substr"
  fi
}

Et utilisez-le comme:

assign PATH /path/to/binaries

Notez que j'ai également corrigé un bogue où si substrest déjà une sous-chaîne d'un des membres séparés par deux points de bigstr, mais pas son propre membre, alors il ne serait pas ajouté. Par exemple, cela permettrait d'ajouter /binà une PATHvariable contenant déjà /usr/bin. Il utilise les extglobensembles pour correspondre soit au début / à la fin de la chaîne, soit à deux points, puis à toute autre chose. Sans extglob, l'alternative serait:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]
Graeme
la source
-gen declaren'est pas disponible dans l'ancienne version de bash, y a-t-il un moyen pour que je puisse rendre cette compatibilité descendante?
ramgorur
2
@ramgorur, vous pouvez l'utiliser exportpour le mettre dans votre environnement (au risque d'écraser quelque chose d'important) ou eval(divers problèmes dont la sécurité si vous ne faites pas attention). Si evalvous utilisez, vous devriez être d'accord si vous le faites comme eval "$target=\$substr". Si vous oubliez le \ , il exécutera potentiellement une commande s'il y a un espace dans le contenu de substr.
Graeme
9

Nouveau dans bash 4.3, est l' -noption de declare& local:

func() {
    local -n ref="$1"
    ref="hello, world"
}

var='goodbye world'
func var
echo "$var"

Cela s'imprime hello, world.

derobert
la source
Le seul problème avec namerefs dans Bash est que vous ne pouvez pas avoir un nameref (dans une fonction par exemple) référençant une variable (en dehors de la fonction) du même nom que le nameref lui-même. Cela peut être résolu pour la version 4.5.
Kusalananda
2

Vous pouvez utiliser evalpour définir un paramètre. Une description de cette commande peut être trouvée ici . L'utilisation suivante de evalest incorrecte:

faux(){
  eval 1 $ = 2 $
}

En ce qui concerne l'évaluation supplémentaire, evaldevez-vous utiliser

attribuer(){
  eval $ 1 = '$ 2'
}

Vérifiez les résultats de l'utilisation de ces fonctions:

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'xxx'
$ 
$ echo: $ X1:
: $ X2:
$ echo: $ X2:
: $ X3:
$ echo: $ X3:
: xxx:
$ 
$ faux Y $ X1
$ echo: $ Y:
: $ X3:
$ 
$ affecter Y $ X1
$ echo: $ Y:
: $ X2:
$ 
$ assign Y "monde bonjour"
$ echo: $ Y:
: hallo world:
$ # les éléments suivants peuvent être inattendus
$ affecter Z $ Y
$ echo ": $ Z:"
:Bonjour:
$ # vous devez donc citer le deuxième argument si c'est une variable
$ affecter Z "$ Y"
$ echo ": $ Z:"
: hallo world:

Mais vous pouvez atteindre votre objectif sans utiliser eval. Je préfère cette façon plus simple.

La fonction suivante effectue la substitution de la bonne manière (j'espère)

augmenter(){
  COURANT local = 1 $
  AUGMENT local = 2 $
  local NOUVEAU
  if [[-z $ CURRENT]]; ensuite
    NOUVEAU = $ AUGMENT
  elif [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT: *) || \
    ($ CURRENT = *: $ AUGMENT) || ($ CURRENT = *: $ AUGMENT: *))]]; ensuite
    NOUVEAU = $ COURANT: $ AUGMENT
  autre
    NOUVEAU = $ COURANT
    Fi
  écho "$ NOUVEAU"
}

Vérifiez la sortie suivante

augmenter / usr / bin / bin
/ usr / bin: / bin

augmenter / usr / bin: / bin / bin
/ usr / bin: / bin

augmenter / usr / bin: / bin: / usr / local / bin / bin
/ usr / bin: / bin: / usr / local / bin

augmenter / bin: / usr / bin / bin
/ bin: / usr / bin

augmenter / bin / bin
/poubelle


augmenter / usr / bin: / bin
/ usr / bin :: / bin

augmenter / usr / bin: / bin: / bin
/ usr / bin: / bin:

augmenter / usr / bin: / bin: / usr / local / bin: / bin
/ usr / bin: / bin: / usr / local / bin:

augmenter / bin: / usr / bin: / bin
/ bin: / usr / bin:

augmenter / bin: / bin
/poubelle:


augmenter: / bin
::/poubelle


augmenter "/ usr lib" "/ usr bin"
/ usr lib: / usr bin

augmenter "/ usr lib: / usr bin" "/ usr bin"
/ usr lib: / usr bin

Vous pouvez maintenant utiliser la augmentfonction de la manière suivante pour définir une variable:

PATH = `augmenter PATH / bin`
CLASSPATH = `augmenter CLASSPATH / bin`
LD_LIBRARY_PATH = `augmenter LD_LIBRARY_PATH / usr / lib`
miracle173
la source
Même votre déclaration d'évaluation est fausse. Ceci, par exemple: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - ferait écho à "OHNO! Avant d'affecter var. Vous pourriez" $ {v ## * [; "$ IFS"]} = '$ l' "pour vous assurer que la chaîne ne peut pas s'étendre à tout ce qui ne sera pas évalué avec le =.
mikeserv
@mikeserv merci pour votre commentaire mais je pense que ce n'est pas un exemple valable. Le premier argument du script d'affectation doit être un nom de variable ou une variable qui contient un nom de variable utilisé à gauche de l' =instruction d'affectation. Vous pouvez affirmer que mon script ne vérifie pas son argument. C'est vrai. Je ne vérifie même pas s'il y a un argument ou si le nombre d'arguments est valide. Mais c'était par intention. Le PO peut ajouter de tels contrôles s'il le souhaite.
miracle173
@mikeserv: Je pense que votre proposition de transformer silencieusement le premier argument en un nom de variable valide n'est pas une bonne idée: 1) une variable est définie / écrasée qui n'était pas prévue par l'utilisateur. 2) l'erreur est cachée à l'utilisateur de la fonction. Ce n'est jamais une bonne idée. Il faut simplement déclencher une erreur si cela se produit.
miracle173
@mikeserv: C'est intéressant quand on veut utiliser votre variable v(mieux sa valeur) comme deuxième argument de la fonction assign. Par conséquent, sa valeur doit se trouver du côté droit d'une affectation. Il est nécessaire de citer l'argument de la fonction assign. J'ai ajouté cette subtilité à mon message.
miracle173
Probablement vrai - et vous n'utilisez pas eval dans votre dernier exemple - ce qui est sage - donc cela n'a pas vraiment d'importance. Mais ce que je dis, c'est que tout code qui utilise eval et qui prend la saisie de l'utilisateur est intrinsèquement risqué - et si vous utilisiez votre exemple, je pourrais créer la fonction conçue pour changer mon chemin d'accès en rm mon chemin d'accès avec peu d'effort.
mikeserv
2

Avec quelques astuces, vous pouvez réellement transmettre des paramètres nommés aux fonctions, ainsi que des tableaux (testés dans bash 3 et 4).

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 framework Bash Infinity , 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'
niieani
la source
1
assign () 
{ 
    if [ -z ${!1} ]; then
        eval $1=$2
    else
        if [[ ${!1} != *$2* ]]; then
            eval $1=${!1}:$2
        fi
    fi
}

$ echo =$x=
==
$ assign x y
$ echo =$x=
=y=
$ assign x y
$ echo =$x=
=y=
$ assign x z
$ echo =$x=
=y:z=

Cela correspond-il?


la source
salut, j'ai essayé de le faire comme le vôtre, mais ne fonctionnant pas, désolé d'être un débutant. Pourriez-vous s'il vous plaît me dire ce qui ne va pas avec ce script ?
ramgorur
1
Votre utilisation de evalest susceptible d'exécuter des commandes arbitraires.
Chris Down
Vous avez une idée pour rendre les evallignes zhe plus sûres? Normalement, quand je pense avoir besoin d'eval, je décide de ne pas utiliser * sh et de passer à une autre langue à la place. D'un autre côté, en l'utilisant dans des scripts pour ajouter des entrées à certaines variables de type PATH, il s'exécutera avec des constantes de chaîne et non des entrées utilisateur ...
1
vous pouvez le faire en evaltoute sécurité - mais cela demande beaucoup de réflexion. Si vous essayez simplement de référencer un paramètre, vous voudriez faire quelque chose comme ceci: de eval "$1=\"\$2\""cette façon, lors de la eval'spremière passe, il n'évalue que 1 $ et la seconde, il vaut = "$ 2". Mais vous devez faire autre chose - ce n'est pas nécessaire ici.
mikeserv
En fait, mon commentaire ci-dessus est faux aussi. Vous devez le faire "${1##*[;"$IFS"]}=\"\$2\""- et même cela n'est accompagné d'aucune garantie. Ou eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". Ce n'est pas facile.
mikeserv
1

Les arguments nommés ne sont tout simplement pas comment la syntaxe de Bash a été conçue. Bash a été conçu pour être une amélioration itérative du shell Bourne. En tant que tel, il doit s'assurer que certaines choses fonctionnent autant que possible entre les deux coques. Donc, il n'est pas censé être plus facile de créer un script dans l'ensemble, il est juste censé être meilleur que Bourne tout en garantissant que si vous prenez un script d'un environnement Bourne, bashc'est aussi simple que possible. Ce n'est pas anodin car de nombreux obus traitent toujours Bourne comme une norme de facto. Étant donné que les gens écrivent leurs scripts pour être compatibles avec Bourne (pour cette portabilité), le besoin reste en vigueur et ne changera probablement jamais.

Vous feriez probablement mieux de regarder un script shell différent (comme pythonou quelque chose) entièrement si c'est possible. Si vous rencontrez les limites d'une langue, vous devez commencer à utiliser une nouvelle langue.

Bratchley
la source
Peut-être que tôt dans bashla vie, c'était vrai. Mais maintenant, des dispositions spécifiques sont prises. Les variables de référence complètes sont désormais disponibles dans bash 4.3- voir la réponse de Derobert .
Graeme
Et si vous regardez ici, vous verrez que vous pouvez faire ce genre de chose très facilement, même avec juste le code portable POSIX: unix.stackexchange.com/a/120531/52934
mikeserv
1

Avec la shsyntaxe standard (fonctionnerait dans bash, et pas seulement dans bash), vous pourriez faire:

assign() {
  eval '
    case :${'"$1"'}: in
      (::) '"$1"'=$2;;   # was empty, copy
      (*:"$2":*) ;;      # already there, do nothing
      (*) '"$1"'=$1:$2;; # otherwise, append with a :
    esac'
}

Comme pour les solutions utilisant bashdes declare, c'est sûr tant qu'il $1contient un nom de variable valide.

Stéphane Chazelas
la source
0

SUR LES ARGS NOMMÉES:

Cela se fait très simplement et bashn'est pas du tout requis - c'est le comportement d'affectation spécifié par POSIX de base via l'expansion des paramètres:

: ${PATH:=this is only assigned to \$PATH if \$PATH is null or unset}

Pour faire une démonstration dans le même esprit que @Graeme, mais de manière portable:

_fn() { echo "$1 ${2:-"$1"} $str" ; }

% str= ; _fn "${str:=hello}"
> hello hello hello

Et là, je ne fais que str=pour m'assurer qu'il a une valeur nulle, car l'expansion des paramètres a la protection intégrée contre la réaffectation de l'environnement shell en ligne si elle est déjà définie.

SOLUTION:

Pour votre problème spécifique, je ne pense pas que des arguments nommés soient nécessaires, bien qu'ils soient certainement possibles. Utilisez $IFSplutôt:

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- $PATH ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH= ; echo "${PATH:="$*"}" ; IFS=$oFS
}

Voici ce que j'obtiens lorsque je l'exécute:

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% dir="/some crazy/dir"
% p=`assign /usr/bin /usr/bin/new "$dir"`
% echo "$p" ; echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new

Remarquez qu'il n'a ajouté que les arguments qui n'étaient pas déjà inclus $PATHou qui ont précédé? Ou même qu'il a fallu plus d'un argument du tout? $IFSest pratique.

mikeserv
la source
salut, je n'ai pas suivi, pourriez-vous s'il vous plaît élaborer un peu plus? Merci.
ramgorur
Je le fais déjà ...
Encore
@ramgorur Quoi de mieux? Désolé, mais la vraie vie a fait irruption et cela m'a pris un peu plus de temps que prévu pour terminer la rédaction.
mikeserv
ici aussi, a succombé à la vraie vie. On dirait que beaucoup d'approches différentes pour coder cette chose, laissez-moi me donner un moment pour m'installer sur la meilleure.
ramgorur
@ramgorur - bien sûr, je voulais juste m'assurer que je ne vous ai pas laissé pendre. À ce sujet - vous choisissez ce que vous voulez, mec. Je ne dirai aucune des autres réponses que je peux voir offrir une solution aussi concise, portable ou aussi robuste assignqu'ici. Si vous avez des questions sur son fonctionnement, je serai ravi d'y répondre. Et au fait, si vous voulez vraiment des arguments nommés, vous voudrez peut-être regarder cette autre réponse de moi dans laquelle je montre comment déclarer une fonction nommée pour les arguments d'une autre fonction: unix.stackexchange.com/a/120531/52934
mikeserv
-2

Je ne trouve rien de tel que le rubis, le python, etc. mais cela me semble plus proche

foo() {
  BAR="$1"; BAZ="$2"; QUUX="$3"; CORGE="$4"
  ...
}

La lisibilité est meilleure à mon avis, 4 lignes sont excessives pour déclarer vos noms de paramètres. Ressemble également aux langues modernes.

Francis Bongiovanni
la source
(1) La question porte sur les fonctions shell. Votre réponse présente une fonction de coquille squelette. Au-delà de cela, votre réponse n'a rien à voir avec la question. (2) Vous pensez que cela augmente la lisibilité de prendre des lignes séparées et de les concaténer en une seule ligne, avec des points-virgules comme séparateurs? Je crois que vous l'avez à l'envers; que votre style sur une ligne est  moins lisible que le style sur plusieurs lignes.
Scott
Si la réduction des parties inutiles est le point, alors pourquoi les guillemets et les points-virgules? a=$1 b=$2 ...fonctionne aussi bien.
ilkkachu