Introduction

dry-cli is a general-purpose framework for developing Command Line Interface (CLI) applications. It represents commands as objects that can be registered and offers support for arguments, options and forwarding variadic arguments to a sub-command.

Usage

The following is an elaborate example showcasing most of the available features.

Imagine you want to build a CLI executable foo for your Ruby project. The entire program is defined as below:

#!/usr/bin/env ruby
require "bundler/setup"
require "dry/cli"

module Foo
  module CLI
    module Commands
      extend Dry::CLI::Registry

      class Version < Dry::CLI::Command
        desc "Print version"

        def call(*)
          puts "1.0.0"
        end
      end

      class Echo < Dry::CLI::Command
        desc "Print input"

        argument :input, desc: "Input to print"

        example [
          "             # Prints 'wuh?'",
          "hello, folks # Prints 'hello, folks'"
        ]

        def call(input: nil, **)
          if input.nil?
            puts "wuh?"
          else
            puts input
          end
        end
      end

      class Start < Dry::CLI::Command
        desc "Start Foo machinery"

        argument :root, required: true, desc: "Root directory"

        example [
          "path/to/root # Start Foo at root directory"
        ]

        def call(root:, **)
          puts "started - root: #{root}"
        end
      end

      class Stop < Dry::CLI::Command
        desc "Stop Foo machinery"

        option :graceful, type: :boolean, default: true, desc: "Graceful stop"

        def call(**options)
          puts "stopped - graceful: #{options.fetch(:graceful)}"
        end
      end

      class Exec < Dry::CLI::Command
        desc "Execute a task"

        argument :task, type: :string, required: true,  desc: "Task to be executed"
        argument :dirs, type: :array,  required: false, desc: "Optional directories"

        def call(task:, dirs: [], **)
          puts "exec - task: #{task}, dirs: #{dirs.inspect}"
        end
      end

      module Generate
        class Configuration < Dry::CLI::Command
          desc "Generate configuration"

          option :apps, type: :array, default: [], desc: "Generate configuration for specific apps"

          def call(apps:, **)
            puts "generated configuration for apps: #{apps.inspect}"
          end
        end

        class Test < Dry::CLI::Command
          desc "Generate tests"

          option :framework, default: "minitest", values: %w[minitest rspec]

          def call(framework:, **)
            puts "generated tests - framework: #{framework}"
          end
        end
      end

      register "version", Version, aliases: ["v", "-v", "--version"]
      register "echo",    Echo
      register "start",   Start
      register "stop",    Stop
      register "exec",    Exec

      register "generate", aliases: ["g"] do |prefix|
        prefix.register "config", Generate::Configuration
        prefix.register "test",   Generate::Test
      end
    end
  end
end

Dry::CLI.new(Foo::CLI::Commands).call

Available commands

With this code in place, we can now have a look at the command line usage by issuing the command foo without any arguments or options:

$ foo
Commands:
  foo echo [INPUT]                       # Print input
  foo exec TASK [DIRS]                   # Execute a task
  foo generate [SUBCOMMAND]
  foo start ROOT                         # Start Foo machinery
  foo stop                               # Stop Foo machinery
  foo version                            # Print version

Help

It is also possible to get help for a particular command using the --help flag:

$ foo echo --help
Command:
  foo echo

Usage:
  foo echo [INPUT]

Description:
  Print input

Arguments:
  INPUT                 # Input to print

Options:
  --help, -h                        # Print this help

Examples:
  foo echo              # Prints 'wuh?'
  foo echo hello, folks # Prints 'hello, folks'

Optional arguments

A command can have optional arguments, which enables a default action in case nothing is provided:

$ foo echo
wuh?

$ foo echo hello
hello

Required arguments

On the other hand, required arguments will throw an error if not provided:

$ foo start .
started - root: .
$ foo start
ERROR: "foo start" was called with no arguments
Usage: "foo start ROOT"

Array arguments

Captures all the remaining arguments in a single array. Please note that array argument must be used as the last argument, as it works as a "catch-all".

$ foo exec test
exec - task: test, dirs: []
$ foo exec test spec/bookshelf/entities spec/bookshelf/repositories
exec - task: test, dirs: ["spec/bookshelf/entities", "spec/bookshelf/repositories"]

Options

An option is a named argument that is passed after the command name and the arguments:

$ foo generate test
generated tests - framework: minitest
$ foo generate test --framework=rspec
generated tests - framework: rspec
$ foo generate test --framework=unknown
Error: "test" was called with arguments "--framework=unknown"

Boolean options

Boolean options are flags that change the behaviour of a command:

$ foo stop
stopped - graceful: true
$ foo stop --no-graceful
stopped - graceful: false

Array options

Array options are similar to arguments but must be named:

$ foo generate config --apps=web,api
generated configuration for apps: ["web", "api"]

Subcommands

Subcommands are simply commands that have been registered under a nested path:

$ foo generate
Commands:
  foo generate config           # Generate configuration
  foo generate test             # Generate tests

Aliases

Aliases are supported:

$ foo version
1.0.0
$ foo v
1.0.0
$ foo -v
1.0.0
$ foo --version
1.0.0

Subcommand aliases

Work similarly to command aliases

$ foo g config
generated configuration for apps: []

octocatEdit on GitHub