1

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?
halfer
  • 19,824
  • 17
  • 99
  • 186
Garret Wilson
  • 18,219
  • 30
  • 144
  • 272

2 Answers2

2

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.

halfer
  • 19,824
  • 17
  • 99
  • 186
torek
  • 448,244
  • 59
  • 642
  • 775
  • Amazing answer!! My first quick read cleared up my questions and taught me some new things. I'm going to let it digest and come back and read it again. And I'm going to open up a bounty to award such a great answer (because correct answers are sometimes hard to come by, not to mention great ones). – Garret Wilson Oct 02 '22 at 21:57
1

In the context of Git aliases, which "shell" is being invoked by sh -c and !?

The answer to "which shell is being invoked by !" is /bin/sh. This doesn't appear to be documented explicitly in the man page, but it is typical behavior for Unix commands that want to execute shell scripts.

The answer to "which shell is being invoked by !sh -c" is of course...sh, because you've asked for it explicitly. That's typically going to be /bin/sh, but it does depend on your $PATH.


With sh -c, apparently a trailing - is needed to make parameters start with $1 instead of $0.

This has nothing to do with git, and is documented in the sh man page:

-c If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, the first argument is assigned to $0 and any remaining arguments are assigned to the positional parameters. The assignment to $0 sets the name of the shell, which is used in warning and error messages.


But is this also needed for the ! syntax?

This is really just the same question.

When you create an alias using !, git simply appends any arguments after the alias to the alias command. So if your alias is invoking sh -c "...", then you need to following the sh man page and realize that the first argument will be interpreted as $0, so you need to pass a dummy argument.

If you're not calling /bin/sh, then you don't need the dummy argument (unless you're calling some other command that does something special with the first argument). So for example if we have:

[alias]
    example = '!echo'

We can write:

git example foo bar

And see as output:

foo bar

In a Windows environment, a typical git install includes bash and a number of other common Unix utilities. Try creating this alias:

[alias]
    example = '!ls /bin'

Run git example and you'll see you have /bin/sh and a bunch of other executables.

larsks
  • 277,717
  • 41
  • 399
  • 399
  • "The answer to 'which shell is being invoked by `!`' is `/bin/sh`." But I'm on Windows 10 using PowerShell, and I don't have a `/bin/sh`. So … there must be more to the story? – Garret Wilson Oct 02 '22 at 21:36
  • "The answer to 'which shell is being invoked by `!sh -c`' is of course...`sh` …" But "of course", on Windows 10, I don't have a `sh`. So it must be invoking some other shell. But which shell? Some built-in Git `sh`? But if it uses a built-in `sh` on Windows, does it use a built-in `sh` on other platforms as well? Whichever the case, where is that documented? (You see now why I asked what seemed to be an obvious question? And how the obvious answer wasn't quite complete? But I do realize I forgot to note that I'm using Windows 10; I'll update the question.) – Garret Wilson Oct 02 '22 at 21:40
  • A typical install of `git` on Windows includes a number of common Unix tools. See my update for how to explore this yourself. – larsks Oct 02 '22 at 22:11