bash: quote distinctions & alias nuances

2021-02-19

 | 

~5 min read

 | 

837 words

Many moons ago, I had an idea for a utility gpnew to handle pushing a new branch to my origin.

That is, I wanted to avoid this situation:

% git push
fatal: The current branch test has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin test

gpnew would take advantage of a second alias I’d written, gbc (which I’d written about previously in the context of rebasing) to find the current branch I’m on:

.zshrc
alias gbc='git branch --show-current'

gpnew was supposed to be simple. I started with an alias.

.zshrc
alias gpnew='git push --set-upstream origin $(gbc)'

By design, it was meant to evaluate the current branch and then slot it right into the space it needed to be for passing upstream.

The issue is it didn’t work!

In fact, worse than not working completely, when I ran gpnew (even after confirming the output of gbc independently), I’d wind up pushing my changes to the wrong branch. Specifically, it’d push them up to the previous branch, frequently develop or main and in either case, not where I wanted them!

After avoiding it for months I finally got sick of copying and pasting the line and dug into ways to solve my problem. While the answer was simple, along the way I learned three lessons:

  1. A novel way to use the HEAD reference in git,
  2. A distinction between single and double quotes in Bash, and
  3. Aliases are frozen in time, while functions are not.

Using head

Let’s start at the top. If all I cared about was the outcome and not the why, the first answer I came across today would have been sufficient. Simply revamp the gpnew alias to git push --set-upstream origin HEAD and call it a day.

This works because HEAD always references where the tip of your git is, which happens to be your current branch.

This was great, and I would have gone with it, but… I wanted to understand!

Note the Quotes

In many programming languages, there’s no semantic difference between a single and double quote. Which you use is often a matter of preference. It turns out Bash is not one of those languages and there are real differences.

codeforester’s answer on Stack Overflow was the one which really clicked for me:

Here is a three-point formula for quotes in general:

Double quotes

In contexts where we want to suppress word splitting and globbing. Also in contexts where we want the literal to be treated as a string, not a regex.

Single quotes

In string literals where we want to suppress interpolation and special treatment of backslashes. In other words, situations where using double quotes would be inappropriate.

No quotes

In contexts where we are absolutely sure that there are no word splitting or globbing issues or we do want word splitting and globbing.

It was the actual example he provided of “command substitutions” (which is exactly what I’m doing) that seemed to suggest I wanted double quotes.

.zshrc
alias gpnew="git push --set-upstream origin $(gbc)"

Frozen Aliases

All of this is good information, however, it doesn’t actually solve the problem. The real culprit in my case is that aliases are evaluated at the launch of the shell and then never reevaluated. They’re frozen.

This is why gbc would show the correct value and gpnew didn’t:

  • gbc was an alias for a git command.
  • gpnew was also an alias git command, but, crucially, one where the arguments are fixed as soon as the shell launches (specifically the branch).

As one example, consider the case where I’m on main when I launch my shell and then switch to a feat-xyz. That would mean when the alias gpnew would be frozen as git push --set-upstream origin main unless I reloaded the shell.

Or, perhaps even more clearly, we can see this with a simple date example:

$ date && alias datedate="echo $(date)"
Fri Feb 19 07:07:06 PST 2021
$ datedate
Fri Feb 19 07:07:06 PST 2021
$ datedate
Fri Feb 19 07:07:06 PST 2021
$ echo ${BASH_ALIASES[datedate]}
echo Fri Feb 19 07:07:06 PST 2021

Now, I may be fast with a keyboard, but it seems unlikely that I could make all of those calls within 1 second (and yet, the time is frozen).

If, instead of using an alias I write a function, however, this work more as expected desired.

$ date && function datenow() { $(echo date); }
Fri Feb 19 07:24:36 PST 2021
$ # wait a few seconds
$ datenow
Fri Feb 19 07:25:12 PST 2021

Once I understood this, I was able to convert my alias gpnew into a function:

function gpnew(){
    git push --set-upstream origin $(gbc)
}

And with that, gpnew is now correctly deriving the value of the current branch on every invocation!


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!