dry-cli-help 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf20fa0aacf131ed2906f0db379bc73dff9e6f73d106e9748f9db3efb589892c
4
- data.tar.gz: 2407d232a6fc663f175431e8c907b1352e6ec13cda833a64530ea1f53e58b58c
3
+ metadata.gz: c804fb8b2d9f9e370e85b45d2e4174c27e0c06aea1139d4af5055c31fb8709f7
4
+ data.tar.gz: ea846d4e056da46f9762e77b9df2a65c145df4003e58b41707446de94341b74c
5
5
  SHA512:
6
- metadata.gz: aaf06bce6593191d6fe7ef465df68afd6679877e1b412f8c8a654c04420b24229268c8a82a4ac9766e3d063a4b34c790636ea2aa15de77327bb291ae131755d8
7
- data.tar.gz: 9c790e6b9b235577b2f92ffacf0f6648c4164ceaa00192f343e1dcb2aeae93326368f9bdace5c90763acc14fcc70411010306141881d778bcf8201142c1e7ffc
6
+ metadata.gz: e95c0a85b03b6a4837726c65419a6caa138f7041bcff9e184585a72bf29d49c6b7542e74bcc3f2941537f36cc06765d17cbd5be953a4ec68a0fd88d589273b9e
7
+ data.tar.gz: 314634677bcdcef0c69b366e01b9356e6abff0956db0896ce5e8d2fb6bf5b87cc6ed4c40b20f5935c74c46bd73bff7a53be1787249f2ebbd80661f25684a09aa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - Unreleased
4
+
5
+ - `--help-include-hidden` prints help that also lists hidden commands and options, each marked `(hidden)`. An option is hidden when declared with `hidden: true`, which dry-cli keeps and its parser ignores; plain help leaves it out. The flag works before or after a command's name and never appears in help itself.
6
+ - Works with the kigster fork of dry-cli as well as dry-cli 1.4: the help integration reads the CLI's streams as `stdout`/`stderr` where those exist and `out`/`err` otherwise, and `help` accepts the fork's `long:` keyword. With `long: true` a command's help prints its `long_desc` when it declares one. The spec helper passes whichever stream keywords `Dry::CLI#call` takes.
7
+ - New setting `command_arguments`, off by default. When set, a listing prints each command with its arguments, required ones bare and optional ones in brackets: `completion SHELL`, `migrate [FILE]`. It applies to the commands of a registry level and to the subcommands on a command's own help.
8
+
3
9
  ## [0.5.1] - 2026-09-21
4
10
 
5
11
  - An empty example, `example [""]`, no longer raises `NoMethodError` and takes the whole help screen with it. It prints the program name on its own, which is what a command that runs with no arguments means by it.
data/README.md CHANGED
@@ -132,7 +132,49 @@ OPTIONS
132
132
  Documentation: https://example.com/my-cli
133
133
  ```
134
134
 
135
- 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`.
135
+ A command reachable as `--version` lists under Options by its dashed names. Commands marked `hidden: true` in the registry stay out of every list unless `--help-include-hidden` asks for them, and a non-dashed alias prints next to its command, as in `build, b`.
136
+
137
+ ### Hidden commands and options
138
+
139
+ dry-cli hides a command registered with `hidden: true`. This gem adds the same for options: declare one with `hidden: true` and plain help leaves it out. dry-cli's parser ignores the key, so the option still works.
140
+
141
+ ```ruby
142
+ class Deploy < Dry::CLI::Command
143
+ desc "Deploy the application"
144
+ option :force, type: :boolean, aliases: ["-f"], desc: "Skip confirmation"
145
+ option :trace, type: :boolean, default: false, hidden: true, desc: "Print every step"
146
+ end
147
+
148
+ register "deploy", Deploy
149
+ register "console", Console, hidden: true
150
+ ```
151
+
152
+ `--help-include-hidden` prints help with every hidden command and option listed and marked. It works anywhere `-h` does, before or after the command's name, and no help screen lists it.
153
+
154
+ ```text
155
+ $ my-cli --help-include-hidden
156
+ USAGE
157
+ my-cli COMMAND [OPTIONS]
158
+
159
+ COMMANDS
160
+ deploy Deploy the application
161
+ console Open a console on a server (hidden)
162
+
163
+ OPTIONS
164
+ -h, --help Show help
165
+
166
+ $ my-cli deploy --help-include-hidden
167
+ USAGE
168
+ my-cli deploy [OPTIONS]
169
+
170
+ DESCRIPTION
171
+ Deploy the application
172
+
173
+ OPTIONS
174
+ -f, --[no-]force Skip confirmation
175
+ --[no-]trace Print every step (hidden; default: false)
176
+ -h, --help Show help
177
+ ```
136
178
 
137
179
  A block that takes an argument receives the configuration instead of running against it:
138
180
 
@@ -309,18 +351,19 @@ In a terminal the headings print bold yellow, usage lines, commands and examples
309
351
 
310
352
  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.
311
353
 
312
- | Setting | Values | Default | What it does |
313
- | ----------------------------- | -------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------- |
314
- | `title` | String | none | Prints the first line of the banner, above the top-level help |
315
- | `description` | String | none | Prints paragraphs under the title, reflowed to the wrap width |
316
- | `epilogue` | String | none | Prints paragraphs at the very end of the top-level help |
317
- | `color` | `true`, `false`, `:auto` | `:auto` | Paints headings, commands, arguments and options; `:auto` paints only a terminal |
318
- | `wrap` | `true`, `false` | `true` | Wraps descriptions with a hanging indent; `false` prints each one on a single line as written |
319
- | `width` | `:terminal`, Integer | `:terminal` | Sets the column text wraps at; `:terminal` follows the terminal's width |
320
- | `margin` | Integer | `0` | Keeps that many columns free at the right edge when `width` is `:terminal` |
321
- | `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 |
322
- | `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 |
323
- | `command_order` | `:registration`, `:alphabetical` | `:registration` | Lists commands in the order you registered them, or sorted by name as dry-cli does |
354
+ | Setting | Values | Default | What it does |
355
+ | ----------------------------- | -------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
356
+ | `title` | String | none | Prints the first line of the banner, above the top-level help |
357
+ | `description` | String | none | Prints paragraphs under the title, reflowed to the wrap width |
358
+ | `epilogue` | String | none | Prints paragraphs at the very end of the top-level help |
359
+ | `color` | `true`, `false`, `:auto` | `:auto` | Paints headings, commands, arguments and options; `:auto` paints only a terminal |
360
+ | `wrap` | `true`, `false` | `true` | Wraps descriptions with a hanging indent; `false` prints each one on a single line as written |
361
+ | `width` | `:terminal`, Integer | `:terminal` | Sets the column text wraps at; `:terminal` follows the terminal's width |
362
+ | `margin` | Integer | `0` | Keeps that many columns free at the right edge when `width` is `:terminal` |
363
+ | `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 |
364
+ | `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 |
365
+ | `command_arguments` | `true`, `false` | `false` | Lists each command with its arguments, as in `deploy ENVIRONMENT` and `migrate [FILE]`: required ones bare, optional ones in brackets |
366
+ | `command_order` | `:registration`, `:alphabetical` | `:registration` | Lists commands in the order you registered them, or sorted by name as dry-cli does |
324
367
 
325
368
  `color :auto` colors a terminal and honors [`NO_COLOR`](https://no-color.org). `width :terminal` reads `COLUMNS`, then the console, then falls back to 80, and `margin` keeps columns free at the right edge.
326
369
 
@@ -67,6 +67,7 @@ module Dry
67
67
  margin: ->(value) { value.is_a?(Integer) && !value.negative? },
68
68
  exit_code_without_arguments: ->(value) { value.is_a?(Integer) && value.between?(0, 255) },
69
69
  banner_on_subcommands: BOOLEAN,
70
+ command_arguments: BOOLEAN,
70
71
  command_order: ->(value) { COMMAND_ORDERS.include?(value) }
71
72
  }.freeze
72
73
 
@@ -80,6 +81,7 @@ module Dry
80
81
  margin: 0,
81
82
  exit_code_without_arguments: 1,
82
83
  banner_on_subcommands: false,
84
+ command_arguments: false,
83
85
  command_order: :registration
84
86
  }.freeze
85
87
 
@@ -6,23 +6,58 @@ module Dry
6
6
  # The only code that touches dry-cli. Everything else renders.
7
7
  module Integration
8
8
  # Prepended to Dry::CLI. Overrides the two private methods dry-cli
9
- # prints help from, and nothing else.
9
+ # prints help from, and the two it dispatches through, and nothing else.
10
10
  #
11
- # Both are `@api private` in dry-cli, which is why the suite asserts they
12
- # exist: a dry-cli release that renames them fails this gem's specs
11
+ # All four are `@api private` in dry-cli, which is why the suite asserts
12
+ # they exist: a dry-cli release that renames them fails this gem's specs
13
13
  # rather than a host's help screen.
14
14
  module CLIMethods
15
15
  HELP_FLAGS = %w[-h --help].freeze
16
16
 
17
+ # Asks for help that also lists hidden commands and options. No help
18
+ # screen lists the flag itself.
19
+ INCLUDE_HIDDEN_FLAG = "--help-include-hidden"
20
+
17
21
  private
18
22
 
19
- # dry-cli calls this for `mycli deploy -h`.
20
- def help(command, prog_name)
23
+ # dry-cli's `call` hands the arguments to one of these two. They are
24
+ # hooked rather than `call`, whose keywords differ between dry-cli 1.4
25
+ # and the kigster fork.
26
+ def perform_command(arguments)
27
+ super(without_include_hidden_flag(arguments))
28
+ end
29
+
30
+ def perform_registry(arguments)
31
+ super(without_include_hidden_flag(arguments))
32
+ end
33
+
34
+ # dry-cli would reject the flag as an unknown option, so it is taken
35
+ # out of the arguments before dry-cli sees them. A `--help` goes last
36
+ # in its place, unless one is there already, so the flag works before
37
+ # the command's name as well as after it.
38
+ #
39
+ # Everything after `--` belongs to the command, flags included.
40
+ def without_include_hidden_flag(arguments)
41
+ @help_include_hidden = false
42
+ options = arguments.take_while { it != "--" }
43
+ index = options.index(INCLUDE_HIDDEN_FLAG)
44
+ return arguments unless index
45
+
46
+ @help_include_hidden = true
47
+ rest = arguments.dup
48
+ rest.delete_at(index)
49
+ rest.insert(options.length - 1, "--help") unless options.intersect?(HELP_FLAGS)
50
+ rest
51
+ end
52
+
53
+ # dry-cli calls this for `mycli deploy -h`, and the kigster fork also
54
+ # for `--help`, with `long: true` so the long description is shown.
55
+ def help(command, prog_name, long: false)
21
56
  screen = Screens::Command.new(
22
- command:, prog_name:, top_level: !kommand.nil?,
23
- config: Help.config, io: out
57
+ command:, prog_name:, long:, top_level: !kommand.nil?,
58
+ config: Help.config, io: help_out, include_hidden: help_include_hidden?
24
59
  )
25
- out.puts screen.render
60
+ help_out.puts screen.render
26
61
  exit(0)
27
62
  end
28
63
 
@@ -35,18 +70,42 @@ module Dry
35
70
 
36
71
  if unmatched.empty?
37
72
  status = config.exit_code_without_arguments
38
- list(result, config, status.zero? ? out : err, status)
73
+ list(result, config, status.zero? ? help_out : help_err, status)
39
74
  elsif HELP_FLAGS.include?(unmatched.first)
40
- list(result, config, out, 0)
75
+ list(result, config, help_out, 0)
41
76
  else
42
77
  suggestion = SpellChecker.call(result, arguments)
43
- err.puts "#{suggestion}\n\n" if suggestion
44
- list(result, config, err, 1)
78
+ help_err.puts "#{suggestion}\n\n" if suggestion
79
+ list(result, config, help_err, 1)
45
80
  end
46
81
  end
47
82
 
83
+ # dry-cli 1.4 holds its streams as `out` and `err`; the kigster fork
84
+ # renamed them `stdout` and `stderr`. Both are supported.
85
+ def help_out
86
+ plain(respond_to?(:stdout, true) ? stdout : out)
87
+ end
88
+
89
+ def help_err
90
+ plain(respond_to?(:stderr, true) ? stderr : err)
91
+ end
92
+
93
+ # The fork wraps each stream in a `Dry::CLI::Stream` that strips ANSI
94
+ # from anything written when it decides the stream has no color. The
95
+ # help screen has already made that decision for the same stream, so
96
+ # it writes to the IO underneath. The test is the class, not
97
+ # `respond_to?(:raw)`: `io/console` gives every IO a `raw` of its own,
98
+ # which puts a terminal into raw mode and raises ENOTTY on a pipe.
99
+ def plain(io)
100
+ defined?(Dry::CLI::Stream) && io.is_a?(Dry::CLI::Stream) ? io.raw : io
101
+ end
102
+
103
+ def help_include_hidden?
104
+ @help_include_hidden
105
+ end
106
+
48
107
  def list(result, config, io, status)
49
- io.puts Screens::Listing.new(result:, config:, io:).render
108
+ io.puts Screens::Listing.new(result:, config:, io:, include_hidden: help_include_hidden?).render
50
109
  exit(status)
51
110
  end
52
111
  end
@@ -17,8 +17,11 @@ module Dry
17
17
  # @param config [Configuration]
18
18
  # @param io [IO] where the screen prints
19
19
  # @param terminal_width [Integer]
20
- def initialize(config:, io:, terminal_width: Terminal.width)
20
+ # @param include_hidden [Boolean] list hidden commands and options too,
21
+ # as `--help-include-hidden` asks
22
+ def initialize(config:, io:, terminal_width: Terminal.width, include_hidden: false)
21
23
  @config = config
24
+ @include_hidden = include_hidden
22
25
  @format = Formatter.new(config, io, terminal_width:)
23
26
  end
24
27
 
@@ -66,15 +69,48 @@ module Dry
66
69
  format.definitions(rows, column)
67
70
  end
68
71
 
69
- # The nodes of one registry level in the configured order, hidden ones left out.
72
+ def include_hidden?
73
+ @include_hidden
74
+ end
75
+
76
+ # The nodes of one registry level in the configured order, hidden
77
+ # ones left out unless asked for.
70
78
  def visible(children)
71
- shown = children.to_a.reject { |_, node| node.hidden }
79
+ shown = children.to_a
80
+ shown = shown.reject { |_, node| node.hidden } unless include_hidden?
72
81
  config.command_order == :alphabetical ? shown.sort_by(&:first) : shown
73
82
  end
74
83
 
84
+ # A command's name as a listing prints it: followed by its arguments
85
+ # when `command_arguments` is set, "deploy ENVIRONMENT".
86
+ def command_term(name, node)
87
+ return name unless config.command_arguments && node.command
88
+
89
+ "#{name}#{usage_arguments(node.command)}"
90
+ end
91
+
92
+ # Required arguments bare, optional ones in brackets: " RULES [OUTPUT]".
93
+ def usage_arguments(command)
94
+ required = command.required_arguments.map { argument_name(it) }
95
+ optional = command.optional_arguments.map { "[#{argument_name(it)}]" }
96
+ names = [*required, *optional]
97
+ " #{names.join(' ')}" unless names.empty?
98
+ end
99
+
100
+ def argument_name(argument)
101
+ name = argument.name.to_s.upcase
102
+ argument.array? ? "#{name}..." : name
103
+ end
104
+
105
+ # A hidden node, listed only when asked for, says so after its description.
106
+ def describe_node(node)
107
+ text = node_description(node)
108
+ node.hidden ? [text, "(hidden)"].compact.join(" ") : text
109
+ end
110
+
75
111
  # A group registered without a command has no description of its own,
76
112
  # so it describes itself by what it contains.
77
- def describe_node(node)
113
+ def node_description(node)
78
114
  return node.command.description if node.command
79
115
 
80
116
  names = visible(node.children).map(&:first)
@@ -12,11 +12,13 @@ module Dry
12
12
  # @param prog_name [String] the program and command path, "mycli db migrate"
13
13
  # @param top_level [Boolean] true when the command is the whole CLI,
14
14
  # as with `Dry::CLI.new(SomeCommand)`
15
- def initialize(command:, prog_name:, top_level: false, **)
15
+ # @param long [Boolean] show the command's `long_desc` when it has one
16
+ def initialize(command:, prog_name:, top_level: false, long: false, **)
16
17
  super(**)
17
18
  @command = command
18
19
  @prog_name = prog_name
19
20
  @top_level = top_level
21
+ @long = long
20
22
  end
21
23
 
22
24
  private
@@ -32,13 +34,20 @@ module Dry
32
34
  end
33
35
 
34
36
  def render_usage
35
- lines = ["#{prog_name}#{usage_arguments} [OPTIONS]"]
37
+ lines = ["#{prog_name}#{usage_arguments(command)} [OPTIONS]"]
36
38
  lines << "#{prog_name} COMMAND [OPTIONS]" if subcommand_rows.any?
37
39
  section(:usage, lines.map { INDENT + format.paint(it, :usage) })
38
40
  end
39
41
 
40
42
  def render_description
41
- section(:description, format.paragraph(command.description, indent: INDENT))
43
+ section(:description, format.paragraph(description, indent: INDENT))
44
+ end
45
+
46
+ # The long description for `--help` where the command declares one
47
+ # (`long_desc`, kigster/dry-cli), otherwise the one-line `desc`.
48
+ def description
49
+ long = command.long_description if @long && command.respond_to?(:long_description)
50
+ long || command.description
42
51
  end
43
52
 
44
53
  def render_subcommands
@@ -60,8 +69,7 @@ module Dry
60
69
  # all", and String#split answers [] for it, so `line` is nil and only
61
70
  # the program name is left to print.
62
71
  def render_examples
63
- rows = command.examples.map do |example|
64
- line, comment = example.split(" # ", 2)
72
+ rows = example_pairs.map do |line, comment|
65
73
  term = [prog_name, line&.strip].reject { it.nil? || it.empty? }.join(" ")
66
74
  Row.new(term: term, text: comment&.strip,
67
75
  term_style: :example, text_style: :example_comment)
@@ -69,20 +77,31 @@ module Dry
69
77
  section(:examples, format.definitions(rows, format.column_for(rows)))
70
78
  end
71
79
 
72
- def aligned_rows
73
- subcommand_rows + argument_rows + option_rows
80
+ # Every example as `[args, comment]`. dry-cli 1.4 keeps one string per
81
+ # example, "args # comment". The kigster fork keeps `[args, description]`
82
+ # pairs, and a list handed to it in the 1.4 spelling arrives as one pair
83
+ # holding the list and an empty description.
84
+ def example_pairs
85
+ command.examples.flat_map { pairs_of(it) }
74
86
  end
75
87
 
76
- def usage_arguments
77
- required = command.required_arguments.map { argument_name(it) }
78
- optional = command.optional_arguments.map { "[#{argument_name(it)}]" }
79
- names = [*required, *optional]
80
- " #{names.join(' ')}" unless names.empty?
88
+ def pairs_of(example)
89
+ return [example.split(" # ", 2)] unless example.is_a?(Array)
90
+
91
+ text, description = example
92
+ return text.flat_map { pairs_of(it) } if text.is_a?(Array)
93
+ return pairs_of(text) if description.nil? || description.empty?
94
+
95
+ [[text, description]]
96
+ end
97
+
98
+ def aligned_rows
99
+ subcommand_rows + argument_rows + option_rows
81
100
  end
82
101
 
83
102
  def subcommand_rows
84
103
  @subcommand_rows ||= visible(command.subcommands).map do |name, node|
85
- Row.new(term: name, text: describe_node(node))
104
+ Row.new(term: command_term(name, node), text: describe_node(node))
86
105
  end
87
106
  end
88
107
 
@@ -92,15 +111,14 @@ module Dry
92
111
  end
93
112
  end
94
113
 
114
+ # An option declared `hidden: true` lists only when hidden ones are asked for.
95
115
  def option_rows
96
- @option_rows ||= command.options.map do |option|
97
- Row.new(term: option_term(option), text: describe(option), term_style: :option)
98
- end
99
- end
116
+ @option_rows ||= command.options.filter_map do |option|
117
+ hidden = option.options.fetch(:hidden, false)
118
+ next if hidden && !include_hidden?
100
119
 
101
- def argument_name(argument)
102
- name = argument.name.to_s.upcase
103
- argument.array? ? "#{name}..." : name
120
+ Row.new(term: option_term(option), text: describe(option, hidden:), term_style: :option)
121
+ end
104
122
  end
105
123
 
106
124
  # Short aliases, then the option, then long aliases: "-f, --[no-]force".
@@ -117,8 +135,9 @@ module Dry
117
135
  end
118
136
 
119
137
  # The description, then what a reader needs to use the value.
120
- def describe(param)
138
+ def describe(param, hidden: false)
121
139
  notes = []
140
+ notes << "hidden" if hidden
122
141
  notes << "required" if param.required?
123
142
  notes << "one of: #{param.values.join(', ')}" if param.values
124
143
  notes << "default: #{param.default.inspect}" unless param.default.nil?
@@ -66,7 +66,7 @@ module Dry
66
66
  @command_rows ||= entries.each_with_object({}) do |(name, node, aliases), rows|
67
67
  next if name.start_with?("-")
68
68
 
69
- term = [name, *aliases.reject { it.start_with?("-") }].join(", ")
69
+ term = [command_term(name, node), *aliases.reject { it.start_with?("-") }].join(", ")
70
70
  rows[[*result.names, name].join(" ")] = Row.new(term:, text: describe_node(node))
71
71
  end
72
72
  end
@@ -9,7 +9,7 @@ module Dry
9
9
  # inherits from Object, so an empty reopening is compatible either way.
10
10
  class CLI
11
11
  module Help
12
- VERSION = "0.5.1"
12
+ VERSION = "0.6.0"
13
13
  end
14
14
  end
15
15
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-cli-help
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul
@@ -86,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0'
88
88
  requirements: []
89
- rubygems_version: 4.0.20
89
+ rubygems_version: 4.0.22
90
90
  specification_version: 4
91
91
  summary: Configurable, wrapped, colored help screens for dry-cli applications
92
92
  test_files: []