배쉬 함수 데코레이터


10

파이썬에서는 함수에 대해 자동으로 적용되고 실행되는 코드로 함수를 장식 할 수 있습니다.

bash에 비슷한 기능이 있습니까?

현재 작업중 인 스크립트에는 필요한 인수를 테스트하고 존재하지 않는 경우 종료하는 상용구가 있으며 디버깅 플래그가 지정된 경우 일부 메시지를 표시합니다.

불행히도이 코드를 모든 함수에 다시 삽입해야하며 변경하려면 모든 함수를 수정해야합니다.

각 함수 에서이 코드를 제거하고 파이썬의 데코레이터와 유사한 모든 함수에 적용하는 방법이 있습니까?


함수 인수의 유효성을 검사하기 위해 적어도 시작점으로 최근에 가져온 이 스크립트 를 사용할 수 있습니다 .
dimo414

답변:


12

zsh익명 함수와 함수 코드가있는 특수 연관 배열을 사용 하면 훨씬 쉽습니다 . 함께 bash그러나 당신이 뭔가를 같이 할 수있는 :

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

다음과 같이 출력됩니다.

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

함수를 두 번 장식하기 위해 decorate를 두 번 호출 할 수 없습니다.

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'

스테판- typeset필요합니까? 그렇지 않으면 선언하지 않습니까?
mikeserv

@mikeserv, eval "_inner_$(typeset -f x)"생성 _inner_x원의 정확한 사본으로 x(동일 functions[_inner_x]=$functions[x]에서 zsh).
Stéphane Chazelas

나는 그것을 얻는다-그러나 왜 당신은 2 개를 전혀 필요로 하는가?
mikeserv

그렇지 않으면 다른 맥락이 필요합니다. 그렇지 않으면 내부 의 내용 을 잡을 수 없습니다 return.
Stéphane Chazelas

1
나는 당신을 따르지 않습니다. 내 대답은 파이썬 데코레이터를 이해
Stéphane Chazelas

5

나는 이미 아래 방법이 여러 번 작동하는 방식과 방법에 대해 이미 논의 했으므로 다시는하지 않을 것입니다. 개인적으로이 주제에 대한 저의 즐겨 찾기는 여기여기에 있습니다 .

읽는 것에 관심이 없지만 여전히 궁금한 점은 함수의 입력에 첨부 된 here-docs 가 함수가 실행 되기 전에 쉘 확장에 대해 평가 되고 함수가 정의 된 상태에서 새로 생성되었다는 것을 이해하십시오. 함수가 호출 마다

알리다

다른 함수를 선언하는 함수 만 있으면됩니다.

_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

IT 운영

여기서는 _fn_init이라는 함수를 선언하도록 요청합니다 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

필수

이 함수를 호출하려면 환경 변수 _if_unset가 설정되어 있지 않으면 죽습니다 .

fn

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

쉘 추적의 순서에 유의하십시오. 설정이 해제 fn되었을 때 호출 될 때 실패 할 _if_unset뿐만 아니라 처음부터 실행되지 않습니다 . 이것은이 문서 확장 작업을 할 때 이해해야 할 가장 중요한 요소입니다. 결국에는 반드시 확장이 먼저 이루어져야합니다 <<input.

/dev/fd/4상위 쉘이 입력을 함수로 전달하기 전에 해당 입력을 평가하고 있기 때문에 오류가 발생 합니다. 필수 환경을 테스트하는 가장 단순하고 효율적인 방법입니다.

어쨌든 실패는 쉽게 해결됩니다.

_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

융통성 있는

변수 common_param는로 선언 된 모든 함수에 대해 입력시 기본값으로 평가됩니다 _fn_init. 그러나 그 가치는 다른 것으로도 변경 될 수 있으며, 마찬가지로 선언 된 모든 기능에 의해 존중 될 것입니다. 나는 이제 껍질 흔적을 남기지 않을 것입니다-우리는 여기서 미지의 영역으로 들어 가지 않을 것입니다.

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 ;

위의 두 함수를 선언하고 설정 _if_unset합니다. 이제 두 함수 중 하나를 호출하기 전에 설정을 해제 common_param하여 호출 할 때 스스로 설정한다는 것을 알 수 있습니다.

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

그리고 이제 발신자의 범위에서 :

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

그러나 이제는 그것이 완전히 다른 것이기를 원합니다.

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.

그리고 내가 설정을 해제하면 _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

초기화

언제든지 기능 상태를 재설정해야하는 경우 쉽게 수행 할 수 있습니다. 함수 내에서만 수행하면됩니다.

. /dev/fd/5

5<<\RESET입력 파일 설명자 에서 함수를 처음 선언하는 데 사용되는 인수를 저장했습니다 . 따라서 .dot언제든지 쉘에서 소스를 소싱하면 처음에 설정 프로세스를 반복합니다. POSIX가 실제로 파일 디스크립터 장치 노드 경로를 지정하지 않는다는 사실을 간과하고 싶다면 셸의 필수 요소입니다 .dot.

이 동작을 쉽게 확장하고 기능에 대해 다른 상태를 구성 할 수 있습니다.

더?

그건 그렇고, 표면을 거의 긁지 않습니다. 필자는 종종 이러한 기술을 사용하여 언제든지 선언 할 수있는 작은 도우미 함수를 기본 함수의 입력 (예 $@: 필요에 따라 추가 위치 배열)에 삽입 할 수 있습니다. 사실-내가 믿는 것처럼, 고차 껍질이 어쨌든하는 것은 이것과 매우 가까운 것이어야합니다. 프로그래밍 방식으로 이름이 매우 쉽게 표시됩니다.

또는 인 - 라인 기능 - - 또한 같은 I 파라미터의 한정된 유형을 수용하고 람다의 라인을 따라 단일 용도 또는 다른 범위 제한 버너 함수를 정의하는 생성 함수를 선언 단순히 그 unset -f'자체 때 S는 을 통하여. 쉘 함수를 전달할있습니다.


eval? 를 사용하는 것보다 파일 디스크립터가 갖는 추가적인 복잡성의 이점은 무엇입니까 ?
Stéphane Chazelas

@StephaneChazelas 내 관점에서 복잡성이 더해지지 않습니다. 사실, 나는 다른 방향으로 봅니다. 또한 인용이 훨씬 쉽고 .dot파일 및 스트림과 함께 작동하므로 다른 종류의 인수 목록 문제가 발생하지 않습니다. 아직도, 그것은 선호의 문제 일 것입니다. 나는 그것이 더 깨끗하다고 ​​생각합니다-특히 당신이 평가를 피할 때-그것은 내가 앉아있는 악몽입니다.
mikeserv

@StephaneChazelas 한 가지 장점이 있지만 꽤 좋습니다. 이 방법으로 초기 평가와 두 번째 평가를 연속 할 필요는 없습니다. heredocument는 입력에 따라 평가되지만, .dot잘 준비가 될 때까지 또는 소스가 될 때까지 소스를 제공 하지 않아도됩니다 . 이를 통해 평가를보다 자유롭게 테스트 할 수 있습니다. 또한 입력에 대한 상태의 유연성을 제공합니다. 다른 방식으로 처리 할 수 ​​있지만 그 관점에서보다 훨씬 덜 위험 eval합니다.
mikeserv

2

기능에 관한 정보를 인쇄 할 수있는 한 가지 방법은

필요한 인수를 테스트하고 존재하지 않는 경우 종료하고 일부 메시지를 표시하십시오.

bash 내장 return및 / 또는 exit모든 스크립트의 시작 부분 (또는 프로그램을 실행하기 전에 매번 소스하는 일부 파일)을 변경하는 것입니다. 그래서 당신은 입력

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

이것을 실행하면 다음을 얻을 수 있습니다.

   function foo returns status 1

필요할 경우 디버깅 플래그로 쉽게 업데이트 할 수 있습니다.

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

이 방법 문장은 변수 VERBOSE가 설정된 경우에만 실행됩니다 (적어도 스크립트에서 verbose를 사용하는 방법입니다). 확실히 함수 꾸미기 문제를 해결하지는 못하지만 함수가 0이 아닌 상태를 반환하는 경우 메시지를 표시 할 수 있습니다.

마찬가지로 스크립트를 종료하려면 exit의 모든 인스턴스를 교체 하여을 재정의 할 수 있습니다 return.

편집 : 여기에 함수가 많이 있고 중첩 된 함수가있는 경우 bash에서 함수를 장식하는 데 사용하는 방법을 여기에 추가하고 싶었습니다. 이 스크립트를 작성할 때 :

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

그리고 출력을 위해 이것을 얻을 수 있습니다 :

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

어떤 함수 오류가 발생했는지 확인하기 위해 함수가 있고이를 디버그하려는 사람에게 도움이 될 수 있습니다. 아래에 설명 할 수있는 세 가지 기능을 기반으로합니다.

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

의견에 가능한 한 많이 넣으려고했지만 여기에도 설명이 있습니다. _ ()함수를 함수로 사용 합니다 foo () { _. 모든 함수의 선언 후에 넣은 것 입니다. 이 함수는 다른 함수에 얼마나 깊은 함수가 있는지에 따라 함수 이름을 적절한 들여 쓰기로 인쇄합니다 (기본 들여 쓰기로 4 개의 공백을 사용합니다). 나는 보통 이것을 보통 인쇄와 분리하기 위해 회색으로 인쇄합니다. 함수를 인수로 장식하거나 인수없이 장식해야하는 경우 데코레이터 함수에서 마지막 행을 수정할 수 있습니다.

함수 내부에 무언가를 인쇄하기 위해 print ()전달 된 모든 것을 적절한 들여 쓰기로 인쇄하는 함수를 소개 했습니다.

이 함수 set_indentation_for_print_function${FUNCNAME[@]}배열의 들여 쓰기를 계산하여 정확히 의미하는 것을 수행합니다 .

이 방법에는 몇 가지 결함이 있습니다. 예를 들어 , 또는 에 print원하는 옵션을 전달할 수 없으며 함수가 1을 반환하면 장식되지 않습니다. 또한 터미널 너비 이상으로 전달되는 인수의 경우 화면에 줄 바꿈되므로 줄 바꿈에 대한 들여 쓰기가 표시되지 않습니다.echo-n-eprint

이 데코레이터를 사용하는 가장 좋은 방법은이 데코레이터를 별도의 파일과 각각의 새 스크립트에 넣어이 파일을 소싱하는 것 source ~/script/hand_made_bash_functions.sh입니다.

bash에 함수 데코레이터를 통합하는 가장 좋은 방법은 각 함수의 본문에 데코레이터를 작성하는 것입니다. 표준 객체 지향 언어와는 달리 모든 변수를 전역으로 설정하는 옵션이 있기 때문에 bash에서 함수 내부에 함수를 작성하는 것이 훨씬 쉽다고 생각합니다. 마치 코드 주위에 레이블을 bash에 넣는 것처럼 만듭니다. 적어도 디버깅 스크립트에 도움이되었습니다.



0

나에게 이것은 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

이 ShellCheck 경고를 비활성화하는 이유는 무엇입니까? 그것들은 정확 해 보입니다 (확실히 인용하여 SC2068 경고를 수정해야합니다 "$@").
dimo414

0

Bash에서 메타 프로그래밍을 많이 (아마도 너무 많이 :))하고 데코레이터가 동작을 다시 구현하는 데 매우 중요하다는 것을 알았습니다. 내 bash-cache 라이브러리는 장식을 사용하여 최소한의 식으로 Bash 함수를 투명하게 메모합니다.

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

분명히 bc::cache장식하는 것 이상을 수행하고 있지만 기본 장식은 bc::copy_function기존 함수를 새 이름으로 복사하여 원래 함수를 데코레이터로 덮어 쓸 수 있습니다.

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

다음 time은 데코 레이팅 된 함수 인 데코레이터의 간단한 예입니다 bc::copy_function.

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

데모:

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

$ time_decorator slow

$ $ slow
done

real    0m2.003s
user    0m0.000s
sys     0m0.002s
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.