Why is this Bash function within a git alias executing twice, and why does adding `exit` fix it?

If I fail to explicitly call exit for certain function-based Bash scripts then there are additional unexpected executions for some functions. What is causing this? The behavior was first noticed while making a git alias as part of answering another user’s question on StackOverflow. That alias was composed of this script (which runs the function twice instead of once):

#!/usr/bin/env bash

github(){
        echo github;            
};

twitter(){ 
        echo twitter;            
};

facebook(){ 
        echo facebook;
};

if [[ $(type -t "$1") == "function" ]];
then 
        "$1";
else
        echo "There is no defined function for $1";
fi;

But this slightly modified script executes as expected (runs the function only once):

  • git add and commit single tracked file in one command
  • How can I automatically retain aliases when cloning a git repository?
  • how to make this git alias?
  • Why does “git push origin @” not work?
  • Git Alias Problem
  • Creating aliases for Git branch names
  • #!/usr/bin/env bash
    
    github(){
            echo github;            
    };
    
    twitter(){ 
            echo twitter;            
    };
    
    facebook(){ 
            echo facebook;
    };
    
    if [[ $(type -t "$1") == "function" ]];
    then 
            "$1";
            exit 0;
    else
            echo "There is no defined function for $1";
            exit 1;
    fi;
    

    Here is exactly what is happening when I run those scripts via a git alias (added set command for debugging purposes only):

    $ git config --global alias.encrypt-for '!set -evu -o pipefail;github(){ echo github;};twitter(){ echo twitter;};facebook(){ echo facebook;};if [[ $(type -t "$1") == "function" ]];then "$1"; exit 0; else echo "There is no defined function for $1"; exit 1; fi;'
    $ git encrypt-for "github"
    type -t "$1"
    github
    
    $ git config --global alias.encrypt-for '!set -evu -o pipefail;github(){ echo github;};twitter(){ echo twitter;};facebook(){ echo facebook;};if [[ $(type -t "$1") == "function" ]];then "$1"; else echo "There is no defined function for $1"; fi;'
    $ git encrypt-for "github"
    type -t "$1"
    github
    github
    

    The output from set -x:

    $ git encrypt-for "github"
    ++ type -t github
    + [[ function == \f\u\n\c\t\i\o\n ]]
    + github
    + echo github
    github
    + github
    + echo github
    github
    

    The output from replacing echo github with echo "I am echo in github" as a way of ruling out the echo command as the source of the second function execution:

    $ git encrypt-for "github"
    ++ type -t github
    + [[ function == \f\u\n\c\t\i\o\n ]]
    + github
    + echo 'I am echo in github'
    I am echo in github
    + github
    + echo 'I am echo in github'
    I am echo in github
    

    The following is the simplest version of the alias/script which gives the undesired behavior of double execution:

    g(){
        echo "once";
    };
    $1;
    

    And this is the resulting output from executing the simplified alias/script (which has the incorrect behavior of executing twice):

    $ git config --global alias.encrypt-for '!g(){ echo "once";};$1;'
    $ git encrypt-for g
    once
    once
    

  • Creating Git alias that gets the last commit
  • Git config alias escaping
  • Klocwork Get the amount of issues in a provided build number
  • Unable to create git alias?
  • Launch TestNG suite from bash script
  • Automatically reword all rebased commits
  • 2 Solutions collect form web for “Why is this Bash function within a git alias executing twice, and why does adding `exit` fix it?”

    That’s because of the way git handles aliases:

    Given an alias

    [alias]
        myalias = !string
    

    where string is any string that represents some code, when calling git myalias args where args is a (possibly empty) list of arguments, git will execute:

        sh -c 'string "$@"' 'string' args
    

    For example:

    [alias]
        banana = !echo "$1,$2,SNIP "
    

    and calling

    git banana one 'two two' three
    

    git will execute:

    sh -c 'echo "$1,$2,SNIP " "$@"' 'echo "$1,$2,SNIP "' one 'two two' three
    

    and so the output will be:

    one,two two,SNIP one two two three
    

    In your case,

    [alias]
        encrypt-for = "!g(){ echo \"once\";};$1;"
    

    and calling

    git encrypt-for g
    

    git will execute:

    sh -c 'g(){ echo "once";};$1;"$@"' 'g(){ echo "once";};$1;' g
    

    For clarity, let me rewrite this in an equivalent form:

    sh -c 'g(){ echo "once";};$1;"$@"' - g
    

    I only replaced the 'g(){ echo "once";};$1;' part (that will be sh‘s $0‘s positional parameter and will not play any role here) by a dummy argument -. It should be clear that it’s like executing:

    g(){ echo "once";};g;g
    

    so you’ll see:

    once
    once
    

    To remedy this: don’t use parameters! just use:

    [alias]
        encrypt-for = "!g(){ echo "once";};"
    

    Now, if you really want to use parameters, make sure that the trailing parameters given are not executed at all. One possibility is to add a trailing comment character like so:

    [alias]
        encrypt-for = "!g(){ echo "once";};$1 #"
    

    For your full example, a cleaner way could also be to wrap everything in a function:

    [alias]
        encrypt-for = "!main() {\
            case $1 in \
                (github) echo github;; \
                (twitter) echo twitter;; \
                (facebook) echo facebook;; \
                (*) echo >&2 \"error, unknown $1"\; exit 1;; \
            esac \
        }; main"
    

    Hopefully you understood what git is doing under the hood with aliases! it really appends "$@" to the alias string and calls sh -c with this string and the given arguments.

    The question has already been answered by gniourf_gniourf so I have created a version of the simplified alias/script which works as I originally intended. Since this is technically an answer and not really part of the question, I have added this as an answer. This answer supplements the other answer by gniourf_gniourf and is not intended to take credit away from his correct answer.

    This fixed version of the simplified script either executes a found function or outputs nothing at all, and the fact that Git is placing #@ at the end of the script is corrected for by the addition of a comment at the end of the script. This is a fixed version of the simplified script (which gives the correct execution behavior of executing once):

    g(){
        echo "once";
    };
    
    if [[ $(type -t "$1") == "function" ]];
    then
    $1;
    fi;
    #
    

    Here is the output from this corrected version of the simplified alias/script (which has the correct behavior: execute once and display nothing for unknown input):

    $git config --global alias.encrypt-for '!g(){ echo "once";};if [[ $(type -t "$1") == "function" ]];then $1; fi;#'
    $ git encrypt-for g
    once
    $ git encrypt-for github
    $ git encrypt-for facebook
    $ exit
    

    The bottom line is that because of the way Git handles aliases (see gniourf_gniourf’s answer answer for an explanation of that) you must workaround the fact $@ will be suffixed to the end of your alias/script.

    Git Baby is a git and github fan, let's start git clone.