I wanted to make a git add-and-commit alias, but getting the positional arguments to handle filenames with spaces in was a lot more complicated than I’d expected.

My naive first try was:

[alias]
  add-and-commit = "!git add $@ && git commit"

which works as long as you don’t have filenames that you don’t need to quote, and that you’re happy using git’s default method for capturing your commit message. You can call it like:

git add-and-commit readme.md example.txt

and it will add the two files readme.md and example.txt and then ask for a commit message from you.

But if one of your files is e.g. called file with a space.txt it barfs:

git add-and-commit readme.md "file with a space.txt"
fatal: pathspec 'file' did not match any files

Also, what if you want to pass your commit message at the same time? Now we need to consider positional parameters, how we ensure they’re escaped properly and how we can grab some of them for the git add portion of the alias, and the others for the git commit portion. The aim is so that we can call:

git add-and-commit readme.md "file with a space.txt" -m "A nice long commit message"

Capture the filenames

Breaking this down, we want to separately capture all the filename arguments (in this case, readme.md and file with space.txt) before the last two, which are the -m and the commit message itself.

bash parameter expansion is a our friend1 here. This Ask Ubuntu answer has some nice examples that helped me out.

We want args $1 to the total number of args ($#) minus 2, for the filenames (-2, because of the -m and then the message itself).

We need each argument quoted in case it has spaces in, so we use the quoted "{$@}" format instead of "{$*}" and we use bash arithetic to subtract 2 from the argument length $(($#-2)).

So to capture all the filenames, correctly quoted:

# arguments, quoted, starting from 1 and going to -2 the total number of arguments
"${@:1:$(($#-2))}"

Capture the commit message arguments

And then we want to capture the last two arguments to pass to git commit. The space before the -2 is important, and don’t forget to use the quoted "${@}" style, because our commit message is bound to have spaces in too.

# a negative value needs either a preceding space or parentheses
"${@: -2}"

Wrapping this all up, I ended up with this, which is slightly more complicated by the need to escape the quoting of the arguments to the git alias. First we git add all the filenames, then we git commit using our -m message.

[alias]
  add-and-commit = "!git add \"${@:1:$(($#-2))}\" && git commit \"${@: -2}\" #"
  1. LOL, no