pythonでは、関数に対して自動的に適用および実行されるコードで関数を装飾できます。
Bashに同様の機能はありますか?
私が現在取り組んでいるスクリプトには、必要な引数をテストし、それらが存在しない場合は終了するボイラープレートがあり、デバッグフラグが指定されている場合はメッセージを表示します。
残念ながら、このコードをすべての関数に再挿入する必要があり、変更したい場合は、すべての関数を変更する必要があります。
このコードを各関数から削除し、Pythonのデコレータのようにすべての関数に適用する方法はありますか?
これは、無名関数と関数コードを持つ特別な連想配列を持つ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
ただし、関数を2回装飾するためにdecorateを2回呼び出すことはできません。
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'
機能に関する情報を印刷する方法の1つは、
必要な引数をテストし、存在しない場合は終了します-そしていくつかのメッセージを表示します
すべてのスクリプトの最初に(またはプログラムを実行する前に毎回ソースするファイルで)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を使用する方法です)。関数の装飾の問題は確かに解決されませんが、関数がゼロ以外のステータスを返した場合にメッセージを表示できます。
同様に、スクリプトを終了する場合は、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
関数を持っていて、それらをデバッグしたい人にとって、どの関数エラーが発生したかを確認することは役に立ちます。これは、以下で説明できる3つの関数に基づいています。
#!/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
のようにecho
にオプションを渡すことができません。 -n
または-e
であり、関数が1を返す場合も、装飾されていません。また、端末の幅よりも多くprint
に渡され、画面に折り返される引数の場合、折り返された行のインデントは表示されません。
これらのデコレータを使用する優れた方法は、それらを別々のファイルに入れ、新しいスクリプトごとにこのファイルをソースにすることですsource ~/script/hand_made_bash_functions.sh
。
関数デコレータをbashに組み込む最良の方法は、各関数の本体にデコレータを記述することだと思います。標準のオブジェクト指向言語とは異なり、すべての変数をグローバルに設定するオプションがあるため、bashの関数内に関数を記述する方がはるかに簡単だと思います。これで、bashでコードの周りにラベルを付けるようになります。少なくとも、それはデバッグスクリプトに役立ちました。
私はBashで多くの(おそらく多すぎる:))メタプログラミングを行っており、動作をその場で再実装するのに非常に貴重なデコレーターを見つけました。私の bash-cache ライブラリは装飾を使用して、最小限のセレモニーでBash関数を透過的にメモします。
my_expensive_function() {
...
} && bc::cache my_expensive_function PWD # key the cache off PWD as well as any args
明らかに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)"
}
次に、bc::copy_function
を使用して、装飾された関数をtime
sするデコレータの簡単な例を示します。
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
たぶん http://sourceforge.net/projects/oobash/ プロジェクトのデコレータの例が役立つでしょう(oobash/docs/examples/decorator.sh)。
私にとって、これは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