When I started my on-call shifts, we had pretty little in the way of automation for day-to-day issues. Tasks like SSH’ing into our cluster, starting a Rails console, or doing a deep search through our gigantic mail directories, were either shelved away in someone’s
bashrc, history log, or just ingrained into someone’s memory. This pain was also felt by a few other of my fellow programmers, and we started cobbling together a Git repo simply named “37s shell scripts”.
This repo started very innocently: a little Ruby script named
console that mapped a product name (
basecamp) to a server name inside of our cluster (
jobs-03), SSH’d in, and then ran a production Rails console. Several more bash and Ruby scripts started to trickle in as we started to share more of our personal code that we used when on-call. Eventually Sam laid down a foundation of bash scripts and directories borrowed from rbenv, and dubbed it “37”.
Enter the 37
37 works in a deceptively simply manner: an executable (
bin/37) parses out subcommands, which map to a single script in the repo’s
libexec/ directory. This gave convention to our scrappy outfit of battle-hardened shell scripts. No longer were they to be randomly named misfits! They each now were subcommands of 37, organized and named by a convention of
libexec/37-SUBCOMMAND. Our first recruits were
37 sftp, and
37 scp, and many more subcommands have been drafted since.
In the 10 months since we started 37, we now have over 30 subcommands at our disposal. The secret to automation was simple: convention, and sharing. From one command, we can run a report for a customer (
37 script basecamp file_usage_summary), open a repo in GitHub (
37 gh), or even search across gigabytes of log data to hunt down a bug (
37 grep bcx all misc [PATTERN]).
I was really inspired by how Sam laid out 37 initially and how we rallied around the conventions set forth to produce an extremely useful tool for everyone at 37signals. I think this pattern is worth sharing, and with some help I’ve extracted the pattern of how rbenv and 37 were crafted into sub: a model for laying out programs by convention.
You’re more than welcome to prepare your own sub, for your organization, or just for your own personal use! They don’t even have to be bash scripts: We write mostly Ruby subcommands for 37, but you could choose any scripting language you’d like for your sub.
I know what you’re thinking: Why is this better than a
~/bin directory, or several aliases? My answer to this is simple: autocompletion! Since each subcommand follows a convention, you can get autocompletion for arguments without having to know any shell scripting trickery.
Here’s an example of autocompletion in action from 37, with some commands omitted. Tabbing after invoking
37 yields a list of available commands, which comes by default with every sub you prepare:
After that, invoking a specific subcommand allows for more autocompletion arguments to be given, if that subcommand has opted in:
It’s really refreshing to be able to wire up autocompletion without any fuss. Right now all subs support bash and zsh autocompletion. I’ve written up a detailed guide for hooking this up yourself.
Applying some convention to how commands are documented allows
sub help to extract helpful information without you having to do extra coding to support it. This is another opt-in feature, enabled by magic comments. Here’s an example from
When you run
37 itself, it will print out the Summary line for each command:
And when you run
37 help console:
I’m really happy to see how far 37 has come: what started as a whipped up pile of shell scripts evolved into an orderly, helpful convention for making our jobs easier. We’ve also hooked up several of our awesome support crew with 37, and it’s been an absolute win for helping our customers faster.
We’d love to get feedback on sub, and the pattern in general. Check out the README to see how to prepare your own!