web-dev-qa-db-ja.com

Bash関数デコレーター

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

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

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

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

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

10
nfarrar

これは、無名関数と関数コードを持つ特別な連想配列を持つ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'
12

機能に関する情報を印刷する方法の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が設定されている場合にのみ実行されます(少なくとも、スクリプトで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でコードの周りにラベルを付けるようになります。少なくとも、それはデバッグスクリプトに役立ちました。

2

私は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を使用して、装飾された関数をtimesするデコレータの簡単な例を示します。

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
0
dimo414

たぶん http://sourceforge.net/projects/oobash/ プロジェクトのデコレータの例が役立つでしょう(oobash/docs/examples/decorator.sh)。

0
user3495

私にとって、これは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
0
Antonia Stevens