Introducing hanami-cli


Introducing hanami-cli: a general purpose Command Line Interface (CLI) for Ruby. Learn why Hanami replaced thor in favor of hanami-cli and how to use it to build a CLI application in 5 minutes.

Why not thor?

For long time we used thor 🔨 to build the Command Line Interface (CLI) of Hanami. But as the time passed, we needed more control on the internals of our implementation.

The Hanami 🌸 command line needs two crucial features: subcommands and extendibility.

Subcommands

A subcommand is a nested command under the main executable. For instance hanami is the executable and generate action is the subcommand.

thor doesn’t play well with subcommands, which lead to a lot of hacks to make it to work 🙀🙀🙀. We reached a point of code fragility, and immobility. We were afraid to touch it and that made impossible to introduce new features.

Extendibility

On the other side, to grow an ecosystem around Hanami, we needed third-party gems to extend the basic set of subcommands that we provide out of the box.

Imagine a developer who wants to integrate Webpack with Hanami, they can build a gem to make it happen. This gem may need to generate some configuration, so it can register a subcommand to do so: hanami generate webpack.

This feature was impossible with thor because it wasn’t designed for it. 😭

hanami-cli

At this point we decided to roll-out our own solution to solve these problems 💪. Good news are: hanami-cli is a general purpose toolkit to build CLIs 🙌.

Following our tradition, we build components that can be used outside of Hanami, and this is the case for hanami-cli too!

To recap: hanami-cli is a thor alternative to build your own CLI outside of Hanami

A quick example

Let’s build a small CLI utility to convert money from one currency to another.

As first thing we generate a new gem via Bundler:

$ gem install bundler
$ bundler gem curex

Then we create the executable for our gem:

$ mkdir exe
$ vim exe/curex

And now we can edit it:

#!/usr/bin/env ruby

require "bundler/setup"
require "curex"
Curex::CLI.new.call

Don’t forget to give it the right permissions:

$ chmod +x exe/curex

Now we need to setup the curex.gemspec file. The fastest way is:

$ curl https://raw.githubusercontent.com/jodosha/curex/master/curex.gemspec > curex.gemspec

At this point we can try to run the executable:

$ ./exe/curex
./exe/curex:5:in `<main>': uninitialized constant Curex::CLI (NameError)

This returns an error because, we haven’t implemented our code yet. Let’s do it now in lib/curex.rb.

module Curex
  require "curex/version"

  class CLI
    def call(*args)
    end
  end
end

By running the code now, it doesn’t raise an exception anymore:

$ ./exe/curex

Our CLI isn’t really useful at the moment. Let’s build our first command:

require "hanami/cli"

module Curex
  require "curex/version"

  class CLI
    def call(*args)
      Hanami::CLI.new(Commands).call(*args)
    end

    module Commands
      extend Hanami::CLI::Registry

      class Convert < Hanami::CLI::Command
        def call(*)
          puts "converting.."
        end
      end

      register "convert", Convert
    end
  end
end

We can try it again:

$ ./exe/curex
Commands:
  curex convert

This output indicates the available (sub)commands, in our case we have only convert:

$ ./exe/curex convert
converting..

Yay! We have our working CLI, now it needs some logic.

require "hanami/cli"
require "bigdecimal"
require "money"
require "money/bank/google_currency"

Money.use_i18n = false
Money.default_bank = Money::Bank::GoogleCurrency.new

module Curex
  require "curex/version"

  class CLI
    def call(*args)
      Hanami::CLI.new(Commands).call(*args)
    end

    module Commands
      extend Hanami::CLI::Registry

      class Convert < Hanami::CLI::Command
        argument :amount, required: true
        argument :from,   required: true
        argument :to,     required: true

        def call(amount:, from:, to:)
          money = Money.new(amount.to_d * 100, from)
          result = money.exchange_to(to)

          puts "#{amount} #{from} == #{result} #{to}"
        end
      end

      register "convert", Convert
    end
  end
end

Let’s try it again:

$ ./exe/curex convert
ERROR: "curex convert" was called with no arguments
Usage: "curex convert AMOUNT FROM TO"

It now tells us which args are required to run the subcommand:

$ ./exe/curex convert 100 USD EUR
100 USD == 84,76 EUR

It works! 🎉

If you want to expand the command with more hints for your users, please check the curex repository or the hanami-cli docs.

Conclusion

In 5 minutes we were able to build a Command Line Interface (CLI) app with hanami-cli.

The first hanami-cli (v0.1.0) version will be released with hanami v1.1.0 in October 2017.

Hanami provides both an integrated web framework (the hanami gem) and also a toolkit of general purpose gems like hanami-cli.

Indeed, in the near future, both dry-rb and Trailblazer will adopt hanami-cli to build their own command lines.

We love Ruby 💎❤️ and we build tools for the entire ecosystem.

Luca Guidi

Family man, software architect, Open Source indie developer, speaker.

Rome, Italy https://lucaguidi.com