Bash関数デコレーター


10

Pythonでは、関数に対して自動的に適用および実行されるコードで関数を装飾できます。

bashに同様の機能はありますか?

私が現在取り組んでいるスクリプトには、必要な引数をテストし、それらが存在しない場合は終了するボイラープレートがあり、デバッグフラグが指定されている場合はメッセージを表示します。

残念ながら、このコードをすべての関数に再挿入する必要があり、変更したい場合は、すべての関数を変更する必要があります。

このコードを各関数から削除し、それをすべての関数に適用する方法はありますか?Pythonのデコレーターと同様です?


関数の引数を検証するために、少なくとも出発点として、最近まとめたこのスクリプトを使用できる場合があります。
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

ただし、関数を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'

ステファン- typeset必要ですか?それ以外の場合は宣言しませんか?
mikeserv 2014

@mikeserv、eval "_inner_$(typeset -f x)"作成_inner_x元の正確なコピーとしてx(同様functions[_inner_x]=$functions[x]zsh)。
ステファンChazelas

わかったけど、なぜ2つ必要なの?
mikeserv 2014

別のコンテキストが必要です。それ以外の場合は、内部のコンテキストをキャッチできませんreturn
ステファンChazelas

1
私はあなたについていきません。私の答えは近いのマップなどの試みである私がするPythonのデコレータを理解するもの
ステファンChazelas

5

以下の方法がいくつかの場面でどのように機能するかについて、その方法と理由についてはすでに説明したので、ここでは再度説明しません。個人的には、このトピックに関する私自身のお気に入りはここここです。

あなたがそれを読むことに興味はないが、それでも興味がある場合は、関数の入力に添付されたヒアドキュメントが関数の実行にシェル展開について評価され、関数が定義されたときの状態で新しく生成されることを理解してください関数が呼び出されるたび

宣言する

他の関数を宣言する関数が必要です。

_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は、によって宣言されたすべての関数の入力時にデフォルト値に評価されます_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 ;

上記では、2つの関数を宣言して設定してい_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「とき自体よ使って。シェル関数を渡すことができます。


ファイル記述子を使用するよりも複雑であることの利点は何evalですか?
ステファンChazelas

@StephaneChazelas私の観点から見ると、複雑さは増しません。実際、私はそれを逆に見ています。また、引用ははるかに簡単で、.dotファイルとストリームで機能するため、そうでない場合と同じ種類の引数リストの問題に遭遇することはありません。それでも、それはおそらく好みの問題です。私は確かにそれがよりクリーンだと思います-特にあなたがevalを評価するようになるとき-それは私が座っているところからの悪夢です。
mikeserv 2014

@StephaneChazelas利点は1つありますが、それは非常に優れています。このメソッドでは、最初の評価と2番目の評価を連続して行う必要はありません。ヒアドキュメントは入力時に評価されますが、.dot準備が整い、準備が整うまで、あるいはいつまでもソースを用意する必要はありません。これにより、評価のテストをもう少し自由に行うことができます。また、入力の状態の柔軟性を提供します。これは他の方法で処理できますが、その観点から見ると、それほど危険ではありませんeval
mikeserv 2014

2

機能に関する情報を印刷する1つの方法は、

必要な引数をテストし、存在しない場合は終了し、いくつかのメッセージを表示します

組み込みのbashを変更するreturnexit、すべてのスクリプトの最初(またはプログラムを実行する前に毎回ソースするファイル)を変更します。だからあなたはタイプする

   #!/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が設定されている場合にのみ実行されます(少なくとも、スクリプトで冗長を使用する方法です)。確かに関数の装飾の問題は解決しませんが、関数がゼロ以外のステータスを返した場合にメッセージを表示できます。

同様に、スクリプトを終了する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などに渡すことができません。また、関数が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キャッシュライブラリは装飾を使用して、最小限のセレモニーで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.