Skip to content

Commands

We have seen how to have options and arguments. Clip also supports commands.

You have already used commands, as many CLI applications use them. For example shards has many commands: install and build are two of them, and you use them by typing shards install and shards build.

A command has its own options and arguments. Actually, the struct Command that we used until now in this tutorial is already a command. Nesting other commands under an existing one requires:

  • to define the new subcommands as their own type
  • to register them inside the wrapping command using the Clip.add_command macro

Commands nested with Clip.add_commands can also use Clip.add_commands to nest other commands. Clip does not enforce any limitation on the nesting level.

Note

Command are often called subcommands. Clip makes no actual distinction between the command on which we call #parse and the nested ones, so we just call them all commands.

Let's change our application. We will move the actual behavior to a command hello and create a new one called goodbye.

require "clip"

module Myapplication
  VERSION = "0.1.0"

  abstract struct Command
    include Clip::Mapper

    Clip.add_commands({
      "hello"   => HelloCommand,
      "goodbye" => GoodbyeCommand,
    })

    getter repeat = 1
    getter name : String
  end

  struct HelloCommand < Command
    include Clip::Mapper
  end

  struct GoodbyeCommand < Command
    include Clip::Mapper

    @[Clip::Option("--yell")]
    getter yell : Bool? = nil
  end

  def self.run
    begin
      command = Command.parse
    rescue ex : Clip::Error
      puts ex
      return
    end

    case command
    when Clip::Mapper::Help
      puts command.help
    when HelloCommand
      hello(command.name, command.repeat)
    when GoodbyeCommand
      goodbye(command.name, command.repeat, command.yell)
    end
  end

  def self.hello(name, repeat)
    repeat.times { puts "Hello #{name}" }
  end

  def self.goodbye(name, repeat, yell)
    msg = "Goodbye #{name}\n" * repeat

    if yell
      puts msg.upcase
    else
      puts msg
    end
  end
end

Myapplication.run

A lot has changed!

We defined two new types: HelloCommand and GoodbyeCommand. Those types inherit from Command and define their own options and arguments. We keep the attributes repeat and name inside Command as both commands use them.

We then register those types as nested commands under Command using Clip.add_commands. The Command#parse method changes to now dispatch the parsing to HelloCommand or GoodbyeCommand according to the user input.

To check what command the user want to run, we use a case clause on the type of the returned object.

Tip

We used a Hash as the Clip.add_commands parameter but a NamedTuple would have worked too.

Note

Structs cannot be inherited, only abstract struct can, that's why Command is now abstract. But that's not a problem as it will never be instantiated now that it has nested commands.

We can check that our two commands behave as expected:

$ shards build
Dependencies are satisfied
Building: myapplication
$ ./bin/myapplication --help
Usage: ./bin/myapplication COMMAND [ARGS]...

Commands:
  hello
  goodbye
  help     Show this message and exit.
$ ./bin/myapplication hello --help
Usage: ./bin/myapplication hello [OPTIONS] NAME

Arguments:
  NAME  [required]

Options:
  --repeat INTEGER  [default: 1]
  --help            Show this message and exit.
$ ./bin/myapplication hello Alice
Hello Alice
$ ./bin/myapplication hello --repeat 2 Alice
Hello Alice
Hello Alice
$ ./bin/myapplication goodbye --help
Usage: ./bin/myapplication goodbye [OPTIONS] NAME

Arguments:
  NAME  [required]

Options:
  --repeat INTEGER  [default: 1]
  --yell
  --help            Show this message and exit.
$ ./bin/myapplication goodbye Alice
Goodbye Alice
$ ./bin/myapplication goodbye --repeat 2 --yell Alice
GOODBYE ALICE
GOODBYE ALICE

You may have noticed that the first help message shows a command help but not an option --help. Indeed, as it is using commands, the help is now a command too. But to simplify the access to the help the --help option is still available, although not shown in the help message.