Как переименовать функцию 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
Как я могу это сделать?
Ответы
Ответ 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