Re: [PATCH v6 2/3] Documentation: alias: add notes on shell expansion

Junio C Hamano
2024-05-26T23:26:57Z

Ian Wienand <iwienand@redhat.com> writes:

> +* If the shell alias is the full path to a binary, it will be executed
> + directly with any arguments as positional arguments.
> +* If the alias contains any white-space or reserved characters, it
> + will be considered an inline script and run as an argument to `sh
> + -c`.
> +* When running as a script, if arguments are provided to the alias
> + call, Git makes them available to the process by appending "$@" to
> + the alias shell command. This is not appended if arguments are not
> + provided.

These are not technically wrong per-se.

> +** For "simple" commands, such as calling a single binary
> + (e.g. `alias.myapp = !myapp --myflag1`) this will result in any
> + arguments becoming additional regular positional arguments to the
> + called binary, appended after any arguments specified in the aliased
> + command.

But the single-command script still receives the arguments in
argv[], so what the alias command has to do is the same. The
earlier ones that are "not technically wrong" are merely
implemenation detail.

In a single command case, e.g., "[alias] single = !one", you may
write

#!/bin/sh
echo "$1" | grep "$2"

in 'one' script, and "git single 1 2" will be turned into

start_command: one 1 2

i.e. one receives two arguments in argv[]. It is an implementation
detail that we can bypass "sh" or "-c" or "$@"

If you write exactly the same thing like

$ git -c 'alias=single=!one ' single 1 2

you'll instead see

start_command: /bin/sh -c 'one' "$@" "one " 1 2

because the trailing SP in the alias disables the optimization to
bypass a more generic 'sh -c ... "$@"' construction. What gets run
is an equivalent of the reader saying

$ /bin/sh -c 'one "$@"' "one " 1 2

bypassing git from the command line.

What the script (one) has to write does not change at all either
case.

As I keep saying over multiple iterations, the above three bullet
points stress too much on the minute implementation detail while
failing to tell readers that the end-user alias receives the rest of
the command line as arguments.

> +** Care should be taken if your alias script has multiple commands
> + (e.g. in a pipeline), references argument variables, or is

"argument variables" -> "arguments".

> + otherwise not expecting the presence of the appended `"$@"`.

"otherwise not expecting" is SIMPLY BUGGY but the readers may not
understand it unless you tell them that the arguments are fed to
their aliased command by appending them.

When you look at the implementation detail of "sh -c '... $@' -
$ARGS" as something to fight against, readers would lose sight to
see the crux of the problem they are trying to solve. I think it is
a wrong way to frame the issue. The problem readers are solving when
coming up with their own alias is this:

How would one write a single-liner that can take arguments
appended at the end?

I think giving that to the readers upfront, i.e. "when you write an
alias, you are forming a single-liner that takes arguments appended
at the end", would go a long way without having them lose sight in
the implementation details of "sometimes args directly come in
argv[], sometimes your alias is wrapped in "sh -c" and "$@" is used.
They do the same thing to feed the arguments to your script.

Going back that 'one' example, if 'echo "$1" | grep "$2"' was what
you wanted to run, how would you write a single-liner that does

echo "$1" | grep "$2"

and can take its arguments at the end? You do *not* want to see
your invocation of the alias

$ git single 1 2

turn into

$ echo "$1" | grep "$2" 1 2

of course, and remember, "$@" is merely an implementation detail
that the end-users do not need to see.

Of course the simplest one-liner, if you had the "one" script
already stored in the file, is to say

$ one 1 2

i.e. "[alias] single = !one". But calling that a "single-liner" is
cheating.

You can do one of two easy things.

$ sh -c 'echo "$1" | grep "$2"' - 1 2
$ e(){ echo "$1" | grep "$2"; };e 1 2

The earlier string (before "1 2" is appended) of either of these
gives you "a single-liner that takes arguments at the end" that does
the "echo the first one, pipe it to grep that looks for the second
one", which you would make the body of the alias. If the reader
understands the earlier example that stores it in a file, the former
is more mechanical and straight-forward rewrite of it. The latter
may be a bit more convoluted, but says the same thing in the same
number of letters.

HTH.