338

Basically I'm trying to alias:

git files 9fa3

...to execute the command:

git diff --name-status 9fa3^ 9fa3

but git doesn't appear to pass positional parameters to the alias command. I have tried:

[alias]
    files = "!git diff --name-status $1^ $1"
    files = "!git diff --name-status {1}^ {1}"

...and a few others but those didn't work.

The degenerate case would be:

$ git echo_reverse_these_params a b c d e
e d c b a

...how can I make this work?

user400575
  • 3,555
  • 2
  • 17
  • 11
  • 23
    Note that in git 1.8.2.1 it's possible to do that without shell function (your original approach with `$1` should work). – Eimantas Aug 15 '13 at 12:49
  • 12
    @Eimantas Would you care to elaborate in an an answer? It doesn't work for me, and I can't find any documentation about it. – pavon Aug 18 '14 at 18:46
  • 1
    @Eimantas there's nothing about this in the [release notes](https://git.kernel.org/cgit/git/git.git/plain/Documentation/RelNotes/1.8.2.1.txt) though. – Knu Oct 01 '16 at 03:52
  • 2
    i can confirm i can run shell commands with arguments without any shenanigans in Git 2.11. – anarcat Feb 14 '18 at 17:20
  • @Eimantas Can you maybe create an answer with a detailed explanation? – leonheess Feb 07 '22 at 20:44

7 Answers7

463

A shell function could help on this:

[alias]
    files = "!f() { git diff --name-status \"$1^\" \"$1\"; }; f"

An alias without ! is treated as a Git command; e.g. commit-all = commit -a.

With the !, it's run as its own command in the shell, letting you use stronger magic like this.

UPD
Because commands are executed at the root of repository you may use ${GIT_PREFIX} variable when referring to the file names in commands

Andre Pastore
  • 2,841
  • 4
  • 33
  • 44
Cascabel
  • 479,068
  • 72
  • 370
  • 318
  • 10
    Thanks, this looks exactly right: [alias] files = "!f() { echo $3 $2 $1; }; f" ; $ git files a b c => c b a – user400575 Jul 23 '10 at 23:20
  • @jefromi @mipadi I'm not a hotshot shell script writer; can you elaborate on why the `!` is needed at beginning of the function definition? Thanks! – Kohányi Róbert Oct 19 '11 at 07:38
  • 1
    @KohányiRóbert: That's actually not a shell script question; that's a particular of git config. An alias without `!` is treated as a Git command; e.g. `commit-all = commit -a`. With the `!`, it's run as its own command in the shell, letting you use stronger magic like this. – Cascabel Oct 19 '11 at 15:04
  • @Jefromi Ah! That's really good to know! I've stumbled upon *this* problem, where creating a new git alias for an already defined alias (like `sta = status` and `st = sta`) would fail with some error message like: "xy isn't a git command". Thanks for the info! – Kohányi Róbert Oct 20 '11 at 06:33
  • HA. I came here trying to accomplish your "files" function. Wish I could up vote twice since you answered my initial question as well as the one I didn't even ask... – blockloop Feb 11 '13 at 15:29
  • 51
    Be careful, `!` will run at the root of the repository, so using relative paths when calling your alias will not give the results you might expect. – Drealmer Aug 08 '13 at 16:28
  • 1
    This solution breaks tab completion for branch names. – void.pointer Jun 23 '14 at 13:52
  • 4
    @RobertDailey It doesn't break it, it just doesn't implement it. See http://stackoverflow.com/questions/342969/how-do-i-get-bash-completion-to-work-with-aliases for how to add it. – Cascabel Jun 23 '14 at 16:08
  • 3
    **Note**: This doesn't quote arguments (which is dangerous in general). Also, a function is unnecessary. See [my answer](http://stackoverflow.com/a/39523506/5353461) for more explanation. – Tom Hale Sep 16 '16 at 03:56
  • With no arguments this borks `fatal: bad revision '^'`. What you would expect it do is a matter of taste. Arguably, you'd want it run on HEAD. This would achieve that: `files = "!f() { git diff --name-status \"${1:-HEAD}\"^ \"${1:-HEAD}\"; }; f"`. Alternatively, and possibly saner, you want to run it against the working tree: `files = "!f() { [ -n \"$1\" ] && git diff --name-status \"$1\"^ \"$1\" || git diff --name-status; }; f"`. – petre Dec 11 '18 at 17:36
  • Why do you wrap it in a function `f `? – Moberg May 06 '20 at 08:30
  • How could I run another command if there are no positional arguments? – Unknow0059 Jan 11 '21 at 02:19
  • @Cascabel Then why is this not working `br-f = "!f(){ git branch -a | grep -i \"$1*\" | grep -o '[^/]*$'; }; f;"` ?? This when fired like `git br-f qa-d` throws the error: `qa-d: command not found` – Vicky Dev Feb 15 '22 at 22:34
  • This only works for me, if I pass the input paramter on, into the function, i.e ($1 at end important!): `f() { echo \"$1\"; ....}; f $1` – Frank N Aug 22 '22 at 09:34
141

The alias you are looking for is:

files = "!git diff --name-status \"$1\"^ \"$1\" #"

With argument validation:

files = "!cd -- \"${GIT_PREFIX:-.}\" && [ x$# != x1 ] && echo commit-ish required >&2 || git diff --name-status \"$1\"^ \"$1\" #"

The final # is important - it prevents all the user-supplied arguments from being processed by the shell (it comments them out).

Note: git puts all user-supplied arguments at the end of the command line. To see this in action, try: GIT_TRACE=2 git files a b c d

The escaped (due to nesting) quotes are important for filenames containing spaces or "; rm -rf --no-preserve-root /;)

Tom Hale
  • 40,825
  • 36
  • 187
  • 242
  • 3
    For the simplest cases this is the right answer, there's really no need to complicate by wrapping it in a function or sh -c. – Ed Randall Mar 08 '17 at 08:08
  • 4
    Yes, `!` already implies `sh -c` (shown when prepending `GIT_TRACE=2`), so there's no need to run a another sub-shell. What issues do you see in more complicated cases? – Tom Hale Mar 09 '17 at 01:18
  • Does this work if you want to set default arguments? e.g. I want to do this to fetch a Github PR: `fp = "! 1=${1:-$(git headBranch)}; 2=${2:-up}; git fetch -fu $2 pull/$1/head:$1; git checkout $1; git branch -u $2 #"`. This works great without the first two statements, but falls down if you use them. (I have `headBranch = symbolic-ref --short HEAD` as well). – gib Aug 12 '17 at 19:17
  • 2
    Worked it out, it works if you set new params, so this is fine: `fp = "! a=${1:-$(git headBranch)}; b=${2:-up}; git fetch -fu $b pull/$a/head:$a; git checkout $a; git branch -u $b #"`. – gib Aug 12 '17 at 19:31
  • why `"` quotes are required? – Eugen Konkov Mar 04 '18 at 13:22
  • 1
    @EugenKonkov Quotes are needed since the expansion of variables could contain spaces, and we want to keep them as a single shell token. – Tom Hale Nov 26 '18 at 07:05
  • This is amazing, thank you Tom! I just used this to define my on `git cat` command, see https://stackoverflow.com/a/54819889/24468 – akuhn Feb 22 '19 at 04:12
  • @Jim are you sure you've only got one bang character? – Tom Hale Dec 30 '19 at 19:08
  • Why is the extra `--` included after the `cd`? It seems to work fine without it. The Bash man page for cd says nothing about `--` that I can see. – bgoodr Aug 12 '21 at 00:42
  • For Windows, I had to use the ugly contraption `&& echo` instead of `#`. This would echo the arguments after the command is executed, but using `REM` to simply ignore them didn't work at all! – ADTC Dec 06 '21 at 22:47
  • Had a really hard time creating my own `!` git alias (which I knew from before), but was hitting an issue which _seemed_ that parameters were being fed at the end of my command too. I even tried placing a `#` at the end, to no avail. Turns out the missing piece was quoting the whole command, including the hash :) Weird that's needed, although it kinda makes sense - git is weird in weird ways, right? lol – igorsantos07 May 11 '23 at 03:05
108

You can also reference sh directly (instead of creating a function):

[alias]
        files = !sh -c 'git diff --name-status $1^ $1' -

(Note the dash at the end of the line -- you'll need that.)

mipadi
  • 398,885
  • 90
  • 523
  • 479
  • 1
    ...are there any tangible benefits as compared to jefromi's answer? I mean: the function def in his answer is "alias-local" and means you don't have bash calling bash, right? In any case thanks for the alternative implementation. – user400575 Jul 26 '10 at 16:19
  • I don't think either solution really has benefits over the other -- just two different ways to do the same thing. – mipadi Jul 26 '10 at 16:56
  • I suspect jefromi's solution is better if you want to share the aliases, or use multiple shells; what if sh doesn't call your current shell? – Jay Levitt Nov 17 '11 at 18:46
  • 8
    If you're sharing the command, you probably want to use `sh`, since that is in itself a shell, and it's available on the vast majority of systems. Using the default shell only works if the command works as written for all shells. – nomothetis Dec 06 '12 at 16:20
  • 17
    I prefer `--` to `-` as it's more familiar and less likely to accidentally mean stdin at some point. ("An argument of - is equivalent to --" in bash(1) is ungoogleable) – bsb Aug 26 '13 at 00:28
  • 4
    See also [Official Git Wiki - Advanced aliases with arguments](https://git.wiki.kernel.org/index.php/Aliases#Advanced_aliases_with_arguments). –  Jun 24 '14 at 01:31
  • I've tried both `-` and `--` and gotten the two-line error `sh: -c: line 0: unexpected EOF while looking for matching `'' `sh: -c: line 1: syntax error: unexpected end of file ` with bash 4.2.1 on cygwin. – user151841 Oct 29 '15 at 13:55
  • 7
    What is the exact meaning of the ending '-' and where is it documented ? – Zitrax Apr 21 '16 at 13:24
  • 8
    **Note**: This doesn't quote arguments (which is dangerous in general). Creating a sub-shell (with `sh -c`) is also unnecessary. See [my answer](http://stackoverflow.com/a/39523506/5353461) for an alternative. – Tom Hale Sep 16 '16 at 04:07
  • Note that this creates an extra copy of the shell (so two in total); additionally, FreeBSD’s (and their derivatives’) `sh` has broken `--` handling (and `-` is plain wrong) for compatibility reasons. I’d agree your best bet is to write the alias in the form from the accepted answer and assume the user’s shell is POSIX sh compatible enough. – mirabilos Mar 02 '18 at 23:48
  • @Zitrax it appears to me to be only documented in the [wiki entry](https://git.wiki.kernel.org/index.php/Aliases#Advanced_aliases_with_arguments) linked in the comment just above yours, not in the alias or syntax sections of `git help config` This might fit with the idea I've seen in the mailing list that shell quirks are considered [out of scope for git documentation](https://lore.kernel.org/git/20200115181351.GB4081287@coredump.intra.peff.net/). (I'd disagree, myself, when it affects git usage like this, but I could see arguments to avoid overdoing it.) – Joshua Goldberg Nov 11 '21 at 15:50
33

Use GIT_TRACE=1 described on the git man page to make the alias processing transparent:

$ git config alias.files
!git diff --name-status $1^ $1
$ GIT_TRACE=1 git files 1d49ec0
trace: exec: 'git-files' '1d49ec0'
trace: run_command: 'git-files' '1d49ec0'
trace: run_command: 'git diff --name-status $1^ $1' '1d49ec0'
trace: exec: '/bin/sh' '-c' 'git diff --name-status $1^ $1 "$@"' 'git diff --name-status $1^ $1' '1d49ec0'
trace: built-in: git 'diff' '--name-status' '1d49ec0^' '1d49ec0' '1d49ec0'
trace: run_command: 'less -R'
trace: exec: '/bin/sh' '-c' 'less -R' 'less -R'
MM      TODO

Your original commands work with git version 1.8.3.4 (Eimantas noted this changed in 1.8.2.1).

The sh -c '..' -- and f() {..}; f options both cleanly handle the "$@" parameters in different ways (see with GIT_TRACE). Appending "#" to an alias would also allow positional parameters without leaving the trailing ones.

bsb
  • 1,847
  • 26
  • 24
  • 1
    thanks for the explanations: those commands work for me on the original problem, following your advice: `files = "!git diff --name-status $1^ $1 #"` `files = "!git diff --name-status $1^"` – user2291758 Apr 21 '15 at 12:19
21

As stated by Drealmer above:

« Be careful, ! will run at the root of the repository, so using relative paths when calling your alias will not give the results you might expect. – Drealmer Aug 8 '13 at 16:28 »

GIT_PREFIX being set by git to the subdirectory you're in, you can circumvent this by first changing the directory :

git config --global alias.ls '!cd "${GIT_PREFIX:-.}"; ls -al'

Pierre-Olivier Vares
  • 1,687
  • 15
  • 20
10

I wanted to do this with an alias that does this:

git checkout $1;
git merge --ff-only $2;
git branch -d $2;

In the end, I created a shell script named git-m that has this content:

#!/bin/bash -x
set -e

#by naming this git-m and putting it in your PATH, git will be able to run it when you type "git m ..."

if [ "$#" -ne 2 ]
then
  echo "Wrong number of arguments. Should be 2, was $#";
  exit 1;
fi

git checkout $1;
git merge --ff-only $2;
git branch -d $2;

This has the benefit that it's much more legible because it's on multiple lines. Plus I like being able to call bash with -x and set -e. You can probably do this whole thing as an alias, but it would be super ugly and difficult to maintain.

Because the file is named git-m you can run it like this: git m foo bar

Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
  • 2
    I like this a lot more too, but I haven't been able to figure out how to use the autocomplete I want with this approach. On aliases you can do this: `'!f() { : git branch ; ... }; f'` and it will autocomplete the alias as a branch which is super handy. – Hassek May 11 '17 at 22:57
  • Yeah, I think I prefer having the non-trivial things done as individual script files on the path. The down side though is yes, you loose automatic completion of things like references. You can though fix this up by manually configuring your own auto-completion. Again though, I like that you can just drop a script into a folder on the path and it will start working, but for the auto-completion, you need to 'load' it, so usually it's in my `.bashrc` file that I source. But I don't think I change how I auto-complete arguments to a script as much as the script itself, and it'd only be during dev. – thecoshman Nov 05 '18 at 16:11
  • GOSH, YES. I was killing myself over trying to use a shell variable on the `!` inline git alias....... – igorsantos07 Aug 27 '23 at 23:46
6

Just bumped into something similar; hope it's oK to post my notes. One thing that confuses me about git aliases with arguments, probably comes from the git help config (I have git version 1.7.9.5):

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. [...]

The way I see it - if an alias "will be treated as a shell command" when prefixed with an exclamation point - why would I need to use a function, or sh -c with arguments; why not just write my command as-is?

I still don't know the answer - but I think actually there is a slight difference in the outcome. Here's a little test - throw this in your .git/config or your ~/.gitconfig:

[alias]
  # ...
  ech = "! echo rem: "
  shech = "! sh -c 'echo rem:' "
  fech = "! f() { echo rem: ; }; f " # must have ; after echo!
  echargs = "! echo 0[[\"$0\"]] 1-\"$1\"/ A-"$@"/ "
  fechargs = "! f() { echo 0[[\"$0\"]] 1-\"$1\"/ A-"$@"/ ; }; f "

Here is what I get running these aliases:

$ git ech word1 word2
rem: word1 word2

$ git shech word1 word2
rem:

$ git fech word1 word2
rem:

$ git echargs word1 word2
0[[ echo 0[["$0"]] 1-"$1"/ A-$@/ ]] 1-word1/ A-word1 word2/ word1 word2

$ git fechargs word1 word2
0[[ f() { echo 0[["$0"]] 1-"$1"/ A-$@/ ; }; f ]] 1-word1/ A-word1 word2/

... or: when you're using a "plain" command after the ! "as-is" in a git alias - then git automatically appends the arguments list to that command! A way to avoid it, is indeed, to call your script as either a function - or as the argument to sh -c.

Another interesting thing here (for me), is that in a shell script, one typically expects the automatic variable $0 to be the filename of the script. But for a git alias function, the $0 argument is, basically, the content of the entire string specifying that command (as entered in the config file).

Which is why, I guess, if you happen to misquote - in the below case, that would be escaping the outer double quotes:

[alias]
  # ...
  fail = ! \"echo 'A' 'B'\"

... - then git would fail with (for me, at least) somewhat cryptic message:

$ git fail
 "echo 'A' 'B'": 1: echo 'A' 'B': not found
fatal: While expanding alias 'fail': ' "echo 'A' 'B'"': No such file or directory

I think, since git "saw" a whole string as only one argument to ! - it tried to run it as an executable file; and correspondingly it failed finding "echo 'A' 'B'" as a file.

In any case, in the context of the git help config quote above, I'd speculate that it's more accurate to state something like: " ... the invocation "git new" is equivalent to running the shell command "gitk --all --not ORIG_HEAD $@", where $@ are the arguments passed to the git command alias from the command line at runtime. ... ". I think that would also explain, why the "direct" approach in OP doesn't work with positional parameters.

Vlad L.
  • 154
  • 1
  • 9
sdaau
  • 36,975
  • 46
  • 198
  • 278
  • nice test. A quick way to check all possibilities! – albfan Apr 27 '13 at 12:11
  • `fail` is trying to run a command called "echo 'A' 'B" (ie. 10 chars long). Same error from `sh -c "'echo a b'"` and same cause, too many layers of quotes – bsb Aug 25 '13 at 22:41