Today I’m happy to introduce Tome, a CLI generator that takes directories of shell scripts and bundles them under a single command, complete with auto-completion, help text, and more.
Here is a quick demo of creating some scripts in a directory, and a command
s from it:
Give it a try and please file bugs or give feedback!
That’s really what this post is all about, but if you want to hear more about why and the history, read on.
The beginning: using sub at Zillow #
In 2011, I worked at Zillow, as a relatively new engineer obsessed with efficiency. I loved writing little scripts that helped simplify my workflow, such as:
- configuring environment variables to work with our development environment.
- installation of binaries that were required locally.
- starting up our web server monolith.
At the time, there were a lot of small things that needed to be done to start developing.
Eventually someone posted about sub in our engineering mailing list, which was created by 37 Signals, a company also obsessed with the developer experience and had a bit of a following at Zillow.
Sub was a great tool: it provided the ability to bootstrap a single command from a directory of scripts, and I saw an opportunity to use this to create a community-owned pool of scripts to help Zillow developers be productive and share productivity tooling as well. I created a git repository, called it zb, and shared it with the other engineers. Zillow-bootstrap did some other things too (like local environment setup with some Python scripts), but the
zb commands were the most famous.
zb was used for years at Zillow after that. We even forked the code and added some features like shell script sourcing, which enables setting environment variables in the parent executing the command.
Using sub personally #
On top of usage at an organization, I found sub to be very useful for my own scripts as well. Single-letter directories help me quickly navigate to the commands that are the most useful, and provide some namespacing.
I call this
s in my setup, short for sub.
Challenges with sub #
sub itself was fairly stable for a while, but it had some challenges.
Sub was designed for forking #
Sub’s design expects the user to fork the whole repository to generate your own command. This coupling of the user’s scripts with the scripts of the core sub command made it difficult to stay up to date, due to dealing with merge conflicts and mapping variables that were renamed to initialize the script.
Sub was written in bash #
sub was written in bash, and in particular relies on commands that are generated to build functionality like completion, etc. It makes the code hard to reason about.
In addition, bash is not the most performant language in the world, so more complex commands can become slower. Here’s the amount of time it takes to call
sub, which maps to
$ time sub real 0m0.122s user 0m0.081s sys 0m0.053s
Considering I may invoke sub hundreds of times in my day, even gaining 50 milliseconds per command would save me a few minutes a day. I think in practice this doesn’t mean a lot, but to the performance-obsessed, any opportunity for optimization looks promising.
Creating Tome #
One of the best parts of sub is its simplicity and minimal set of functionality, which made it an appealing project for a re-write! After a couple of months, Tome was born.
Tome addresses the challenges above :
- written in Rust, enabling incredibly low overhead on top of the underlying scripts.
- complete separation of the commands themselves (instances) and the core functionality that drives it (added into
- some other features contributed like fish support (thanks Zander Hill!).
The initialization is straightforward as well: add
tome to your path after downloading it from the releases, and initialize with a one-liner in your rc file:
eval "$(tome init my-commands ~/my-scripts zsh)"
my-commandsis the command you want to make (I recommend a short, 1-2 character acronym to not type a lot).
~/my-scriptsis the directory with the scripts and directories.
zshshould be replaced with the shell you are invoking.
In particular I’m very happy with the choice of Rust: it enables fairly portable static binaries, as well as a significant boost in performance:
Example of calling help using tome (rust):
$ time s real 0m0.003s user 0m0.003s sys 0m0.000s
Recall the 100ms+ time when using sub above.
One of the last things I did before I left Zillow was switch us over to use Tome, although in the 2 years since then It could very well be that Zillow-bootstrap use has decreased significantly: the Zillow tech stack was rapidly modernizing, and then eliminated the need for many of the scripts that were used to bootstrap the dev environment.
- Tome was inspired by Sub, which was heavily used at Zillow.
- There were some difficulties in development and shortcomings in features.
- Using bash as the language it was written in had performance and maintenance overhead.
- It was built to be forked, which made it difficult to keep forked command up to date.
- Tome was written to address those, and has much lower performance overhead as well as features like Fish completion!
That’s it! Please give Tome a try.