Carl Meyer
Carl Meyer

Reputation: 126571

How do I rename a bash function?

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

Answers (9)

Maëlan
Maëlan

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.

A partial solution using aliases

Explanation

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.

Code

The code presented below is written in the spirit of @ingidotnet’s excellent answer, adding to it support for recursive functions.

  • It only uses shell builtins, no external programs (such as tail or sed).
  • It does not perform unsound, textual substitutions (well, except for a very small assumption about the output format of declare -fp).
  • It is correctly quoted.
  • It supports copying some recursive functions.

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"
}

Example 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

Example 2

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

A complete but heavier solution using function re-definitions

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.

Code

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"
    )"
}

Example 2

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

ingydotnet
ingydotnet

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:

  • No need to call declare twice. Error checking still works.
  • Eliminate temp var (func) by using the _ special variable.
    • Note: using test -n ... was the only way I could come up with to preserve _ and still be able to return on error.
  • Change return 1 to return (which returns the current status code)
  • Use a pattern substitution rather than prefix removal.

Once copy_function is defined, it makes rename_function trivial. (Just don't rename copy_function;-)

Upvotes: 22

Andy
Andy

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

Tino
Tino

Reputation: 10459

To sum up all the other solutions and partially correct them, here is the solution which:

  • does not use declare twice
  • does not need external programs (like tail)
  • does no unexpected replacements
  • is relatively short
  • protects you against usual programming bugs thanks to correct quoting

But:

  • It probably does not work on recursive functions, as the function name used for recursion within the copy is not replaced. Getting such a replacement right is a much too complex task. If you want to use such replacements, you can try this answer https://stackoverflow.com/a/18839557 whith 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

parched
parched

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

Dmitri Rubinstein
Dmitri Rubinstein

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

Gareth Stockwell
Gareth Stockwell

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

Carl Meyer
Carl Meyer

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

Evan Broder
Evan Broder

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

Related Questions