Skip to content

(3 of 4) Render help through a configurable pipeline (if accepted, should be stacked on #173) - #174

Open
kigster wants to merge 4 commits into
dry-rb:mainfrom
kigster:kig/help-rendering-pipeline
Open

kigster wants to merge 4 commits into
dry-rb:mainfrom
kigster:kig/help-rendering-pipeline

Conversation

@kigster

@kigster kigster commented Oct 8, 2026 •

Copy link
Copy Markdown

Proposal 3 of 4 in the dry-cli plugin-hooks series. Should have been stacked on top of #173, but GitHub won't let me choose as the base branch that lives on a fork. Upstream issue/forum thread: pending.

Summary

Help and listings become a Dry::CLI::Screen that passes through a configurable renderer and filters, replacing the need to override private #help / #spell_checker.

 parse → help(names, prog_name, long:)
-  stdout.puts Banner.call(command, prog_name, long:)
-  exit(0)
+  show(kind: :command, reason: :help, node: tree.dig(*names), status: 0, …)
 spell_checker(result, arguments)
-  stderr.puts suggestion; stderr.puts Usage.call(result); exit(1)
+  show(kind: :listing, reason: :no_command | :help | :unknown, suggestion:, status: 1, …)
 namespace without subcommand
-  stderr.puts Usage.call(result)
+  show(kind: :listing, reason: :no_command, status: nil, …)   # prints, no exit, as before

+show(**fields)
+  screen = config.help.renderer.call(Screen.new(**fields))   # fills screen.text
+  screen = config.help.filters.reduce(screen) { |s, f| f.call(s) }
+  screen.io.puts(screen.text)                                 # stdout if status 0, else stderr
+  halt(screen.status) if screen.status
Dry::CLI.configure do |config|
  config.help.renderer = MyGem::Renderer
  config.help.filters << (rewrap >> colorize)                          # Proc#>> / dry-transformer
  config.help.filters << ->(s) { s.reason == :help ? s.with(status: 0) : s }
end
Dry.CLI(MyApp::Commands, config: my_config)                           # per CLI
  • Screen is a frozen Data: kind, reason, node (a Tree::Node), prog_name, long, arguments, suggestion, text, status, stdout, stderr.
  • Config is shaped like dry-configurable but has no dependency; dry-cli stays dependency-free.
  • Parse errors (ERROR: … was called with …) stay outside the pipeline.

Evidence

  • Before: dry-cli-help prepends Dry::CLI and overrides #help(command, prog_name), which already broke when long: was added.
    After: the existing 661 examples pass unchanged (default output is byte-for-byte the same). spec/unit/dry/cli/help_pipeline_spec.rb (20) covers every screen reason, filter order, >>, status/stream changes, a namespace listing that doesn't exit, and per-CLI vs process-wide config. Full suite 681 examples, 0 failures; rubocop clean.

Merge Danger

Door: two-way

Blast Radius: small

Anyone overriding the private #help(command, prog_name, long:) sees its first parameter change from the command to the names leading to it. That was private API.

Dry::CLI#call and Command.new take `kernel:`, defaulting to Kernel. Help,
errors and typos stop the CLI by raising a private Halt, which #call turns
into kernel.exit, so a kernel whose exit returns (as Aruba's does) still
stops the CLI. A command's own `exit` goes through its kernel too.

Dry::CLI::Launcher[registry] runs a CLI with the arguments, streams and
kernel in the order Aruba's in-process launcher passes them, so a CLI can
be tested in the same process with StringIO streams.
Registry#tree, Dry::CLI#tree and Dry::CLI::Tree.for return a live view of
the registered commands, for gems that describe a CLI rather than run it:
help screens, shell completion, documentation generators. Until now they
could only walk CommandRegistry internals.

Tree::Node#resolve shares its lookup with dispatch (CommandRegistry.lookup),
so the command it finds is the one the CLI would run. Tree::Param is a
frozen snapshot of an option or argument, including every declared key.
Help and command listings are built as a Dry::CLI::Screen, passed through
a renderer and a list of filters, then printed to the stream its status
chooses. Renderer and filters are any #call(screen) -> screen, so they
compose with >> (procs, dry-transformer functions).

Dry::CLI.configure sets them process-wide; Dry.CLI(..., config:) per CLI.
The default renderer wraps Banner and Usage, and the existing suite passes
unchanged, so unconfigured output is byte-for-byte what it was.

This replaces overriding the private #help and #spell_checker, which is
what dry-cli-help has to do today.
@kigster kigster changed the title Render help through a configurable pipeline (3 of 4), needs to stack on kigster:kig/command-tree-introspection Render help through a configurable pipeline (3 of 4), needs to stack on kigster:kig/command-tree-introspection Oct 8, 2026
@kigster kigster changed the title Render help through a configurable pipeline (3 of 4), needs to stack on kigster:kig/command-tree-introspection (3 of 4) Render help through a configurable pipeline, needs to stack on kigster:kig/command-tree-introspection Oct 8, 2026
@kigster kigster changed the title (3 of 4) Render help through a configurable pipeline, needs to stack on kigster:kig/command-tree-introspection (3 of 4) Render help through a configurable pipeline (if accepted, should be stacked on #172) Oct 8, 2026
@kigster kigster changed the title (3 of 4) Render help through a configurable pipeline (if accepted, should be stacked on #172) (3 of 4) Render help through a configurable pipeline (if accepted, should be stacked on #173) Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant