Dekorator funkcji Bash

10

W Pythonie możemy dekorować funkcje kodem, który jest automatycznie stosowany i wykonywany względem funkcji.

Czy jest jakaś podobna funkcja w bash?

W skrypcie, nad którym aktualnie pracuję, mam pewne okno testowe, które testuje wymagane argumenty i kończy działanie, jeśli nie istnieją - i wyświetla niektóre komunikaty, jeśli określono flagę debugowania.

Niestety muszę ponownie wstawić ten kod do każdej funkcji i jeśli chcę go zmienić, będę musiał zmodyfikować każdą funkcję.

Czy istnieje sposób, aby usunąć ten kod z każdej funkcji i zastosować go do wszystkich funkcji, podobnie jak dekoratory w pythonie?

nfarrar
źródło
Do sprawdzania poprawności argumentów funkcji możesz użyć tego skryptu, który niedawno ściągnąłem, przynajmniej jako punkt wyjścia.
dimo414

Odpowiedzi:

12

Byłoby to o wiele łatwiejsze dzięki zshanonimowym funkcjom i specjalnej tablicy asocjacyjnej z kodami funkcji. Dzięki bashjednak można zrobić coś takiego:

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

Co da wynik:

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

Nie można jednak wywołać dekorowania dwa razy, aby dwukrotnie ozdobić swoją funkcję.

Z 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
źródło
Stephane - jest typesetkonieczny? Czy nie zadeklarowałby tego inaczej?
mikeserv
@mikeserv, eval "_inner_$(typeset -f x)"tworzy _inner_xjako dokładną kopię oryginału x(taki sam jak functions[_inner_x]=$functions[x]w zsh).
Stéphane Chazelas
Rozumiem - ale dlaczego w ogóle potrzebujesz dwóch?
mikeserv
Potrzebny jest inny kontekst, w przeciwnym razie nie będzie w stanie złapać wewnętrzne S” return.
Stéphane Chazelas
1
Nie podążam tam za tobą. Moja odpowiedź jest próbą dokładnej mapy tego, co rozumiem jako dekoratorów Pythona
Stéphane Chazelas
5

Już kilkakrotnie omawiałem sposoby i sposoby działania poniższych metod, więc nie zrobię tego ponownie. Osobiście moje ulubione na ten temat są tutaj i tutaj .

Jeśli nie jesteś zainteresowany czytaniem tego, ale nadal jesteś ciekawy, po prostu zrozum, że tutaj-dokumenty dołączone do danych wejściowych funkcji są oceniane pod kątem rozszerzenia powłoki przed uruchomieniem funkcji i że są generowane na nowo w stanie, w jakim były, gdy funkcja została zdefiniowana za każdym razem, gdy funkcja jest wywoływana.

OGŁOSIĆ

Potrzebujesz tylko funkcji, która deklaruje inne funkcje.

_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

URUCHOM

Wzywam tutaj _fn_initdo zadeklarowania funkcji o nazwie 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

WYMAGANY

Jeśli chcę wywołać tę funkcję, umrze, chyba że _if_unsetzostanie ustawiona zmienna środowiskowa .

fn

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

Zwróć uwagę na kolejność śladów powłoki - nie tylko fnbłąd kończy się, gdy _if_unsetjest wywoływany, gdy jest rozbrojony, ale nigdy nie działa w pierwszej kolejności . Jest to najważniejszy czynnik, który należy zrozumieć podczas pracy z rozszerzeniami dokumentu tutaj - muszą one zawsze występować jako pierwsze, ponieważ w końcu są <<input.

Błąd wynika z /dev/fd/4tego, że powłoka nadrzędna ocenia dane wejściowe przed przekazaniem ich do funkcji. Jest to najprostszy i najbardziej wydajny sposób testowania wymaganego środowiska.

Tak czy inaczej, awarię można łatwo naprawić.

_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

ELASTYCZNE

Zmienna common_paramjest przetwarzana na wartość domyślną na wejściu dla każdej funkcji zadeklarowanej przez _fn_init. Ale ta wartość jest również zmienna na każdą inną, która będzie również honorowana przez każdą funkcję podobnie zadeklarowaną. Zostawię teraz ślady pocisków - nie wchodzimy tutaj na żadne niezbadane terytorium ani nic.

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 ;

Powyżej deklaruję dwie funkcje i ustawiam _if_unset. Teraz, przed wywołaniem którejkolwiek z funkcji, rozbroję common_param, abyś mógł zobaczyć, że sami ją ustawią, kiedy je wywołam.

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

A teraz z zakresu dzwoniącego:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Ale teraz chcę, aby było to coś zupełnie innego:

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.

A jeśli się rozbroję _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

RESETOWANIE

Jeśli chcesz w dowolnym momencie zresetować stan funkcji, możesz to łatwo zrobić. Musisz tylko zrobić (z poziomu funkcji):

. /dev/fd/5

Zapisałem argumenty użyte do początkowej deklaracji funkcji w 5<<\RESETdeskryptorze pliku wejściowego. Więc .dotzaopatrywaniu że w powłoce w każdej chwili będzie powtórzyć proces, które wyróżniają go na pierwszym miejscu. Wszystko jest dość łatwe, naprawdę i prawie w pełni przenośne, jeśli chcesz przeoczyć fakt, że POSIX tak naprawdę nie określa ścieżek węzłów urządzeń deskryptorów plików (które są niezbędne dla powłoki .dot).

Możesz łatwo rozwinąć to zachowanie i skonfigurować różne stany dla swojej funkcji.

WIĘCEJ?

Nawiasem mówiąc, to ledwo rysuje powierzchnię. Często używam tych technik, aby osadzać małe funkcje pomocnicze, które można zadeklarować w dowolnym momencie na wejściu funkcji głównej - na przykład $@w razie potrzeby dla dodatkowych tablic pozycjonujących . W rzeczywistości - jak sądzę, musi to być coś bardzo zbliżonego do tego, co robią pociski wyższego rzędu. Widać, że są bardzo łatwo programowo nazwane.

Chciałbym również zadeklarować funkcję generatora, która akceptuje ograniczony typ parametru, a następnie definiuje funkcję palnika do jednorazowego użytku lub w inny sposób ograniczoną zakresem wzdłuż linii lambda - lub funkcji liniowej - która jest po prostu unset -fsama, gdy przez. Możesz przekazać funkcję powłoki.

mikeserv
źródło
Jaka jest zaleta dodatkowej złożoności deskryptorów plików w porównaniu do używania eval?
Stéphane Chazelas
@StephaneChazelas Z mojej perspektywy nie ma żadnej złożoności. W rzeczywistości widzę to na odwrót. Również cytowanie jest znacznie łatwiejsze i .dotdziała z plikami i strumieniami, więc nie napotykasz tego samego rodzaju problemów z listą argumentów, które w przeciwnym razie możesz mieć. Mimo to jest to prawdopodobnie kwestia preferencji. Z pewnością uważam, że jest czystszy - szczególnie gdy zaczynasz ewaluować eval - to koszmar z miejsca, w którym siedzę.
mikeserv
@StephaneChazelas Jest jednak jedna zaleta - i to całkiem niezła. Dzięki tej metodzie początkowa i druga ewaluacja nie muszą być ze sobą powiązane. Dokument hered jest oceniany na wejściu, ale nie musisz go szukać, .dotdopóki nie będziesz dobry i gotowy - ani nigdy. Umożliwia to nieco więcej swobody w testowaniu jego ocen. I zapewnia elastyczność stanu na wejściu - z którą można sobie poradzić na inne sposoby - ale z tej perspektywy jest o wiele mniej niebezpieczny niż jest eval.
mikeserv
2

Myślę, że jednym ze sposobów drukowania informacji o funkcji, kiedy ty

przetestuj wymagane argumenty i wyjdź, jeśli nie istnieją - i wyświetl niektóre komunikaty

jest zmiana wbudowanego bash returni / lub exitna początku każdego skryptu (lub w jakimś pliku, który za każdym razem pozyskujesz przed uruchomieniem programu). Więc piszesz

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

Jeśli to uruchomisz, otrzymasz:

   function foo returns status 1

W razie potrzeby można to łatwo zaktualizować za pomocą flagi debugowania, nieco tak:

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

W ten sposób instrukcja zostanie wykonana tylko wtedy, gdy ustawiona jest zmienna VERBOSE (przynajmniej tak używam verbose w moich skryptach). Z pewnością nie rozwiązuje problemu funkcji dekorowania, ale może wyświetlać komunikaty w przypadku, gdy funkcja zwraca stan niezerowy.

Podobnie możesz przedefiniować exit, zastępując wszystkie wystąpienia return, jeśli chcesz wyjść ze skryptu.

EDYCJA: Chciałem dodać tutaj sposób, w jaki używam do dekorowania funkcji w bash, jeśli mam ich wiele i zagnieżdżone. Kiedy piszę ten skrypt:

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

A dla danych wyjściowych mogę uzyskać:

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

Może być pomocny dla kogoś, kto ma funkcje i chce je debugować, aby zobaczyć, w którym wystąpił błąd funkcji. Opiera się na trzech funkcjach, które można opisać poniżej:

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

Starałem się umieścić jak najwięcej w komentarzach, ale tutaj jest też opis: Używam _ ()funkcji jako dekorator, jeden ja umieścić po deklaracji każdej funkcji: foo () { _. Ta funkcja wypisuje nazwę funkcji z odpowiednim wcięciem, w zależności od głębokości funkcji w innej funkcji (jako wcięcie domyślne używam 4 liczby spacji). Zwykle drukuję to na szaro, aby oddzielić to od zwykłego wydruku. Jeśli funkcja ma być dekorowana argumentami lub bez niej, można zmodyfikować przedostatnią linię w funkcji dekoratora.

Aby wydrukować coś wewnątrz funkcji, wprowadziłem print ()funkcję, która drukuje wszystko, co jest do niej przekazywane z odpowiednim wcięciem.

Funkcja set_indentation_for_print_functionrobi dokładnie to, co oznacza, obliczając wcięcie z ${FUNCNAME[@]}tablicy.

W ten sposób występują pewne wady, na przykład nie można przekazać opcji printpolubienia echo, np. -nLub -e, a także, jeśli funkcja zwraca 1, nie jest dekorowana. A także w przypadku argumentów przekazywanych na printwięcej niż szerokość terminala, które zostaną zawinięte na ekranie, nie będzie widać wcięcia dla zawiniętej linii.

Świetnym sposobem na użycie tych dekoratorów jest umieszczenie ich w osobnym pliku i w każdym nowym skrypcie, aby pobrać ten plik source ~/script/hand_made_bash_functions.sh.

Myślę, że najlepszym sposobem na włączenie dekoratora funkcji w bash jest napisanie dekoratora w treści każdej funkcji. Myślę, że o wiele łatwiej jest pisać funkcje wewnątrz funkcji w bash, ponieważ ma opcję ustawienia wszystkich zmiennych globalnych, a nie w standardowych językach obiektowych. To sprawia, że ​​umieszczasz etykiety wokół kodu w bash. Przynajmniej pomogło mi to w skryptach debugujących.

Nikiforov Alexander
źródło
0

Dla mnie jest to najprostszy sposób na wdrożenie wzoru dekoratora w 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
źródło
Dlaczego wyłączasz te ostrzeżenia ShellCheck? Wydają się poprawne (na pewno ostrzeżenie SC2068 powinno zostać naprawione przez cytowanie "$@").
dimo414
0

Dużo (być może za dużo :) wykonuję metaprogramowanie w Bash i znalazłem dekoratorów nieocenionych przy ponownym wdrażaniu zachowań w locie. Moja biblioteka pamięci podręcznej bash używa dekoracji do przezroczystego zapamiętywania funkcji Bash przy minimalnej ceremonii:

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

Oczywiście bc::cacherobi coś więcej niż tylko dekorowanie, ale bazowa dekoracja polega na bc::copy_functionskopiowaniu istniejącej funkcji do nowej nazwy, dzięki czemu oryginalną funkcję można zastąpić dekoratorem.

# 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)"
}

Oto prosty przykład dekoratora, który timejest dekorowaną funkcją, przy użyciu bc::copy_function:

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

Próbny:

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

$ time_decorator slow

$ $ slow
done

real    0m2.003s
user    0m0.000s
sys     0m0.002s
dimo414
źródło