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 python command.

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.

In short, 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 /usr/bin/ruby.

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

GitBeGone

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 PATH.

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
~$

Finding Git

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 "$@" into 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:

  • The push.ogg file 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 ~/.bash_profile.

Ooh, baby baby!


Get a script that does it all for you here