Decorador de funciones bash

10

En python podemos decorar funciones con código que se aplica y ejecuta automáticamente contra funciones.

¿Hay alguna característica similar en bash?

En la secuencia de comandos en la que estoy trabajando actualmente, tengo algunas repeticiones que prueban los argumentos necesarios y salen si no existen, y muestran algunos mensajes si se especifica el indicador de depuración.

Desafortunadamente, tengo que reinsertar este código en cada función y si quiero cambiarlo, tendré que modificar cada función.

¿Hay alguna forma de eliminar este código de cada función y aplicarlo a todas las funciones, de forma similar a los decoradores en python?

nfarrar
fuente

Respuestas:

12

Eso sería mucho más fácil con zshfunciones anónimas y una matriz asociativa especial con códigos de función. Sin bashembargo, podrías hacer algo como:

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

Lo que daría salida:

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

Sin embargo, no puede llamar a decorar dos veces para decorar su función dos veces.

Con 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
fuente
Stephane: ¿es typesetnecesario? ¿No lo declararía de otra manera?
mikeserv
@mikeserv, eval "_inner_$(typeset -f x)"crea _inner_xcomo una copia exacta del original x(igual que functions[_inner_x]=$functions[x]en zsh).
Stéphane Chazelas
Lo entiendo, pero ¿por qué necesitas dos?
mikeserv
Es necesario un contexto diferente de lo contrario no sería capaz de atrapar a los internos 's return.
Stéphane Chazelas
1
No te sigo allí. Mi respuesta es un intento como un mapa cercano de lo que entiendo que son los decoradores de pitón
Stéphane Chazelas
5

Ya he discutido los cómo y los porqués de la forma en que funcionan los métodos a continuación en varias ocasiones antes, así que no lo volveré a hacer. Personalmente, mis favoritos sobre el tema están aquí y aquí .

Si no está interesado en leer eso, pero todavía tiene curiosidad, comprenda que los documentos adjuntos a la entrada de la función se evalúan para la expansión del shell antes de que se ejecute la función, y que se generan nuevamente en el estado en que estaban cuando se definió la función cada vez que se llama a la función.

DECLARAR

Solo necesita una función que declare otras funciones.

_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

EJECUTARLO

Aquí llamo _fn_initpara declararme una función llamada 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

NECESARIO

Si quiero llamar a esta función, morirá a menos que se establezca la variable de entorno _if_unset.

fn

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

Tenga en cuenta el orden de los rastreos de shell: no solo fnfalla cuando se llama cuando no _if_unsetestá configurado, sino que nunca se ejecuta en primer lugar . Este es el factor más importante para entender cuando se trabaja con expansiones de documentos aquí: siempre deben ocurrir primero porque <<inputdespués de todo lo son .

El error proviene de /dev/fd/4que el shell principal está evaluando esa entrada antes de pasarla a la función. Es la forma más simple y eficiente de probar el entorno requerido.

De todos modos, la falla se remedia fácilmente.

_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

FLEXIBLE

La variable common_paramse evalúa a un valor predeterminado en la entrada para cada función declarada por _fn_init. Pero ese valor también se puede cambiar a cualquier otro que también sea honrado por cada función declarada de manera similar. Ahora dejaré los rastros de la cáscara: no vamos a entrar en ningún territorio desconocido aquí ni nada.

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 ;

Arriba declaro dos funciones y conjunto _if_unset. Ahora, antes de llamar a cualquiera de las funciones, lo desarmaré common_parampara que pueda ver que lo configurarán ellos mismos cuando los llame.

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

Y ahora desde el alcance de la persona que llama:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Pero ahora quiero que sea algo completamente distinto:

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.

¿Y si me desarmo _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

REINICIAR

Si necesita restablecer el estado de la función en cualquier momento, puede hacerlo fácilmente. Solo necesita hacer (desde dentro de la función):

. /dev/fd/5

Guardé los argumentos utilizados para declarar inicialmente la función en el 5<<\RESETdescriptor de archivo de entrada. Por lo tanto, la .dotfuente en el shell en cualquier momento repetirá el proceso que lo configuró en primer lugar. Todo es bastante fácil, realmente y prácticamente totalmente portátil si está dispuesto a pasar por alto el hecho de que POSIX no especifica realmente las rutas de nodo del dispositivo descriptor de archivos (que son una necesidad para el shell .dot).

Podría expandir fácilmente este comportamiento y configurar diferentes estados para su función.

¿MÁS?

Esto apenas rasca la superficie, por cierto. A menudo uso estas técnicas para incrustar pequeñas funciones auxiliares declarables en cualquier momento en la entrada de una función principal, por ejemplo, para $@matrices posicionales adicionales según sea necesario. De hecho, como creo, debe ser algo muy parecido a esto que los proyectiles de orden superior hacen de todos modos. Puedes ver que son muy fáciles de nombrar mediante programación.

También me gusta declarar una función generadora que acepte un tipo limitado de parámetro y luego defina una función de quemador de un solo uso o limitada por el alcance a lo largo de las líneas de una lambda, o una función en línea, que simplemente unset -fes en sí misma cuando mediante. Puede pasar una función de shell.

mikeserv
fuente
¿Cuál es la ventaja de esa complejidad adicional con los descriptores de archivo sobre el uso eval?
Stéphane Chazelas
@StephaneChazelas No hay complejidad adicional desde mi perspectiva. De hecho, lo veo al revés. Además, la cita es mucho más fácil y .dotfunciona con archivos y secuencias para que no se encuentre con el mismo tipo de problemas de listas de argumentos que de otro modo podría tener. Aún así, es probablemente una cuestión de preferencia. Ciertamente creo que es más limpio, especialmente cuando te pones a evaluar eval, es una pesadilla desde donde estoy sentado.
mikeserv
@StephaneChazelas Sin embargo, hay una ventaja, y es bastante buena. La evaluación inicial y la segunda evaluación no necesitan ser consecutivas con este método. El documento aquí se evalúa en la entrada, pero no tiene que buscarlo .dothasta que esté bien y listo, o nunca. Esto le permite un poco más de libertad para probar sus evaluaciones. Y proporciona la flexibilidad del estado en la entrada, que se puede manejar de otras maneras, pero es mucho menos peligroso desde esa perspectiva de lo que es eval.
mikeserv
2

Creo que una forma de imprimir información sobre la función, cuando

pruebe los argumentos necesarios y salga si no existen, y muestre algunos mensajes

es cambiar bash incorporado returny / o exital comienzo de cada script (o en algún archivo, que obtiene cada vez antes de ejecutar el programa). Entonces escribes

   #!/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 ejecuta esto obtendrá:

   function foo returns status 1

Eso puede actualizarse fácilmente con el indicador de depuración si lo necesita, algo así:

   #!/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
    }    

De esta manera, la declaración se ejecutará solo cuando se establezca la variable VERBOSE (al menos así es como uso verbose en mis scripts). Ciertamente no resuelve el problema de la función de decoración, pero puede mostrar mensajes en caso de que la función devuelva un estado distinto de cero.

Del mismo modo, puede redefinir exit, reemplazando todas las instancias de return, si desea salir del script.

EDITAR: quería agregar aquí la forma en que uso para decorar funciones en bash, si tengo muchas de ellas y también anidadas. Cuando escribo este 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

Y para la salida puedo obtener esto:

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

Puede ser útil para alguien que tiene funciones y quiere depurarlas, para ver en qué función se produjo el error. Se basa en tres funciones, que pueden describirse a continuación:

#!/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
}

Traté de poner lo más posible en los comentarios, pero aquí está también la descripción: Consumo _ ()funcionan como decorador, la puse después de la declaración de cada función: foo () { _. Esta función imprime el nombre de la función con la sangría adecuada, dependiendo de qué tan profunda sea la función en otra función (como sangría predeterminada utilizo 4 números de espacios). Normalmente imprimo esto en gris, para separar esto de la impresión habitual. Si se necesita decorar la función con argumentos, o sin ella, se puede modificar la última línea en la función decoradora.

Para imprimir algo dentro de la función, introduje una print ()función que imprime todo lo que se le pasa con la sangría adecuada.

La función set_indentation_for_print_functionhace exactamente lo que representa, calculando la sangría de la ${FUNCNAME[@]}matriz.

De esta manera tiene algunos defectos, por ejemplo, uno no puede pasar las opciones a printgustar echo, por ejemplo , -no -e, y también si la función devuelve 1, no está decorado. Y también para argumentos, pasados ​​a printmás del ancho del terminal, que se ajustarán en la pantalla, uno no verá la sangría para la línea ajustada.

La mejor manera de usar estos decoradores es colocarlos en un archivo separado y en cada nuevo script para obtener este archivo source ~/script/hand_made_bash_functions.sh.

Creo que la mejor manera de incorporar el decorador de funciones en bash es escribir el decorador en el cuerpo de cada función. Creo que es mucho más fácil escribir la función dentro de la función en bash, porque tiene la opción de establecer todas las variables globales, no como en los lenguajes orientados a objetos estándar. Eso hace que parezca que estás poniendo etiquetas alrededor de tu código en bash. Al menos eso me ayudó para los scripts de depuración.

Nikiforov Alexander
fuente
0

Para mí, esto se siente como la forma más sencilla de implementar un patrón de decorador dentro 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
fuente