0

Is it possible to save the old function (named foo) in such a way that you can call it within a new function (also named foo), perhaps from a different name, or such?

To make this concrete, here's specifically what I'm trying to do:

In bash, the command_not_found_handle is a function which, if defined, is called whenever bash cannot find the command which the user is trying to run. For example, Ubuntu uses this to suggest packages which can be installed:

$ pv
The program 'pv' is currently not installed. You can install it by typing:
sudo apt-get install pv

This is really nice, and I'd like to keep this behavior. However, I'd also like to add another command_not_found handle, which simply runs a given DEFAULT_COMMAND with whatever command line the user typed, like so:

$ DEFAULT_COMMAND=git
$ status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working
directory)

        modified:   .bashrc
        modified:   .gitconfig
        modified:   .vimrc

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        .bash_profile
        .gitglobalignore
        .gvimrc
        node_modules/

no changes added to commit (use "git add" and/or "git commit -a")

I already have a function which does the above, and it works like a dream; however, using it overrides the default Ubuntu command_not_found handle, which can be handy to have when I can't remember a package name for a given executable.

How do I get both? Thanks!

NB: The obvious solution is to find and copy the Ubuntu built-in command_not_found logic into my script; but that is not ideal, because it means I have to manually update it later, when Ubuntu changes the way they do things. I'm hoping for something more generic, if possible.

Edit: It would be best not to have to resort to string manipulation, as can be done by saving the text of the function to a variable, mangling it, then evaling it.

jpaugh
  • 6,634
  • 4
  • 38
  • 90
  • 2
    See http://stackoverflow.com/questions/6916856/can-bash-show-a-functions-definition for how to get the function definition. You can replace the function name and call `eval` to execute it. – Barmar Aug 04 '15 at 00:58
  • Thanks! `declare -f` gets me most of the way there! If you feel like writing it out, I'll give you "Accepted answer" for this; elsewise, I can. – jpaugh Aug 04 '15 at 13:23
  • This isn't really quite a duplicate. I'm trying to accomplish a different thing, and it's useful to know whether http://stackoverflow.com/q/6916856/712526 is the best way. (I was really hoping it could be done without string manipulation) – jpaugh Aug 06 '15 at 00:30
  • If I just post an answer saying to use `declare -f command_not_found`, that's essentially a duplicate. So unless I go the extra step and write the code that parses it, so it can save it with a new name, it's not really an answer. You can answer your own question, so I suggest you do so. – Barmar Aug 06 '15 at 00:37
  • I can see you don't need (or mind) the rep. I was just being overly cordial. I'll have a solution together in the next hour. – jpaugh Aug 06 '15 at 00:40
  • 1
    Yeah, I lose this much rep in my seat cushions. :) – Barmar Aug 06 '15 at 00:41
  • @Barmar Thanks for all your help. Be sure to put plastic under your couch, to catch all the loose rep! – jpaugh Aug 06 '15 at 02:19

1 Answers1

1

Based on Barmar's suggestion, I was able to implement a workable solution. The following function can be used to rename an arbitrary bash function to another name.

renameFunction () {
    local oldName="$1"; shift
    local newName="$1"

    local definition="$(declare -f "$oldName")"
    if [[ $? -gt 0 ]]; then
        echo >&2 "renameFunction: $oldName is not a function"
        return
    fi

    if declare -f  "$newName" >/dev/null 2>/dev/null; then
        echo >&2 "renameFunction: $newName is already defined"
        return
    fi

    eval "$(echo "${definition/"$oldName"/"$newName"}")"
    # Does not work for recursive functions (like "//" would), but also
    # doesn't break if $oldName is a substring of something else

    unset "$oldName"
}

Notes

  • The last line

    unset "$oldName"
    

    is optional — and without it, this becomes a "copy function" utility.

  • The pattern substitution would work for a recursive function if it were changed to the following (note the //):

    eval "$(echo "${definition//"$oldName"/"$newName"}")"
    

    However, this fails if the function name is a substring of something else within the definition. Since recursion is relatively rare in shell scripts, I took the less brittle approach.

  • The quoting is correct, despite being too complex for the SO syntax highlighter. (The quoting is also unnecessary, unless you like to play with $IFS.)


For completeness' sake, here's how I'm using this function:

# The DEFAULT_CMD is the command to run if the command line could
# not be understood.  Set the DEFAULT_CMD to git, once; the user can
# change it at any time
DEFAULT_CMD=git

# Save the old command_not_found_handle for reuse
renameFunction command_not_found_handle __PREVIOUS_COMMAND_NOT_FOUND_HANDLE

command_not_found_handle () {
    eval '"$DEFAULT_CMD" $DEFAULT_CMD_PREFIX_ARGS "$@" $DEFAULT_CMD_POSTFIX_ARGS'
    if [ $? -gt 0 ]; then
        __PREVIOUS_COMMAND_NOT_FOUND_HANDLE "$@"
    fi
}
export DEFAULT_CMD

command_not_found_handle is called by bash whenever it cannot find program or command that the user specified. It receives as its arguments the entire command-line.

This function tries to execute the command-line as a "sub command" of the given DEFAULT_CMD. If it does not succeed, it tries the old command_not_found_handle

jpaugh
  • 6,634
  • 4
  • 38
  • 90
  • I don't like this solution because of the string mangling and `eval`ing. Can you, gentle reader, do better? :-) – jpaugh Aug 06 '15 at 02:31