Bash Function Decorator

10

В python мы можем украшать функции кодом, который автоматически применяется и выполняется для функций.

Есть ли подобная особенность в bash?

В сценарии, над которым я сейчас работаю, у меня есть несколько шаблонов, которые проверяют необходимые аргументы и завершают работу, если они не существуют, и отображают некоторые сообщения, если установлен флаг отладки.

К сожалению, я должен повторно вставить этот код в каждую функцию, и если я хочу изменить его, мне придется изменить каждую функцию.

Есть ли способ удалить этот код из каждой функции и применить его ко всем функциям, аналогично декораторам в python?

nfarrar
источник
Для проверки аргументов функций вы можете использовать этот скрипт, который я недавно собрал, по крайней мере, в качестве отправной точки.
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
Я не следую за тобой там. Мой ответ - попытка в качестве близкой карты того, что я понимаю как декораторы питона
Стефан Шазелас
5

Я уже несколько раз обсуждал, как и почему методы, описанные ниже, работают, поэтому я не буду делать это снова. Лично мои любимые по теме здесь и здесь .

Если вам не интересно читать это, но все же любопытно, просто поймите, что документы here, прикрепленные к входу функции, оцениваются на предмет расширения оболочки перед выполнением функции, и что они генерируются заново в том состоянии, в котором они были, когда функция была определена каждый раз, когда функция вызывается.

DECLARE

Вам просто нужна функция, которая объявляет другие функции.

_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сама по себе, когда через. Вы можете передать функцию оболочки вокруг.

mikeserv
источник
В чем преимущество такой дополнительной сложности с файловыми дескрипторами по сравнению с использованием eval?
Стефан Шазелас
@ StephaneChazelas Там нет дополнительной сложности с моей точки зрения. На самом деле, я вижу это наоборот. Кроме того, цитирование намного проще и .dotработает с файлами и потоками, поэтому вы не столкнетесь с такими же проблемами списка аргументов, которые могли бы возникнуть в противном случае. Тем не менее, это, вероятно, вопрос предпочтений. Я, конечно, думаю, что это чище - особенно когда вы начинаете ценить Eval - это кошмар с того места, где я сижу.
mikeserv
@StephaneChazelas Есть одно преимущество - и оно довольно хорошее. Первоначальный eval и второй eval не должны быть спиной к спине с этим методом. Heredocument оценивается на входе, но вам не нужно исходить, .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, если вы хотите выйти из скрипта.

РЕДАКТИРОВАТЬ: Я хотел бы добавить сюда, как я использую для украшения функций в 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[@]}массива.

У этого способа есть некоторые недостатки, например, нельзя передать опции , например , printlike или , а также, если функция возвращает 1, она не оформлена. А также для аргументов, передаваемых на ширину терминала, которые будут перенесены на экран, отступ для обернутой строки не будет виден.echo-n-eprint

Отличный способ использовать эти декораторы - поместить их в отдельный файл и в каждом новом скрипте получить этот файл 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
dimo414
источник