GLI
The easy way to make command-suite CLI apps
GLI is the easiest way to make a CLI app that takes commands, in a vein similar to
git
or gem
(GLI stands for "Git-Like Interface"). GLI uses a
simple DSL, but retains all the power you'd expect from the built-int OptionParser
.
If you're looking to make a vanilla CLI app that doesn't need command support,
be sure to check out optparse-plus, which gives you all the power
of OptionParser
, but none of the verbosity.
Here's a simple todo list application:
#!/usr/bin/env ruby require 'gli' require 'hacer' class App extend GLI::App program_desc 'A simple todo list' flag [:t,:tasklist], :default_value => File.join(ENV['HOME'],'.todolist') pre do |global_options,command,options,args| $todo_list = Hacer::Todolist.new(global_options[:tasklist]) end command :add do |c| c.action do |global_options,options,args| $todo_list.create(args) end end command :list do |c| c.action do $todo_list.list.each do |todo| printf("%5d - %s\n",todo.todo_id,todo.text) end end end command :done do |c| c.action do |global_options,options,args| id = args.shift.to_i $todo_list.list.each do |todo| $todo_list.complete(todo) if todo.todo_id == id end end end end exit App.run(ARGV)
We can now use our app like so:
$ todo help NAME todo - A simple todo list SYNOPSIS todo [global options] command [command options] [arguments...] GLOBAL OPTIONS --help - Show this message -t, --tasklist=arg - (default: /Users/davec/.todolist) COMMANDS add - done - help - Shows a list of commands or help for one command list - $ todo add "Take out trash" $ todo add "Rake leaves" $ todo add "Clean Kitchen" $ todo list 0 - ["Take out trash"] 1 - ["Rake leaves"] 2 - ["Clean Kitchen"] $ todo done 1 $ todo list 0 - ["Take out trash"] 2 - ["Clean Kitchen"]
We can make our app so much better. For a longer demo, or just a quick reference, check out the annotated source for a much more sophisticated application. Meanwhile, let's tour some of the features.
Commands
The entire UI of your app is commands, and you can create them with the command
method. It takes a block, which is given a GLI::Command
instance. You can call DSL methods on this to describe how your command works. The thing you are required to do is call the action
methods, which takes a block. This block is executed when the user executes the command on the command-line. To access the command-line options and arguments, the block we give
to action can accept parameters. We'll see those in a little bit.
One thing we didn't see is how to document our command. This is done Rake-stye via the methods desc
and long_desc
. desc
is
short one-line description that shows up in the main help, while long_desc
is a longer description, possibly multi-paragraph, that shows up in the help
for this command (e.g. when the user runs todo help list
). Let's add documentation to our list
command.
desc 'List tasks' long_desc 'Lists all tasks that have yet to be completed by the user. Each task has an id, which you can use to complete it using the "done" command.' command :list do |c| c.action do $todo_list.list.each do |todo| printf("%5d - %s\n",todo.todo_id,todo.text) end end end
Command-Line Options
Command-line options for a command-suite come in two forms:
- Global Options come before the command on the command-line, and generally affect every command. In our simple todo app, the option
-t
(or its long-form equivalent--tasklist
) tells all commands where tro find the task list. - Command Options come after the command on the command-line, and are specific to the command itself. For example, we might have a switch
that tells
list
to list completed tasks as well as outstanding tasks.
In GLI, global options are specified outside of any command block (much like we specified -t
with flag [:t,:tasklist]
. Command options
are specified inside a command
block. Let's add a global switch for verbosity, and a command option to list
to show
completed tasks as well. Options can be documented in the same way, using desc
.
desc 'Be verbose' switch [:v,:verbose] desc 'List tasks' long_desc 'Lists all tasks that have yet to be completed by the user. Each task has an id, which you can use to complete it using the "done" command.' command :list do |c| c.desc 'Show completed tasks as well as incomplete tasks' c.switch [:a,:all] c.action do |global_options,options,args| show = options[:all] ? :all : :incomplete $todo_list.list(show).each do |todo| printf("%5d - %s\n",todo.todo_id,todo.text) end end end
Notice that we changed the parameters to our action
block. It now gets a Hash
of the global options, a
Hash
of the command options, and the unparsed arguments as an Array
. We reach into options
to see if --all
(or -a
) was specified, and show all tasks if so.
Error Handling
Essentially, you don't need to do anything special; just allow exceptions to bubble up. The exception's message will
be shown to the user, and their backtrace will be surpressed. If you need to exit the program with an error message, you
can certainly raise an exception, but you can also call either exit_now!
or help_now!
.
The former exits with the message, while the latter also shows the command-line help. Let's add in some checks
in our done
command to make sure that the id is passed and that it's an id in our task list.
command :done do |c| c.action do |global_options,options,args| help_now!('id is required') if args.empty? todo = $todo_list.list.select { |todo| todo.todo_id == id }.first exit_now!("No todo with id #{id}") if todo.nil? $todo_list.complete(todo) end end
Pre- and Post-Hooks
You may have noticed the call to pre
in our simple app. This is a block of code called after the command-line
has been successfully parsed, but before the command is executed. It has access to the global and command-specific options, as well
as the command itself, and the unparsed arguments.
We used it to read our todo list so that each command has access to it. We set this in a global variable. You might not like this,
and you could, alternatively, set it inside the global_options
hash:
pre do |global_options,command,options,args| # Replace the string with the actual list object global_options[:tasklist] = Hacer::Todolist.new(global_options[:tasklist]) end command :add do |c| c.action do |global_options,options,args| # Now, we can read it directly global_options[:tasklist].create(args) end end
If we had any cleanup to do, that could go in a post
block:
post do |global_options,command,options,args| global_options[:tasklist].save_to_disk! end
This block is only called if there were no errors.
You can also use an around hook, which inverts the relationship and makes it easier to manage global setup for certain types of resources. For example, if you wanted to have a file open during the command execution:
around do |global_options,command,options,args,code| File.open(global_options[:filename]) do |file| options[:file] = file code.call end end
This won't stop the pre/post hooks, so you can use all three if it makes the most sense for your app.
You can also configure how error handling is done by declaring an error
block. The block you give to error
doesn't have access to any of the parsed options
or arguments (since the error could happen at any time during the lifecycle of your app). If the block
evaluates to true, GLI's normal error handling will occur. If it evaluates to false, GLI will not do anything.
Bootstrapping
In addition to the DSL and other helper methods, GLI is bundled with a command-line app, gli
,
that you can use to bootstrap your application. By bootstrapping your app this way, you get, in seconds:
- A basic project structure
- An outline of your executable
- Gemfile and gemspec
- Scaffold for unit tests
- Scaffold for Aruba-and-Cucumber-powered integration tests to make testing your app a snap
- Rakefile to tie it all together
gli
has a command, init
that takes the name of your app as an argument, followed by a list of commands your app will have (don't worry, you can easily add more using the methods and techniques we've already discussed):
$ gli init todo add list complete $ cd todo $ bundle exec bin/todo help NAME todo - A simple todo list SYNOPSIS todo [global options] command [command options] [arguments...] GLOBAL OPTIONS --help - Show this message -t, --tasklist=arg - (default: /Users/davec/.todolist) COMMANDS add - done - help - Shows a list of commands or help for one command list - $ rake test Run options: # Running tests: . Finished tests in 0.151210s, 112.4264 tests/s, 218.2395 assertions/s. 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips $ rake cucumber . 1 scenarios (3 passed) 3 steps (3 passed) 0m0.049s
All you need to do is fill in your logic!
How can I learn more?
There are three places to dig deeper into GLI:
- A code walkthrough of a more sophisticated app that has subcommands
- The Wiki
- API Documentation
Of course, the source code is also available.