[PATCH] alias: document caveats and add trace of prepared command

Ian Wienand
2024-05-22T02:42:09Z

There are a number of hidden caveats when using command/shell
expansion ("!") in an alias. The command is exec'd, unless it can be
split, when it is then run as an argument to "sh -c". Split commands
have "$@" appended.

Firstly this updates the documentation to explain this a little
clearer.

Secondly, this adds a trace point in prepare_cmd where this
substitution is done, so we can see the command being run without
having to resort to strace/code inspection. e.g. "test = !echo" you
will show:

$ GIT_TRACE=1 git test hello
10:58:56.877234 git.c:755 trace: exec: git-test hello
10:58:56.877382 run-command.c:657 trace: run_command: git-test hello
10:58:56.878655 run-command.c:657 trace: run_command: echo hello
10:58:56.878747 run-command.c:437 trace: prepare_cmd: /usr/bin/echo hello
hello

For a "split" alias, e.g. test = "!echo $*" you will see

$ GIT_TRACE=1 git test hello
11:00:45.959420 git.c:755 trace: exec: git-test hello
11:00:45.959737 run-command.c:657 trace: run_command: git-test hello
11:00:45.961424 run-command.c:657 trace: run_command: 'echo $*' hello
11:00:45.961528 run-command.c:437 trace: prepare_cmd: /bin/sh -c 'echo $* "$@"' 'echo $*' hello
hello hello

which clearly shows you the appended "$@". This can be very helpful
when an alias is giving you an unexpected synatx error that is very
difficult figure out from only the run_command trace point,
e.g. test = "!for i in 1 2 3; do echo $i; done"

$ GIT_TRACE=1 test hello
11:02:39.813030 git.c:755 trace: exec: git-test hello
11:02:39.813233 run-command.c:657 trace: run_command: git-test hello
11:02:39.814384 run-command.c:657 trace: run_command: 'for i in 1 2 3; do echo $i; done' hello
11:02:39.814468 run-command.c:437 trace: prepare_cmd: /bin/sh -c 'for i in 1 2 3; do echo $i; done "$@"' 'for i in 1 2 3; do echo $i; done' hello
for i in 1 2 3; do echo $i; done: -c: line 1: syntax error near unexpected token `"$@"'
for i in 1 2 3; do echo $i; done: -c: line 1: `for i in 1 2 3; do echo $i; done "$@"'

Signed-off-by: Ian Wienand <iwienand@redhat.com>
---
Documentation/config/alias.txt | 26 +++++++++++++++++++++-----
run-command.c | 1 +
2 files changed, 22 insertions(+), 5 deletions(-)

diff --git a/Documentation/config/alias.txt b/Documentation/config/alias.txt
index 01df96fab3..a3f090d79d 100644
--- a/Documentation/config/alias.txt
+++ b/Documentation/config/alias.txt
@@ -21,8 +21,24 @@ If the alias expansion is prefixed with an exclamation point,
it will be treated as a shell command. For example, defining
`alias.new = !gitk --all --not ORIG_HEAD`, the invocation
`git new` is equivalent to running the shell command
-`gitk --all --not ORIG_HEAD`. Note that shell commands will be
-executed from the top-level directory of a 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].
+`gitk --all --not ORIG_HEAD`. Note:
++
+* Shell commands will be executed from the top-level directory of a
+ 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].
+* Single commands will be executed directly. Commands that can be "split"
+ (contain whitespace or shell-reserved characters) will be run as shell
+ commands via an argument to `sh -c`.
+* When arguments are present to a "split" command running in a shell,
+ the shell command will have `"$@"` appended. The first non-command
+ argument to `sh -c` (i.e. `$0` to the command) is always the alias
+ string, and other user specified arguments will follow.
+** This may initially be confusing if your command references argument
+ variables or is not expecting the presence of `"$@"`. For example:
+ `alias.echo = "!echo $1"` when run as `git echo arg` will execute
+ `sh -c "echo $1 $@" "echo $1" "1"` resulting in output `1 1`.
+ An alias `alias.for = "!for i in 1 2 3; do echo $i; done"` will fail
+ if any arguments are specified to `git for` as the appended `"$@"` will
+ create invalid shell syntax. Setting `GIT_TRACE=1` can help debug
+ the command being run.
diff --git a/run-command.c b/run-command.c
index 1b821042b4..36b2b2f194 100644
--- a/run-command.c
+++ b/run-command.c
@@ -435,6 +435,7 @@ static int prepare_cmd(struct strvec *out, const struct child_process *cmd)
}
}

+ trace_argv_printf(&out->v[1], "trace: prepare_cmd:");
return 0;
}

--
2.45.1