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):
#!/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
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.