Dekorator Fungsi Bash

10

Dalam python kita dapat menghias fungsi dengan kode yang secara otomatis diterapkan dan dieksekusi terhadap fungsi.

Apakah ada fitur serupa di bash?

Dalam skrip yang sedang saya kerjakan, saya memiliki beberapa pelat yang menguji argumen yang diperlukan dan keluar jika tidak ada - dan menampilkan beberapa pesan jika bendera debug ditentukan.

Sayangnya saya harus memasukkan kembali kode ini ke setiap fungsi dan jika saya ingin mengubahnya, saya harus memodifikasi setiap fungsi.

Apakah ada cara untuk menghapus kode ini dari setiap fungsi dan menerapkannya ke semua fungsi, mirip dengan dekorator dengan python?

nfarrar
sumber
Untuk memvalidasi argumen fungsi, Anda mungkin dapat menggunakan skrip ini yang baru-baru ini saya kumpulkan, setidaknya sebagai titik awal.
dimo414

Jawaban:

12

Itu akan jauh lebih mudah dengan zshyang memiliki fungsi anonim dan array asosiatif khusus dengan kode fungsi. Dengan bashnamun Anda bisa melakukan sesuatu seperti:

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

Yang akan menghasilkan:

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

Anda tidak dapat memanggil hiasi dua kali untuk menghias fungsi Anda dua kali.

Dengan 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'
Stéphane Chazelas
sumber
Stephane - typesetperlu? Bukankah itu akan menyatakan sebaliknya?
mikeserv
@mikeserv, eval "_inner_$(typeset -f x)"menciptakan _inner_xsebagai salinan yang asli x(sama seperti functions[_inner_x]=$functions[x]di zsh).
Stéphane Chazelas
Saya mengerti - tetapi mengapa Anda perlu dua?
mikeserv
Anda perlu konteks yang berbeda jika Anda tidak akan mampu menangkap batin 's return.
Stéphane Chazelas
1
Aku tidak mengikutimu di sana. Jawaban saya adalah upaya sebagai peta dekat dari apa yang saya pahami sebagai dekorator python
Stéphane Chazelas
5

Saya sudah membahas bagaimana dan mengapa metode di bawah ini berfungsi pada beberapa kesempatan sebelumnya sehingga saya tidak akan melakukannya lagi. Secara pribadi, favorit saya sendiri pada topik ada di sini dan di sini .

Jika Anda tidak tertarik membaca itu tetapi masih penasaran, pahami saja bahwa dokumen di sini yang dilampirkan pada input fungsi dievaluasi untuk ekspansi shell sebelum fungsi tersebut berjalan, dan bahwa mereka dihasilkan kembali dalam keadaan seperti ketika fungsi tersebut didefinisikan. setiap kali fungsi dipanggil.

MENYATAKAN

Anda hanya perlu fungsi yang mendeklarasikan fungsi lainnya.

_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

MENJALANKANNYA

Di sini saya meminta _fn_inituntuk mendeklarasikan saya sebuah fungsi yang dipanggil 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

YG DIBUTUHKAN

Jika saya ingin memanggil fungsi ini akan mati kecuali variabel lingkungan _if_unsetdiatur.

fn

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

Harap perhatikan urutan jejak shell - tidak hanya fngagal ketika dipanggil saat _if_unsettidak disetel, tetapi juga tidak pernah berjalan di tempat pertama . Ini adalah faktor yang paling penting untuk dipahami ketika bekerja dengan ekspansi dokumen-di sini - mereka harus selalu terjadi terlebih dahulu karena mereka <<inputsemua.

Kesalahan berasal dari /dev/fd/4karena shell induk mengevaluasi input itu sebelum menyerahkannya ke fungsi. Ini adalah cara paling sederhana dan paling efisien untuk menguji lingkungan yang diperlukan.

Bagaimanapun, kegagalannya mudah diatasi.

_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

FLEKSIBEL

Variabel common_paramdievaluasi ke nilai default pada input untuk setiap fungsi yang dideklarasikan oleh _fn_init. Tetapi nilai itu juga dapat diubah ke yang lain yang juga akan dihormati oleh setiap fungsi yang dinyatakan secara serupa. Saya akan meninggalkan jejak shell sekarang - kita tidak akan pergi ke wilayah yang belum dipetakan di sini atau apa pun.

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 ;

Di atas saya mendeklarasikan dua fungsi dan set _if_unset. Sekarang, sebelum memanggil salah satu fungsi, saya akan menghapusnya common_paramsehingga Anda dapat melihat mereka akan mengaturnya sendiri ketika saya memanggil mereka.

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

Dan sekarang dari ruang lingkup penelepon:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Tapi sekarang saya ingin semuanya menjadi sesuatu yang lain:

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.

Dan jika saya tidak disetel _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

RESET

Jika Anda perlu mengatur ulang status fungsi kapan saja hal itu mudah dilakukan. Anda hanya perlu melakukan (dari dalam fungsi):

. /dev/fd/5

Saya menyimpan argumen yang digunakan untuk mendeklarasikan fungsi pada 5<<\RESETfile-descriptor input. Jadi .dotsumber yang ada di shell kapan saja akan mengulangi proses yang mengaturnya di tempat pertama. Ini semua sangat mudah, benar-benar, dan cukup portabel sepenuhnya jika Anda bersedia mengabaikan fakta bahwa POSIX tidak benar-benar menentukan jalur simpul perangkat deskriptor file (yang merupakan keharusan bagi shell .dot).

Anda dapat dengan mudah memperluas perilaku ini dan mengonfigurasi berbagai status untuk fungsi Anda.

LEBIH?

Omong-omong, ini nyaris tidak menggores permukaan. Saya sering menggunakan teknik ini untuk menanamkan fungsi pembantu kecil yang dapat dideklarasikan kapan saja ke dalam input fungsi utama - misalnya, untuk $@array posisi tambahan yang diperlukan. Bahkan - seperti yang saya yakini, pasti ada sesuatu yang sangat dekat dengan ini yang dilakukan oleh shell dengan urutan lebih tinggi. Anda dapat melihat mereka secara mudah diberi nama pemrograman.

Saya juga ingin mendeklarasikan fungsi generator yang menerima tipe parameter terbatas dan kemudian mendefinisikan fungsi burner sekali pakai atau fungsi lingkup terbatas sepanjang garis lambda - atau fungsi in-line - yang hanya ada unset -fsaat melalui. Anda dapat melewati fungsi shell di sekitar.

mikeserv
sumber
Apa keuntungan dari kerumitan ekstra dengan deskriptor file daripada menggunakan eval?
Stéphane Chazelas
@StephaneChazelas Tidak ada kerumitan tambahan dari sudut pandang saya. Bahkan, saya melihatnya sebaliknya. Juga, mengutipnya jauh lebih mudah, dan .dotberfungsi dengan file dan stream sehingga Anda tidak mengalami masalah daftar argumen yang sama dengan yang Anda mungkin sebaliknya. Tetap saja, itu mungkin masalah preferensi. Saya tentu berpikir itu lebih bersih - terutama ketika Anda masuk ke eval eval - itu adalah mimpi buruk dari tempat saya duduk.
mikeserv
@StephaneChazelas Tapi ada satu keuntungan - dan ini cukup bagus. Eval awal dan eval kedua tidak perlu kembali ke belakang dengan metode ini. Dokumen warisan dievaluasi berdasarkan input, tetapi Anda tidak perlu .dotsumber sampai Anda baik dan siap - atau selamanya. Ini memungkinkan Anda sedikit lebih bebas dalam menguji evaluasinya. Dan itu memberikan fleksibilitas keadaan input - yang dapat ditangani dengan cara lain - tetapi jauh lebih berbahaya dari perspektif itu eval.
mikeserv
2

Saya pikir salah satu cara untuk mencetak informasi tentang fungsi, ketika Anda

uji argumen yang diperlukan dan keluar jika tidak ada - dan tampilkan beberapa pesan

adalah mengubah bash builtin returndan / atau exitdi awal setiap skrip (atau dalam beberapa file, yang Anda sumber setiap kali sebelum menjalankan program). Jadi, Anda mengetik

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

Jika Anda menjalankan ini, Anda akan mendapatkan:

   function foo returns status 1

Itu mungkin mudah diperbarui dengan bendera debugging jika Anda perlu, agak seperti ini:

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

Pernyataan cara ini akan dieksekusi hanya ketika variabel VERBOSE diatur (setidaknya itulah cara saya menggunakan verbose dalam skrip saya). Ini tentu tidak menyelesaikan masalah fungsi dekorasi, tetapi dapat menampilkan pesan jika fungsi mengembalikan status tidak nol.

Demikian pula Anda dapat mendefinisikan ulang exit, dengan mengganti semua instance return, jika Anda ingin keluar dari skrip.

EDIT: Saya ingin menambahkan di sini cara saya gunakan untuk menghias fungsi di bash, jika saya punya banyak dari mereka dan yang bersarang juga. Ketika saya menulis skrip ini:

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

Dan untuk hasilnya saya bisa mendapatkan ini:

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

Dapat bermanfaat bagi seseorang yang memiliki fungsi dan ingin men-debugnya, untuk melihat kesalahan fungsi yang terjadi. Ini didasarkan pada tiga fungsi, yang dapat dijelaskan di bawah ini:

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

Saya mencoba untuk menempatkan sebanyak mungkin dalam komentar, tapi di sini juga deskripsi: Saya menggunakan _ ()fungsi sebagai dekorator, yang saya diletakkan setelah deklarasi setiap fungsi: foo () { _. Fungsi ini mencetak nama fungsi dengan lekukan yang tepat, tergantung seberapa dalam fungsi dalam fungsi lain (sebagai lekukan standar saya menggunakan 4 jumlah spasi). Saya biasanya mencetak ini dalam warna abu-abu, untuk memisahkan ini dari cetakan yang biasa. Jika fungsi perlu didekorasi dengan argumen, atau tanpa argumen, seseorang dapat memodifikasi baris pra-terakhir dalam fungsi dekorator.

Untuk mencetak sesuatu di dalam fungsi, saya memperkenalkan print ()fungsi yang mencetak semua yang diteruskan dengan indentasi yang tepat.

Fungsi set_indentation_for_print_functiontidak persis apa artinya, menghitung lekukan dari ${FUNCNAME[@]}array.

Cara ini memiliki beberapa kekurangan, misalnya seseorang tidak dapat melewatkan opsi untuk printsuka echo, misalnya -natau -e, dan juga jika fungsi mengembalikan 1, itu tidak dihiasi. Dan juga untuk argumen, diteruskan ke printlebih dari lebar terminal, yang akan dibungkus di layar, orang tidak akan melihat lekukan untuk garis yang dibungkus.

Cara terbaik untuk menggunakan dekorator ini adalah dengan meletakkannya di file terpisah dan di setiap skrip baru untuk sumber file ini source ~/script/hand_made_bash_functions.sh.

Saya pikir cara terbaik untuk menggabungkan fungsi dekorator dalam bash, adalah dengan menulis dekorator dalam tubuh masing-masing fungsi. Saya pikir itu jauh lebih mudah untuk menulis fungsi di dalam fungsi dalam bash, karena memiliki opsi untuk mengatur semua variabel global, tidak seperti di Bahasa Berorientasi Objek standar. Itu membuatnya seolah-olah Anda menempatkan label di sekitar kode Anda di bash. Setidaknya itu membantu saya untuk skrip debugging.

Nikiforov Alexander
sumber
0

Bagi saya ini terasa seperti cara paling sederhana untuk menerapkan pola dekorator di dalam 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
Antonia Stevens
sumber
Mengapa Anda menonaktifkan peringatan ShellCheck ini? Mereka tampaknya benar (tentu saja peringatan SC2068 harus diperbaiki dengan mengutip "$@").
dimo414
0

Saya melakukan banyak (mungkin terlalu banyak :)) metaprogramming di Bash, dan telah menemukan dekorator berharga untuk menerapkan kembali perilaku dengan cepat. Saya pesta-cache yang menggunakan perpustakaan dekorasi untuk transparan memoize fungsi Bash dengan upacara minimal:

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

Jelas bc::cachemelakukan lebih dari sekadar mendekorasi, tetapi dekorasi yang mendasarinya bergantung pada bc::copy_functionmenyalin fungsi yang ada ke nama baru, sehingga fungsi aslinya dapat ditimpa dengan dekorator.

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

Berikut adalah contoh sederhana dari dekorator yang timemerupakan fungsi yang didekorasi, menggunakan bc::copy_function:

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

Demo:

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

$ time_decorator slow

$ $ slow
done

real    0m2.003s
user    0m0.000s
sys     0m0.002s
dimo414
sumber