Comment définir une fonction bash similaire à la fois

10

J'ai ces fonctions dans ~/.bashrc:

function guard() {
    if [ -e 'Gemfile' ]; then
    bundle exec guard "$@"
    else
    command guard "$@"
    fi
}
function rspec() {
    if [ -e 'Gemfile' ]; then
    bundle exec rspec "$@"
    else
    command rspec "$@"
    fi
}
function rake() {
    if [ -e 'Gemfile' ]; then
        bundle exec rake "$@"
    else
        command rake "$@"
    fi
}

Comme vous le voyez, ces fonctions sont très similaires. Je veux définir ces 3 fonctions à la fois. Y a-t-il un moyen de le faire?

environnement

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
fer à repasser
la source

Réponses:

8
$ cat t.sh
#!/bin/bash

for func in guard rspec rake; do
        eval "
        ${func}() {
                local foo=(command ${func})
                [ -e 'Gemfile' ] && foo=(bundle exec ${func})
                \"\${foo[@]}\" \"\$@\"
        }
        "
done

type guard rspec rake

.

$ ./t.sh
guard is a function
guard ()
{
    local foo=(command guard);
    [ -e 'Gemfile' ] && foo=(bundle exec guard);
    "${foo[@]}" "$@"
}
rspec is a function
rspec ()
{
    local foo=(command rspec);
    [ -e 'Gemfile' ] && foo=(bundle exec rspec);
    "${foo[@]}" "$@"
}
rake is a function
rake ()
{
    local foo=(command rake);
    [ -e 'Gemfile' ] && foo=(bundle exec rake);
    "${foo[@]}" "$@"
}

Les mises en garde habituelles evals'appliquent.

Adrian Frühwirth
la source
N'est-il pas mangé par le for loop?je veux dire, les variables déclarées dans un for loopdisparaissent généralement - je m'attendrais à la même chose pour les mêmes fonctions.
mikeserv
Qu'est ce qui te fait penser ça? bash -c 'for i in 1; do :; done; echo $i'=> 1. Le typemontre clairement que les fonctions existent en dehors de la portée de la boucle.
Adrian Frühwirth
1
@mikeserv Même avec bashl'étendue dynamique de, tout ce que vous pouvez obtenir est une localvariable locale à la portée d'une fonction entière , les variables ne "disparaissent" certainement pas après une boucle. En fait, puisqu'il n'y a pas de fonction impliquée ici, il n'est même pas possible de définir une localvariable dans ce cas.
Adrian Frühwirth
À droite - localement pour la boucle for - ils ont une portée locale. Ils disparaissent dès que leur shell de boucle parent le fait. Cela ne se produit-il pas ici?
mikeserv
Non, comme je viens de l'expliquer, il n'y a pas de concept comme "local à la boucle for" dans les scripts shell et mon message et l'exemple dans mon commentaire ci-dessus le montrent clairement.
Adrian Frühwirth
7
_gem_dec() { shift $# ; . /dev/fd/3
} 3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC
for func in guard rspec rake ; do _gem_dec $func ; done
echo "_guard ; _rspec ; _rake are all functions now."

La volonté ci-dessus . source /dev/fd/3qui est introduite dans la _gem_dec()fonction chaque fois qu'elle est appelée comme here-document. _gem_dec'stâche pré-évaluée uniquement doit recevoir un paramètre et la pré-évaluer à la fois comme bundle execcible et comme nom de la fonction dans laquelle elle est ciblée.

NOTE: . sourcing shell expansions results in twice-evaluated variables - just like eval. It can be risky.

Dans le cas ci-dessus cependant, je ne pense pas qu'il puisse y avoir de risque.

Si le bloc de code ci - dessus est copié dans un .bashrcfichier, non seulement les fonctions shell _guard(), _rspec()et_rake() être déclarés à la connexion, mais la _gem_dec()fonction sera également disponible pour l' exécution à tout moment à votre invite du shell (ou autre) et ainsi de nouvelles fonctions templated peut être déclaré à tout moment avec:

_gem_dec $new_templated_function_name

Et merci à @Andrew de m'avoir montré qu'ils ne seraient pas mangés par un for loop.

MAIS COMMENT?

J'utilise le 3descripteur de fichier ci-dessus pour rester stdin, stdout, and stderr, or <&0 >&1 >&2ouvert par habitude - bien que, comme c'est également le cas pour quelques-unes des autres précautions par défaut que j'implémente ici - parce que la fonction résultante est si simple, ce n'est vraiment pas nécessaire. C'est cependant une bonne pratique. L'appel shift $#est une autre de ces précautions inutiles.

Pourtant, lorsqu'un fichier est spécifié comme <inputou>output avec [optional num]<fileou [optional num]>fileredirection du noyau , il se lit dans un descripteur de fichier, accessible via les character devicefichiers spéciaux /dev/fd/[0-9]*. Si le [optional num]spécificateur est omis, il 0<fileest supposé pour l'entrée et 1>filepour la sortie. Considère ceci:

l='line %d\n' ; printf "$l" 1 2 3 4 5 6 >/dev/fd/1
> line 1
> line 2
> line 3
> line 4
> line 5
> line 6

( printf "$l" 4 5 6 >/dev/fd/3 ; printf "$l" 1 2 3 ) >/tmp/sample 3>/tmp/sample2

( cat /tmp/sample2 ) </tmp/sample
> line 4
> line 5
> line 6

( cat /dev/fd/0 ) </tmp/sample
> line 1
> line 2
> line 3

( cat /dev/fd/3 ) </tmp/sample 3</tmp/sample2
> line 4
> line 5
> line 6

Et parce que a here-documentn'est qu'un moyen de décrire un fichier en ligne dans un bloc de code, quand nous faisons:

<<'HEREDOC'
[$CODE]
HEREDOC

On pourrait aussi bien faire:

echo '[$CODE]' >/dev/fd/0

Avec une distinction très importante . Si vous ne faites pas "'\quote'"l' <<"'\LIMITER"'un here-documentpuis l'interpréteur de commandes évaluer shell $expansioncomme:

echo "[$CODE]" >/dev/fd/0

Donc, pour _gem_dec(), le 3<<-FUNC here-documentest évalué comme un fichier en entrée, le même que s'il l'était, 3<~/some.file sauf que parce que nous laissons le FUNClimiteur libre de guillemets, il est d'abord évalué pour $expansion.L'important à ce sujet est qu'il est en entrée, ce qui signifie il n'existe que pour _gem_dec(),mais il est également évalué avant l' _gem_dec()exécution de la fonction car notre shell doit lire et évaluer son $expansionsavant de le transmettre en entrée.

Permet guard,par exemple:

_gem_dec guard

Donc, le shell doit d'abord gérer l'entrée, ce qui signifie lire:

3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC

Dans le descripteur de fichier 3 et en l'évaluant pour l'expansion du shell. Si à ce moment vous avez couru:

cat /dev/fd/3

Ou:

cat <&3

Comme ce sont deux commandes équivalentes, vous verriez *:

_guard() { [ ! -e 'Gemfile' ] && { 
    command guard "$@" ; return $?
    } || bundle exec guard "$@"
}

... avant tout, aucun code de la fonction ne s'exécute. C'est la fonction <input, après tout. Pour plus d'exemples, voir ma réponse à une autre question ici .

(* Techniquement ce n'est pas tout à fait vrai. Parce que j'utilise un chef de file -dashavant here-doc limiter, le serait surtout justifié à gauche. Mais je l' -dashque je puisse <tab-insert>pour une meilleure lisibilité en premier lieu , donc je ne vais pas dépouiller les <tab-inserts>avant en vous l'offrant à lire ...)

La plus belle partie à ce sujet est la citation - notez que les '"citations restent et que seules les \citations ont été supprimées. C'est probablement pour cette raison plus que toute autre que si vous devez évaluer deux fois un shell, $expansionje recommanderai le here-documentcar les citations sont beaucoup plus faciles que eval.

Quoi qu'il en soit, le code ci-dessus est maintenant exactement comme un fichier alimenté, comme 3<~/heredoc.files'il attendait que la _gem_dec()fonction démarre et accepte son entrée /dev/fd/3.

Donc, lorsque nous commençons, _gem_dec()la première chose que je fais est de lancer tous les paramètres positionnels, car notre prochaine étape est une expansion du shell évaluée deux fois et je ne veux pas que le contenu $expansionssoit interprété comme l'un de mes $1 $2 $3...paramètres actuels . Donc je:

shift $#

shiftjette autant positional parametersque vous spécifiez et commence $1avec ce qui reste. Donc , si j'appelé _gem_dec one two threeà l'invite des _gem_dec's $1 $2 $3paramètres de position serait être one two threeet le nombre total actuel de position, ou $#serait 3. Si j'ai appelé alors shift 2,les valeurs de oneettwo serais shifted loin, la valeur $1changerait à threeet $#élargirais à 1. Alors shift $#que les jette tous. Faire cela est strictement préventif et n'est qu'une habitude que j'ai développée après avoir fait ce genre de chose pendant un certain temps. Le voici (subshell)un peu étalé pour plus de clarté:

( set -- one two three ; echo "$1 $2 $3" ; echo $# )
> one two three
> 3

( set -- one two three ; shift 2 ; echo "$1 $2 $3" ; echo $# )
> three
> 1

( set -- one two three ; shift $# ; echo "$1 $2 $3" ; echo $# )
>
> 0

Quoi qu'il en soit, la prochaine étape est celle où la magie opère. Si vous . ~/some.shà l'invite du shell, toutes les fonctions et variables d'environnement déclarées dans ~/some.shseront alors appelables à votre invite du shell. La même chose est vraie ici, sauf que nous . source le character devicefichier spécial pour notre descripteur de fichier, ou . /dev/fd/3- qui est où notre here-documentfichier en ligne a été tracé - et nous avons déclaré notre fonction. Et c'est comme ça que ça marche.

_guard

Fait maintenant tout ce que votre _guardfonction est censée faire.

Addenda:

Une excellente façon de dire enregistrez vos positions:

f() { . /dev/fd/3
} 3<<-ARGS
    args='${args:-"$@"}'
ARGS

ÉDITER:

Lorsque j'ai répondu à cette question pour la première fois, je me suis davantage concentré sur le problème de la déclaration d'un shell function()capable de déclarer d'autres fonctions qui persisteraient dans le $ENVrepassage actuel du shell que sur ce que le demandeur ferait avec lesdites fonctions persistantes. Depuis lors, je me suis rendu compte que ma solution initialement proposée offrait 3<<-FUNCla forme suivante:

3<<-FUNC
    _${1}() { 
        if [ -e 'Gemfile' ]; then
            bundle exec $1 "\$@"
        else 
            command _${1} "\$@"
    }
FUNC

N'aurait probablement pas fonctionné comme prévu pour le demandeur, car j'ai spécifiquement modifié le nom de la fonction déclarative $1à partir de _${1}laquelle, s'il était appelé comme _gem_dec guardpar exemple, entraînerait la _gem_decdéclaration d'une fonction nommée _guardpar opposition à juste guard.

Remarque: Un tel comportement est une question d'habitude pour moi - j'opère généralement sur la présomption que les fonctions du shell ne devraient occuper que les leurs_namespaceafin d'éviter leur intrusion dans lenamespaceshellcommandsproprement dit.

Ce n'est pas une habitude universelle, cependant, comme en témoigne l'utilisation du demandeur d' commandappeler $1.

Un examen plus approfondi me porte à croire ce qui suit:

Le demandeur veut que les fonctions shell soient nommées guard, rspec, or rakequi, une fois appelées, compileront à nouveau une rubyfonction du même nom dans laquelle ifle fichier Gemfileexiste en $PATH OU if Gemfile n'existe pas, la fonction shell devrait exécuter la rubyfonction du même nom.

Cela n'aurait pas fonctionné auparavant parce que j'ai également modifié le $1appelé par commandpour lire:

command _${1}

Ce qui n'aurait pas entraîné l'exécution de la rubyfonction que la fonction shell a compilée comme suit:

bundle exec $1

J'espère que vous pouvez voir (comme finalement je l'ai fait) qu'il semble que le demandeur n'utilise que commanddu tout pour spécifier indirectement namespacecar commandpréférera appeler un fichier exécutable $PATHsur une fonction shell du même nom.

Si mon analyse est correcte (comme j'espère que le demandeur le confirmera), alors ceci:

_${1}() { [ ! -e 'Gemfile' ] && { 
    command $1 "\$@" ; return \$?
    } || bundle exec $1 "\$@"
}

Devrait mieux satisfaire ces conditions à l'exception que l'appel guardà l'invite tentera uniquement d'exécuter un fichier exécutable dans $PATHnamed guardalors que l'appel _guardà l'invite vérifiera l' Gemfile'sexistence et compilera en conséquence ou exécutera l' guardexécutable dans $PATH. De cette manière namespaceest protégé et, au moins comme je le perçois, l'intention du demandeur est toujours remplie.

En fait, en supposant que notre fonction shell _${1}()et l'exécutable ${PATH}/${1}sont les deux seules façons dont notre shell pourrait interpréter un appel à l'un $1ou l' autre ou _${1}alors l'utilisation de commandla fonction est désormais entièrement redondante. Pourtant, je l'ai laissé rester car je n'aime pas faire la même erreur deux fois ... d'affilée de toute façon.

Si cela est inacceptable pour le demandeur et qu'il / elle préférerait supprimer _complètement le contenu, alors, dans sa forme actuelle, la modification _underscoredevrait être tout ce que le demandeur doit faire pour répondre à ses besoins tels que je les comprends.

Mis à part ce changement, j'ai également modifié la fonction pour utiliser &&et / ou|| les conditions de court-circuit du shell plutôt que la if/thensyntaxe d' origine . De cette façon , la commanddéclaration est évaluée uniquement du tout si Gemfilec'est pas $PATH. Cette modification nécessite toutefois l'ajout de return $?pour garantir que l' bundleinstruction n'est pas exécutée si l'événement Gemfilen'existe pas, mais la ruby $1fonction renvoie autre chose que 0.

Enfin, je dois noter que cette solution n'implémente que des constructions de shell portables. En d'autres termes, cela devrait produire des résultats identiques dans tout shell revendiquant la compatibilité POSIX. Bien qu'il serait, bien sûr, absurde pour moi de prétendre que tous les systèmes compatibles POSIX doivent gérer la ruby bundledirective, au moins les impératifs du shell qui l'appelle devraient se comporter de la même manière, que le shell appelant soit shou dash. De plus, ce qui précède fonctionnera comme prévu (en supposant au moins à mi-chemin de shoptstoute façon) dans les deux bashet zsh.

mikeserv
la source
Je mets votre code ~/.bashrcet j'appelle . ~/.bashrc, puis ces trois fonctions sont exécutées. Peut-être que le comportement diffère selon l'environnement, j'ai donc ajouté mon environnement à la question. De plus, je ne comprenais pas pourquoi la dernière ligne _guard ; _rspec ; _rakeétait nécessaire. J'ai recherché shiftet descripteur de fichier, il semble que cela dépasse ma compréhension actuelle.
ironsand
Je viens de mettre ça là pour montrer qu'ils étaient appelables. Désolé - j'ai mis un écho maintenant. Vous pouvez donc les appeler en tant que fonctions, comme vous l'avez démontré.
mikeserv
@Tetsu - est-ce plus logique maintenant?
mikeserv
J'ai lu votre réponse 3 fois, mais honnêtement, j'ai besoin de plus de connaissances pour comprendre l'explication. Même si je vous en suis très reconnaissant, je le relirai lorsque j'aurai plus d'expérience.
ironsand
@Tetsu Peut-être que c'est plus clair maintenant ...? Je pense avoir réalisé et corrigé une erreur que j'ai commise précédemment. Veuillez me le faire savoir, si vous le souhaitez.
mikeserv
2
function threeinone () {
    local var="$1"
    if [ $# -ne 1 ]; then
        return 1
    fi
    if ! [ "$1" = "guard" -o "$1" = "rspec" -o "$1" = "rake" ]; then
        return 1
    fi
    shift
    if [ -e 'Gemfile' ]; then
        bundle exec "$var" "$@"
    else
        command "$var" "$@"
    fi
}

threeinone guard
threeinone rspec
threeinone rake
Hauke ​​Laging
la source