Bash Function Decorator


10

In Python können wir Funktionen mit Code dekorieren, der automatisch angewendet und für Funktionen ausgeführt wird.

Gibt es eine ähnliche Funktion in Bash?

In dem Skript, an dem ich gerade arbeite, habe ich ein Boilerplate, das die erforderlichen Argumente testet und beendet, wenn sie nicht vorhanden sind - und einige Meldungen anzeigt, wenn das Debugging-Flag angegeben ist.

Leider muss ich diesen Code in jede Funktion erneut einfügen und wenn ich ihn ändern möchte, muss ich jede Funktion ändern.

Gibt es eine Möglichkeit, diesen Code aus jeder Funktion zu entfernen und auf alle Funktionen anzuwenden, ähnlich wie bei Dekoratoren in Python?

Antworten:


12

Das wäre viel einfacher mit zshanonymen Funktionen und einem speziellen assoziativen Array mit Funktionscodes. Mit bashjedoch könnte man so etwas machen wie:

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

Welches würde ausgeben:

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

Sie können nicht zweimal dekorieren aufrufen, um Ihre Funktion zweimal zu dekorieren.

Mit 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 - ist typesetnotwendig? Würde es es nicht anders erklären?
Mikeserv

@mikeserv, eval "_inner_$(typeset -f x)"erstellt _inner_xals exakte Kopie des Originals x(wie functions[_inner_x]=$functions[x]in zsh).
Stéphane Chazelas

Ich verstehe das - aber warum brauchst du überhaupt zwei?
Mikeserv

Sie brauchen einen anderen Kontext, sonst könnten Sie die inneren nicht erfassen return.
Stéphane Chazelas

1
Ich folge dir dort nicht. Meine Antwort ist ein Versuch als eine genaue Karte dessen, was ich unter Python-Dekorateuren verstehe
Stéphane Chazelas

5

Ich habe bereits mehrmals darüber gesprochen, wie und warum die folgenden Methoden funktionieren, damit ich es nicht noch einmal mache. Persönlich sind meine eigenen Favoriten zum Thema hier und hier .

Wenn Sie sich bei der Lektüre nicht interessiert , aber immer noch neugierig nur verstehen , dass die hier docs , die Funktion des Eingangs angebracht sind für Shell - Erweiterung ausgewertet , bevor die Funktion ausgeführt wird , und dass sie neu erzeugt in dem Zustand , sie waren , als die Funktion definiert wurde jedes Mal, wenn die Funktion aufgerufen wird.

ERKLÄREN

Sie benötigen lediglich eine Funktion, die andere Funktionen deklariert.

_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

STARTE ES

Hier rufe ich _fn_initauf, mir eine aufgerufene Funktion zu deklarieren 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

ERFORDERLICH

Wenn ich diese Funktion aufrufen möchte, stirbt sie, sofern die Umgebungsvariable nicht festgelegt _if_unsetist.

fn

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

Bitte beachten Sie die Reihenfolge der Shell-Traces - der fnFehler schlägt nicht nur fehl, wenn er aufgerufen wird, wenn er nicht festgelegt _if_unsetist, sondern er wird überhaupt nicht ausgeführt . Dies ist der wichtigste Faktor, den Sie bei der Arbeit mit Here-Document-Erweiterungen verstehen sollten - sie müssen immer zuerst auftreten, weil sie es schließlich sind <<input.

Der Fehler tritt auf, /dev/fd/4weil die übergeordnete Shell diese Eingabe auswertet, bevor sie an die Funktion übergeben wird. Dies ist die einfachste und effizienteste Methode zum Testen der erforderlichen Umgebung.

Auf jeden Fall kann der Fehler leicht behoben werden.

_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

FLEXIBEL

Die Variable common_paramwird bei der Eingabe für jede von deklarierte Funktion auf einen Standardwert ausgewertet _fn_init. Dieser Wert kann aber auch in jeden anderen geändert werden, der auch von jeder ähnlich deklarierten Funktion berücksichtigt wird. Ich werde jetzt die Spuren der Muschel weglassen - wir betreten hier kein Neuland oder so.

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 ;

Oben deklariere ich zwei Funktionen und setze _if_unset. Bevor ich eine der beiden Funktionen aufrufe, werde ich sie deaktivieren, common_paramdamit Sie sehen können, dass sie sie selbst einstellen, wenn ich sie aufrufe.

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

Und jetzt aus dem Bereich des Anrufers:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Aber jetzt möchte ich, dass es etwas ganz anderes ist:

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.

Und wenn ich abgesetzt habe _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

RESET

Wenn Sie den Status der Funktion jederzeit zurücksetzen müssen, ist dies problemlos möglich. Sie müssen nur tun (innerhalb der Funktion):

. /dev/fd/5

Ich habe die Argumente, mit denen die Funktion ursprünglich deklariert wurde, im 5<<\RESETEingabedateideskriptor gespeichert. So .dotSourcing , dass jederzeit in der Shell den Vorgang wiederholen , die es bis in die erste Stelle gesetzt. Es ist wirklich alles ziemlich einfach und ziemlich vollständig portierbar, wenn Sie die Tatsache übersehen möchten, dass POSIX die Dateikonstruktions-Geräteknotenpfade (die für die Shell erforderlich sind) nicht wirklich spezifiziert .dot.

Sie können dieses Verhalten leicht erweitern und verschiedene Zustände für Ihre Funktion konfigurieren.

MEHR?

Das kratzt übrigens kaum an der Oberfläche. Ich verwende diese Techniken oft, um kleine Hilfsfunktionen, die jederzeit deklarierbar sind, in die Eingabe einer Hauptfunktion einzubetten - zum Beispiel für zusätzliche Positionsarrays $@nach Bedarf. In der Tat - wie ich glaube, muss es etwas sehr Nahes sein, das die Muscheln höherer Ordnung sowieso tun. Sie können sehen, dass sie sehr einfach programmgesteuert benannt werden.

Ich deklariere auch gerne eine Generatorfunktion, die einen begrenzten Parametertyp akzeptiert und dann eine Einweg- oder anderweitig bereichsbegrenzte Brennerfunktion nach dem Vorbild eines Lambda - oder einer Inline-Funktion - definiert, die sich einfach unset -fselbst ergibt, wenn durch. Sie können eine Shell-Funktion weitergeben.


Was ist der Vorteil dieser zusätzlichen Komplexität bei Dateideskriptoren gegenüber der Verwendung eval?
Stéphane Chazelas

@StephaneChazelas Aus meiner Sicht gibt es keine zusätzliche Komplexität. Tatsächlich sehe ich es umgekehrt. Außerdem ist das Zitieren viel einfacher und .dotfunktioniert mit Dateien und Streams, sodass Sie nicht auf die gleichen Probleme mit Argumentlisten stoßen, die Sie sonst möglicherweise hätten. Trotzdem ist es wahrscheinlich eine Frage der Präferenz. Ich denke auf jeden Fall, dass es sauberer ist - besonders wenn man sich mit der Bewertung befasst - das ist ein Albtraum von meinem Platz aus.
Mikesserv

@StephaneChazelas Es gibt jedoch einen Vorteil - und es ist ein ziemlich guter. Die anfängliche Bewertung und die zweite Bewertung müssen bei dieser Methode nicht hintereinander erfolgen. Das Heredokument wird bei der Eingabe ausgewertet, aber Sie müssen es erst beschaffen, wenn Sie .dotgut und bereit sind - oder jemals. Dies ermöglicht Ihnen ein wenig mehr Freiheit beim Testen der Bewertungen. Und es bietet die Flexibilität des Zustands bei der Eingabe - die auf andere Weise gehandhabt werden kann -, aber aus dieser Perspektive ist es weitaus weniger gefährlich als es ist eval.
Mikesserv

2

Ich denke, eine Möglichkeit, Informationen über die Funktion zu drucken, wenn Sie

Testen Sie die erforderlichen Argumente und beenden Sie sie, wenn sie nicht vorhanden sind - und zeigen Sie einige Meldungen an

ist, bash returnund / oder exitam Anfang jedes Skripts (oder in einer Datei, die Sie jedes Mal vor dem Ausführen des Programms beziehen) zu ändern . Also tippst du

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

Wenn Sie dies ausführen, erhalten Sie:

   function foo returns status 1

Das kann leicht mit dem Debugging-Flag aktualisiert werden, wenn Sie es brauchen, ungefähr so:

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

Diese Anweisung wird nur ausgeführt, wenn die Variable VERBOSE gesetzt ist (zumindest verwende ich so ausführlich in meinen Skripten). Es löst sicherlich nicht das Problem der Dekorationsfunktion, kann jedoch Meldungen anzeigen, falls die Funktion den Status ungleich Null zurückgibt.

Ebenso können Sie neu definieren exit, indem Sie alle Instanzen von ersetzen return, wenn Sie das Skript beenden möchten.

EDIT: Ich wollte hier die Art und Weise hinzufügen, wie ich Funktionen in Bash dekoriere, wenn ich viele davon habe und auch verschachtelte. Wenn ich dieses Skript schreibe:

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

Und für die Ausgabe kann ich das bekommen:

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

Es kann für jemanden hilfreich sein, der über Funktionen verfügt und diese debuggen möchte, um festzustellen, in welchem ​​Funktionsfehler ein Fehler aufgetreten ist. Es basiert auf drei Funktionen, die im Folgenden beschrieben werden können:

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

Ich habe versucht, so viel wie möglich in Kommentare zu schreiben, aber hier ist auch die Beschreibung: Ich benutze die _ ()Funktion als Dekorateur, die ich nach der Deklaration jeder Funktion eingefügt habe : foo () { _. Diese Funktion druckt den Funktionsnamen mit der richtigen Einrückung, je nachdem, wie tief die Funktion in einer anderen Funktion ist (als Standardeinrückung verwende ich 4 Leerzeichen). Normalerweise drucke ich dies in Grau, um dies vom üblichen Druck zu trennen. Wenn die Funktion mit oder ohne Argumente dekoriert werden muss, kann die vorletzte Zeile in der Dekorationsfunktion geändert werden.

Um etwas innerhalb der Funktion zu drucken, habe ich eine print ()Funktion eingeführt , die alles, was an sie übergeben wird, mit dem richtigen Einzug druckt.

Die Funktion set_indentation_for_print_functionmacht genau das, wofür sie steht, und berechnet die Einrückung aus dem ${FUNCNAME[@]}Array.

Dieser Weg hat einige Fehler, zum Beispiel kann man keine Optionen übergeben, um sie zu printmögen echo, zB -noder -e, und auch wenn die Funktion 1 zurückgibt, ist sie nicht dekoriert. Und auch für Argumente, die an printmehr als die Terminalbreite übergeben werden und auf dem Bildschirm umbrochen werden, wird der Einzug für die umbrochene Zeile nicht angezeigt.

Die beste Möglichkeit, diese Dekoratoren zu verwenden, besteht darin, sie in einer separaten Datei und in jedem neuen Skript abzulegen, um diese Datei zu erstellen source ~/script/hand_made_bash_functions.sh.

Ich denke, der beste Weg, den Funktionsdekorator in bash zu integrieren, besteht darin, den Dekorator in den Körper jeder Funktion zu schreiben. Ich denke, es ist viel einfacher, Funktionen innerhalb von Funktionen in Bash zu schreiben, da es die Option gibt, alle Variablen global festzulegen, nicht wie in objektorientierten Standardsprachen. Das macht es so, als würden Sie in bash Beschriftungen um Ihren Code setzen. Zumindest hat mir das beim Debuggen von Skripten geholfen.



0

Für mich ist dies der einfachste Weg, ein Dekorationsmuster in bash zu implementieren.

#!/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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.