Skip to content
David Copeland edited this page Nov 13, 2013 · 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 bootstraping 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

You may think that flags and switches are scoped to, and associated with, subcommands. In a certain sense they are, but their implementation was not done this way. In reality, all flags and switches of commands and subcommands are "owned" by the top level command. Issue 96 exists to correct this, which would eliminate the complexity I'll now describe.

Suppose we have this code:

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
  end

  # ...

end

When we get help for list, we'll see only one option available, --long. When we get help for list tasks, we won't see the --long option but we will see the --late option. This is how you would expect things to work.

Suppose, however, that we want to give both switches a short-form option of -l:

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 generate an exception with a message like "l has already been specified as a switch". This is the implementation bleeding through. Until 96 is addressed, this code is essentially not possible.

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 to design your user interface to avoid this issue.

In general, simple apps that have light nesting will not encounter this issue.

If you'd like to help fix it, please read the thread in the issue, and then examine gli_option_parser.rb as a start.