Bash Function Decorator


10

In Python possiamo decorare le funzioni con il codice che viene automaticamente applicato ed eseguito contro le funzioni.

C'è qualche caratteristica simile in bash?

Nello script su cui sto attualmente lavorando, ho alcuni piatti che testano gli argomenti richiesti e escono se non esistono - e visualizzano alcuni messaggi se viene specificato il flag di debug.

Sfortunatamente devo reinserire questo codice in ogni funzione e se voglio cambiarlo, dovrò modificare ogni funzione.

C'è un modo per rimuovere questo codice da ogni funzione e averlo applicato a tutte le funzioni, in modo simile ai decoratori in Python?


Per convalidare gli argomenti delle funzioni potresti essere in grado di usare questo script che ho messo insieme di recente, almeno come punto di partenza.
dimo414,

Risposte:


12

Sarebbe molto più semplice con zshfunzioni anonime e un array associativo speciale con codici funzione. Con bashcomunque potresti fare qualcosa del tipo:

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

Che produrrebbe:

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

Tuttavia, non puoi chiamare decorare due volte per decorare due volte la tua funzione.

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'

Stephane - è typesetnecessario? Non lo dichiarerebbe altrimenti?
Mikeserv,

@mikeserv, eval "_inner_$(typeset -f x)"crea _inner_xuna copia esatta dell'originale x(come functions[_inner_x]=$functions[x]in zsh).
Stéphane Chazelas,

Lo capisco, ma perché ne hai bisogno di due?
Mikeserv,

Avete bisogno di un contesto diverso altrimenti non sarebbe in grado di cogliere le interiori s' return.
Stéphane Chazelas,

1
Non ti seguo lì. La mia risposta è un tentativo come una mappa ravvicinata di ciò che capisco che siano i decoratori di pitoni
Stéphane Chazelas,

5

Ho già discusso di come e come il modo in cui i metodi seguenti funzionano in diverse occasioni prima, quindi non lo farò più. Personalmente, i miei preferiti sull'argomento sono qui e qui .

Se non sei interessato a leggerlo, ma sei ancora curioso, basta capire che i documenti qui allegati all'input della funzione vengono valutati per l'espansione della shell prima dell'esecuzione della funzione e che vengono generati di nuovo nello stato in cui si trovavano quando la funzione è stata definita ogni volta che viene chiamata la funzione.

DICHIARARE

Hai solo bisogno di una funzione che dichiari altre funzioni.

_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

ESEGUIRLO

Qui invito _fn_inita dichiararmi una funzione chiamata 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

NECESSARIO

Se voglio chiamare questa funzione, morirà a meno che non _if_unsetsia impostata la variabile di ambiente .

fn

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

Notare l'ordine delle tracce della shell: non solo fnfallisce quando viene chiamato quando _if_unsetnon è impostato, ma non viene mai eseguito in primo luogo . Questo è il fattore più importante da capire quando si lavora con espansioni di documenti qui: devono sempre verificarsi prima perché, <<inputdopo tutto, lo sono .

L'errore deriva dal /dev/fd/4fatto che la shell padre sta valutando quell'input prima di consegnarlo alla funzione. È il modo più semplice ed efficiente per testare l'ambiente richiesto.

Ad ogni modo, l'errore è facilmente risolto.

_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

FLESSIBILE

La variabile common_paramviene valutata su un valore predefinito in input per ogni funzione dichiarata da _fn_init. Ma quel valore è anche mutevole per ogni altro che sarà onorato anche da ogni funzione dichiarata in modo simile. Lascerò le tracce della conchiglia ora - non entreremo in nessun territorio inesplorato qui o altro.

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 ;

Sopra dichiaro due funzioni e ho impostato _if_unset. Ora, prima di chiamare una delle due funzioni, deselezionerò in common_parammodo da poter vedere che si configureranno automaticamente quando le chiamo.

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

E ora dall'ambito del chiamante:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Ma ora voglio che sia completamente qualcos'altro:

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.

E se mi disinserissi _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

RIPRISTINA

Se è necessario ripristinare lo stato della funzione in qualsiasi momento, è possibile farlo facilmente. Devi solo fare (dall'interno della funzione):

. /dev/fd/5

Ho salvato gli argomenti utilizzati per dichiarare inizialmente la funzione nel 5<<\RESETdescrittore di file di input. In modo .dottale che nella shell in qualsiasi momento ripeterà il processo che lo ha impostato in primo luogo. È tutto abbastanza semplice, davvero e praticamente completamente portatile se si è disposti a trascurare il fatto che POSIX in realtà non specifica i percorsi dei nodi del dispositivo descrittore di file (che sono una necessità per la shell .dot).

È possibile espandere facilmente questo comportamento e configurare stati diversi per la propria funzione.

DI PIÙ?

Questo a malapena graffia la superficie. Uso spesso queste tecniche per incorporare in qualsiasi momento piccole funzioni di supporto dichiarabili nell'input di una funzione principale, ad esempio per ulteriori $@array posizionali, se necessario. In effetti - come credo, dev'essere qualcosa di molto vicino a ciò che fanno comunque i gusci di ordine superiore. Puoi vedere che sono nominati in modo molto programmatico.

Mi piace anche dichiarare una funzione del generatore che accetta un tipo limitato di parametro e quindi definisce una funzione del bruciatore monouso o comunque limitata dall'ambito lungo le linee di una lambda - o una funzione in linea - che è semplicemente unset -fse stessa quando attraverso. È possibile passare una funzione shell in giro.


Qual è il vantaggio di quella complessità in più con i descrittori di file rispetto all'uso eval?
Stéphane Chazelas,

@StephaneChazelas Non c'è complessità aggiunta dal mio punto di vista. In effetti, lo vedo al contrario. Inoltre, la quotazione è molto più semplice e .dotfunziona con file e stream in modo da non incorrere nello stesso tipo di problemi dell'elenco di argomenti che potresti altrimenti. Tuttavia, è probabilmente una questione di preferenza. Sicuramente penso che sia più pulito, specialmente quando entri in valutazione negativa - è un incubo da dove mi siedo.
Mikeserv,

@StephaneChazelas C'è un vantaggio però - ed è piuttosto buono. La valutazione iniziale e la seconda valutazione non devono necessariamente essere eseguite con questo metodo. Il documento ereditario viene valutato in base all'input, ma non è necessario procurarsi .dotfino a quando non si è bravi e pronti - o mai. Ciò ti consente un po 'più di libertà nel testare le sue valutazioni. E fornisce la flessibilità dello stato sull'input - che può essere gestito in altri modi - ma è molto meno pericoloso da quella prospettiva di quanto non lo sia eval.
mikeserv,

2

Penso che un modo per stampare informazioni sulla funzione, quando tu

testare gli argomenti richiesti e uscire se non esistono - e visualizzare alcuni messaggi

è cambiare bash builtin returne / o exitall'inizio di ogni script (o in qualche file, che si procede ogni volta prima di eseguire il programma). Quindi scrivi

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

Se esegui questo otterrai:

   function foo returns status 1

Questo può essere facilmente aggiornato con il flag di debug se necessario, un po 'come questo:

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

In questo modo l'istruzione verrà eseguita solo quando è impostata la variabile VERBOSE (almeno è così che uso verbose nei miei script). Certamente non risolve il problema della funzione di decorazione, ma può visualizzare messaggi nel caso in cui la funzione restituisca uno stato diverso da zero.

Allo stesso modo è possibile ridefinire exit, sostituendo tutte le istanze di return, se si desidera uscire dallo script.

EDIT: volevo aggiungere qui il modo in cui uso per decorare le funzioni in bash, se ne ho molte e anche quelle nidificate. Quando scrivo questo 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

E per l'output posso ottenere questo:

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

Può essere utile per qualcuno che ha funzioni e vuole eseguirne il debug, per vedere in quale errore di funzione si è verificato. Si basa su tre funzioni, che possono essere descritte di seguito:

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

Ho provato a mettere il più possibile nei commenti, ma qui è anche la descrizione: io uso _ ()la funzione come decoratore, quella che ho messo dopo la dichiarazione di ogni funzione: foo () { _. Questa funzione stampa il nome della funzione con il rientro corretto, a seconda della profondità della funzione in un'altra funzione (come rientro predefinito utilizzo 4 spazi). Di solito lo stampo in grigio, per separarlo dalla solita stampa. Se la funzione è necessaria per essere decorata con argomenti, o senza, si può modificare l'ultima riga nella funzione decoratore.

Per stampare qualcosa all'interno della funzione, ho introdotto la print ()funzione che stampa tutto ciò che gli viene passato con il rientro corretto.

La funzione set_indentation_for_print_functionfa esattamente ciò che rappresenta, calcolando il rientro ${FUNCNAME[@]}dall'array.

In questo modo presenta alcuni difetti, ad esempio non è possibile passare opzioni a cui printpiacere echo, ad esempio -no -e, e anche se la funzione restituisce 1, non è decorata. E anche per gli argomenti, passati a printuna larghezza superiore a quella del terminale, che verranno racchiusi sullo schermo, non si vedrà il rientro per la linea avvolta.

Il modo migliore di usare questi decoratori è metterli in un file separato e in ogni nuovo script per trovare questo file source ~/script/hand_made_bash_functions.sh.

Penso che il modo migliore per incorporare il decoratore di funzioni in bash sia scrivere il decoratore nel corpo di ogni funzione. Penso che sia molto più semplice scrivere la funzione all'interno della funzione in bash, perché ha l'opzione per impostare tutte le variabili globali, non come nei linguaggi orientati agli oggetti standard. Questo fa sì che tu stia mettendo etichette attorno al tuo codice in bash. Almeno questo mi ha aiutato per gli script di debug.



0

Per me questo sembra il modo più semplice per implementare un motivo decorativo all'interno di 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

Perché disabiliti questi avvisi ShellCheck? Sembrano corretti (sicuramente l'avvertimento SC2068 dovrebbe essere corretto citando "$@").
dimo414,

0

Faccio molta (forse troppo :)) metaprogrammazione in Bash e ho trovato i decoratori preziosi per la reimplementazione al volo del comportamento. La mia libreria bash-cache utilizza la decorazione per memorizzare in modo trasparente le funzioni di Bash con una cerimonia minima:

my_expensive_function() {
  ...
} && bc::cache my_expensive_function

Ovviamente non si bc::cachetratta solo di decorare, ma la decorazione sottostante si basa sulla bc::copy_functioncopia di una funzione esistente con un nuovo nome, in modo che la funzione originale possa essere sovrascritta con un decoratore.

# Given a name and an existing function, create a new function called name that
# executes the same commands as the initial function.
bc::copy_function() {
  local function="${1:?Missing function}"
  local new_name="${2:?Missing new function name}"
  declare -F "$function" &> /dev/null || {
    echo "No such function ${function}" >&2; return 1
  }
  eval "$(printf "%s()" "$new_name"; declare -f "$function" | tail -n +2)"
}

Ecco un semplice esempio di decoratore che timeè la funzione decorata, usando bc::copy_function:

time_decorator() {
  bc::copy_function "$1" "time_dec::${1}" || return
  eval "${1}() { time time_dec::${1} "'"\$@"; }'
}

demo:

$ slow() { sleep 2; echo done; }

$ time_decorator slow

$ $ slow
done

real    0m2.003s
user    0m0.000s
sys     0m0.002s
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.