leitmotif is a very simple templating tool that generates directories from prototypes stored in git repositories. Its design prioritizes simplicity and a minimal set of external dependencies.

While the leitmotif tool is still under development and some interesting features are planned for future work, it is already useful for many project-templating tasks. This post will show you how to get started.

Installing Leitmotif

You can install leitmotif either by using the RubyGem or by using the standalone version, which only depends on Ruby 2 and the Thor gem.

To install the RubyGem, simply run gem install leitmotif.

To install the standalone script, run the following commands:

1
2
3
gem install thor --version=0.17 && \
    curl -O https://github.com/willb/leitmotif/raw/master/extra/standalone/leitmotif && \
    chmod 755 leitmotif

In the future, I expect that leitmotif packages will be available for CentOS and Fedora.

Using Leitmotif

The leitmotif tool is self-documenting. Run leitmotif help to see options.

Copying a prototype from a remote repository

Use leitmotif clone URL to make a local copy of the remote prototype repository at URL in your local Leitmotif prototype store (under your home directory).

Listing locally-installed prototypes

Use leitmotif list to see the prototypes you have installed in your local store.

Instantiating prototypes

Use leitmotif generate PROTOTYPE OUTPUT_DIR to instantiate PROTOTYPE in OUTPUT_DIR. In this form, PROTOTYPE must be the path to a git repository containing a Leitmotif prototype. This command supports the following options:

  • --local: treat PROTOTYPE as the name of a prototype repository in the local store rather than as a path
  • --clobber: delete OUTPUT_DIR before processing the prototype, if it exists
  • --ref: a git tag, branch, or SHA to use from the prototype repository (defaults to master if unspecified)
  • --bindings KEY:VALUE ...: a list of variable bindings to use while instantiating the prototype
  • --verbose: provide additional output as leitmotif works

Creating Leitmotif prototypes

A Leitmotif prototype is just a git repository with a particular structure. Specifically, a prototype must have two entries in the repository root directory:

  1. a YAML file named .leitmotif that contains metadata about the prototype, and
  2. a directory named proto, which contains the prototype itself.

Prototype metadata

A .leitmotif file is just a YAML representation of a hash of metadata options. The following keys can appear in a .leitmotif file:

  • :name: the name of the prototype (used for documentation)
  • :version: the version of the prototype (used for documentation)
  • :required: a list of variables that must have user-provided values when the prototype is instantiated
  • :ignore: a list of files to copy to the instantiated prototype without processing by the templating engine
  • :defaults: a hash consisting of default values for prototype variables

Here’s an example .leitmotif file, from a prototype for Spark application development:

1
2
3
4
5
6
7
8
9
10
11
---
:name: sparkdev
:version: '0'
:required:
  - name
:ignore:
  - sbt
:defaults:
  :version: '0.0.1'
  :scala_version: '2.10.4'
  :spark_version: '1.1.0'

Prototypes and templating

With the exception of the files in the :ignore list, all of the files in a prototype repository are processed by ERB after they’re copied to the output directory. For more details on eRuby, see elsewhere, but here are a few basics to get you started:

  • If a template file contains Ruby code surrounded by <% and %>, that code is evaluated at prototype instantiation time with user-supplied variable bindings.
  • If a template file contains Ruby code surrounded by <%= and %>, that code is evaluated at prototype instantiation time with user-supplied variable bindings and the result of evaluating that code is substituted into the document at that point.

Combining these two, we can see how to use loops in a file:

1
2
3
4
5
6
7
8
Specifications of properties are often terser than explicit expected results.  For example:

<% 99.downto(1).each do |bottles| %>
   * <%= bottles %> bottles of beer on the wall, 
     <%= bottles %> bottles of beer; 
     take one down, pass it around, 
     <%= bottles > 1 ? "#{bottles} bottles" : "just one bottle" %> of beer on the wall
<% end %>

The above will generate a Markdown file containing a bulleted list that will strike terror into the heart of any adult who has ever been on a bus full of middle-schoolers.

Coming soon

I wrote this tool to solve an immediate need1 and will be updating it as new requirements become apparent. However, there are a few things that are already on my roadmap:

  • automated test coverage (currently — and shamefully! — there is none)2
  • additional commands, e.g., to inspect installed prototypes
  • post-instantiation actions, e.g., to rename a file or create a directory based on the result of a variable expansion

Of course, I welcome your feedback, issue reports, and patches as well.


  1. Specifically, my need to be lazy when creating new projects.

  2. I’m probably getting too old to program in untyped languages.

  development, leitmotif, • You may reply to this post on Twitter or