Bash Function Decorator

9

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

Existe algum recurso semelhante no bash?

No script no qual estou trabalhando atualmente, tenho um clichê que testa os argumentos necessários e sai se eles não existirem - e exibe algumas mensagens se o sinalizador de depuração for especificado.

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

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

    
por nfarrar 21.04.2014 / 15:47

5 respostas

11

Isso seria muito mais fácil com zsh que tem funções anônimas e uma matriz associativa especial com códigos de função. Com bash , no entanto, você poderia 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'
    
por 21.04.2014 / 19:12
5

Eu já discuti como e por que os métodos abaixo funcionam em várias ocasiões antes, então não farei isso novamente. Pessoalmente, os meus favoritos sobre o tema são aqui e aqui .

Se você não estiver interessado em ler isso, mas ainda assim curioso, apenas entenda que os aqui-docs anexados à entrada da função são avaliados para a expansão do shell anterior a função é executada e gerada novamente no estado em que estavam quando a função foi definida a cada hora em 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

RODA TI

Aqui eu chamo _fn_init para 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

REQUERIDO

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

fn

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

Por favor, note a ordem dos rastreios do shell - não só o fn falha quando chamado quando _if_unset não está definido, mas nunca é executado 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 elas são <<input , afinal.

O erro vem de /dev/fd/4 porque 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 corrigida.

_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 é mutável para qualquer outro, que também será honrado por todas as funções igualmente declaradas. Vou deixar os rastros do shell agora - não estamos entrando em 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, eu retiro common_param para que você possa ver que elas serão definidas 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 quero que seja algo totalmente 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 desativar o _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

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

. /dev/fd/5

Salvei os argumentos usados para declarar inicialmente a função no descritor de arquivo 5<<\RESET input. Portanto, .dot sourcing que no shell a qualquer momento irá repetir o processo que o configurou em primeiro lugar. É tudo muito fácil, realmente e praticamente totalmente portátil se você estiver disposto a ignorar o fato de que POSIX não especifica realmente os caminhos do nó do dispositivo do descritor de arquivo (que são uma necessidade para o .dot do shell).

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

MAIS?

Isso apenas arranha a superfície, a propósito. 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 eu acredito, deve ser algo muito próximo disso que os shells de maior ordem fazem de qualquer maneira. Você pode ver que eles são facilmente chamados de forma programática.

Eu também gostaria de declarar uma função geradora que aceita um tipo limitado de parâmetro e então define uma função de queimador de uso único ou de escopo limitado ao longo das linhas de uma função lambda - ou uma função em linha - que simplesmenteunset -f quando se passa. Você pode passar uma função de shell por aí.

    
por 21.04.2014 / 22:42
2

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

test the required arguments and exit if they don't exist - and display some messages

é alterar o bash incorporado em return e / ou exit no início de cada script (ou em algum arquivo, que você fonte toda vez 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, você receberá:

   function foo returns status 1

Isso pode ser facilmente atualizado com o sinalizador de depuração, se necessário, 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
    }    

Desta forma, a declaração será executada somente quando a variável VERBOSE estiver configurada (pelo menos é assim que eu uso verbose em meus scripts). Certamente não resolve o problema da função de decoração, mas pode exibir mensagens no caso de a função retornar um status diferente de zero.

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

EDIT: Eu queria adicionar aqui a maneira que eu uso para decorar funções no bash, se eu tenho muitas delas e aninhadas também. Quando eu 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, posso obter isto:

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 tenha funções e queira depurá-las, para ver em qual função ocorreu o erro. É 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 nos comentários, mas aqui está também a descrição: Eu uso _ () function como decorator, o que eu coloco após a declaração de cada função: foo () { _ . Esta função imprime o nome da função com o recuo adequado, dependendo da profundidade da função em outra função (como um recuo padrão eu uso 4 número de espaços). Eu costumo imprimir isso em cinza, para separar isso da impressão usual. Se a função for necessária para ser decorada com argumentos, ou sem, pode-se modificar a linha anterior à última na função de decorador.

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

A função set_indentation_for_print_function faz exatamente o que ela representa, calculando o recuo da matriz ${FUNCNAME[@]} .

Dessa forma, existem algumas falhas, por exemplo, não é possível passar opções para print , como echo , por exemplo, -n ou -e , e também se a função retornar 1, ela não será decorada. E também para argumentos, passados para print mais do que a largura do terminal, que será empacotado na tela, não se verá recuo para linha embrulhada.

A ótima maneira de usar esses decoradores é colocá-los em um arquivo separado e em cada novo script para obter 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 o 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 volta do seu código no bash. Pelo menos isso me ajudou em scripts de depuração.

    
por 11.02.2016 / 16:44
0

Talvez os exemplos de decoradores no projeto link possam ajudar você (oobash / docs / examples / decorator.sh).

    
por 12.09.2014 / 22:36
0

Para mim, isso parece a maneira mais simples de implementar um padrão de 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
    
por 12.10.2018 / 17:14

Tags