Подтвердить что ты не робот

Как переименовать функцию bash?

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

Чтобы дать краткий пример наивной попытки, которую я не ожидал работать (и это действительно не так):

$ theirfunc() { echo "do their thing"; }
$ _orig_theirfunc() { theirfunc; }
$ theirfunc() { echo "do my thing"; _orig_theirfunc }
$ theirfunc
do my thing
do my thing
do my thing
...

Очевидно, что я не хочу бесконечной рекурсии, я хочу:

do my thing
do their thing

Как я могу это сделать?

4b9b3361

Ответ 1

Здесь можно устранить временный файл:

$ theirfunc() { echo "do their thing"; }
$ eval "$(echo "orig_theirfunc()"; declare -f theirfunc | tail -n +2)"
$ theirfunc() { echo "do my thing"; orig_theirfunc; }
$ theirfunc
do my thing
do their thing

Ответ 2

Далее играли в гольф функции copy_function и rename_function, чтобы:

copy_function() {
  test -n "$(declare -f "$1")" || return 
  eval "${_/$1/$2}"
}

rename_function() {
  copy_function "[email protected]" || return
  unset -f "$1"
}

Начиная с решения @Dmitri Rubinstein:

  • Не нужно звонить declare дважды. Проверка ошибок все еще работает.
  • Исключите временную переменную (func), используя специальную переменную _.
    • Примечание: использование test -n ... было единственным способом, которым я мог придумать, чтобы сохранить _ и все еще иметь возможность вернуться при ошибке.
  • Измените return 1 на return (который возвращает текущий код состояния)
  • Используйте замену шаблона вместо удаления префикса.

Как только copy_function определен, он делает rename_function тривиальным. (Только не переименовывайте copy_function ;-)

Ответ 3

Ага. Нашел решение, хотя оно не очень красивое:

$ theirfunc() { echo "do their thing"; }
$ echo "orig_theirfunc()" > tmpfile
$ declare -f theirfunc | tail -n +2 >> tmpfile
$ source tmpfile
$ theirfunc() { echo "do my thing"; orig_theirfunc; }
$ theirfunc
do my thing
do their thing

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

Обновить: bash мастер Evan Broder поднялся до задачи (см. принятый ответ выше). Я переформулировал его ответ на общую функцию "copy_function":

# copies function named $1 to name $2
copy_function() {
    declare -F $1 > /dev/null || return 1
    eval "$(echo "${2}()"; declare -f ${1} | tail -n +2)"
}

Можно использовать так:

$ theirfunc() { echo "do their thing"; }
$ copy_function theirfunc orig_theirfunc
$ theirfunc() { echo "do my thing"; orig_theirfunc; }
$ theirfunc
do my thing
do their thing

Очень приятно!

Ответ 4

Если вы просто хотите добавить что-то к имени, скажем orig_, то я думаю, что самый простой

eval orig_"$(declare -f theirfun)"

Ответ 5

Функция copy_function может быть улучшена с помощью расширения оболочки вместо команды tail:

copy_function() {
  declare -F "$1" > /dev/null || return 1
  local func="$(declare -f "$1")"
  eval "${2}(${func#*\(}"
}

Ответ 6

Подводя итог всем остальным решениям и частично их исправьте, вот решение, которое:

  • не использует declare дважды
  • не нужны внешние программы (например, tail)
  • нет неожиданных замен
  • относительно короткий
  • защищает вас от обычных ошибок программирования благодаря правильному цитированию

Но:

  • Вероятно, это не работает для рекурсивных функций, поскольку имя функции, используемое для рекурсии внутри копии, не заменяется. Получение такого права на замену является слишком сложной задачей. Если вы хотите использовать такие замены, вы можете попробовать этот ответ fooobar.com/questions/204284/... с eval "${_//$1/$2}" вместо eval "${_/$1/$2}" (обратите внимание на double //). Однако замена имени терпит неудачу на слишком простых именах функций (например, a) и не выполняется для рассчитанной рекурсии (например, command_help() { case "$1" in ''|-help) echo "help [command]"; return;; esac; "command_$1" -help "${@:2}"; })

Все вместе:

: rename_fn oldname newname
rename_fn()
{
  local a
  a="$(declare -f "$1")" &&
  eval "function $2 ${a#*"()"}" &&
  unset -f "$1";
}

теперь тесты:

somefn() { echo one; }
rename_fn somefn thatfn
somefn() { echo two; }
somefn
thatfn

по мере необходимости:

two
one

Теперь попробуйте несколько более сложных случаев, которые все дают ожидаемые результаты или сбой:

rename_fn unknown "a b"; echo $?
rename_fn "a b" murx; echo $?

a(){ echo HW; }; rename_fn " a " b; echo $?; a
a(){ echo "'HW'"; }; rename_fn a b; echo $?; b
a(){ echo '"HW"'; }; rename_fn a b; echo $?; b
a(){ echo '"HW"'; }; rename_fn a "b c"; echo $?; a

Можно утверждать, что следующая ошибка -

a(){ echo HW; }; rename_fn a " b "; echo $?; b

поскольку он должен потерпеть неудачу, поскольку " b " не является правильным именем функции. Если вы действительно этого хотите, вам нужен следующий вариант:

rename_fn()
{
  local a
  a="$(declare -f "$1")" &&
  eval "function $(printf %q "$2") ${a#*"()"}" &&
  unset -f "$1";
}

Теперь это поймает и этот искусственный случай. (Обратите внимание, что printf с %q является встроенным bash.)

Конечно, вы можете разбить его на копию + переименовать следующим образом:

copy_fn() { local a; a="$(declare -f "$1")" && eval "function $(printf %q "$2") ${a#*"()"}"; }
rename_fn() { copy_fn "[email protected]" && unset -f "$1"; }

Надеюсь, это решение на 101%. Если это требует улучшения, прокомментируйте:)

Ответ 7

Вот функция, основанная на подходе @Evan Broder:

# Syntax: rename_function <old_name> <new_name>
function rename_function()
{
    local old_name=$1
    local new_name=$2
    eval "$(echo "${new_name}()"; declare -f ${old_name} | tail -n +2)"
    unset -f ${old_name}
}

Как только это определено, вы можете просто сделать rename_function func orig_func

Обратите внимание, что вы можете использовать связанный подход для украшения/изменения/переноса существующих функций, как в ответе @phs:

# Syntax: prepend_to_function <name> [statements...]
function prepend_to_function()
{
    local name=$1
    shift
    local body="[email protected]"
    eval "$(echo "${name}(){"; echo ${body}; declare -f ${name} | tail -n +3)"
}

# Syntax: append_to_function <name> [statements...]
function append_to_function()
{
    local name=$1
    shift
    local body="[email protected]"
    eval "$(declare -f ${name} | head -n -1; echo ${body}; echo '}')"
}

Как только они определены, скажем, у вас есть существующая функция следующим образом:

function foo()
{
    echo stuff
}

Затем вы можете сделать:

prepend_to_function foo echo before
append_to_function foo echo after

Используя declare -f foo, мы видим эффект:

foo ()
{
    echo before;
    echo stuff;
    echo after
}

Ответ 8

Для тех из нас, кто вынужден быть совместимым с bash 3.2 (вы знаете, о ком мы говорим), declare -f не работает. Я нашел type может работать

eval "$(type my_func | sed $'1d;2c\\\nmy_func_copy()\n')"

В форме функции это будет выглядеть как

copy_function()
{
  eval "$(type "${1}"| sed $'1d;2c\\\n'"${2}"$'()\n')"
}

И если вы действительно не хотите полагаться на sed...

function copy_function()
{
  eval "$({
  IFS='' read -r line
  IFS='' read -r line
  echo "${2} ()"
  while IFS='' read -r line || [[ -n "$line" ]]; do
    echo "$line"
  done
  }< <(type "${1}"))"
}

Но это немного многословно для меня

Ответ 9

Я знаю, это старый вопрос, но никто еще не решил проблему с рекурсией.

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

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

Частичное решение с использованием псевдонимов

Объяснение

Из man bash, раздел "Псевдонимы" (выделено мной):

Правила, касающиеся определения и использования псевдонимов, несколько сбивает с толку. Bash всегда читает хотя бы одну полную строку ввода, и все строки, составляющие составную команду, перед выполнением любого из команды в этой строке или составная команда. Псевдонимы раскрывается при чтении команды, а не при ее выполнении. Поэтому определение псевдонима появляется в той же строке, что и другая команда не вступает в силу, пока не будет прочитана следующая строка ввода. Команды после определения псевдонима в этой строке не влияет новый псевдоним. Такое поведение также является проблемой, когда функции казнены. Псевдонимы раскрываются при чтении определения функции, не когда функция выполняется, потому что определение функции сама команда. Как следствие, псевдонимы, определенные в функции недоступно до тех пор, пока эта функция не будет выполнена. Быть в безопасности, всегда помещайте определения псевдонимов в отдельной строке и не используйте псевдоним в составных командах.

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

Как уже объяснялось другими ответами, тело функции для копирования можно получить с помощью declare -fp $old_name. Затем вы помещаете новое имя функции поверх этого тела вместо старого (используя механизм подстановки переменных Bashs) и передаете все это в eval, чтобы определить новую функцию.

Код

Код, представленный ниже, написан в духе @ingidotnets превосходного ответа, добавив в него поддержку рекурсивных функций.

  • Он использует только встроенные функции оболочки, а не внешние программы (такие как tail или sed).
  • Он не выполняет неправильных текстовых подстановок (ну, за исключением очень небольшого предположения о формате вывода declare -fp).
  • Это правильно указано.
  • Он поддерживает копирование некоторых рекурсивных функций.

Однако здесь есть подводный камень: трюк с псевдонимом, по-видимому, не улавливает все возможные рекурсивные вызовы. Он пропускает как минимум вызовы вида $(old_name ...).

function copy_function() {
    declare old="$1"
    declare new="$2"
    # input checks:
    if [[ ! "$old" =~ ^[a-zA-Z0-9._-]+$ ]] ; then
        printf >&2 'copy_function: %q is (probably) not a valid function name\n' "$old"
        return 1
    elif [[ ! "$new" =~ ^[a-zA-Z0-9._-]+$ ]] ; then
        printf >&2 'copy_function: %q is (probably) not a valid function name\n' "$new"
        return 1
    fi
    # find the definition of the existing function:
    declare def ; def="$(declare -fp "$old")" || return
    # create an alias, in order to substitute $old for $new in function body:
    declare former_alias="$(alias "$old" 2>/dev/null)"
    alias "$old=$new"
    # define the function $new:
    eval "${def/#$old ()/$new ()}"
    # remove the alias, restoring the former one if needed:
    unalias "$old"
    [ -n "$former_alias" ] && eval "$former_alias" || true
}

rename_function() {
    copy_function "[email protected]" || return
    unset -f "$1"
}

Пример 1

Следующий код:

# a recursive function which prints a range of numbers
function enum() {
    declare -i i="$1"
    declare -i j="$2"
    if [ $i -gt $j ] ; then
        return
    elif [ $i -eq $j ] ; then
        echo $i
    else
        declare -i k=$(( i + j ))
        [ $k -lt 0 ] && k=$(( k-1 ))
        k=$(( k / 2 ))
        enum $i $k
        enum $(( k+1 )) $j
    fi
}
rename_function enum range
declare -fp enum range
range 1 5

будет работать как положено (протестировано с bash 5.0.7):

bash: declare: enum: not found
range () 
{ 
    declare -i i="$1";
    declare -i j="$2";
    if [ $i -gt $j ]; then
        return;
    else
        if [ $i -eq $j ]; then
            echo $i;
        else
            declare -i k=$(( i + j ));
            [ $k -lt 0 ] && k=$(( k-1 ));
            k=$(( k / 2 ));
            range $i $k;
            range $((k+1)) $j;
        fi;
    fi
}

1
2
3
4
5

Пример 2

Однако следующая рекурсивная функция не будет правильно переименована.

# the Fibonacci function
function fib() {
    declare -i n="$1"
    if [ $n -le 1 ] ; then
        echo $n
    else
        declare -i x=$(fib $(( n-2 )))
        declare -i y=$(fib $(( n-1 )))
        echo $(( x + y ))
    fi
}
rename_function fib FIB
declare -fp fib FIB
FIB 5

Вывод:

bash: declare: fib: not found
FIB () 
{ 
    declare -i n="$1";
    if [ $n -le 1 ]; then
        echo $n;
    else
        declare -i x=$(fib $(( n-2 )));
        declare -i y=$(fib $(( n-1 )));
        echo $(( x + y ));
    fi
}

bash: fib: command not found
bash: fib: command not found
0

Полное, но более сложное решение с использованием переопределений функций

Вот альтернативный подход. Просто определите новую функцию как функцию-обертку, которая локально переопределяет исходную функцию и вызывает ее.

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

Код

Вот код, соответствующий этой идее. Насколько я знаю, у него нет недостатка.

function copy_function() {
    declare old="$1"
    declare new="$2"
    # input checks:
    if [[ ! "$old" =~ ^[a-zA-Z0-9._-]+$ ]] ; then
        printf >&2 'copy_function: %q is (probably) not a valid function name\n' "$old"
        return 1
    elif [[ ! "$new" =~ ^[a-zA-Z0-9._-]+$ ]] ; then
        printf >&2 'copy_function: %q is (probably) not a valid function name\n' "$new"
        return 1
    fi
    # find the definition of the existing function:
    declare def ; def="$(declare -fp "$old")" || return
    # define the new function as a wrapper around the old function:
    eval "$(printf '
            function %s() {
                # save the current function $old, if any:
                declare former_def="$(declare -fp %s 2>/dev/null)"
                # re-define the original function $old:
                %s
                # call the original function $old:
                %s "[email protected]"
                # restore the current function $old, if any:
                declare -i ret=$?
                if [ -z "$former_def" ] ; then
                    unset -f %s
                else
                    eval "$former_def"
                fi
                return $ret
            }
        ' "$new" "$old" "$def" "$old" "$old"
    )"
}

Пример 2

На этот раз пример 2 сверху работает, как и ожидалось:

bash: declare: fib: not found
FIB () 
{ 
    declare former_def="$(declare -fp fib 2>/dev/null)";
    function fib () 
    { 
        declare -i n="$1";
        if [ $n -le 1 ]; then
            echo $n;
        else
            declare -i x=$(fib $(( n-2 )));
            declare -i y=$(fib $(( n-1 )));
            echo $(( x + y ));
        fi
    };
    fib "[email protected]";
    declare -i ret=$?;
    if [ -z "$former_def" ]; then
        unset -f fib;
    else
        eval "$former_def";
    fi;
    return $ret
}

55