Skip to content
David Copeland edited this page Jun 30, 2015 · 8 revisions

GLI2 added subcommand support. This allows you to have deeply nested commands beyond the initial one, e.g. to implement something like git remote add.


If you are upgrading from GLI 2.5.x or earlier, you will likely want to add this to your app

subcommand_option_handling :normal

This is needed for backwards compatibility and should be added automatically by GLI when bootstrapping apps with 2.6 or greater.

See https://github.com/davetron5000/gli/issues/96#issuecomment-27238249 for more info


Adding this creates some complexity and nuances to how your app behaves and how you declare your UI. The design was intended to make obvious patterns simple, but have some flexibility for more complex UIs.

Basics

Before GLI2, a call to command took a block, and inside this block you could only declare flags, switches, and the action block:

desc "adds a remote repo to your local config"
command :remote_add do |c|
  c.flag :flag
  c.switch :verbose
  c.action do |global_options,options,args|
    # ... logic here
  end
end

The most basic way to add subcommands in a GLI2-powered app is to use command inside an existing command block.

desc "manage remotes"
command :remote do |c|
  c.desc "adds a remote repo to your local config"
  c.command :add do |add|
    add.action do |global_options,options,args|
      # ... logic here
    end
  end
end

Note that the desc for :remote is documentation for the namespace created by :remote, so we've used a more generic description.

You can nest this as many times deep as you like:

desc "manage remotes"
command :remote do |c|
  c.command :repo do |repo|
    repo.desc "adds a remote repo to your local config"
    repo.command :add do |add|
      add.action do |global_options,options,args|
        # ... logic here
      end
    end
  end
end

If you are just using subcommands as namespaces, and have few or no command line options, this is all you need to know.

The code/UI can get tricky in the following situations:

  • You want to choose a default subcommand to execute when the user omits in on the command-line (e.g what should git remote do?)
  • You want to use commands and non-leaf subcommands as actions in their own right, not just as namespaces.
  • You have a lot of options between subcommands that share the same name

Default subcommands

By default, omitting a subcommand will show the help for the last command entered on the command line, e.g. executing git remote would show all the possible subcommands of remote, with their help text.

You can, however, designate one subcommand as the default to execute when the subcommand is omitted. This is what happens when you execute git stash - the default command, save, is executed.

To designate the default command in GLI2, use default_command (being sure to call it after you've declared the command you are designating)

desc "manipulate the stash"
command :stash do |c|
  c.desc "list items in the stash"
  c.command :list do
    # ...
  end
  c.desc "save current changes to stash"
  c.command :save do
    # ...
  end
  c.desc "apply topmost stash to current wd"
  c.command :apply do
    # ...
  end
  c.default_command :save
end

Now, if you do git stash, the save command will be executed. If you were to get help, via git stash help, you'd see that save is listed as the default.

Suppose, however, you want an actual action to be run when the subcommand is omitted.

Default actions

Coming back to git remote, when you just execute that, it lists the remote repos. There is no subcommand for that. To do this in GLI2, we simply give the command an action block as normal, however we must use the default_desc command to describe this particular action because, as you recall, the desc describes the namespace. Let's see an example:

desc "manage remote repos"
command :remote do |c|
  c.desc "add a remote repo"
  c.command :add do |add|
    # ...
  end

  # ...

  c.default_desc "show a list of existing remotes"
  c.action do |global_options,options,args|
    # ...
  end
end

We can see how this works by getting help via git help remote

NAME
    remote - manage remote repos

SYNOPSIS
    git [global options] remote [command options] 
    git [global options] remote [command options]  add

COMMANDS
    <default> - show a list of existing remotes
    add       - add a remote repo

We can see that our top-level desc is used to describe the namespace remote, but that our default_desc is used to describe the default action when the subcommand is omitted. We also see that GLI generated an example command line where the subcommand is omitted.

Both this, and the default command are necessary complexities of handling subcommands. With regard to options (flags and switches), there is some additional unnecessary complexity.

Flags and Switches

Since GLI 2.6 flags and switches are scoped to, and associated with, subcommands. Suppose we have this code:

desc "List things"
command :list do |c|
  c.desc "show long form output"
  c.switch [:l,:long]

  c.command :tasks do |tasks|
    tasks.desc "Show only tasks that are late"
    tasks.switch [:l,:late]
  end
end

This would define a separate sets of options for parent list and sub list tasks commands. Options are allowed to have same names (in either short or long version).

This creates a slight issue as to how to access the value of each option. In this example, how can the action for list tasks access the value for :l, given that options[:l] will return the value for the :late switch?

You can navigate a hierarchy of options by using the parent command's options.

Accessing parent command's options

In order to access options defined in parent command you can use GLI::Command::PARENT key:

desc "List things"
command :list do |c|
  c.desc "show long form output"
  c.switch :long

  c.command :tasks do |tasks|
    tasks.desc "Show only tasks that are late"
    tasks.switch :late
    tasks.action do |global_options, options, args|
      if options[GLI::Command::PARENT][:long]
        # display tasks using long form output
      end
    end
  end
end

You can follows this all the way to the top, if you have deeply nested commands, e.g. options[GLI::Command::PARENT][GLI::Command::PARENT][:foo]

Before GLI 2.6

In older GLI versions it wasn't possible to have option with a same name on parent and subcommand level. As a workaround, you could do this:

desc "List things"
command :list do |c|
  c.desc "show long form output"
  c.switch :long

  c.switch :l

  c.command :tasks do |tasks|
    tasks.desc "Show only tasks that are late"
    tasks.switch :late
  end
end

When the user executes todo list tasks -l, options[:l] will return true (however options[:late] would still be false).

I realize this is less than ideal, so my suggestion would be update GLI to latest version.