Skip to content

Repository files navigation

dry-cli-help

Ruby Coverage

Configurable, wrapped, colored help screens for dry-cli applications.

Note

The original design, the settings and the decisions behind them are in docs/SPECIFICATION.md, written for version 0.1.0.

dry-cli prints help as it finds it: no title, no description of the program, no color, one line per description however long, and commands sorted alphabetically. This gem keeps the command structure you already declared and changes only what the user reads before a command runs. Progress bars, spinners and error panels belong in dry-cli-ui.

Before and after

my-cli compile -h with dry-cli alone:

Command:
  my-cli compile

Usage:
  my-cli compile RULES [OUTPUT]

Description:
  Compile tax rules

Arguments:
  RULES                             # REQUIRED Rule file to compile
  OUTPUT                            # Where to write the compiled rules

Options:
  --format=VALUE, -f VALUE          # Output format: (json/yaml), default: "json"
  --[no-]strict                     # Treat warnings as errors
  --help, -h                        # Print this help

With require "dry/cli/help":

USAGE
  my-cli compile RULES [OUTPUT] [OPTIONS]

DESCRIPTION
  Compile tax rules

ARGUMENTS
  RULES               Rule file to compile (required)
  OUTPUT              Where to write the compiled rules

OPTIONS
  -f, --format=VALUE  Output format (one of: json, yaml; default: "json")
  --[no-]strict       Treat warnings as errors
  -h, --help          Show help

Headings are bold and yellow, commands green, options and arguments cyan, and every description wraps to the terminal with a hanging indent.

The same comparison as screenshots, taken from examples/rbcheck:

Standard dry-cli Help Screen Require dry/cli/help
original with

Installation

gem install dry-cli-help

Or add gem "dry-cli-help" to your Gemfile.

It requires Ruby 4.0 or newer, and depends on dry-cli and pastel.

Usage

Require it after dry-cli. That alone changes every help screen in the process.

require "dry/cli"
require "dry/cli/help"

Then make every setting once, in one block, before the CLI runs. Your registry, commands and options stay plain dry-cli: the gem adds nothing to them, so you can add or remove it without touching a command.

Dry::CLI::Help.configure do
  title "MyCLI"

  description <<~TEXT
    Compile, validate, and evaluate rules.
  TEXT

  epilogue "Documentation: https://example.com/my-cli"

  color :auto
  width :terminal
  wrap true
end

module My
  module CLI
    extend Dry::CLI::Registry

    register "compile",  Compile
    register "validate", Validate
    register "evaluate", Evaluate
    register "version",  Version, aliases: ["--version", "-v"]
  end
end

my-cli -h then prints:

MyCLI

Compile, validate, and evaluate rules.

USAGE
  my-cli COMMAND [OPTIONS]

COMMANDS
  compile        Compile tax rules
  validate       Validate the rule corpus
  evaluate       Evaluate a tax return
  version        Show version

OPTIONS
  -h, --help     Show help
  -v, --version  Show version

Documentation: https://example.com/my-cli

A command reachable as --version lists under Options by its dashed names. Commands marked hidden: true in the registry stay out of every list, and a non-dashed alias prints next to its command, as in build, b.

A block that takes an argument receives the configuration instead of running against it:

Dry::CLI::Help.configure do |config|
  config.width = 100
  config.color = false
end

Every setting validates its value and raises ArgumentError on one it cannot use. Dry::CLI::Help.config returns the current settings, and Dry::CLI::Help.reset! forgets them all, which is handy between tests.

Examples

The examples folder holds three single-file tools built on dry-cli. Each one loads and configures this gem only when you pass --with-dry-cli-help, or -w for short, so you can run the same command both ways and compare:

examples/rbcheck -h                      # dry-cli's own help
examples/rbcheck -h --with-dry-cli-help  # the same help, through this gem

examples/rbcheck has two commands, version and check-ruby-syntax [DIR]. Condensed:

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

if [ARGV.delete("--with-dry-cli-help"), ARGV.delete("-w")].any?
  require "dry/cli/help"

  Dry::CLI::Help.configure do
    title "rbcheck"
    description "Small checks for Ruby projects."
    epilogue "Report bugs at https://example.com/rbcheck/issues"
  end
end

module Rbcheck
  extend Dry::CLI::Registry

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

    def call(**) = puts("rbcheck 1.0.0")
  end

  class CheckRubySyntax < Dry::CLI::Command
    desc "Check every Ruby file under a directory for syntax errors, and report "\
         "each file that fails to parse with its line number"

    argument :dir, desc: "Directory to scan", default: "."
    option :exclude, type: :array, desc: "Glob patterns to skip, such as vendor/**"
    option :quiet, type: :boolean, default: false, aliases: ["-q"],
           desc: "Print only the files that fail"

    example ["lib # check one directory", "--exclude=vendor/** # skip vendored gems"]

    def call(dir:, quiet:, exclude: [], **)
      # ... parses every file under dir with Prism ...
    end
  end

  register "version", Version, aliases: ["--version", "-v"]
  register "check-ruby-syntax", CheckRubySyntax
end

Dry::CLI.new(Rbcheck).call

Every screen below comes from running it in an 80-column terminal.

rbcheck -h

dry-cli alone prints a list of commands to stderr and exits 1, and does not wrap the long description.

Commands:
  rbcheck check-ruby-syntax [DIR]                 # Check every Ruby file under a directory for syntax errors, and report each file that fails to parse with its line number
  rbcheck version                                 # Print the version

With this gem, rbcheck -h prints the help below to stdout and exits 0.

Note

Whether a CLI run with no arguments should exit 1 or 0 is debatable. dry-cli exits 1, and so does this gem by default: plain rbcheck prints the same screen to stderr and exits 1. Set exit_code_without_arguments to 0 to treat it like -h instead.

rbcheck

Small checks for Ruby projects.

USAGE
  rbcheck COMMAND [OPTIONS]

COMMANDS
  version            Print the version
  check-ruby-syntax  Check every Ruby file under a directory for syntax errors,
                     and report each file that fails to parse with its line
                     number

OPTIONS
  -h, --help         Show help
  -v, --version      Print the version

Report bugs at https://example.com/rbcheck/issues
  • The title, description and epilogue come from the configure block.
  • Commands list in the order they were registered, not alphabetically.
  • The long description wraps to the terminal under its own column.
  • version is also reachable as --version and -v, so it lists under Options too. dry-cli never shows those aliases.

rbcheck check-ruby-syntax -h

dry-cli alone:

Command:
  rbcheck check-ruby-syntax

Usage:
  rbcheck check-ruby-syntax [DIR]

Description:
  Check every Ruby file under a directory for syntax errors, and report each file that fails to parse with its line number

Arguments:
  DIR                               # Directory to scan

Options:
  --exclude=VALUE1,VALUE2,..        # Glob patterns to skip, such as vendor/**
  --[no-]quiet, -q                  # Print only the files that fail, default: false
  --help, -h                        # Print this help

Examples:
  rbcheck check-ruby-syntax lib # check one directory
  rbcheck check-ruby-syntax --exclude=vendor/** # skip vendored gems

With this gem:

USAGE
  rbcheck check-ruby-syntax [DIR] [OPTIONS]

DESCRIPTION
  Check every Ruby file under a directory for syntax errors, and report each
  file that fails to parse with its line number

ARGUMENTS
  DIR                         Directory to scan (default: ".")

OPTIONS
  --exclude=VALUE1,VALUE2,..  Glob patterns to skip, such as vendor/**
  -q, --[no-]quiet            Print only the files that fail (default: false)
  -h, --help                  Show help

EXAMPLES
  rbcheck check-ruby-syntax lib             check one directory
  rbcheck check-ruby-syntax --exclude=vendor/**
                                            skip vendored gems
  • The Command: section repeats the usage line, so it is gone, and the usage line shows that the command takes options.
  • The argument's default of "." shows up; dry-cli leaves it out.
  • Short aliases come first (-q, --[no-]quiet), and every description lines up in one column instead of trailing a #.
  • Each example's comment moves into a column of its own. An example longer than that column puts its comment on the next line.

In a terminal the headings print bold yellow, usage lines, commands and examples green, arguments and options cyan, and example comments bold black.

examples/todo shows nested commands, custom headings and a styles block. examples/deploy shows a fixed width, a banner above command help, reordered sections and help without a command exiting 0. examples/README.md lists what each one demonstrates.

DSL-based Configuration API

The gem offers a compact DSL in the general spirit of Ruby and dry-rb in particular, and makes the following methods available within the configure block.

Setting Values Default What it does
title String none Prints the first line of the banner, above the top-level help
description String none Prints paragraphs under the title, reflowed to the wrap width
epilogue String none Prints paragraphs at the very end of the top-level help
color true, false, :auto :auto Paints headings, commands, arguments and options; :auto paints only a terminal
wrap true, false true Wraps descriptions with a hanging indent; false prints each one on a single line as written
width :terminal, Integer :terminal Sets the column text wraps at; :terminal follows the terminal's width
margin Integer 0 Keeps that many columns free at the right edge when width is :terminal
exit_code_without_arguments 0 to 255 1 Sets the exit status of my-cli or my-cli db run with no command; 0 also prints the help to stdout instead of stderr
banner_on_subcommands true, false false Prints the title and description above command help and group listings too, not only above the top-level help
command_order :registration, :alphabetical :registration Lists commands in the order you registered them, or sorted by name as dry-cli does

color :auto colors a terminal and honors NO_COLOR. width :terminal reads COLUMNS, then the console, then falls back to 80, and margin keeps columns free at the right edge.

Running the program, or a group such as my-cli db, with no command prints its help to stderr and exits 1, as dry-cli does. With

exit_code_without_arguments 0

it prints to stdout and exits 0 instead. -h and --help always print to stdout and exit 0. A mistyped command prints dry-cli's suggestion, then the help, to stderr and exits 1.

The banner (title and description) and the epilogue print on the top-level help. A CLI built from a single command, Dry::CLI.new(Deploy), counts as top level, so its command help gets both.

Headings, sections and groups

Dry::CLI::Help.configure do
  heading :commands, "Available commands"

  group "Rules", "compile", "validate"
  group "Returns", "evaluate"

  hide :examples
  sections :banner, :usage, :commands, :options, :epilogue
end
  • heading replaces one section's heading text. It takes :usage, :description, :commands, :subcommands, :arguments, :options or :examples; banner and epilogue have no heading. How headings are cased belongs to their style, below.
  • group lists commands under a heading of their own, in the order given. Ungrouped commands stay under Commands, and groups print below them in the order declared. A group with no commands raises ArgumentError; a group whose commands a screen does not list prints no heading there. A nested command is named by its full path, such as "db migrate", and its group shows on my-cli db -h.
  • sections sets the order; a section left out is hidden. hide hides sections without restating the order.

The sections are banner, usage, description, commands, subcommands, arguments, options, examples and epilogue. Each screen prints the ones that apply to it:

Screen Sections it can print
A listing: my-cli, my-cli -h, my-cli db banner, usage, commands, options, epilogue
A command: my-cli compile -h banner, usage, description, subcommands, arguments, options, examples, epilogue

A few details of those screens:

  • A command that also has subcommands gets a second usage line, my-cli db COMMAND [OPTIONS], and a Subcommands section.
  • A group registered without a command of its own describes itself by what it holds, as in Subcommands: add, remove.
  • An array argument prints as FILES....
  • Every definition list on a screen shares one description column, never wider than half the wrap width. A term longer than that column puts its description on the next line.
  • Text never wraps narrower than 20 columns, however small the terminal.

Styles

Every element's look is declared together, in one styles block inside configure:

Dry::CLI::Help.configure do
  styles do
    title           :bold
    heading         :bold, :yellow, case: :UPPERCASE
    usage           :green
    command         :green
    argument        :cyan
    option          :cyan
    example         :green
    example_comment :bold, :black
  end
end

Those are the defaults. Name only the elements you want to change; a line with no styles, such as example_comment, prints that element plain.

Element Applies to
title The banner title
heading Every section and group heading
usage Every usage line, whole
command Command names in a list
argument Argument names
option Option names
example The command line of an example
example_comment The part of an example after the first # surrounded by spaces, as in "lib # check one directory"

heading alone takes case:, one of :UPPERCASE, :Capitalize, :lowercase or :as_is, each written the way it cases. :Capitalize raises only the first letter. heading case: :as_is with no styles changes the case and keeps the colors.

The Colors module

Every style is also available to your own code:

class Deploy < Dry::CLI::Command
  include Dry::CLI::Help::Colors

  def call(**)
    puts green("Deployed.")
    puts red.bold("Rolled back.")
  end
end

The methods are the eight colors black red green yellow blue magenta cyan white, their bright_ forms, the on_ and on_bright_ backgrounds, and clear bold dim italic underline inverse hidden strikethrough. Dry::CLI::Help::Colors.enabled takes true, false or :auto (the default, which colors only a terminal and honors NO_COLOR). Help screens ignore that switch and follow the color setting.

How it works

The gem prepends one module to Dry::CLI, overriding the two private methods dry-cli prints help from. It does not replace Dry::CLI::Banner or Dry::CLI::Usage.

---
config:
  layout: elk
  theme: forest
---
flowchart TB
    argv["ARGV"] --> cli_call["Dry::CLI#call"]

    cli_call -->|"command found, --help given"| help_method["#help"]
    cli_call -->|"no command, a group, -h at a level, a typo"| spell_checker["#spell_checker"]

    help_method --> command_screen["Screens::Command"]
    spell_checker --> listing_screen["Screens::Listing"]

    command_screen --> formatter["Formatter"]
    listing_screen --> formatter

    help_config["Help.configure"] --> formatter

    formatter --> output["stdout or stderr"]
Loading

Both methods are @api private in dry-cli. spec/dry/cli/help/dry_cli_contract_spec.rb asserts every internal the gem reads, so a dry-cli release that moves one fails this suite, naming what moved.

Development

just install        # bundle install
just test           # the suite; a full run enforces 100% line and branch coverage
just test-coverage  # measure coverage even for a partial run
just lint           # rubocop
just ci             # rubocop, then the suite with coverage
just lefthook       # every pre-commit hook against every file
just format         # mdformat --wrap no on every Markdown file, then rubocop -a
just doc            # YARD documentation
just build          # build the .gem into pkg/
bin/console         # IRB with the gem loaded

Contributing

Bug reports and pull requests are welcome at https://github.com/kigster/dry-cli-help.

Warning

The dry- prefix and the Dry::CLI::Help namespace do not imply endorsement by dry-rb.

This is an independent and opinionated gem that extends theirs.

Note to Dry-Rb Maintainers

First — hats off to all of you who tirelessly built out one of the most valuable collections of libraries in the Ruby ecosystem.

While I admire and would be willing to contribute any or all of the extension gem's code to the original gem, I feel that creating plugins and extensions allows the author to fully express their needs and wants, and then, if the authors of dry-cli become interested in any of them, I would be honored to submit a PR to dry-cli itself.

This method offered a very open road to extensibility and experimentation. If the code quality or design is not up to the level required for direct contributions to dry-rb, then let it be known that:

  1. We would be very happy to receive any feedback and improve, refactor, and update the gem assuming it improves it
  2. Roll any part of the codebase as a PR to the dry-cli core.
  3. We hold the authors of dry-rb in high regard, and generally would love to collaborate, as long as the feedback loop/cycle is not so long that the context of the changes gets lost in time, as with so many contributions made to other gems in the past.

License

MIT. See LICENSE.txt.

About

An extension to dry-cli ruby gem that supports a global banner, color, and wrapping long descriptions

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages