Preamble: Why, yes, you’d be correct to point out that all of what follows could be done with a git hook. I wanted to explore shims.
After working with enough Python and Ruby projects and their respective environment / versioning system (rvm and pyenv, mainly), I finally figured to unlock the secret of the small, the powerful, shim. Shims are how rvm allows you to install multiple version of Ruby simultaneously, but use the right one when you type
ruby from a given location. Similarly with pyenv and the
What follows applies to a bash environment. If you’re using Windows then I can’t help you. If you have another shell such as zsh or fish, the same likely applies but I don’t actually know.
ruby is an executable that presumably lives somewhere on your
PATH. When you type it in, your shell environment looks through your
PATH (go ahead, open up a terminal and type
echo $PATH to see it). Then type
which ruby to see where your Ruby currently lives. If you have rvm installed and set up, you’ll likely see it in a folder that matches
.../.rvm/shims/.... Otherwise it might be somwehre like
And that’s the key to it. rvm creates a “shim” – it puts another executable called
ruby in a path location that’s in front of where your system Ruby may live. That way, your shell finds the shim first. The shim reads whatever settings and files rvm is configured to read, then points you to the appropriate ruby version.
Once I realized how simple yet powerful this concept was, I immediately needed to apply it to a previous toy I made: a script called “gitpush”. This script would do the same thing a
git push, but would play a clip of Salt-N-Pepa’s “Push It” while doing so. I called it
gitpush because I didn’t at the time know how to hook into
git push, and, besides, that felt a little too brazen. Too much stick, not enough speaking softly.
But now we have the shim. Let’s roll.
Making a pass-through shim for git
It’s easy enough to intercept the
git command. I’ll create a folder in my home directoy, called
.pushit, slap an executable file there called
git, and add
~/.pushit to the front of my
cd $HOME mkdir .pushit cd .pushit touch git chmod +x git export PATH="$HOME/.pushit:$PATH"
Well, great, now I’ve lost the ability to use git. And unlike rvm or pyenv, we don’t have a controlled location where we want to call the executable of our choosing. This depends on what could be a very varied user-specific setup.
~$ which git /Users/andrew/.pushit/git ~$ git status ~$
Fortunately, finding the original git is actualyl quite easy. All we have to do is remove our shim directory from out
PATH and call
git again. In bash, you can do this easily by prepending your command with an environment variable assignment.
~$ PATH='' which git -bash: which: No such file or directory
Hehe, we don’t even know where to find
which when we erase our PATH. It’s ok, a variable assignment does not persist:
~$ echo $PATH /Users/andrew/.pushit:/Users/andrew/.rvm/gems/ruby-2.6.0/bin:/[~~~REDACTED~~~] ~$ which git /Users/andrew/.pushit/git
So we can set a temporary
PATH that excludes our shim directory, the
git will resolve to the user’s desired git as if the shim didn’t exist! We want to keep it tight, though, so we’re going to use a lightweight program called sed, which comes with every nix distribution that I’ve ever seen, including OS X Darwin.
Finding our path
Since we know where we put the shim, we know what to remove from our path.
~$ echo $PATH /Users/andrew/.pushit:/Users/andrew/.rvm/gems/ruby-2.6.0/bin:[~~~REDACTED~~~] ~$ echo $PATH | sed "s*$HOME/.pushit:**" /Users/andrew/.rvm/gems/ruby-2.6.0/bin:[~~~REDACTED~~~]
You may be used to seeing
sed substitution with slashes, as in
sed s/target/replacement/. Well since our path values have slashed this actually causes a problem, and it’s rather difficult to get bash to do the right thing. Without getting into details about bash variable expansion, which is a sure way to trip up any modern developer, suffice it to say it’s just easier to take advantage of the fact that sed lets you use characters other than the slash, and
* is a great choice because it can’t be used in a file path or name. So,
sed s*target*replacement*. And, in this case,
replacement is blank.
Git ‘er done
So now we have the tools we need to call the originally intended
git in our
git shim. Paste the following into your
git shim file, if you’re following along:
#! /usr/bin/env bash SHIM_PATH="$HOME/.pushit" WITHOUT_SHIM_PATH=$(echo "$PATH" | sed "s*$SHIM_PATH:**") PATH=$WITHOUT_SHIM_PATH git "$@"
Notice that we’re passing
git. This passes through all the provided arguments, preserving effective quotation around spaces and such. It’s the same as
git "$1" "$2" "$3" .... Without the quotes you’d get into a world of trouble as soon as you had an argument with a space. Again, let’s not get into the depths of bash variable expansion. It’s an ancient elder beast with strange ways that deserve our respect and reverence.
So now we’re back to regular gittin’ it:
~$ git status fatal: not a git repository (or any of the parent directories): .git
Ah! Push It!
So far all we’ve done is intercept the call to
git and pass it straight through. Now all we have to do is detect if we’re trying to call
git push in some form, and if so, play the clip and continue.
Here’s the clip, which you can download here.
Let’s put that file at
~/.pushit/push.ogg. We’re going to use the
play binary, which comes with sox. We’ll play
push.ogg under two conditions:
push.oggfile exists before trying to play it. That way if it got deleted then we’ll just move on. We check this with
[ -f $SHIM_PATH/push.ogg ]
- The first argument (that is, the highest level git subcommand) is
"push". This is checked by
[ "$1" = "push" ]
#! /usr/bin/env bash SHIM_PATH="$HOME/.pushit" WITHOUT_SHIM_PATH=$(echo "$PATH" | sed "s*$SHIM_PATH:**") [ -f $SHIM_PATH/push.ogg -a "$1" = "push" ] && play -q $SHIM_PATH/push.ogg & PATH=$WITHOUT_SHIM_PATH git "$@"
The only thing left to do is make our shim path modification somewhere permanent, such as our
Ooh, baby baby!
Get a script that does it all for you here