Bash Function Decorator

10

Em python, podemos decorar funções com código que é automaticamente aplicado e executado em funções.

Existe algum recurso semelhante no bash?

No script em que estou trabalhando, tenho alguns boilerplate que testam os argumentos necessários e saem se eles não existirem - e exibem algumas mensagens se o sinalizador de depuração for especificado.

Infelizmente, tenho que reinserir esse código em todas as funções e, se quiser alterá-lo, terei que modificar todas as funções.

Existe uma maneira de remover esse código de cada função e aplicá-lo a todas as funções, semelhante aos decoradores em python?

nfarrar
fonte

Respostas:

12

Isso seria muito mais fácil, zshpois possui funções anônimas e uma matriz associativa especial com códigos de função. No bashentanto, você pode fazer 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

Qual seria a saída:

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

Você não pode chamar decorar duas vezes para decorar sua função duas vezes.

Com 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
fonte
Stephane - é typesetnecessário? Não o declararia de outra forma?
mikeserv
@mikeserv, eval "_inner_$(typeset -f x)"cria _inner_xcomo uma cópia exata do original x(igual a functions[_inner_x]=$functions[x]em zsh).
Stéphane Chazelas
Entendi - mas por que você precisa de duas?
precisa saber é o seguinte
Você precisa de um contexto diferente caso contrário você não seria capaz de pegar os internos s' return.
Stéphane Chazelas
1
Eu não sigo você até lá. Minha resposta é uma tentativa como um mapa perto do que eu entendo decoradores python para ser
Stéphane Chazelas
5

Eu já discuti como e por que os métodos abaixo funcionam em várias ocasiões antes, então não o farei novamente. Pessoalmente, meus próprios favoritos no tópico estão aqui e aqui .

Se você não estiver interessado em ler isso, mas ainda assim curioso, entenda que os documentos aqui anexados à entrada da função são avaliados quanto à expansão do shell antes da execução da função e que eles são gerados novamente no estado em que estavam quando a função foi definida toda vez que a função é chamada.

DECLARAR

Você só precisa de uma função que declare outras funções.

_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

EXECUTÁ-LO

Aqui apelo _fn_initpara me declarar uma função chamada 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

REQUERIDOS

Se eu quiser chamar essa função, ela morrerá, a menos que a variável de ambiente _if_unsetesteja definida.

fn

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

Observe a ordem dos rastreamentos do shell - não apenas fnfalha quando chamada quando _if_unsetestá desconfigurada, mas nunca é executada em primeiro lugar . Esse é o fator mais importante a ser entendido ao trabalhar com expansões de documentos aqui - elas sempre devem ocorrer primeiro porque são <<inputafinal.

O erro ocorre /dev/fd/4porque o shell pai está avaliando essa entrada antes de entregá-la à função. É a maneira mais simples e eficiente de testar o ambiente necessário.

De qualquer forma, a falha é facilmente remediada.

_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

FLEXÍVEL

A variável common_paramé avaliada como um valor padrão na entrada para cada função declarada por _fn_init. Mas esse valor também pode ser alterado por qualquer outro que também seja respeitado por todas as funções declaradas da mesma forma. Vou deixar de fora os traços de concha agora - não estamos entrando em nenhum território desconhecido aqui ou algo assim.

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 ;

Acima, declaro duas funções e defino _if_unset. Agora, antes de chamar qualquer uma das funções, desmarcarei common_parampara que você possa ver que elas próprias definirão quando eu as chamar.

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 agora do escopo do chamador:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Mas agora eu quero que seja algo completamente diferente:

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 eu desarmar _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

REDEFINIR

Se você precisar redefinir o estado da função a qualquer momento, isso é fácil. Você só precisa fazer (de dentro da função):

. /dev/fd/5

Salvei os argumentos usados ​​para declarar inicialmente a função no 5<<\RESETdescritor de arquivo de entrada. Assim, a .dotfonte que no shell a qualquer momento repetirá o processo que o configurou em primeiro lugar. É tudo muito fácil, realmente e totalmente portátil, se você estiver disposto a ignorar o fato de que o POSIX não especifica realmente os caminhos dos nós dos dispositivos descritores de arquivos (que são uma necessidade para os shell .dot).

Você pode facilmente expandir esse comportamento e configurar estados diferentes para sua função.

MAIS?

A propósito, isso apenas arranha a superfície. Costumo usar essas técnicas para incorporar pequenas funções auxiliares declaráveis ​​a qualquer momento na entrada de uma função principal - por exemplo, para $@matrizes posicionais adicionais, conforme necessário. De fato - como acredito, deve ser algo muito próximo disso que as conchas de ordem superior fazem de qualquer maneira. Você pode ver que eles são facilmente nomeados programaticamente.

Também gosto de declarar uma função geradora que aceita um tipo limitado de parâmetro e, em seguida, define uma função de queimador de uso único ou de escopo limitado, ao longo das linhas de uma lambda - ou uma função em linha - que simplesmente unset -fé a mesma quando através. Você pode passar uma função shell ao redor.

mikeserv
fonte
Qual é a vantagem dessa complexidade extra com os descritores de arquivo em relação ao uso eval?
Stéphane Chazelas
@StephaneChazelas Não há complexidade adicional da minha perspectiva. Na verdade, eu vejo o contrário. Além disso, a citação é muito mais fácil e .dotfunciona com arquivos e fluxos, para que você não tenha o mesmo tipo de problemas da lista de argumentos que poderia. Ainda assim, é provavelmente uma questão de preferência. Eu certamente acho que é mais limpo - especialmente quando você faz uma avaliação - é um pesadelo de onde eu sento.
precisa saber é o seguinte
@StephaneChazelas No entanto, há uma vantagem - e é muito boa. A avaliação inicial e a segunda avaliação não precisam estar de volta com este método. O documento hereditário é avaliado com base na entrada, mas você não precisa procurar .dotaté que esteja bom e pronto - ou nunca. Isso permite um pouco mais de liberdade ao testar suas avaliações. E fornece a flexibilidade do estado na entrada - que pode ser tratada de outras maneiras -, mas é muito menos perigoso dessa perspectiva do que é eval.
precisa saber é o seguinte
2

Eu acho que uma maneira de imprimir informações sobre a função, quando você

teste os argumentos necessários e saia se eles não existirem - e exiba algumas mensagens

é alterar o bash interno returne / ou exitno início de cada script (ou em algum arquivo que você sempre utiliza antes de executar o programa). Então você digita

   #!/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 você executar isso, receberá:

   function foo returns status 1

Isso pode ser facilmente atualizado com o sinalizador de depuração, se você precisar, mais ou menos assim:

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

Essa instrução way será executada apenas quando a variável VERBOSE estiver configurada (pelo menos é assim que eu uso o verbose nos meus scripts). Certamente não resolve o problema da função de decoração, mas pode exibir mensagens caso a função retorne um status diferente de zero.

Da mesma forma, você pode redefinir exit, substituindo todas as instâncias de return, se desejar sair do script.

Edição: Eu queria adicionar aqui a maneira que eu uso para decorar funções no bash, se eu tiver muitos deles e aninhados também. Quando escrevo 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

E para a saída eu posso conseguir isso:

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

Pode ser útil para alguém que possui funções e deseja depurá-las, para ver em qual erro de função ocorreu. É baseado em três funções, que podem ser descritas abaixo:

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

Eu tentei colocar o máximo possível em comentários, mas aqui é também a descrição: Eu uso _ ()função como decorador, o que eu colocar após a declaração de cada função: foo () { _. Esta função imprime o nome da função com o recuo apropriado, dependendo da profundidade da função em outra função (como recuo padrão, eu uso 4 números de espaços). Costumo imprimir em cinza, para separar da impressão usual. Se a função precisar ser decorada com argumentos, ou sem, pode-se modificar a pré-última linha na função decorador.

Para imprimir algo dentro da função, introduzi a print ()função que imprime tudo o que é passado com o recuo apropriado.

A função set_indentation_for_print_functionfaz exatamente o que significa, calculando o recuo da ${FUNCNAME[@]}matriz.

Dessa forma, existem algumas falhas, por exemplo, não se pode passar opções para printgostar echo, por exemplo , -nou -e, e também se a função retornar 1, ela não será decorada. E também para argumentos passados ​​para printmais do que a largura do terminal, que serão quebrados na tela, não se verá o recuo da linha quebrada.

A melhor maneira de usar esses decoradores é colocá-los em um arquivo separado e em cada novo script para originar esse arquivo source ~/script/hand_made_bash_functions.sh.

Eu acho que a melhor maneira de incorporar o decorador de funções no bash é escrever decorador no corpo de cada função. Eu acho que é muito mais fácil escrever função dentro da função no bash, porque ele tem a opção de definir todas as variáveis ​​globais, não como nas Linguagens Orientadas a Objetos padrão. Isso faz com que você esteja colocando rótulos em seu código no bash. Pelo menos isso me ajudou em scripts de depuração.

Nikiforov Alexander
fonte
0

Para mim, isso parece a maneira mais simples de implementar um padrão decorador dentro do 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
fonte