The last time Hackerfall tried to access this page, it returned a not found error. A cached version of the page is below, or click here to continue anyway

Jonathan Clem

Building a Command-Line Application with Crystal

Published on March 23, 2017

One of the things I do quite frequently as a developer is setting up a development environment. I do lots of prototyping, so Im always creating new applications that have differing environment requirements. Typically, I use foreman to run an application in an environment defined in a .env file (e.g. foreman run mix phoenix.server), and I define an applications environment needs in an app.json file. This works well, but filling in a .env file from the requirements outlined in an app.json file is really tedious.

In order to solve this, I decided to build a command-line application to help with filling in .env files. The application would need to:

  1. Parse the env section in the app.json file
  2. Merge the parsed env with values from the environments section, if needed
  3. Merge any existing values from an existing .env file
  4. Prompt the user to optionally override any values
  5. Write the values back to the .env file

I wanted the application to be easy to install with as few requirements as possible. For me, that narrowed the choices down to Crystal and Go. I decided to go with Crystal, because although I appreciate that its very easy to cross-compile Go with no needed dependencies, I like Crystals Ruby-like syntax and useful built-in command-line option parser.

If you like, you can skip to the end result at jclem/bstrap (brew tap jclem/bstrap && brew install bstrap on a Mac). In this post, Im going to reflect on what I liked about building this small application with Crystal and what was difficult.

Parsing Command-Line Options

One of the first things that I found useful was, as I mentioned before, Crystals command-line option parser that comes as part of its standard library. I wanted to have the commonly seen sort of command line options where a user can pass a full option name or an alias. This was just as easy to do in Crystal as it is in Ruby:

require "option_parser"

class MyCLI
  def run
    path = "./default-path.txt"

    OptionParser.parse! do |parser|
      parser.banner = "Usage mycli [arguments]"

      parser.on("-p PATH", "--path PATH", "Path to a file") do |opt_path|
        path = opt_path

      parser.on("-h", "--help", "Show this help") do
        puts parser
        exit 0

    puts "Your path is #{path}."

With just a handful of lines and some other simple code to call this class, a user can mycli -p file.txt, mycli --path=file.txt, and mycli --help. I was really happy with how easy this was.

If youve ever used Rubys OptionParser class before, youll notice that the Crystal equivalent is almost identical. A more complete example of option parsing in Crystal is in the bstrap repo, or you can try out similar code in a Crystal playground.

Parsing JSON

The next hurdle for me, parsing the JSON contents of an app.json file, was the one I knew Id have the most trouble with. Crystal is a statically type-checked language, so I knew that there might be a good deal of boilerplate involved in parsing an app.json file and ensuring that Im working with the types I expect to be working with. Further complicating things is the fact that the app.json specification allows multiple different types for many values. For example, an entry in env can be either a string representing the default value of that environment variable or an object describing the environment variable, e.g.:

  "env": {
    "NODE_ENV": "production",
      "description": "A URL pointing to a PostgreSQL database"

The first step in parsing JSON was to write a simple function to read the app.json file, parse it, and return a hash or raise if the root of the JSON document is not an object. This was relatively straightforwardIll define a function called parse_app_json_env (well add the env parsing to it soon):

class Bstrap::AppJSON
  class InvalidAppJSON < Exception

  def parse_app_json_env(path : String)
    raw_json =

    if app_json = JSON.parse(raw_json).as_h?
      raise"app.json was file not an object")
  rescue JSON::ParseException
    raise"app.json was not valid JSON")

The basic form of this function was relatively straightforward. First, we read the file at the given path and parse it as JSON (notice that we rescue invalid JSON and return our custom exception). Then, we check whether the parsed JSON is an object. We do this because in JSON, a document may be an object, an array, or a scalar value. Obviously, we want to ensure that the contents of our app.json arent, for example, an array or simply an integer.

It took me a little bit of getting used to, but Crystal actually makes this checking pretty easy. The JSON.parse class method returns a type called JSON::Any. This type is simply a wrapper around all possible JSON types, and provides some useful methods to ensure were wrapping the type we want. In the above example, youll see #as_h? called. This method returns the type Hash(String, JSON::Type)? meaning either nil or a hash with string keys and JSON type values. Putting things together, we can check #as_h? and either return that hash or raise an error because the contents of our app.json file was something other than an object.

This was relatively straightforward, but remember that I want this method to return the parsed env object, not just the raw parsed app.json file. I updated my parse_app_json_env to call a new method called parse_env that would take care of this:

def parse_app_json_env(path : String)
  raw_json =

  if app_json = JSON.parse(raw_json).as_h?
    raise"app.json was file not an object")
rescue JSON::ParseException
  raise"app.json was not valid JSON")

The parse_env method had a tricky job, because as I said before, the app.json schema allows values in env to be either strings or objects. For the sake of programming ease, I wanted to ensure that this method was always returning a hash whose values were other hashes, regardless of what was parsed. To express this, I defined a couple of new type aliases:

type JSONObject = Hash(String, JSON::Type)
type ParsedEnv = Hash(String, JSONObject)

I first defined JSONObject simply to refer to a hash whose keys are strings and whose values are JSON types. I could have been more specific to the env format and created a type whose keys are strings and whose values are either strings or booleans (for the required key from the app.json schema), but this didnt seem necessary.

The ParsedEnv type refers to a hash whose keys are strings and whose values are JSONObjects.

With these new types in hand, I could create the parse_env function that would read the env from an app.json hash and return a ParsedEnv:

private def parse_env(app_json : JSONHash) : ParsedEnv
  parsed =

  case env = app_json.fetch("env", nil) # Ensure we have an "env"
  when Hash
    env.reduce(parsed) do |parsed, (key, value)|
      case value
      when String
        parsed[key] = {"value" =>}
      when Hash
        parsed[key] = value
        raise "env" value was not a string or an object))
  when nil
    raise "env" was not an object))

I think that the above isnt particularly pretty, but it does the job. I fetch the env value and assert that its an object (we just return an empty ParsedEnv if its not present, which is acceptable), and then iterate over its key-value pairs. For each pair, we then have to check the value and ensure that its a string or a hash, and raise otherwise.

The above example also introduces some of the things that are still mysteries to me about the Crystal type system: ParsedEnv is an alias for Hash(String, JSONObject), and JSONObject is an alias for Hash(String, JSON::Type). JSON::Type, in turn, is an alias for a number of other types, including String. Why, then is it necessary for me to restrict the type of value to JSON::Type when the compiler already knows that it is a String?

Another thing that wasnt apparent to me in the example above at first is that I could call If I were to declare parsed = {}, the compiler would complain that I should declare an empty hash in a way that includes the expected key-value types, e.g. parsed = {} of String => JSONObject. I have a lot of these in bstrap, still, and didnt realize until someone told me that I could call .new on a type alias, instead, and get the same result.

Overall, I found JSON parsing a little bit easier than Ive found it with other type-checked languages such as Go. The weirdness of type restrictions and the tedium of checking everything as early as possible is a little tiresome, but helps prevent bugs.


Elixir has spoiled me. This command-line application has file reading, JSON parsing, and file writing, so there is plenty of opportunity for exceptions to be thrown. Given an imaginary program that reads a file, parses its JSON, and then writes ok to the file, an error-handled Elixir program might look like this:

with {:ok, raw_file} <-,
     {:ok, map}      <- Poison.parse(raw_file),
     :ok             <- File.write(path, "ok") do
  {:error, :enoent}  -> {:error, "Could not read file"}
  {:error, :invalid} -> {:error, "File contained invalid JSON"}
  {:error, _}        -> {:error, "Other error"}

In Crystal, the same error handling might look like this:

  raw_file =
  map = Poison.parse(raw_file)
  File.write(path, "ok")
rescue Enoent
  raise "Could not read file"
rescue JSON::ParseException
  raise "Could not parse file"
rescue ex
  raise "Other error"

I greatly prefer the Elixir way, not only because I think that it reads better, but also because in Crystal, Im frequently having to look up and see what possible exceptions a particular method might raise (or whether it might raise any at all). Elixir indicates this clearly by either suffixing a function name with a bang, e.g. Poison.parse! or by returning a tagged tuple, where {:ok, value} means success, and {:error, error} indicates the error that occurred. For some reason, this makes me feel much more assured that I am properly handling errors, rather than ending every method with a rescue clause.

There is a similar pattern available in the Bluebird Promises library for JavaScript. Bluebird allows me to chain a set of promises, and then pattern match my error handling based on a predicate (which may be a function or an error constructor):

  .catch(ReadError, handleReadError)
  .catch(ParseError, handleParseError)
  .catch(WriteError, handleWriteError)

I really like this pattern and was happy when the with keyword made its way into Elixir, which feels very similar. I wish Crystal/Ruby had something like this.


Overall, I really enjoyed writing bstrap in Crystal, and I think Ill continue to use it for building command-line applications. It cant do fancy things like statically link libraries like Go can so that I can just send someone a binary (I have to install some libs with Homebrew, instead), but the ease of programming in an extremely fast Ruby-like language with static type checking definitely makes up for that.

Continue reading on