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.