cli-kit is a ruby Command-Line application framework. Its primary design goals are:
- Modularity: The framework tries not to own your application, but rather to live on its edges.
- Startup Time:
cli-kitencourages heavy use of autoloading (and uses it extensively internally) to reduce the amount of code loaded and evaluated whilst booting your application. We are able to achieve a 130ms runtime in a project with 21kLoC and ~50 commands.
cli-kit is developed and heavily used by the Developer Infrastructure team at Shopify. We use it
to build a number of internal developer tools, along with
cli-ui.
To begin creating your first cli-kit application, run:
gem install cli-kit
cli-kit new myprojectWhere myproject is the name of the application you wish to create. Then, you will be prompted to
select how the project consumes cli-kit and cli-ui. The available options are:
- Vendor (faster execution, more difficult to update dependencies)
- Bundler (slower execution, easier dependency management)
You're now ready to write your very first cli-kit application!
Version 5 includes breaking changes and significant new features. Notably:
autocallis completely removed;- A help system and command-line option parsing system is added.
Migrating away from autocall is as simple as replacing autocall(:X) { y } with X = y.
Using the new help system is not yet well-documented, but some hints can be found in the
gen/ilb/gen/commands directory of this repo. Existing commands in existing apps should continue to
work.
To use the new help system, commands should all define invoke instead of call. invoke takes
name as before, but the first argument is now an instance of Opts, defined in the command class,
which must be a subclass of CLI::Kit::Opts. For example:
class MyCommand < CLI::Kit::BaseCommand
class Opts < CLI::Kit::Opts
def force
flag(short: '-f', long: '--force', description: 'Force the operation')
end
end
def invoke(op, _name)
if op.force
puts 'Forcing the operation'
else
puts 'Not forcing the operation'
end
end
endThis particular structure was chosen to allow the code to by fully typed using sorbet, as invoke
can be tagged with sig { params(op: Opts, _name: String).void }, and the #force method visibly
exists.
-h/--help is installed as a default flag, and running this command with --help is handled before
reaching #invoke, to print a help message (for which several other class methods on BaseCommand
provide text: command_name, desc, long_desc, usage, example, and help_sections). See
gen/lib/gen/commands/new.rb for an example.
The executable for your cli-kit app is stored in the "exe" directory. To execute the app, simply
run:
./exe/myproject/exe/- Location of the executables for your application./lib/- Location of the resources for your app (modules, classes, helpers, etc).myproject.rb- This file is the starting point for where to look for all other files. It configures autoload for the app.myproject/- Stores the various commands/entry points.entry_point.rb- This is the file that is first called from the executable. It handles loading and commands.commands.rb- Registers the various commands that your application is able to handle.commands/- Stores Ruby files for each command (help, new, add, etc).
Let's say that you'd like your program to be able to handle a specific task, and you'd like to
register a new handler for the command for that task, like myproject add to add 2 numbers, like
in a calculator app.
To do this, open /lib/myproject/commands.rb. Then, add a new line into the module, like this:
register :Add, 'add', 'myproject/commands/add'The format for this is register :<CommandClass>, '<command-at-cli>', '<path/to/command.rb>'
The action for a specific command is stored in its own Ruby file, in the /lib/myproject/commands/
directory. Here is an example of the add command in our previous to-do app example:
require 'myproject'
module Myproject
module Commands
class Add < Myproject::Command
def call(args, _name)
# command action goes here
end
def self.help
# help or instructions go here
end
end
end
endThe call(args, _name) method is what actually runs when the myproject add command is executed.
- Note: The
argsparameter represents all the arguments the user has specified.
Let's say that you are trying to compute the sum of 2 numbers that the user has specified as arguments. For example:
def call(args, _name)
sum = args.map(&:to_i).inject(&:+)
puts sum
endAbove, you'll notice that we also have a self.help method. This method is what runs when the user
has incorrectly used the command, or has requested help. For example:
def self.help
"Print the sum of 2 numbers.\nUsage: {{command:#{Myproject::TOOL_NAME} add}} 5 7"
endcli-kit also features cli-ui, another gem from us here at Shopify, which allows for the use of
powerful command-line user interface elements. For more details on how to use cli-ui, visit the
cli-ui repo.