Bash Fonksiyon Dekoratörü


10

Python'da fonksiyonları, işlevlere karşı otomatik olarak uygulanan ve yürütülen kodla dekore edebiliriz.

Bash'da benzer bir özellik var mı?

Şu anda üzerinde çalıştığım komut dosyasında, gerekli bağımsız değişkenleri sınayan ve mevcut değilse çıkış yapan bazı hatalar var ve hata ayıklama bayrağı belirtilmişse bazı iletiler görüntüleniyor.

Ne yazık ki bu kodu her işleve yeniden yerleştirmek zorundayım ve değiştirmek isterseniz, her işlevi değiştirmek zorunda kalacağım.

Bu kodu her işlevden kaldırmanın ve python'daki dekoratörler gibi tüm işlevlere uygulanmasının bir yolu var mı?


İşlev argümanlarını doğrulamak için , yakın zamanda bir araya getirdiğim bu komut dosyasını , en azından bir başlangıç ​​noktası olarak kullanabilirsiniz.
dimo414

Yanıtlar:


12

zshAnonim işlevlere ve işlev kodlarına sahip özel bir ilişkisel diziye sahip olmak çok daha kolay olurdu . İle bashancak bir şey gibi yapabileceği:

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

Hangi çıktı:

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

Ancak fonksiyonunuzu iki kez dekore etmek için iki kez dekore etme diyemezsiniz.

İle 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'

Stephane - typesetgerekli mi? Aksi beyan etmez mi?
mikeserv

@mikeserv, orijinalin tam kopyası olarak eval "_inner_$(typeset -f x)"oluşturur ( içinde olduğu gibi ). _inner_xxfunctions[_inner_x]=$functions[x]zsh
Stéphane Chazelas

Bunu anlıyorum - ama neden ikiye ihtiyacınız var?
mikeserv

Farklı bir bağlama ihtiyacınız var, aksi takdirde kısmı yakalayamazsınız 's return.
Stéphane Chazelas


5

Zaten aşağıdaki yöntemlerin birkaç kez nasıl çalıştığını ve nedenlerini tartıştım, bu yüzden tekrar yapmayacağım. Şahsen, konuyla ilgili kendi favorilerim burada ve burada .

Bunu okumakla ilgilenmiyorsanız, ancak hala işlevin girdisine eklenen burada bulunan belgelerin, işlev çalışmadan önce kabuk genişletme için değerlendirildiğini ve işlev tanımlandığında bulundukları durumda yeniden oluşturulduğunu anlayın. her zaman fonksiyonu olarak adlandırılır.

BİLDİRMEK

Sadece diğer işlevleri bildiren bir işleve ihtiyacınız var.

_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

ÇALIŞTIR

İşte ben çağırıyorum _fn_init bana bir fonksiyon bildirmeyefn .

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

GEREKLİDİR

Bu işlevi çağırmak istersem, ortam değişkeni _if_unsetayarlanmadıkça ölecektir .

fn

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

Lütfen kabuk izlerinin sırasına dikkat edin - sadece ayarlanmadığında fnçağrıldığında başarısız olmaz _if_unset, aynı zamanda asla ilk sırada çalışmaz . Bu belge genişlemeleriyle çalışırken anlaşılması gereken en önemli faktör budur - her zaman önce gerçekleşmelidir çünkü <<inputsonuçta oldukları gibi .

Hata /dev/fd/4, üst kabuk işleve teslim edilmeden önce bu girdiyi değerlendirdiği için gelir . Gerekli ortamı test etmenin en basit, en etkili yoludur.

Her neyse, arıza kolayca giderilebilir.

_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

ESNEK

Değişken common_param, tarafından bildirilen her işlev için girişte varsayılan bir değer olarak değerlendirilir _fn_init. Ancak bu değer, benzer şekilde beyan edilen her fonksiyon tarafından da onurlandırılacak olan diğer değerlerle de değiştirilebilir. Şimdi kabuk izlerini bırakacağım - burada herhangi bir haritaya girmeyeceğiz.

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 ;

Yukarıda iki işlevi beyan edip ayarladım _if_unset. Şimdi, herhangi bir işlevi çağırmadan önce, common_paramonları aradığımda kendileri ayarlayacaklarını görebilmeniz için ayarlayacağım.

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

Ve şimdi arayanın kapsamından:

echo $common_param

#OUTPUT#
REQ/RESET added to all funcs

Ama şimdi tamamen başka bir şey olmasını istiyorum:

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.

Ve eğer ayarlamazsam _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

SIFIRLA

Herhangi bir zamanda işlevin durumunu sıfırlamanız gerekirse, kolayca yapılabilir. Sadece yapmanız gerekir (fonksiyonun içinden):

. /dev/fd/5

Başlangıçta 5<<\RESETgirdi dosya tanımlayıcısında işlevi bildirmek için kullanılan bağımsız değişkenleri kaydettim . Öyleyse .dot, kabukta herhangi bir zamanda kaynak yapmak, onu ilk etapta oluşturan süreci tekrarlayacaktır. POSIX'in dosya tanımlayıcı cihaz düğümü yollarını (kabuklar için bir gerekliliktir .dot) gerçekten belirtmediğini görmezden gelmek istiyorsanız, oldukça kolay, gerçekten ve tamamen taşınabilir .

Bu davranışı kolayca genişletebilir ve işleviniz için farklı durumlar yapılandırabilirsiniz.

DAHA?

Bu arada yüzeyi zar zor çizer. Bu teknikleri genellikle herhangi bir zamanda beyan edilebilir küçük yardımcı işlevleri bir ana işlevin girişine yerleştirmek için kullanırım - örneğin, ek konumlandırma için$@ gerektiğinde diziler için. Aslında - inandığım gibi, üst düzey mermilerin zaten yapması buna çok yakın bir şey olmalı. Programlı olarak çok kolay adlandırıldıklarını görebilirsiniz.

Ya da bir in-line fonksiyonu - - aynı zamanda gibi parametrenin sınırlı tür kabul eder ve daha sonra bir lambda çizgisinde bir tek kullanımlık ya da başka kapsamı sınırlı brülör-fonksiyonu tanımlayan bir jeneratör işlevi bildirmek için sadece bu unset -f'kendini zaman s vasıtasıyla. Sen bir kabuk işlevi etrafta geçebilir.


Dosya tanımlayıcıları kullanmaya kıyasla bu ekstra karmaşıklığın avantajı nedir eval?
Stéphane Chazelas

@StephaneChazelas Benim bakış açımdan ek bir karmaşıklık yok. Aslında tam tersini görüyorum. Ayrıca, alıntı yapmak çok daha kolaydır ve .dotdosyalar ve akışlarla çalışır, böylece başka türden bir argüman listesi sorunlarıyla karşılaşmazsınız. Yine de, muhtemelen bir tercih meselesi. Kesinlikle daha temiz olduğunu düşünüyorum - özellikle eval değerlendirmeye başladığınızda - bu oturduğum yerden bir kabus.
mikeserv

@StephaneChazelas Yine de bir avantaj var - ve oldukça iyi bir avantaj. İlk değerlendirme ve ikinci değerlendirme bu yöntemle arka arkaya olmak zorunda değildir. Heredocument girişte değerlendirilir, ancak .dotiyi ve hazır olana kadar ya da hiç gelene kadar kaynak yapmanız gerekmez . Bu, değerlendirmelerini test etmede biraz daha fazla özgürlük sağlar. Ve girdide devletin esnekliğini sağlar - ki bu başka yollarla da ele alınabilir - ama bu bakış açısından olduğundan çok daha az tehlikelidir eval.
mikeserv

2

Bence fonksiyon hakkında bilgi yazdırmanın bir yolu,

gerekli bağımsız değişkenleri test edin ve yoksa çıkın ve bazı iletileri görüntüleyin

bash yerleşimini returnve / veya exither komut dosyasının başlangıcında (veya programı yürütmeden önce kaynakladığınız bazı dosyalarda) değiştirmektir. Yani sen yaz

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

Bunu çalıştırırsanız alacaksınız:

   function foo returns status 1

İhtiyacınız varsa, hata ayıklama bayrağıyla kolayca güncellenebilir:

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

Bu yol ifadesi yalnızca VERBOSE değişkeni ayarlandığında yürütülür (en azından komut dosyalarında ayrıntılı olarak bu şekilde kullanıyorum). Dekorasyon fonksiyonunu kesinlikle çözmez, ancak fonksiyonun sıfırdan farklı bir duruma dönmesi durumunda mesajları görüntüleyebilir.

Benzer şekilde , komut dosyasından çıkmak istiyorsanız, exittüm örneklerini değiştirerek yeniden tanımlayabilirsiniz return.

EDIT: Ben de onları ve iç içe olan bir sürü varsa bash fonksiyonları süslemek için kullandığım şekilde eklemek istedim. Bu betiği yazdığımda:

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

Ve çıktı için bunu alabilirim:

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

İşlevleri olan ve hatalarını ayıklamak isteyen, hangi işlev hatasının meydana geldiğini görmek yararlı olabilir. Aşağıda açıklanan üç işleve dayanmaktadır:

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

Ben yorumlarda mümkün olduğunca koymak için çalıştı, ama burada da açıklamasıdır: Kullandığım _ ()dekoratör olarak işlev, her fonksiyonun ilanından sonra koymak: biri foo () { _. Bu işlev, diğer işlevdeki derin işlevin ne kadar derin olduğuna bağlı olarak işlev adını uygun girintiyle yazdırır (varsayılan girinti olarak 4 boşluk kullanıyorum). Bunu her zamanki baskıdan ayırmak için genellikle gri renkte yazdırırım. Fonksiyonun bağımsız değişkenlerle veya onsuz olarak dekore edilmesi gerekiyorsa, dekoratör işlevindeki son satır öncesi değişiklik yapılabilir.

İşlev içinde bir şey yazdırmak için print (), kendisine iletilen her şeyi doğru girintiyle basan bir işlev tanıttım.

İşlev set_indentation_for_print_function, ${FUNCNAME[@]}diziden girintiyi hesaplayarak tam olarak ne anlama geldiğini yapar .

Örnek bir seçenekleri için geçemez için Bu şekilde, bazı kusurları vardır printister echo, örneğin -nya -e, hem de fonksiyon döner 1, bu dekore değilse. Ayrıca print, ekranın içine sarılacak olan terminal genişliğinden daha fazlasına iletilen argümanlar için, sarılmış çizginin girintisini görmeyiz.

Bu dekoratörleri kullanmanın en iyi yolu, bunları ayrı bir dosyaya ve her yeni betiğe bu dosyayı kaynaklamaktır source ~/script/hand_made_bash_functions.sh.

Fonksiyon dekoratörünü bash'a dahil etmenin en iyi yolunun, her fonksiyonun gövdesine dekoratör yazmak olduğunu düşünüyorum. Bence fonksiyon içinde bash içinde fonksiyon yazmak çok daha kolay, çünkü tüm değişkenleri global olarak ayarlayabiliyor, standart Nesne Odaklı Diller gibi değil. Bu, sanki kodunuzun etrafına etiket basmış gibi yapar. En azından bu hata ayıklama komut dosyaları için bana yardımcı oldu.



0

Bana göre bu, bash içine bir dekoratör deseni uygulamanın en basit yolu gibi geliyor.

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

Bu ShellCheck uyarılarını neden devre dışı bırakıyorsunuz? Doğru görünüyorlar (kesinlikle SC2068 uyarısı alıntı yaparak düzeltilmelidir "$@").
dimo414

0

Bash'de çok fazla (belki de çok fazla :)) metaprogramlama yapıyorum ve dekoratörleri anında davranışları yeniden uygulamak için çok değerli buldum. Benim bash-cache şeffaf kütüphane kullanımları dekorasyon asgari törenle Bash işlevleri memoize:

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

Açıkçası bc::cachesadece dekorasyondan daha fazlasını yapıyor, ancak altta yatan dekorasyon bc::copy_functionmevcut bir işlevi yeni bir isme kopyalamaya dayanıyor , böylece orijinal fonksiyon bir dekoratörle üzerine yazılabilir.

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

timeSüslü işlev olan bir dekoratörün basit bir örneği 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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.