Декоратор функції 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

Ви не можете зателефонувати прикрасити двічі, щоб прикрасити свою функцію двічі.

З 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

@mikeserv, eval "_inner_$(typeset -f x)"створює _inner_xяк точну копію оригіналу x(те саме, що і functions[_inner_x]=$functions[x]в zsh).
Стефан Шазелас

Я це розумію - але навіщо вам взагалі два?
mikeserv

Вам потрібен інший контекст інакше ви не змогли б вловити внутрішню «S return.
Стефан Шазелас

1
Я не слідкую за тобою там. Моя відповідь - це спроба наблизити карту до того, що я розумію декораторів пітона
Stéphane 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 ;

Вище оголошую дві функції і встановлюю _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працює з файлами та потоками, тому ви не зіткнетесь з тими ж проблемами зі списком аргументів, які могли б бути в іншому випадку. Все-таки це, мабуть, питання переваги. Я, звичайно, вважаю, що це чистіше - особливо коли ти потрапляєш у прихильний евал - це кошмар, з якого я сиджу.
mikeserv

@StephaneChazelas Однак є одна перевага - і це дуже непогано. Початковий eval і другий eval не повинні повертатися назад до цього методу. Геодокумент оцінюється на вході, але вам не доведеться вводити .dotджерело, поки ви не будете хорошими і готовими - або коли-небудь. Це дає вам трохи більше свободи в тестуванні її оцінок. І це забезпечує гнучкість стану на вході - що може бути вирішено іншими способами - але з цієї точки зору це набагато менш небезпечно, ніж є eval.
mikeserv

2

Я думаю, що один із способів надрукувати інформацію про функцію, коли ти

протестуйте необхідні аргументи та вийдіть, якщо їх не існує - і відобразіть деякі повідомлення

полягає в тому, щоб змінити bash вбудований 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, якщо ви хочете вийти зі скрипту.

EDIT: Я хотів додати сюди те, як я використовую для прикраси функцій в 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

Це може бути корисним тому, хто має функції та хоче їх налагодити, щоб побачити, в якій помилці виникла функція. Він заснований на трьох функціях, які можна описати нижче:

#!/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. Принаймні, це допомогло мені для налагодження сценаріїв.



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-cache використовує декор для прозорого запам’ятовування функцій 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.