Reputation: 126571
I am developing some convenience wrappers around another software package that defines a bash function. I would like to replace their bash function with an identically-named function of my own, while still being able to run their function from within mine. In other words, I need to either rename their function, or create some kind of persistent alias to it that won't be modified when I create my function of the same name.
To give a brief example of a naive attempt that I didn't expect to work (and indeed it does not):
$ 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
...
Obviously I don't want infinite recursion, I want:
do my thing
do their thing
How can I do this?
Upvotes: 41
Views: 9847
Reputation: 4192
I know it’s an old question, but no one has addressed the issue with recursion yet.
There is a clean way of copying recursive functions, relying on an obscure corner of Bash. So obscure, in fact, that finding an application for it came as a surprise to me. So here is it.
However, as explained below, this trick is not enough to handle all recursive functions. That is why I also present another solution, which may be more straightforward but also much more costly.
From man bash
, section “ALIASES” (emphasis mine):
The rules concerning the definition and use of aliases are somewhat confusing. Bash always reads at least one complete line of input, and all lines that make up a compound command, before executing any of the commands on that line or the compound command. Aliases are expanded when a command is read, not when it is executed. Therefore, an alias definition appearing on the same line as another command does not take effect until the next line of input is read. The commands following the alias definition on that line are not affected by the new alias. This behavior is also an issue when functions are executed. Aliases are expanded when a function definition is read, not when the function is executed, because a function definition is itself a command. As a consequence, aliases defined in a function are not available until after that function is executed. To be safe, always put alias definitions on a separate line, and do not use alias in compound commands.
Otherwise said, when a function is defined, all occurrences in its body of aliases which exist at that time are expanded. (And, conversely, no alias expansion takes place when the function is called.) This can be exploited to substitute recursive calls inside a function body, without resorting to dirty, unsound sed
invocations.
As already explained by other answers, the body of the function to copy can be obtained with declare -fp $old_name
. You then put the new name of the function on top of this body instead of the old name (using Bash’s variable substitution mechanism), and feed the whole thing to eval
to define the new function.
The code presented below is written in the spirit of @ingidotnet’s excellent answer, adding to it support for recursive functions.
tail
or sed
).declare -fp
).However there is a pitfall: the alias trick apparently does not catch all possible recursive calls. It misses at least the calls of the form $(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 "$@" || return
unset -f "$1"
}
The following code:
# 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
will work as expected (tested with 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
However, the following recursive function won’t be properly renamed.
# 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
The output is:
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
Here is an alternative approach. Simply define the new function as a wrapper function which re-defines the original function locally and calls it.
As compared to the alias trick, this tackles all recursive calls, but is much more costly, as the original function is be re-defined and restored at every call of the new function.
Here is the code corresponding to that idea. To the best of my knowledge, it has no remaining flaw.
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 "$@"
# 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"
)"
}
This time, example 2 from above works as expected:
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 "$@";
declare -i ret=$?;
if [ -z "$former_def" ]; then
unset -f fib;
else
eval "$former_def";
fi;
return $ret
}
55
Upvotes: 2
Reputation: 2613
Further golfed the copy_function
and rename_function
functions to:
copy_function() {
test -n "$(declare -f "$1")" || return
eval "${_/$1/$2}"
}
rename_function() {
copy_function "$@" || return
unset -f "$1"
}
Starting from @Dmitri Rubinstein's solution:
declare
twice. Error checking still works.func
) by using the _
special variable.
test -n ...
was the only way I could come up with to preserve _
and still be able to return on error.return 1
to return
(which returns the current status code)Once copy_function
is defined, it makes rename_function
trivial. (Just don't rename copy_function
;-)
Upvotes: 22
Reputation: 3215
For those of us forced to be compatible with bash 3.2 (you know who we are talking about), declare -f
doesn't work. I found type
can work
eval "$(type my_func | sed $'1d;2c\\\nmy_func_copy()\n')"
In function form, it would look like
copy_function()
{
eval "$(type "${1}"| sed $'1d;2c\\\n'"${2}"$'()\n')"
}
And if you really want to not rely on 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}"))"
}
But that's a bit wordy for me
Upvotes: 2
Reputation: 10459
To sum up all the other solutions and partially correct them, here is the solution which:
declare
twicetail
)But:
eval "${_//$1/$2}"
instead of eval "${_/$1/$2}"
(Note the double //
). However replacing the name fails on too simple function names (like a
) and it fails for calculated recursion (like command_help() { case "$1" in ''|-help) echo "help [command]"; return;; esac; "command_$1" -help "${@:2}"; }
)Everything combined:
: rename_fn oldname newname
rename_fn()
{
local a
a="$(declare -f "$1")" &&
eval "function $2 ${a#*"()"}" &&
unset -f "$1";
}
now the tests:
somefn() { echo one; }
rename_fn somefn thatfn
somefn() { echo two; }
somefn
thatfn
outputs as required:
two
one
Now try some more complicated cases, which all give the expected results or fails:
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
One can argue that following is still a bug:
a(){ echo HW; }; rename_fn a " b "; echo $?; b
as it should fail as " b "
is not a correct function name. If you really want this, you need following variant:
rename_fn()
{
local a
a="$(declare -f "$1")" &&
eval "function $(printf %q "$2") ${a#*"()"}" &&
unset -f "$1";
}
Now this catches this artificial case, too. (Please note that printf
with %q
is a bash
builtin.)
Of course you can split this up into copy+rename like this:
copy_fn() { local a; a="$(declare -f "$1")" && eval "function $(printf %q "$2") ${a#*"()"}"; }
rename_fn() { copy_fn "$@" && unset -f "$1"; }
I hope this is the 101% solution. If it needs improvement, please comment ;)
Upvotes: 6
Reputation: 631
If you just want to prepend something to the name, say orig_
, then I think the simplest is
eval orig_"$(declare -f theirfun)"
Upvotes: 14
Reputation: 441
The copy_function can be improved by using shell parameter expansion instead of tail command:
copy_function() {
declare -F "$1" > /dev/null || return 1
local func="$(declare -f "$1")"
eval "${2}(${func#*\(}"
}
Upvotes: 6
Reputation: 3122
Here is a function based on @Evan Broder's approach:
# 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}
}
Once this is defined, you can simply do rename_function func orig_func
Note that you can use a related approach to decorate/modify/wrap existing functions, as in @phs's answer:
# Syntax: prepend_to_function <name> [statements...]
function prepend_to_function()
{
local name=$1
shift
local body="$@"
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="$@"
eval "$(declare -f ${name} | head -n -1; echo ${body}; echo '}')"
}
Once these are defined, let's say you have an existing function as follows:
function foo()
{
echo stuff
}
Then you can do:
prepend_to_function foo echo before
append_to_function foo echo after
Using declare -f foo
, we can see the effect:
foo ()
{
echo before;
echo stuff;
echo after
}
Upvotes: 2
Reputation: 126571
Aha. Found a solution, though it's not real pretty:
$ 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
I'm sure this could be improved by a real bash wizard. In particular it'd be nice to ditch the need for a tempfile.
Update: bash wizard Evan Broder rose to the challenge (see accepted answer above). I reformulated his answer into a generic "copy_function" 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)"
}
Can be used like so:
$ theirfunc() { echo "do their thing"; }
$ copy_function theirfunc orig_theirfunc
$ theirfunc() { echo "do my thing"; orig_theirfunc; }
$ theirfunc
do my thing
do their thing
Very nice!
Upvotes: 13
Reputation: 774
Here's a way to eliminate the temp file:
$ 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
Upvotes: 52