बैश फंक्शन डेकोरेटर


10

अजगर में हम फ़ंक्शन को कोड के साथ सजा सकते हैं जो स्वचालित रूप से लागू होता है और फ़ंक्शन के खिलाफ निष्पादित होता है।

क्या बैश में भी ऐसी ही कोई विशेषता है?

वर्तमान में मैं जिस स्क्रिप्ट पर काम कर रहा हूं, मेरे पास कुछ बॉयलरप्लेट हैं जो आवश्यक तर्कों का परीक्षण करते हैं और यदि वे मौजूद नहीं हैं तो बाहर निकलें - और डिबगिंग ध्वज निर्दिष्ट होने पर कुछ संदेश प्रदर्शित करें।

दुर्भाग्य से मुझे हर फ़ंक्शन में इस कोड को पुन: स्थापित करना है और अगर मैं इसे बदलना चाहता हूं, तो मुझे हर फ़ंक्शन को संशोधित करना होगा।

क्या प्रत्येक फ़ंक्शन से इस कोड को हटाने का एक तरीका है और क्या यह अजगर के समान सज्जाकारों के लिए सभी कार्यों पर लागू होता है?


फ़ंक्शन तर्कों को मान्य करने के लिए आप इस स्क्रिप्ट का उपयोग करने में सक्षम हो सकते हैं जिसे मैंने हाल ही में एक साथ खींचा है, कम से कम एक शुरुआती बिंदु के रूप में।
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

यद्यपि आप अपने कार्य को दो बार सजाने के लिए दो बार सजा नहीं सकते।

के साथ 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, eval "_inner_$(typeset -f x)"बनाता है _inner_xमूल की एक सटीक प्रतिलिपि के रूप में x(के रूप में ही functions[_inner_x]=$functions[x]में zsh)।
स्टीफन चेज़लस

मुझे वह मिलता है - लेकिन आपको दो की आवश्यकता क्यों है?
मिकसेर्व

आप एक अलग संदर्भ की जरूरत है अन्यथा आप को पकड़ने के लिए सक्षम नहीं होगा भीतरी की return
स्टीफन चेज़लस

1
मैं तुम्हें वहाँ का पालन नहीं करते। मेरा जवाब यह है कि मैं अजगर सज्जाकारों को समझने के लिए
स्टीफन चेज़लस

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 ;

ऊपर मैंने दो कार्यों की घोषणा की और सेट किया _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) निर्दिष्ट नहीं करता है ।

आप आसानी से इस व्यवहार का विस्तार कर सकते हैं और अपने कार्य के लिए विभिन्न राज्यों को कॉन्फ़िगर कर सकते हैं।

अधिक?

यह मुश्किल से सतह को खरोंचता है, वैसे। मैं अक्सर इन तकनीकों का उपयोग मुख्य कार्य के इनपुट में किसी भी समय घोषित छोटे सहायक कार्यों को करने के लिए करता हूं - उदाहरण के लिए, अतिरिक्त स्थितिगत $@सरणियों के लिए आवश्यकतानुसार। वास्तव में - जैसा कि मेरा मानना ​​है, यह इस के बहुत करीब होना चाहिए कि उच्चतर आदेश शेल वैसे भी करते हैं। आप देख सकते हैं कि वे बहुत आसानी से प्रोग्राम नाम से हैं।

मैं एक जनरेटर फ़ंक्शन की घोषणा करना भी पसंद करता हूं जो एक सीमित प्रकार के पैरामीटर को स्वीकार करता है और फिर एकल-उपयोग या अन्यथा स्कोप-सीमित बर्नर-फ़ंक्शन को एक लैम्ब्डा की तर्ज पर परिभाषित करता है - या एक इन-लाइन फ़ंक्शन - जो कि बस unset -fतब ही होता है के माध्यम से। आप एक शेल फंक्शन पास कर सकते हैं।


उपयोग करने पर फ़ाइल विवरणकर्ताओं के साथ उस अतिरिक्त जटिलता का क्या लाभ है eval?
स्टीफन चेज़लस

@StephaneChazelas मेरे दृष्टिकोण से कोई जटिलता नहीं है। वास्तव में, मैं इसे दूसरे तरीके से देख रहा हूं। इसके अलावा, उद्धृत करना बहुत आसान है, और .dotफ़ाइलों और धाराओं के साथ काम करता है ताकि आप उसी तरह की तर्क सूची समस्याओं में न चलें जो आप अन्यथा कर सकते हैं। फिर भी, शायद यह वरीयता का मामला है। मुझे निश्चित रूप से लगता है कि यह साफ-सुथरा है - खासतौर पर तब जब आप इवलिंग से बाहर निकलते हैं - यह एक बुरा सपना है जहां से मैं बैठता हूं।
मोकेसर

@StephaneChazelas हालांकि एक फायदा है - और यह एक बहुत अच्छा है। प्रारंभिक eval और दूसरा eval को इस विधि के साथ वापस करने की आवश्यकता नहीं है। Heredocument का मूल्यांकन इनपुट पर किया जाता है, लेकिन आपके पास .dotतब तक स्रोत नहीं है जब तक आप अच्छे और तैयार न हों - या कभी भी। यह आपको इसके मूल्यांकन के परीक्षण में थोड़ी अधिक स्वतंत्रता देता है। और यह इनपुट पर राज्य का लचीलापन प्रदान करता है - जिसे अन्य तरीकों से नियंत्रित किया जा सकता है - लेकिन यह उस दृष्टिकोण से बहुत कम खतरनाक है eval
मिकसेर्व

2

मुझे लगता है कि फ़ंक्शन के बारे में जानकारी प्रिंट करने का एक तरीका है, जब आप

यदि वे मौजूद नहीं हैं, तो आवश्यक तर्कों और निकास का परीक्षण करें - और कुछ संदेश प्रदर्शित करें

बैश बिलिन 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 सेट किया गया हो (कम से कम मैं अपनी स्क्रिप्ट में क्रिया का उपयोग कैसे करूं)। यह निश्चित रूप से सजाने के कार्य की समस्या को हल नहीं करता है, लेकिन यह मामले में संदेश प्रदर्शित कर सकता है गैर-शून्य स्थिति।

इसी तरह आप स्क्रिप्ट से बाहर निकलना चाहते हैं, तो exitसभी उदाहरणों को बदलकर, फिर से परिभाषित कर सकते हैं return

संपादित करें: मैं यहाँ जोड़ना चाहता था जिस तरह से मैं बैश में कार्यों को सजाने के लिए उपयोग करता हूं, अगर मेरे पास बहुत सारे हैं और साथ ही साथ नेस्टेड हैं। जब मैं यह स्क्रिप्ट लिखता हूं:

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

यह किसी के लिए मददगार हो सकता है जिसके पास फ़ंक्शन हैं और उन्हें डिबग करना चाहता है, जिसमें यह देखने के लिए कि फ़ंक्शन त्रुटि हुई। यह तीन कार्यों पर आधारित है, जिन्हें नीचे वर्णित किया जा सकता है:

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

मुझे लगता है कि बैश में फ़ंक्शन डेकोरेटर को शामिल करने का सबसे अच्छा तरीका है, प्रत्येक फ़ंक्शन के शरीर में डेकोरेटर लिखना है। मुझे लगता है कि बैश में फ़ंक्शन के अंदर फ़ंक्शन लिखना बहुत आसान है, क्योंकि इसमें सभी चर वैश्विक सेट करने का विकल्प है, मानक ऑब्जेक्ट-ओरिएंटेड भाषाओं की तरह नहीं। इससे ऐसा लगता है जैसे आप अपने कोड के चारों ओर लेबल लगा रहे हैं। कम से कम जिसने मुझे डिबगिंग स्क्रिप्ट के लिए मदद की।


0

हो सकता है कि http://sourceforge.net/projects/oobash/ प्रोजेक्ट में डेकोरेटर उदाहरण आपकी मदद कर सकते हैं (ओबाश / डॉक्स / उदाहरण / डेकोरेटर.श)।


0

मेरे लिए यह बैश के अंदर एक डेकोरेटर पैटर्न को लागू करने का सबसे सरल तरीका है।

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

आप इन शेलचेक चेतावनियों को अक्षम क्यों कर रहे हैं? वे सही प्रतीत होते हैं (निश्चित रूप से SC2068 चेतावनी को उद्धृत करके तय किया जाना चाहिए "$@")।
dimo414

0

मैं बैश में बहुत (शायद बहुत अधिक :)) मेटाप्रोग्रामिंग करता हूं, और डेकोरेटर को मक्खी पर फिर से व्यवहार करने के लिए अमूल्य पाया है। मेरे बाश-कैश लाइब्रेरी न्यूनतम समारोह के साथ बाश कार्यों को पारदर्शी बनाने के लिए सजावट का उपयोग करता है:

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
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.