diff mbox series

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

Message ID 20240525234454.1489598-2-iwienand@redhat.com (mailing list archive)
State Superseded
Headers show
Series [v6,1/3] Documentation: alias: rework notes into points | expand

Commit Message

Ian Wienand May 25, 2024, 11:44 p.m. UTC
When writing inline shell for shell-expansion aliases (i.e. prefixed
with "!"), there are some caveats around argument parsing to be aware
of.  This series of notes attempts to explain what is happening more
clearly.

Signed-off-by: Ian Wienand <iwienand@redhat.com>
---
 Documentation/config/alias.txt | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

Comments

Junio C Hamano May 26, 2024, 11:26 p.m. UTC | #1
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.
Ian Wienand May 27, 2024, 12:22 a.m. UTC | #2
On Sun, May 26, 2024 at 04:26:51PM -0700, Junio C Hamano wrote:
> 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.

OK, I can agree that perhaps I've been a bit to fixated on the
addition of "$@" and the mechanics of this.  I will propose again with
this trimmed.  What I didn't have to help me at the time was the full
command in GIT_TRACE, which I think is probably a more appropriate way
to communicate these details of what's actually hitting the exec
calls.

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

So the reason I fell into this, and I wonder how much this plays out
for others too, is that shipping these workflow bits as stand-alone
scripts would mean no !shell tricks required, it's all very logical
and I would never have looked at any of this :) However, when all you
have is a hammer ... since a git config .inc file was needed for other
things, it has been overloaded into essentially being mini
package-manger that avoids having to install additional dependencies;
one-liner shell script at a time :)

> You can do one of two easy things.
> 
>     $ sh -c 'echo "$1" | grep "$2"' -      1 2

Ok, I think "sh -c" in ! aliases is a bit confusing, personally.  You
end up two shells nested deep, and you really have to explain what's
going on with $0; it's very easy to miss.

>     $ e(){ echo "$1" | grep "$2"; };e      1 2

This method, which is used elsewhere in the docs as well, I think
makes the most sense.  So I've left that in as the example.

-i
diff mbox series

Patch

diff --git a/Documentation/config/alias.txt b/Documentation/config/alias.txt
index 40851ef429..f32b86cde3 100644
--- a/Documentation/config/alias.txt
+++ b/Documentation/config/alias.txt
@@ -27,3 +27,31 @@  it will be treated as a shell command.  For example, defining
   repository, which may not necessarily be the current directory.
 * `GIT_PREFIX` is set as returned by running `git rev-parse --show-prefix`
   from the original current directory. See linkgit:git-rev-parse[1].
+* 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.
+** 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.
+** Care should be taken if your alias script has multiple commands
+   (e.g. in a pipeline), references argument variables, or is
+   otherwise not expecting the presence of the appended `"$@"`.  For
+   example: `alias.echo = "!echo $1"` when run as `git echo arg` will
+   actually execute `sh -c "echo $1 $@" "echo $1" "arg"` resulting in
+   output `arg arg`.  When writing such aliases, you should ensure
+   that the appended "$@" when arguments are present does not cause
+   syntax errors or unintended side-effects.
+** A convenient way to deal with this is to write your script
+   operations in an inline function that is then called with any
+   arguments from the command-line.  For example `alias.cmd = "!c() {
+   cmd $1 | cmd $2 ; }; c" will allow you to work with separate
+   arguments.
+** Setting `GIT_TRACE=1` can help debug the command being run.