Décorateur de fonction Bash

10

En python, nous pouvons décorer des fonctions avec du code qui est automatiquement appliqué et exécuté par rapport aux fonctions.

Existe-t-il une fonctionnalité similaire dans bash?

Dans le script sur lequel je travaille actuellement, j'ai un passe-partout qui teste les arguments requis et quitte s'ils n'existent pas - et affiche certains messages si l'indicateur de débogage est spécifié.

Malheureusement, je dois réinsérer ce code dans chaque fonction et si je veux le changer, je devrai modifier chaque fonction.

Existe-t-il un moyen de supprimer ce code de chaque fonction et de l'appliquer à toutes les fonctions, comme les décorateurs en python?

nfarrar
la source

Réponses:

12

Ce serait beaucoup plus facile avec zshdes fonctions anonymes et un tableau associatif spécial avec des codes de fonction. Avec bashcependant, vous pourriez faire quelque chose comme:

decorate() {
  eval "
    _inner_$(typeset -f "$1")
    $1"'() {
      echo >&2 "Calling function '"$1"' with $# arguments"
      _inner_'"$1"' "$@"
      local ret=$?
      echo >&2 "Function '"$1"' returned with exit status $ret"
      return "$ret"
    }'
}

f() {
  echo test
  return 12
}
decorate f
f a b

Ce qui produirait:

Calling function f with 2 arguments
test
Function f returned with exit status 12

Cependant, vous ne pouvez pas appeler deux fois décorer pour décorer votre fonction.

Avec zsh:

decorate()
  functions[$1]='
    echo >&2 "Calling function '$1' with $# arguments"
    () { '$functions[$1]'; } "$@"
    local ret=$?
    echo >&2 "function '$1' returned with status $ret"
    return $ret'
Stéphane Chazelas
la source
Stéphane - est typesetnécessaire? Ne le déclarerait-il pas autrement?
mikeserv
@mikeserv, eval "_inner_$(typeset -f x)"crée _inner_xune copie exacte de l'original x(identique functions[_inner_x]=$functions[x]à zsh).
Stéphane Chazelas
Je comprends - mais pourquoi en avez-vous besoin de deux?
mikeserv
Vous avez besoin d' un contexte différent , sinon vous ne seriez pas en mesure de saisir les intérieurs de » return.
Stéphane Chazelas
1
Je ne vous y suis pas. Ma réponse est une tentative comme une carte proche de ce que je comprends être les décorateurs en python
Stéphane Chazelas
5

J'ai déjà discuté du comment et du pourquoi du fonctionnement des méthodes ci-dessous à plusieurs reprises auparavant, donc je ne le referai pas. Personnellement, mes propres favoris sur le sujet sont ici et ici .

Si vous n'êtes pas intéressé à lire cela, mais toujours curieux, comprenez simplement que les documents ici attachés à l'entrée de la fonction sont évalués pour l'expansion du shell avant que la fonction ne s'exécute, et qu'ils sont générés à nouveau dans l'état où ils étaient lorsque la fonction a été définie chaque fois que la fonction est appelée.

DÉCLARER

Vous avez juste besoin d'une fonction qui déclare d'autres fonctions.

_fn_init() { . /dev/fd/4 ; } 4<<INIT
    ${1}() { $(shift ; printf %s\\n "$@")
     } 4<<-REQ 5<<-\\RESET
            : \${_if_unset?shell will ERR and print this to stderr}
            : \${common_param="REQ/RESET added to all funcs"}
        REQ
            _fn_init $(printf "'%s' " "$@")
        RESET
INIT

EXÉCUTER

Ici, j'appelle _fn_inità me déclarer une fonction appelée fn.

set -vx
_fn_init fn \
    'echo "this would be command 1"' \
    'echo "$common_param"'

#OUTPUT#
+ _fn_init fn 'echo "this would be command 1"' 'echo "$common_param"'
shift ; printf %s\\n "$@"
++ shift
++ printf '%s\n' 'echo "this would be command 1"' 'echo "$common_param"'
printf "'%s' " "$@"
++ printf ''\''%s'\'' ' fn 'echo "this would be command 1"' 'echo "$common_param"'
#ALL OF THE ABOVE OCCURS BEFORE _fn_init RUNS#
#FIRST AND ONLY COMMAND ACTUALLY IN FUNCTION BODY BELOW#
+ . /dev/fd/4

    #fn AFTER _fn_init .dot SOURCES IT#
    fn() { echo "this would be command 1"
        echo "$common_param"
    } 4<<-REQ 5<<-\RESET
            : ${_if_unset?shell will ERR and print this to stderr}
            : ${common_param="REQ/RESET added to all funcs"}
        REQ
            _fn_init 'fn' \
               'echo "this would be command 1"' \
               'echo "$common_param"'
        RESET

OBLIGATOIRE

Si je veux appeler cette fonction, elle mourra à moins que la variable d'environnement ne _if_unsetsoit définie.

fn

#OUTPUT#
+ fn
/dev/fd/4: line 1: _if_unset: shell will ERR and print this to stderr

Veuillez noter l'ordre des traces du shell - non seulement l' fnéchec lorsqu'il est appelé quand _if_unsetn'est pas défini, mais il ne s'exécute jamais en premier lieu . C'est le facteur le plus important à comprendre lorsque vous travaillez avec des extensions de document ici - elles doivent toujours se produire en premier car elles le sont <<inputaprès tout.

L'erreur provient du /dev/fd/4fait que le shell parent évalue cette entrée avant de la transmettre à la fonction. C'est le moyen le plus simple et le plus efficace de tester l'environnement requis.

Quoi qu'il en soit, l'échec est facilement résolu.

_if_unset=set fn

#OUTPUT#
+ _if_unset=set
+ fn
+ echo 'this would be command 1'
this would be command 1
+ echo 'REQ/RESET added to all funcs'
REQ/RESET added to all funcs

SOUPLE

La variable common_paramest évaluée à une valeur par défaut en entrée pour chaque fonction déclarée par _fn_init. Mais cette valeur est également modifiable par toute autre qui sera également honorée par chaque fonction déclarée de la même manière. Je vais laisser les traces d'obus maintenant - nous n'entrerons dans aucun territoire inexploré ici ou quoi que ce soit.

set +vx
_fn_init 'fn' \
               'echo "Hi! I am the first function."' \
               'echo "$common_param"'
_fn_init 'fn2' \
               'echo "This is another function."' \
               'echo "$common_param"'
_if_unset=set ;

Ci-dessus, je déclare deux fonctions et définit _if_unset. Maintenant, avant d'appeler l'une ou l'autre fonction, je vais la désélectionner common_parampour que vous puissiez voir qu'ils la régleront eux-mêmes lorsque je les appellerai.

unset common_param ; echo
fn ; echo
fn2 ; echo

#OUTPUT#
Hi! I am the first function.
REQ/RESET added to all funcs

This is another function.
REQ/RESET added to all funcs

Et maintenant de la portée de l'appelant:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Mais maintenant, je veux que ce soit autre chose:

common_param="Our common parameter is now something else entirely."
fn ; echo 
fn2 ; echo

#OUTPUT#
Hi! I am the first function.
Our common parameter is now something else entirely.

This is another function.
Our common parameter is now something else entirely.

Et si je ne bouge pas _if_unset?

unset _if_unset ; echo
echo "fn:"
fn ; echo
echo "fn2:"
fn2 ; echo

#OUTPUT#
fn:
dash: 1: _if_unset: shell will ERR and print this to stderr

fn2:
dash: 1: _if_unset: shell will ERR and print this to stderr

RÉINITIALISER

Si vous devez réinitialiser l'état de la fonction à tout moment, cela se fait facilement. Il vous suffit de faire (depuis la fonction):

. /dev/fd/5

J'ai enregistré les arguments utilisés pour déclarer initialement la fonction dans le 5<<\RESETdescripteur de fichier d'entrée. Donc, l' .dotapprovisionnement dans le shell à tout moment répétera le processus qui l'a configuré en premier lieu. Tout cela est assez facile, vraiment et à peu près entièrement portable si vous êtes prêt à ignorer le fait que POSIX ne spécifie pas réellement les chemins de nœud du périphérique descripteur de fichier (qui sont une nécessité pour le shell .dot).

Vous pouvez facilement développer ce comportement et configurer différents états pour votre fonction.

PLUS?

Soit dit en passant, cela raye à peine la surface. J'utilise souvent ces techniques pour incorporer de petites fonctions d'aide déclarables à tout moment dans l'entrée d'une fonction principale - par exemple, pour des $@tableaux de position supplémentaires selon les besoins. En fait - comme je le crois, ce doit être quelque chose de très proche de cela que les obus d'ordre supérieur font de toute façon. Vous pouvez voir qu'ils sont très facilement nommés par programmation.

J'aime aussi déclarer une fonction de générateur qui accepte un type de paramètre limité et définit ensuite une fonction de brûleur à usage unique ou à portée limitée le long des lignes d'une lambda - ou d'une fonction en ligne - qui est tout simplement unset -felle-même lorsque à travers. Vous pouvez passer une fonction shell autour.

mikeserv
la source
Quel est l'avantage de cette complexité supplémentaire avec les descripteurs de fichiers par rapport à l'utilisation eval?
Stéphane Chazelas
@StephaneChazelas Il n'y a pas de complexité supplémentaire de mon point de vue. En fait, je le vois dans l'autre sens. En outre, la citation est beaucoup plus facile et .dotfonctionne avec des fichiers et des flux afin que vous ne rencontriez pas le même type de problèmes de liste d'arguments que vous pourriez autrement. Pourtant, c'est probablement une question de préférence. Je pense certainement que c'est plus propre - surtout quand vous vous évaluez à évaluer - c'est un cauchemar d'où je m'assois.
mikeserv
@StephaneChazelas Il y a cependant un avantage - et il est plutôt bon. L'évaluation initiale et la deuxième évaluation n'ont pas besoin d'être dos à dos avec cette méthode. Le document hérité est évalué en entrée, mais vous n'avez pas besoin de .dotvous approvisionner tant que vous n'êtes pas bon et prêt - ou jamais. Cela vous donne un peu plus de liberté pour tester ses évaluations. Et il offre la flexibilité de l'état sur l'entrée - qui peut être gérée autrement - mais c'est beaucoup moins dangereux de ce point de vue que ce n'est le cas eval.
mikeserv
2

Je pense qu'une façon d'imprimer des informations sur la fonction, lorsque vous

tester les arguments requis et quitter s'ils n'existent pas - et afficher certains messages

est de changer le bash intégré returnet / ou exitau début de chaque script (ou dans un fichier, que vous sourcez à chaque fois avant d'exécuter le programme). Alors vous tapez

   #!/bin/bash
   return () {
       if [ -z $1 ] ; then
           builtin return
       else
           if [ $1 -gt 0 ] ; then
                echo function ${FUNCNAME[1]} returns status $1 
                builtin return $1
           else
                builtin return 0
           fi
       fi
   }
   foo () {
       [ 1 != 2 ] && return 1
   }
   foo

Si vous exécutez cela, vous obtiendrez:

   function foo returns status 1

Cela peut être facilement mis à jour avec l'indicateur de débogage si vous en avez besoin, un peu comme ceci:

   #!/bin/bash
   VERBOSE=1
   return () {
       if [ -z $1 ] ; then
           builtin return
       else
           if [ $1 -gt 0 ] ; then
               [ ! -z $VERBOSE ] && [ $VERBOSE -gt 0 ] && echo function ${FUNCNAME[1]} returns status $1  
               builtin return $1
           else
               builtin return 0
           fi
       fi
    }    

Cette instruction way ne sera exécutée que lorsque la variable VERBOSE est définie (du moins c'est ainsi que j'utilise verbose dans mes scripts). Cela ne résout certainement pas le problème de la fonction de décoration, mais il peut afficher des messages au cas où la fonction retourne un état non nul.

De même, vous pouvez redéfinir exit, en remplaçant toutes les instances de return, si vous souhaitez quitter le script.

EDIT: Je voulais ajouter ici la façon dont j'utilise pour décorer les fonctions en bash, si j'en ai beaucoup et des imbriquées également. Quand j'écris ce script:

#!/bin/bash 
outer () { _
    inner1 () { _
        print "inner 1 command"
    }   
    inner2 () { _
        double_inner2 () { _
            print "double_inner1 command"
        } 
        double_inner2
        print "inner 2 command"
    } 
    inner1
    inner2
    inner1
    print "just command in outer"
}
foo_with_args () { _ $@
    print "command in foo with args"
}
echo command in body of script
outer
foo_with_args

Et pour la sortie, je peux obtenir ceci:

command in body of script
    outer: 
        inner1: 
            inner 1 command
        inner2: 
            double_inner2: 
                double_inner1 command
            inner 2 command
        inner1: 
            inner 1 command
        just command in outer
    foo_with_args: 1 2 3
        command in foo with args

Il peut être utile à quelqu'un qui a des fonctions et souhaite les déboguer, de voir dans quelle erreur de fonction s'est produite. Il est basé sur trois fonctions, qui peuvent être décrites ci-dessous:

#!/bin/bash 
set_indentation_for_print_function () {
    default_number_of_indentation_spaces="4"
    #                            number_of_spaces_of_current_function is set to (max number of inner function - 3) * default_number_of_indentation_spaces 
    #                            -3 is because we dont consider main function in FUNCNAME array - which is if your run bash decoration from any script,
    #                            decoration_function "_" itself and set_indentation_for_print_function.
    number_of_spaces_of_current_function=`echo ${#FUNCNAME[@]} | awk \
        -v default_number_of_indentation_spaces="$default_number_of_indentation_spaces" '
        { print ($1-3)*default_number_of_indentation_spaces}
        '`
    #                            actual indent is sum of default_number_of_indentation_spaces + number_of_spaces_of_current_function
    let INDENT=$number_of_spaces_of_current_function+$default_number_of_indentation_spaces
}
print () { # print anything inside function with proper indent
    set_indentation_for_print_function
    awk -v l="${INDENT:=0}" 'BEGIN {for(i=1;i<=l;i++) printf(" ")}' # print INDENT spaces before echo
    echo $@
}
_ () { # decorator itself, prints funcname: args
    set_indentation_for_print_function
    let INDENT=$INDENT-$default_number_of_indentation_spaces # we remove def_number here, because function has to be right from usual print
    awk -v l="${INDENT:=0}" 'BEGIN {for(i=1;i<=l;i++) printf(" ")}' # print INDENT spaces before echo
    #tput setaf 0 && tput bold # uncomment this for grey color of decorator
    [ $INDENT -ne 0 ] && echo "${FUNCNAME[1]}: $@" # here we avoid situation where decorator is used inside the body of script and not in the function
    #tput sgr0 # resets grey color
}

J'ai essayé de mettre autant que possible dans les commentaires, mais voici aussi la description: J'utilise la _ ()fonction comme décorateur, celui que je mets après la déclaration de toutes les fonctions: foo () { _. Cette fonction imprime le nom de la fonction avec l'indentation appropriée, en fonction de la profondeur de la fonction dans une autre fonction (en tant qu'indentation par défaut, j'utilise 4 nombres d'espaces). J'imprime généralement ceci en gris, pour le séparer de l'impression habituelle. Si la fonction doit être décorée avec ou sans arguments, on peut modifier l'avant-dernière ligne dans la fonction décorateur.

Afin d'imprimer quelque chose à l'intérieur de la fonction, j'ai introduit une print ()fonction qui imprime tout ce qui lui est transmis avec un retrait correct.

La fonction set_indentation_for_print_functionfait exactement ce qu'elle représente, calculant l'indentation à partir du ${FUNCNAME[@]}tableau.

Cette façon a quelques défauts, par exemple on ne peut pas passer d'options à printaimer echo, par exemple -nou -e, et aussi si la fonction retourne 1, elle n'est pas décorée. Et aussi pour les arguments, passés à printplus de la largeur du terminal, qui seront enveloppés à l'écran, on ne verra pas le retrait pour la ligne enveloppée.

La meilleure façon d'utiliser ces décorateurs est de les mettre dans un fichier séparé et dans chaque nouveau script pour source ce fichier source ~/script/hand_made_bash_functions.sh.

Je pense que la meilleure façon d'incorporer le décorateur de fonctions dans bash est d'écrire le décorateur dans le corps de chaque fonction. Je pense qu'il est beaucoup plus facile d'écrire une fonction à l'intérieur d'une fonction dans bash, car il a la possibilité de définir toutes les variables de manière globale, pas comme dans les langages orientés objet standard. Cela donne l'impression que vous mettez des étiquettes autour de votre code dans bash. Au moins, cela m'a aidé pour un débogage de scripts.

Nikiforov Alexander
la source
0

Pour moi, cela semble être le moyen le plus simple d'implémenter un motif de décoration à l'intérieur de bash.

#!/bin/bash

function decorator {
    if [ "${FUNCNAME[0]}" != "${FUNCNAME[2]}" ] ; then
        echo "Turn stuff on"
        #shellcheck disable=2068
        ${@}
        echo "Turn stuff off"
        return 0
    fi
    return 1
}

function highly_decorated {
    echo 'Inside highly decorated, calling decorator function'
    decorator "${FUNCNAME[0]}" "${@}" && return
    echo 'Done calling decorator, do other stuff'
    echo 'other stuff'
}

echo 'Running highly decorated'
# shellcheck disable=SC2119
highly_decorated
Antonia Stevens
la source
Pourquoi désactivez-vous ces avertissements ShellCheck? Ils semblent corrects (certainement l'avertissement SC2068 devrait être corrigé en citant "$@").
dimo414 il y a