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:

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:

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:

Of course, the source code is also available.