8

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
Community
  • 1
  • 1
Emily Mabrey
  • 1,528
  • 1
  • 12
  • 29
  • 6
    I can't reproduce this; it may be something in your environment. Have you tried running this in a clean environment? – Explosion Pills Dec 14 '16 at 20:30
  • 1
    The first script doesn't exhibit the behavior you're claiming. Does your real script contain more code below the `if`/`else`? – John Kugelman Dec 14 '16 at 20:38
  • I added an example image of the console output I get when executing the scripts. The added `set` command was only added while I created the image, to help make it more clear what exactly was happening, it isn't present otherwise so I shouldn't be part of the strange behavior. – Emily Mabrey Dec 14 '16 at 20:42
  • A good way to debug your script is to run `bash -x {YOUR_SCRIPT}' and you will see how the script is executed. This will help you understand and debug your issue. – Wayne Foux Dec 14 '16 at 20:44
  • @JohnKugelman no the entirety of the script is shown. – Emily Mabrey Dec 14 '16 at 20:44
  • And I don't think the example using a git alias is the problem either, this happened when I wrote the script out in a .sh file as well and executed it directly. – Emily Mabrey Dec 14 '16 at 20:45
  • 1
    Can you change `set -v` to `set -x`? – John Kugelman Dec 14 '16 at 20:57
  • 1
    Can you test with simplifications? Try `git config --global alias.encrypt-for '!g(){ echo github;};g;'` and add parts until it breaks. Perhaps some old alias or function is out there, try renaming your functions into `em_github`. – Walter A Dec 15 '16 at 20:57
  • @WalterA I have added the simplest version of the script that still displays the problem to the question via an edit. I verified that there is no interference from Bash functions by using `type -t g`. I also verified my git aliases by running `git config --get-regexp '^(alias){1}([.]){1}(.)+'` to list all aliases and the commands they run. – Emily Mabrey Dec 16 '16 at 09:35

2 Answers2

11

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.

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
  • So, simply removing the $1 at the end of my alias script would give me the desired behavior- if a defined function is specified execute it otherwise error, and it would also not cause double execution. So the alias should be `!g(){ echo "once";};`? If that is correct I will test it and see if it resolves the problem. – Emily Mabrey Dec 23 '16 at 21:09
  • @EmilyMabrey: yes, that should work. Now for your actual example, don't overlook my suggestion of wrapping it all in a function (that function can call other functions, of course). – gniourf_gniourf Dec 23 '16 at 21:32
  • This appears to be the correct answer! Thanks for your help! I had actually figured out that a "$@" was being added to the end of the script, but I had no idea where it was coming from. My original question mentioned this stray and unexplained "$@" suffix, but upon the criticism of other people (they seemed to think I was mistakenly misinterpreting things) I removed it. It is interesting to see my original instincts about what might be causing the problem were right, but I still wouldn't have gotten much farther without help, so thank you :) – Emily Mabrey Dec 23 '16 at 21:37
  • I will award the bounty sometime later, as currently I can't award it for at least 19 more hours. – Emily Mabrey Dec 23 '16 at 21:38
  • @EmilyMabrey: no problem, glad it helped! (and hopefully will help some other people in the future too!). I looked at the question edit history, and indeed, your original title was about understanding why `"$@"` gets executed… go figure why some downvoted and flagged for close! – gniourf_gniourf Dec 23 '16 at 21:42
  • 1
    I added an answer containing the edit I originally made to my question since adding an answer to my question seemed against the rules/wrong. You will be getting the bounty no matter what though, so please don't worry about my answer interfering somehow. Thanks so much for you help! (If you celebrate Christmas, I hope you have a merry Christmas!) – Emily Mabrey Dec 23 '16 at 22:29
  • I awarded the bounty! Merry Christmas Eve! – Emily Mabrey Dec 24 '16 at 18:05
2

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.

Emily Mabrey
  • 1,528
  • 1
  • 12
  • 29