Python에서는 함수를 자동으로 적용하고 실행하는 코드로 함수를 꾸밀 수 있습니다.
Bash에도 비슷한 기능이 있나요?
현재 작업 중인 스크립트에는 필수 매개변수를 테스트하고 해당 매개변수가 없으면 종료하며 디버그 플래그가 지정되면 일부 메시지를 표시하는 상용구가 있습니다.
불행하게도 이 코드를 모든 함수에 다시 삽입해야 하고, 코드를 변경하려면 모든 함수를 수정해야 합니다.
Python의 데코레이터와 비슷하게 각 함수에서 이 코드를 제거하고 모든 함수에 적용하는 방법이 있나요?
답변1
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
그러나 데코레이터를 두 번 호출하여 함수를 두 번 꾸밀 수는 없습니다.
그리고 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'
답변2
다음 방법이 어떻게, 왜 작동하는지 이전에 여러 번 논의했으므로 다시는 수행하지 않겠습니다. 개인적으로 제가 가장 좋아하는 테마는여기그리고여기.
읽는 데 관심이 없지만 여전히 궁금하다면 함수 입력에 첨부된 문서가 셸 확장에 대해 평가된다는 점을 이해하세요.앞으로함수가 실행되고 정의 당시의 상태로 다시 생성됩니다.모든함수가 호출된 시간입니다.
발표하다
필요한 것은 다른 함수를 선언하는 함수뿐입니다.
_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
달리다
_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
input 의 기본값으로 평가됩니다 _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가 실제로 파일 설명자 장치 노드 경로(shell에서 필요)를 지정하지 않는다는 사실을 무시한다면 .dot
이는 모두 매우 간단하고 실질적이며 거의 완전히 이식 가능합니다.
이 동작을 쉽게 확장하고 기능에 대해 다양한 상태를 구성할 수 있습니다.
더?
그건 그렇고, 이것은 단지 표면을 긁는 것뿐입니다. 나는 종종 이러한 기술을 사용하여 작고 즉시 선언 가능한 도우미 함수를 기본 함수의 입력에 포함합니다. 예를 들어 $@
필요에 따라 추가 위치 배열을 추가합니다. 사실 - 내가 믿는 것처럼, 어쨌든 높은 수준의 쉘은 이것에 매우 가까운 일을 하고 있을 것입니다. 프로그래밍 방식으로 쉽게 이름이 지정되는 것을 볼 수 있습니다.
또한 제한된 유형의 인수를 허용하는 생성기 함수를 선언한 다음 자체적으로 전달되는 람다 또는 인라인 함수 라인을 따라 일회성 또는 제한된 범위의 버너 함수를 정의하는 것을 좋아합니다 unset -f
. 너쉘 함수를 전달할 수 있습니다.
답변3
함수에 대한 정보를 인쇄하는 한 가지 방법은 다음과 같습니다.
필수 매개변수를 테스트하고 존재하지 않으면 종료 - 일부 메시지와 함께
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[@]}
배열의 들여쓰기를 계산합니다.
이 접근 방식에는 or 와 같은 print
like to 에 옵션을 전달할 수 없고 함수가 1을 반환하면 수정되지 않는 등 몇 가지 단점이 있습니다 . 터미널 너비를 초과 하도록 전달된 인수의 경우 인수는 화면에서 줄 바꿈되며 사람들은 줄 바꿈의 들여쓰기를 볼 수 없습니다.echo
-n
-e
print
이러한 데코레이터를 사용하는 좋은 방법은 이를 별도의 파일에 넣고 모든 새 스크립트에서 해당 파일을 가져오는 것입니다 source ~/script/hand_made_bash_functions.sh
.
Bash에 함수 데코레이터를 통합하는 가장 좋은 방법은 각 함수의 본문에 데코레이터를 작성하는 것입니다. 표준 객체 지향 언어와 달리 모든 변수를 전역 변수로 만드는 옵션이 있기 때문에 bash의 함수 내에서 함수를 작성하는 것이 훨씬 쉽다고 생각합니다. 이를 통해 Bash에서와 마찬가지로 코드 주위에 태그를 넣을 수 있습니다. 적어도 이것은 스크립트를 디버깅하는 데 도움이 됩니다.
답변4
나에게는 이것이 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