Writing a CLI in Ruby using the Trollop gem (follow up)


On our last post we learned to create a basic CLI application using Trollop. We learned how to parse command line options with different types and how to ask for help. Today I want to explore some more advanced options.

Sub-commands

A sub-command is a way of branching our code to perform different actions depending on a command string.

A great example for this is the git CLI. Calling git status will perform something completely different than calling git branch or git clone. This is not just a matter of refining our options, but performing entirely different tasks.

In order to put this in action, we'll create a script that wil explore my blogging framework settings.

We'll create a sub-command tha will list all the posts files (by reading the posts directory), with an option to sort them by name or by creation date (which'll be a boolean for simplicity).

And a second one for exploring my post ideas, that I save on a file called IDEAS.org, with an option to limit the number of retrieved ideas.

Finally, we'll add a global option --dry-run to just print the passed options and exit.

  require "trollop"
  require "awesome_print"

  SUB_COMMANDS = %w(posts ideas)
  global_opts = Trollop::options do
    banner "My writing framework information:"
    opt :dry_run, "Just show me what I passed in"
    stop_on SUB_COMMANDS
  end

  cmd = ARGV.shift

  cmd_opts = case cmd
             when "posts"
               Trollop::options do
                 opt :by_time
               end
             when "ideas"
               Trollop::options do
                 opt :limit, "Limit the number of ideas", default: 5
               end
             else
               Trollop::die "Unknown subcommand #{cmd.inspect}"
             end


  if global_opts[:dry_run]
    puts "Global options:"
    ap global_opts
    puts "Subcommand:"
    ap cmd
    puts "Subcommand options:"
    ap cmd_opts
    puts "Remaining arguments:"
    ap ARGV

    exit
  end

  case cmd
  when "posts"
    puts "Blog posts: "
    command = "ls ./posts/"
    command << " -t" if cmd_opts[:by_time]

    system command
  when "ideas"
    puts "Post ideas"
    ideas = File.open("./IDEAS.org")
              .each_line
              .take(cmd_opts[:limit])

    puts ideas
  end

Now we can run our script to fetch our post list sorted by name (default) or by time:

  $ wf.rb posts
  Blog posts:
  ls /home/fedex/Dropbox/wordpress_rake/posts/
  case_vs_hash.org    erb-to-haml.org            ruby_scratch_buffer.org     trollop.org                writing_framework_fixup.org
  dig.org             hyper.org                  secret_santa_algorithm.org  trollop_advanced.org       writing_framework.org
  duplicate-line.org  moving_lines_in_emacs.org  trollop                     WIP_a_jim_weirich_day.org  writing_framework_putting_it_all_together.org

  $ wf.rb posts --by-time
  Blog posts:
  ls /home/fedex/Dropbox/wordpress_rake/posts/ -t
  trollop_advanced.org  erb-to-haml.org            secret_santa_algorithm.org  hyper.org			writing_framework_fixup.org
  trollop               dig.org                    case_vs_hash.org            ruby_scratch_buffer.org          writing_framework_putting_it_all_together.org
  trollop.org           moving_lines_in_emacs.org  duplicate-line.org          WIP_a_jim_weirich_day.org	writing_framework.org

We can list our top 5 post ideas (default), or maybe just the top 3:

  $ wf.rb ideas
  Post ideas
  * Habitica
  * yield_self vs tap
  * My default Ruby packages
  * Org capture
  ** what is org capture and how to configure it

  $ wf.rb ideas --limit 3
  Post ideas
  * Habitica
  * yield_self vs tap
  * My default Ruby packages

Or try a dry run:

  $ wf.rb ideas --limit 3 --dry-run
  Error: unknown argument '--dry-run'.
  Try --help for help.

Oops, thah didn'go well. The problem is that the global options need to be passed before any sub-command or else trollop will try to parse them as sub-command options.

  $ wf.rb --dry-run ideas --limit 3
  Global options:
  {
            :dry_run => true,
               :help => false,
      :dry_run_given => true
  }

  Subcommand:
  "ideas"

  Subcommand options:
  {
            :limit => 3,
             :help => false,
      :limit_given => true
  }

  Remaining arguments:
  []

And now it works just fine.

Conclusion

I find trollop to be my preferred tool for building simple CLIs. I find it to be very powerful and simple to use. I hope you try it out for your next CLI.

Saluti.