Search code examples
gitshellgit-bashgit-alias

Documentation and clarification for Git alias shell invocation with arguments


I'm using Git version 2.37.3.windows.1 on Windows 10. From reading the discussion at Git alias with positional parameters and the Git Wiki, I've learned a couple of things about Git aliases:

I can invoke a shell command using something like this. The trailing - is so that the the CLI parameters start with $1 and not $0.

example = !sh -c 'ls $2 $1' -

I can also use this form. The trailing # is to "ignore" the CLI argument, which will be repeated at the end.

example = "!ls #2 #1 #"

But that all leaves me with some additional questions. Most importantly, where is all this documented? I've read the git-config, documentation, but it only mentions a couple of things, like the use of the exclamation mark.

  1. In the context of Git aliases, which "shell" is being invoked by sh -c and !? Does this invoke the OS-specific shell in use (e.g. PowerShell on Windows), or is this some Git built-in Bash shell that is allows consistent behavior across platforms? (For example, do I use PowerShell quoting rules, or Bash quoting rules?)
  2. With sh -c, apparently a trailing - is needed to make parameters start with $1 instead of $0. But is this also needed for the ! syntax? Why or why not? And where is this documented?
  3. Where is it documented the use of the trailing # to ignore the "duplicated" argument(s) from the command line?

Solution

  • Summary

    Git requires a POSIX shell. That's why Git-for-Windows comes with git-bash, which is a port of bash that runs on Windows. When using Git on Windows, Git is configured to invoke that bash in its "POSIX shell" mode as much as possible.

    Long

    Most importantly, where is all this documented?

    It's not. It is all left as implied. When Git expands an alias, if the alias begins with !, Git tacks on all the arguments, as you noted. It then passes the result to /bin/sh with some flags. For the remainder of the exercise, you must know how /bin/sh works, or consult its documentation.

    This means that you want:

    example = "!ls \"$2\" \"$1\" #"
    

    (not #1 #2 #) or, arguably somewhat better:

    example = "!f() { ls \"$2\" \"$1\"; }; f"
    

    Quoting in such Git aliases gets very messy because:

    • the .gitconfig file is parsed by Git's configuration reader, which eats one layer of quotes and backslashes; and then
    • the alias is fed to the shell, which parses it and therefore eats one more layer of quotes and backslashes.
    1. In the context of Git aliases, which "shell" is being invoked by sh -c and !?

    ! invokes /bin/sh, unless your Git was built with SHELL_PATH set to some other value at compile time.

    sh invokes whatever command the shell that invokes finds as sh. Normally that would also be /bin/sh, but it depends on your $PATH setting.

    1. With sh -c, apparently a trailing - is needed to make parameters start with $1 instead of $0. But is this also needed for the ! syntax? Why or why not? And where is this documented?

    In order: correct, no, and "in the /bin/sh documentation". Running:

    sh -c 'echo "$@"' foo bar baz
    

    (assuming sh invokes /bin/sh as usual) produces:

    bar baz
    

    What happened to foo? It's in $0:

    $ sh -c 'echo "$0"' foo bar baz
    foo
    

    If you run sh without -c, the argument in that position is treated as a file path name:

    $ sh 'echo "$0"' foo bar baz
    sh: cannot open echo "$0": No such file or directory
    

    Note that since there is no -c option, $0 is the literal text echo "$0", and foo has become $1.

    With sh -c, however, the argument after -c is the command to run, and then the remaining positional arguments become the $0, $1 etc values. Git passes the entire expanded alias and its arguments to sh -c, so:

    git example "argument one" "argument 2"
    

    runs:

    sh -c 'ls $2 $1 #' 'argument one' 'argument 2'
    

    as we can see by setting GIT_TRACE=1 (but here I replaced "ls" with "echo" and took out the comment # character):

    14:35:17.709995 git.c:702               trace: exec: git-example 'argument one' 'argument 2'
    14:35:17.710362 run-command.c:663       trace: run_command: git-example 'argument one' 'argument 2'
    14:35:17.711074 run-command.c:663       trace: run_command: 'echo $1 $2' 'argument one' 'argument 2'
    argument one argument 2 argument one argument 2
    

    Now we can see (sort of) why we want to put double quotes around $1 and the like: $1 is at this point the literal string argument one. However, using it in a command, as in echo $1, subjects $1 to the shell's word splitting (based on what's in $IFS at this point). So this becomes two arguments, argument and one, to the echo or other command. By quoting $1 and $2, we keep them as single arguments. That's why we want:

    example = "!ls \"$2\" \"$1\" #"
    
    1. Where is it documented the use of the trailing # to ignore the "duplicated" argument(s) from the command line?

    Again, that's in the shell documentation.

    Now, the reason to use a shell function, such f, is that we get much better control. It doesn't really change anything fundamentally, it's just that any variables we set inside the function can be local to that function, if we like, and we can write other functions and call them. There's nothing you can't do without them, and for simple cases like this one, there's no actual advantage to using shell functions. But if you're going to write a complex shell script for Git, you probably want to make use of shell functions in general, and write it as a program that Git can invoke, rather than as an alias.

    For instance, you can create a shell script named git-example and place it somewhere in your own $PATH. Running git example 'argument one' more args will invoke your shell script with three arguments: $1 set to argument one, $2 set to more, and so on. There are no horrible quoting issues like there are with Git aliases (there are only the regular horrible quoting issues that all shell scripts have!).

    Note that Git alters $PATH when it runs your program so that its library of executables, including shell script fragments that you can source or . to give you various useful shell functions, is there in $PATH. So your shell script can include the line:

    . git-sh-setup
    

    and get access to a bunch of useful functions. Use git --exec-path to see where these programs live.