Acclimate

A CLI building toolkit. Pronounced A CLI Mate.

Installation

Add this line to your application's Gemfile:

gem 'acclimate'

And then execute:

$ bundle

Or install it yourself as:

$ gem install acclimate

Usage

Acclimate provides a toolkit to aid in the boilerplate pieces of a CLI project including: configuration, commands, helpers for the CLI class(es), error handling and some output helpers. Acclimate assumes the usage of Thor for creating the CLI portion of your CLI project. Please refer to Thor's documentation for further help.

CliHelper

Acclimate's CliHelper module provides convenience methods as well as some sane defaults for Thor.

The #execute Method

The execute method provides a common API to direct your CLI commands to actual command class instances to do the work.

module AwesomeCli
  class Cli < Thor
    include Acclimate::CliHelper

    desc "some-command", "Does something useful"
    option :do_it_good, type: :boolean
    def some_command( filepath )
      execute AwesomeCli::Command::SomeCommand, filepath: filepath
    end
  end
end

The call to #execute instantiates the command class, and merges the filepath argument into the options arguments, passing it into the command class' initializer.

Configuration

Acclimate's configuration class provides a base from which to build a custom configuration class in your CLI project. The simplest implementation of a configuration class for your project is to inherit from Acclimate::Configuration and do nothing else.

module AwesomeCli
  class Configuration < Acclimate::Configuration
  end
end

Now you can use the configuration class simply by passing a hash of values into it.

conf = AwesomeCli::Configuration.new( env: 'development', filepath: 'some/file/path' )

conf[:env]               #=> 'development'
conf['env']              #=> 'development'
conf.env                 #=> 'development'
conf.slice( :env )       #=> { :env => 'development }
conf.slice( :env ).class #=> AwesomeCli::Configuration

Pretty much any method that works on a standard Hash will work on the configuration. This magic is a result of Acclimate::Configuration inheriting from Hashie::Mash.

In addition, any commands (see the commands section below) that inherit from Acclimate::Command accept the command line options in their initializer and expose a #config method that wraps the options in a configuration object for you.

Commands

Acclimate's command class provides a base from which to build the commands for your CLI. Whn building an Acclimate CLI, we use commands to encapsulate the actual behavior of each CLI defined command or sub-command. This serves multiple purposes. First, it keeps the command logic out of the CLI class(es), which is basically the view or GUI of a CLI project. Second, having the actual logic encapsulated in a command means including our CLI gem into another project and borrowing the behavior is possible through direct utilization of the command class(es), without interacting with the CLI classes.

Building a command class is as simple as inheriting from Acclimate::Command and implementing an #execute method.

module AwesomeCli
  class SomeCommand < Acclimate::Command
    def execute
      # do something useful ...
    end
  end
end

Due to the fact that your commands will certainly have some additional common behavior that Acclimate does not provide, it is a good idea to have a base class for your commands. For instance, in order to use your custom configuration class as opposed to an instance of Acclimate::Configuration, you must override the #config_klass method in your command(s). You command base class is the ideal place to do so.

module AwesomeCli
  class CommandBase < Acclimate::Command
  protected
    def a_helper
      # do something helpful
    end

    def config_klass
      AwesomeCli::Configuration
    end
  end

  class SomeCommand < CommandBase
    def execute
      # do something useful ...
    end
  end
end

Output

Acclimate's output module can be included in any class in your CLI and is already included in any class that inherits from Acclimate::Command or includes Acclimate::CliHelper. In addition to it own output helpers, the module also include Thor's shell module.

The #confirm Method

Acclimate's #confirm method is the accepted strategy for outputting command status throughout the execution of the command. The #confirm method outputs a statement, executes a provided block, then outputs either OK or ERROR depending on the results of the block.

module AwesomeCli
  module SomeCommand < CommandBase
    def execute
      confirm "Doing something useful" do
        # something useful here
      end

      confirm "Doing something helpful" do
        # something helpful here
      end
    end
  end
end

Executing the previous command

command = AwesomeCli::SomeCommand.new
command.execute

Results in the following output

Doing something useful ... OK
Doing something helpful ... OK

Error Handling

Acclimate can handle known error conditions gracefully for you. In the cases wher eyou would like to output a useful error message but not overwhelm your user with a Ruby backtrace, simply raise the Acclimate::Error.

raise Acclimate::error, 'External service unavailable, please try the command again later'

Errors and the #confirm Method

If an error is encountered within the block that should be gracefully handled by Acclimate as opposed to an ugly Ruby stacktrace, you may raise Acclimate::Error.

module AwesomeCli
  module SomeCommand < CommandBase
    def execute
      confirm "Doing something useful" do
        raise Acclimate::Error, 'A known error description'
      end
    end
  end
end

Executing the previous command

command = AwesomeCli::SomeCommand.new
command.execute

Results in the following output

Doing something useful ... ERROR
Error: A known error condition

By default, the exit code for an error is 1. This can be overridden when raising an error.

raise Acclimate::Error.new( 'A known error description', exit_code: 35 )

In some cases it is not enough to raise an Acclimate::Error and let the graceful error handling ensue. You may need to clean something up or want to output additional error details. It is this case the Acclimate::ConfirmationError is for.

Let's assume your command is parsing a text file and wants to output which lines were unparseable along with an error message.

module AwesomeCli
  module ParseTextFile < CommandBase
    def execute
      confirm "Parsing text file" do
        parse_file
        raise_if_in_error_lines!
      end
    end

  protected

    def raise_if_in_error_lines!
      raise Acclimate::ConfirmationError.new( 'Unparseable lines were encountered', exit_code: 12, 
                                                                                    finish_proc: report_in_error_lines )
    end

    def report_in_error_lines
      -> {
        in_error_lines.each do |line|
          say_stderr( "  #{line}" )
        end
      }
    end

    def parse_file
      File.readlines( config.filepath ).each do |line|
        begin
          parse_line( line )
        rescue
          in_error_lines << line
        end
      end
    end

    def parse_line( line )
      # some parsing logic ...
    end

    def in_error_lines
      @in_error_lines ||= []
    end

  end
end

Executing the previous command

command = AwesomeCli::ParseTextFile.new
command.execute

Results in the following output

Parsing text file ... ERROR
Unparseable lines were encountered
  1 something unparseable
  7 something else unparseable