dry-cli-help 0.1.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +312 -72
- data/Rakefile +1 -1
- data/lib/dry/cli/help/configuration.rb +80 -47
- data/lib/dry/cli/help/formatter.rb +6 -3
- data/lib/dry/cli/help/integration.rb +2 -30
- data/lib/dry/cli/help/screens/command.rb +6 -6
- data/lib/dry/cli/help/screens/listing.rb +2 -2
- data/lib/dry/cli/help/version.rb +1 -1
- data/lib/dry/cli/help.rb +13 -17
- metadata +3 -10
- data/SPECIFICATION.md +0 -200
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10fb8e86e571ad96c6f523f4807d040b0421ebe1fa65e334738fa785e0212196
|
|
4
|
+
data.tar.gz: 408f6dd175e32d407f38306c9c61211a9b8e9d7e82f8870cde02ce58cfe64a23
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f42c06e5cb90eae00d6fc11f1fa4cbfed1f789005b4aa6dc0ea27cd60522de63019804c154e818efc4eb4bc52e673765ad638f881f94faa445b0b1a6ce637060
|
|
7
|
+
data.tar.gz: 9e711ac3f6b30580e1e979f391019de8f1c5d155b9ded6b1c72881e698d79347e337299aca96316b9c9806507d9b1d7022c0f30d9bf715083e60a7a651775c82
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - Unreleased
|
|
4
|
+
|
|
5
|
+
- The `dry-cli` dependency no longer pins a version range; any release satisfies it.
|
|
6
|
+
- `SPECIFICATION.md` moves to `docs/SPECIFICATION.md`, marked as the specification for 0.1.0, and stays out of the YARD documentation.
|
|
7
|
+
- The README documents which sections each screen prints, how groups and nested commands list, the description column and minimum wrap width, `Help.config`, `Help.reset!`, and the `ArgumentError` a bad setting raises.
|
|
8
|
+
|
|
9
|
+
## [0.2.1] - 2026-09-15
|
|
10
|
+
|
|
11
|
+
- Every setting is made once, in `Dry::CLI::Help.configure`, which now also accepts a block without an argument and runs it against the configuration. The `help` block on registries is removed, along with `Help.config_for` and `Configuration#merge`: the gem adds nothing to dry-cli's registry or command DSL.
|
|
12
|
+
- A `styles` block declares every element's look in one place, replacing `style(element, *names)`. New elements `usage`, `example` and `example_comment`; `comment` is renamed `example_comment` and defaults to bold black.
|
|
13
|
+
- Heading case moves into the heading style: `styles { heading :bold, case: :Capitalize }`, one of `:UPPERCASE`, `:Capitalize`, `:lowercase` or `:as_is`. The `heading_case` setting is removed.
|
|
14
|
+
- `examples/` holds three single-file dry-cli tools that switch this gem on with `--with-dry-cli-help` or `-w`.
|
|
15
|
+
|
|
16
|
+
## [0.2.0] - 2026-09-14
|
|
17
|
+
|
|
18
|
+
- Documentation, justfile, lefthook and packaging fixes after the conversion. No change to behavior.
|
|
19
|
+
|
|
3
20
|
## [0.1.0] - 2026-09-12
|
|
4
21
|
|
|
5
22
|
- Initial release as `dry-cli-help`, converted from `dry-cli-autocomplete`. The shell completion generator is removed; that gem remains the place for it.
|
data/README.md
CHANGED
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
Configurable, wrapped, colored help screens for [dry-cli](https://github.com/dry-rb/dry-cli) applications.
|
|
6
6
|
|
|
7
7
|
> [!NOTE]
|
|
8
|
-
> The design, the settings and
|
|
8
|
+
> The original design, the settings and the decisions behind them are in [docs/SPECIFICATION.md](docs/SPECIFICATION.md), written for version 0.1.0.
|
|
9
9
|
|
|
10
10
|
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`.
|
|
11
11
|
|
|
12
12
|
## Before and after
|
|
13
13
|
|
|
14
|
-
`
|
|
14
|
+
`my-cli compile -h` with dry-cli alone:
|
|
15
15
|
|
|
16
16
|
```text
|
|
17
17
|
Command:
|
|
18
|
-
|
|
18
|
+
my-cli compile
|
|
19
19
|
|
|
20
20
|
Usage:
|
|
21
|
-
|
|
21
|
+
my-cli compile RULES [OUTPUT]
|
|
22
22
|
|
|
23
23
|
Description:
|
|
24
24
|
Compile tax rules
|
|
@@ -37,7 +37,7 @@ With `require "dry/cli/help"`:
|
|
|
37
37
|
|
|
38
38
|
```text
|
|
39
39
|
USAGE
|
|
40
|
-
|
|
40
|
+
my-cli compile RULES [OUTPUT] [OPTIONS]
|
|
41
41
|
|
|
42
42
|
DESCRIPTION
|
|
43
43
|
Compile tax rules
|
|
@@ -54,6 +54,13 @@ OPTIONS
|
|
|
54
54
|
|
|
55
55
|
Headings are bold and yellow, commands green, options and arguments cyan, and every description wraps to the terminal with a hanging indent.
|
|
56
56
|
|
|
57
|
+
The same comparison as screenshots, taken from `examples/rbcheck`:
|
|
58
|
+
|
|
59
|
+
| Standard dry-cli Help Screen | Require `dry/cli/help` |
|
|
60
|
+
| :---------------------------------------------- | :--------------------------------------- |
|
|
61
|
+
|  |  |
|
|
62
|
+
| | |
|
|
63
|
+
|
|
57
64
|
## Installation
|
|
58
65
|
|
|
59
66
|
```bash
|
|
@@ -62,6 +69,8 @@ gem install dry-cli-help
|
|
|
62
69
|
|
|
63
70
|
Or add `gem "dry-cli-help"` to your `Gemfile`.
|
|
64
71
|
|
|
72
|
+
It requires Ruby 4.0 or newer, and depends on `dry-cli` and `pastel`.
|
|
73
|
+
|
|
65
74
|
## Usage
|
|
66
75
|
|
|
67
76
|
Require it after dry-cli. That alone changes every help screen in the process.
|
|
@@ -71,44 +80,44 @@ require "dry/cli"
|
|
|
71
80
|
require "dry/cli/help"
|
|
72
81
|
```
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
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.
|
|
75
84
|
|
|
76
85
|
```ruby
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
extend Dry::CLI::Registry
|
|
86
|
+
Dry::CLI::Help.configure do
|
|
87
|
+
title "MyCLI"
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
description <<~TEXT
|
|
90
|
+
Compile, validate, and evaluate rules.
|
|
91
|
+
TEXT
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
Compile, validate, and evaluate tax rules.
|
|
86
|
-
TEXT
|
|
93
|
+
epilogue "Documentation: https://example.com/my-cli"
|
|
87
94
|
|
|
88
|
-
|
|
95
|
+
color :auto
|
|
96
|
+
width :terminal
|
|
97
|
+
wrap true
|
|
98
|
+
end
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
end
|
|
100
|
+
module My
|
|
101
|
+
module CLI
|
|
102
|
+
extend Dry::CLI::Registry
|
|
94
103
|
|
|
95
|
-
register "compile",
|
|
104
|
+
register "compile", Compile
|
|
96
105
|
register "validate", Validate
|
|
97
106
|
register "evaluate", Evaluate
|
|
98
|
-
register "version",
|
|
107
|
+
register "version", Version, aliases: ["--version", "-v"]
|
|
99
108
|
end
|
|
100
109
|
end
|
|
101
110
|
```
|
|
102
111
|
|
|
103
|
-
`
|
|
112
|
+
`my-cli -h` then prints:
|
|
104
113
|
|
|
105
114
|
```text
|
|
106
|
-
|
|
115
|
+
MyCLI
|
|
107
116
|
|
|
108
|
-
Compile, validate, and evaluate
|
|
117
|
+
Compile, validate, and evaluate rules.
|
|
109
118
|
|
|
110
119
|
USAGE
|
|
111
|
-
|
|
120
|
+
my-cli COMMAND [OPTIONS]
|
|
112
121
|
|
|
113
122
|
COMMANDS
|
|
114
123
|
compile Compile tax rules
|
|
@@ -120,12 +129,12 @@ OPTIONS
|
|
|
120
129
|
-h, --help Show help
|
|
121
130
|
-v, --version Show version
|
|
122
131
|
|
|
123
|
-
Documentation: https://example.com/
|
|
132
|
+
Documentation: https://example.com/my-cli
|
|
124
133
|
```
|
|
125
134
|
|
|
126
|
-
A command reachable as `--version` lists under Options by its dashed names.
|
|
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`.
|
|
127
136
|
|
|
128
|
-
|
|
137
|
+
A block that takes an argument receives the configuration instead of running against it:
|
|
129
138
|
|
|
130
139
|
```ruby
|
|
131
140
|
Dry::CLI::Help.configure do |config|
|
|
@@ -134,34 +143,202 @@ Dry::CLI::Help.configure do |config|
|
|
|
134
143
|
end
|
|
135
144
|
```
|
|
136
145
|
|
|
137
|
-
|
|
146
|
+
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.
|
|
147
|
+
|
|
148
|
+
## Examples
|
|
149
|
+
|
|
150
|
+
The [`examples`](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:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
examples/rbcheck -h # dry-cli's own help
|
|
154
|
+
examples/rbcheck -h --with-dry-cli-help # the same help, through this gem
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`examples/rbcheck` has two commands, `version` and `check-ruby-syntax [DIR]`. Condensed:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
#!/usr/bin/env ruby
|
|
161
|
+
require "dry/cli"
|
|
162
|
+
|
|
163
|
+
if [ARGV.delete("--with-dry-cli-help"), ARGV.delete("-w")].any?
|
|
164
|
+
require "dry/cli/help"
|
|
165
|
+
|
|
166
|
+
Dry::CLI::Help.configure do
|
|
167
|
+
title "rbcheck"
|
|
168
|
+
description "Small checks for Ruby projects."
|
|
169
|
+
epilogue "Report bugs at https://example.com/rbcheck/issues"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
module Rbcheck
|
|
174
|
+
extend Dry::CLI::Registry
|
|
175
|
+
|
|
176
|
+
class Version < Dry::CLI::Command
|
|
177
|
+
desc "Print the version"
|
|
178
|
+
|
|
179
|
+
def call(**) = puts("rbcheck 1.0.0")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class CheckRubySyntax < Dry::CLI::Command
|
|
183
|
+
desc "Check every Ruby file under a directory for syntax errors, and report "\
|
|
184
|
+
"each file that fails to parse with its line number"
|
|
185
|
+
|
|
186
|
+
argument :dir, desc: "Directory to scan", default: "."
|
|
187
|
+
option :exclude, type: :array, desc: "Glob patterns to skip, such as vendor/**"
|
|
188
|
+
option :quiet, type: :boolean, default: false, aliases: ["-q"],
|
|
189
|
+
desc: "Print only the files that fail"
|
|
190
|
+
|
|
191
|
+
example ["lib # check one directory", "--exclude=vendor/** # skip vendored gems"]
|
|
192
|
+
|
|
193
|
+
def call(dir:, quiet:, exclude: [], **)
|
|
194
|
+
# ... parses every file under dir with Prism ...
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
register "version", Version, aliases: ["--version", "-v"]
|
|
199
|
+
register "check-ruby-syntax", CheckRubySyntax
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
Dry::CLI.new(Rbcheck).call
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Every screen below comes from running it in an 80-column terminal.
|
|
206
|
+
|
|
207
|
+
### `rbcheck -h`
|
|
208
|
+
|
|
209
|
+
dry-cli alone prints a list of commands to stderr and exits 1, and does not wrap the long description.
|
|
210
|
+
|
|
211
|
+
```text
|
|
212
|
+
Commands:
|
|
213
|
+
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
|
|
214
|
+
rbcheck version # Print the version
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
With this gem, `rbcheck -h` prints the help below to stdout and exits 0.
|
|
218
|
+
|
|
219
|
+
> [!NOTE]
|
|
220
|
+
> 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`](#dsl-based-configuration-api) to `0` to treat it like `-h` instead.
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
rbcheck
|
|
224
|
+
|
|
225
|
+
Small checks for Ruby projects.
|
|
226
|
+
|
|
227
|
+
USAGE
|
|
228
|
+
rbcheck COMMAND [OPTIONS]
|
|
229
|
+
|
|
230
|
+
COMMANDS
|
|
231
|
+
version Print the version
|
|
232
|
+
check-ruby-syntax Check every Ruby file under a directory for syntax errors,
|
|
233
|
+
and report each file that fails to parse with its line
|
|
234
|
+
number
|
|
235
|
+
|
|
236
|
+
OPTIONS
|
|
237
|
+
-h, --help Show help
|
|
238
|
+
-v, --version Print the version
|
|
239
|
+
|
|
240
|
+
Report bugs at https://example.com/rbcheck/issues
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- The title, description and epilogue come from the `configure` block.
|
|
244
|
+
- Commands list in the order they were registered, not alphabetically.
|
|
245
|
+
- The long description wraps to the terminal under its own column.
|
|
246
|
+
- `version` is also reachable as `--version` and `-v`, so it lists under Options too. dry-cli never shows those aliases.
|
|
247
|
+
|
|
248
|
+
### `rbcheck check-ruby-syntax -h`
|
|
249
|
+
|
|
250
|
+
dry-cli alone:
|
|
251
|
+
|
|
252
|
+
```text
|
|
253
|
+
Command:
|
|
254
|
+
rbcheck check-ruby-syntax
|
|
255
|
+
|
|
256
|
+
Usage:
|
|
257
|
+
rbcheck check-ruby-syntax [DIR]
|
|
258
|
+
|
|
259
|
+
Description:
|
|
260
|
+
Check every Ruby file under a directory for syntax errors, and report each file that fails to parse with its line number
|
|
261
|
+
|
|
262
|
+
Arguments:
|
|
263
|
+
DIR # Directory to scan
|
|
264
|
+
|
|
265
|
+
Options:
|
|
266
|
+
--exclude=VALUE1,VALUE2,.. # Glob patterns to skip, such as vendor/**
|
|
267
|
+
--[no-]quiet, -q # Print only the files that fail, default: false
|
|
268
|
+
--help, -h # Print this help
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
rbcheck check-ruby-syntax lib # check one directory
|
|
272
|
+
rbcheck check-ruby-syntax --exclude=vendor/** # skip vendored gems
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
With this gem:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
USAGE
|
|
279
|
+
rbcheck check-ruby-syntax [DIR] [OPTIONS]
|
|
280
|
+
|
|
281
|
+
DESCRIPTION
|
|
282
|
+
Check every Ruby file under a directory for syntax errors, and report each
|
|
283
|
+
file that fails to parse with its line number
|
|
138
284
|
|
|
139
|
-
|
|
285
|
+
ARGUMENTS
|
|
286
|
+
DIR Directory to scan (default: ".")
|
|
287
|
+
|
|
288
|
+
OPTIONS
|
|
289
|
+
--exclude=VALUE1,VALUE2,.. Glob patterns to skip, such as vendor/**
|
|
290
|
+
-q, --[no-]quiet Print only the files that fail (default: false)
|
|
291
|
+
-h, --help Show help
|
|
292
|
+
|
|
293
|
+
EXAMPLES
|
|
294
|
+
rbcheck check-ruby-syntax lib check one directory
|
|
295
|
+
rbcheck check-ruby-syntax --exclude=vendor/**
|
|
296
|
+
skip vendored gems
|
|
297
|
+
```
|
|
140
298
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
299
|
+
- The `Command:` section repeats the usage line, so it is gone, and the usage line shows that the command takes options.
|
|
300
|
+
- The argument's default of `"."` shows up; dry-cli leaves it out.
|
|
301
|
+
- Short aliases come first (`-q, --[no-]quiet`), and every description lines up in one column instead of trailing a `#`.
|
|
302
|
+
- Each example's comment moves into a column of its own. An example longer than that column puts its comment on the next line.
|
|
303
|
+
|
|
304
|
+
In a terminal the headings print bold yellow, usage lines, commands and examples green, arguments and options cyan, and example comments bold black.
|
|
305
|
+
|
|
306
|
+
`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`](examples/README.md) lists what each one demonstrates.
|
|
307
|
+
|
|
308
|
+
## DSL-based Configuration API
|
|
309
|
+
|
|
310
|
+
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
|
+
|
|
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 |
|
|
154
324
|
|
|
155
325
|
`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.
|
|
156
326
|
|
|
157
|
-
Running the program with no command prints
|
|
327
|
+
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
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
exit_code_without_arguments 0
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
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.
|
|
334
|
+
|
|
335
|
+
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.
|
|
158
336
|
|
|
159
337
|
### Headings, sections and groups
|
|
160
338
|
|
|
161
339
|
```ruby
|
|
162
|
-
|
|
340
|
+
Dry::CLI::Help.configure do
|
|
163
341
|
heading :commands, "Available commands"
|
|
164
|
-
heading_case :capitalize
|
|
165
342
|
|
|
166
343
|
group "Rules", "compile", "validate"
|
|
167
344
|
group "Returns", "evaluate"
|
|
@@ -171,22 +348,58 @@ help do
|
|
|
171
348
|
end
|
|
172
349
|
```
|
|
173
350
|
|
|
174
|
-
- `heading` replaces one section's heading text.
|
|
175
|
-
- `group` lists commands under a heading of their own, in the order given. Ungrouped commands stay under Commands. A group
|
|
351
|
+
- `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.
|
|
352
|
+
- `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`.
|
|
176
353
|
- `sections` sets the order; a section left out is hidden. `hide` hides sections without restating the order.
|
|
177
354
|
|
|
178
|
-
The sections are `banner`, `usage`, `description`, `commands`, `subcommands`, `arguments`, `options`, `examples` and `epilogue`. Each screen prints the ones that apply to it
|
|
355
|
+
The sections are `banner`, `usage`, `description`, `commands`, `subcommands`, `arguments`, `options`, `examples` and `epilogue`. Each screen prints the ones that apply to it:
|
|
356
|
+
|
|
357
|
+
| Screen | Sections it can print |
|
|
358
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
359
|
+
| A listing: `my-cli`, `my-cli -h`, `my-cli db` | `banner`, `usage`, `commands`, `options`, `epilogue` |
|
|
360
|
+
| A command: `my-cli compile -h` | `banner`, `usage`, `description`, `subcommands`, `arguments`, `options`, `examples`, `epilogue` |
|
|
361
|
+
|
|
362
|
+
A few details of those screens:
|
|
363
|
+
|
|
364
|
+
- A command that also has subcommands gets a second usage line, `my-cli db COMMAND [OPTIONS]`, and a Subcommands section.
|
|
365
|
+
- A group registered without a command of its own describes itself by what it holds, as in `Subcommands: add, remove`.
|
|
366
|
+
- An array argument prints as `FILES...`.
|
|
367
|
+
- 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.
|
|
368
|
+
- Text never wraps narrower than 20 columns, however small the terminal.
|
|
179
369
|
|
|
180
370
|
### Styles
|
|
181
371
|
|
|
372
|
+
Every element's look is declared together, in one `styles` block inside `configure`:
|
|
373
|
+
|
|
182
374
|
```ruby
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
375
|
+
Dry::CLI::Help.configure do
|
|
376
|
+
styles do
|
|
377
|
+
title :bold
|
|
378
|
+
heading :bold, :yellow, case: :UPPERCASE
|
|
379
|
+
usage :green
|
|
380
|
+
command :green
|
|
381
|
+
argument :cyan
|
|
382
|
+
option :cyan
|
|
383
|
+
example :green
|
|
384
|
+
example_comment :bold, :black
|
|
385
|
+
end
|
|
186
386
|
end
|
|
187
387
|
```
|
|
188
388
|
|
|
189
|
-
|
|
389
|
+
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.
|
|
390
|
+
|
|
391
|
+
| Element | Applies to |
|
|
392
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------- |
|
|
393
|
+
| `title` | The banner title |
|
|
394
|
+
| `heading` | Every section and group heading |
|
|
395
|
+
| `usage` | Every usage line, whole |
|
|
396
|
+
| `command` | Command names in a list |
|
|
397
|
+
| `argument` | Argument names |
|
|
398
|
+
| `option` | Option names |
|
|
399
|
+
| `example` | The command line of an example |
|
|
400
|
+
| `example_comment` | The part of an example after the first `#` surrounded by spaces, as in `"lib # check one directory"` |
|
|
401
|
+
|
|
402
|
+
`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.
|
|
190
403
|
|
|
191
404
|
### The Colors module
|
|
192
405
|
|
|
@@ -203,23 +416,33 @@ class Deploy < Dry::CLI::Command
|
|
|
203
416
|
end
|
|
204
417
|
```
|
|
205
418
|
|
|
206
|
-
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
|
|
419
|
+
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.
|
|
207
420
|
|
|
208
421
|
## How it works
|
|
209
422
|
|
|
210
423
|
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`.
|
|
211
424
|
|
|
212
425
|
```mermaid
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
426
|
+
---
|
|
427
|
+
config:
|
|
428
|
+
layout: elk
|
|
429
|
+
theme: forest
|
|
430
|
+
---
|
|
431
|
+
flowchart TB
|
|
432
|
+
argv["ARGV"] --> cli_call["Dry::CLI#call"]
|
|
433
|
+
|
|
434
|
+
cli_call -->|"command found, --help given"| help_method["#help"]
|
|
435
|
+
cli_call -->|"no command, a group, -h at a level, a typo"| spell_checker["#spell_checker"]
|
|
436
|
+
|
|
437
|
+
help_method --> command_screen["Screens::Command"]
|
|
438
|
+
spell_checker --> listing_screen["Screens::Listing"]
|
|
439
|
+
|
|
440
|
+
command_screen --> formatter["Formatter"]
|
|
441
|
+
listing_screen --> formatter
|
|
442
|
+
|
|
443
|
+
help_config["Help.configure"] --> formatter
|
|
444
|
+
|
|
445
|
+
formatter --> output["stdout or stderr"]
|
|
223
446
|
```
|
|
224
447
|
|
|
225
448
|
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.
|
|
@@ -227,13 +450,16 @@ Both methods are `@api private` in dry-cli. `spec/dry/cli/help/dry_cli_contract_
|
|
|
227
450
|
## Development
|
|
228
451
|
|
|
229
452
|
```bash
|
|
230
|
-
just install
|
|
231
|
-
just test
|
|
232
|
-
just
|
|
233
|
-
just
|
|
234
|
-
just
|
|
235
|
-
just
|
|
236
|
-
|
|
453
|
+
just install # bundle install
|
|
454
|
+
just test # the suite; a full run enforces 100% line and branch coverage
|
|
455
|
+
just test-coverage # measure coverage even for a partial run
|
|
456
|
+
just lint # rubocop
|
|
457
|
+
just ci # rubocop, then the suite with coverage
|
|
458
|
+
just lefthook # every pre-commit hook against every file
|
|
459
|
+
just format # mdformat --wrap no on every Markdown file, then rubocop -a
|
|
460
|
+
just doc # YARD documentation
|
|
461
|
+
just build # build the .gem into pkg/
|
|
462
|
+
bin/console # IRB with the gem loaded
|
|
237
463
|
```
|
|
238
464
|
|
|
239
465
|
## Contributing
|
|
@@ -241,7 +467,21 @@ bin/console # IRB with the gem loaded
|
|
|
241
467
|
Bug reports and pull requests are welcome at <https://github.com/kigster/dry-cli-help>.
|
|
242
468
|
|
|
243
469
|
> [!WARNING]
|
|
244
|
-
> The `dry-` prefix and the `Dry::CLI::Help` namespace do not imply endorsement by dry-rb.
|
|
470
|
+
> The `dry-` prefix and the `Dry::CLI::Help` namespace do not imply endorsement by dry-rb.
|
|
471
|
+
>
|
|
472
|
+
> This is an independent and opinionated gem that extends theirs.
|
|
473
|
+
|
|
474
|
+
## Note to Dry-Rb Maintainers
|
|
475
|
+
|
|
476
|
+
First — hats off to all of you who tirelessly built out one of the most valuable collections of libraries in the Ruby ecosystem.
|
|
477
|
+
|
|
478
|
+
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.
|
|
479
|
+
|
|
480
|
+
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:
|
|
481
|
+
|
|
482
|
+
1. We would be very happy to receive any feedback and improve, refactor, and update the gem assuming it improves it
|
|
483
|
+
1. Roll any part of the codebase as a PR to the `dry-cli` core.
|
|
484
|
+
1. 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.
|
|
245
485
|
|
|
246
486
|
## License
|
|
247
487
|
|
data/Rakefile
CHANGED
|
@@ -28,7 +28,7 @@ end
|
|
|
28
28
|
task build: :permissions
|
|
29
29
|
|
|
30
30
|
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
31
|
-
t.files = %w[lib/**/*.rb - README.md LICENSE.txt CHANGELOG.md
|
|
31
|
+
t.files = %w[lib/**/*.rb - README.md LICENSE.txt CHANGELOG.md]
|
|
32
32
|
t.options.unshift("--title", '"dry-cli-help: configurable help screens for dry-cli"')
|
|
33
33
|
t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
|
|
34
34
|
end
|
|
@@ -3,19 +3,18 @@
|
|
|
3
3
|
module Dry
|
|
4
4
|
class CLI
|
|
5
5
|
module Help
|
|
6
|
-
#
|
|
6
|
+
# Every help setting, made once for the whole process, two ways:
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
# title "
|
|
8
|
+
# Dry::CLI::Help.configure do
|
|
9
|
+
# title "MyCLI" # as a DSL
|
|
10
10
|
# end
|
|
11
11
|
#
|
|
12
12
|
# Dry::CLI::Help.configure do |config|
|
|
13
|
-
# config.title = "
|
|
13
|
+
# config.title = "MyCLI" # on the yielded object
|
|
14
14
|
# end
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
# {DEFAULTS},
|
|
18
|
-
# process-wide ones through {#merge} without either copying the other.
|
|
16
|
+
# Every element's look is declared in one {#styles} block. Anything not
|
|
17
|
+
# set reads from {DEFAULTS}, {HEADINGS} or {STYLES}.
|
|
19
18
|
class Configuration
|
|
20
19
|
# Every section, in default order.
|
|
21
20
|
SECTIONS = %i[
|
|
@@ -37,13 +36,18 @@ module Dry
|
|
|
37
36
|
STYLES = {
|
|
38
37
|
title: %i[bold],
|
|
39
38
|
heading: %i[bold yellow],
|
|
39
|
+
usage: %i[green],
|
|
40
40
|
command: %i[green],
|
|
41
41
|
argument: %i[cyan],
|
|
42
42
|
option: %i[cyan],
|
|
43
|
-
|
|
43
|
+
example: %i[green],
|
|
44
|
+
example_comment: %i[bold black]
|
|
44
45
|
}.freeze
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
# How headings can be cased; each name is written the way it cases.
|
|
48
|
+
# `:Capitalize` raises only the first letter.
|
|
49
|
+
HEADING_CASES = %i[UPPERCASE Capitalize lowercase as_is].freeze
|
|
50
|
+
DEFAULT_HEADING_CASE = :UPPERCASE
|
|
47
51
|
COMMAND_ORDERS = %i[registration alphabetical].freeze
|
|
48
52
|
|
|
49
53
|
# Narrowest column text wraps at, however small the terminal.
|
|
@@ -63,7 +67,6 @@ module Dry
|
|
|
63
67
|
margin: ->(value) { value.is_a?(Integer) && !value.negative? },
|
|
64
68
|
exit_code_without_arguments: ->(value) { value.is_a?(Integer) && value.between?(0, 255) },
|
|
65
69
|
banner_on_subcommands: BOOLEAN,
|
|
66
|
-
heading_case: ->(value) { HEADING_CASES.include?(value) },
|
|
67
70
|
command_order: ->(value) { COMMAND_ORDERS.include?(value) }
|
|
68
71
|
}.freeze
|
|
69
72
|
|
|
@@ -77,7 +80,6 @@ module Dry
|
|
|
77
80
|
margin: 0,
|
|
78
81
|
exit_code_without_arguments: 1,
|
|
79
82
|
banner_on_subcommands: false,
|
|
80
|
-
heading_case: :upcase,
|
|
81
83
|
command_order: :registration
|
|
82
84
|
}.freeze
|
|
83
85
|
|
|
@@ -122,26 +124,36 @@ module Dry
|
|
|
122
124
|
HEADINGS.merge(@values.fetch(:headings, {}))
|
|
123
125
|
end
|
|
124
126
|
|
|
125
|
-
#
|
|
127
|
+
# Declare how elements look, all in one place. Each line names an
|
|
128
|
+
# element from {STYLES} and its styles from {Colors::STYLES}; no styles
|
|
129
|
+
# prints it plain. Elements left out keep their defaults. `heading` also
|
|
130
|
+
# takes `case:`, one of {HEADING_CASES}. A block taking an argument
|
|
131
|
+
# receives the declarations instead.
|
|
132
|
+
#
|
|
133
|
+
# @example
|
|
134
|
+
# styles do
|
|
135
|
+
# heading :bold, :blue, case: :Capitalize
|
|
136
|
+
# example_comment # plain
|
|
137
|
+
# end
|
|
126
138
|
#
|
|
127
|
-
# @param element [Symbol] a key of {STYLES}
|
|
128
|
-
# @param names [Array<Symbol>] names from {Colors::STYLES}
|
|
129
|
-
# @return [Array<Symbol>]
|
|
130
|
-
def style(element, *names)
|
|
131
|
-
raise ArgumentError, "#{element.inspect} is not a styled element" unless STYLES.key?(element)
|
|
132
|
-
|
|
133
|
-
unknown = names - Colors::STYLES
|
|
134
|
-
raise ArgumentError, "unknown styles: #{unknown.inspect}" unless unknown.empty?
|
|
135
|
-
|
|
136
|
-
@values[:styles] = @values.fetch(:styles, {}).merge(element => names.freeze)
|
|
137
|
-
names
|
|
138
|
-
end
|
|
139
|
-
|
|
140
139
|
# @return [Hash{Symbol => Array<Symbol>}] every element's styles
|
|
141
|
-
def styles
|
|
140
|
+
def styles(&block)
|
|
141
|
+
if block
|
|
142
|
+
sheet = StyleSheet.new
|
|
143
|
+
block.arity == 1 ? yield(sheet) : sheet.instance_eval(&block)
|
|
144
|
+
@values[:styles] = @values.fetch(:styles, {}).merge(sheet.styles)
|
|
145
|
+
@values[:heading_case] = sheet.heading_case if sheet.heading_case
|
|
146
|
+
end
|
|
142
147
|
STYLES.merge(@values.fetch(:styles, {}))
|
|
143
148
|
end
|
|
144
149
|
|
|
150
|
+
# How every heading is cased, set by `heading ..., case:` in {#styles}.
|
|
151
|
+
#
|
|
152
|
+
# @return [Symbol] one of {HEADING_CASES}
|
|
153
|
+
def heading_case
|
|
154
|
+
@values.fetch(:heading_case, DEFAULT_HEADING_CASE)
|
|
155
|
+
end
|
|
156
|
+
|
|
145
157
|
# List commands under a heading of their own, in the order given.
|
|
146
158
|
# Groups print in the order they are declared, after ungrouped commands.
|
|
147
159
|
#
|
|
@@ -206,27 +218,6 @@ module Dry
|
|
|
206
218
|
[columns, MIN_WIDTH].max
|
|
207
219
|
end
|
|
208
220
|
|
|
209
|
-
# These settings with another instance's laid over them. Headings and
|
|
210
|
-
# styles merge key by key, hidden sections add up, and any other setting
|
|
211
|
-
# the other instance made replaces this one's.
|
|
212
|
-
#
|
|
213
|
-
# @param other [Configuration]
|
|
214
|
-
# @return [Configuration] a new instance
|
|
215
|
-
def merge(other)
|
|
216
|
-
combined = @values.merge(other.values) do |key, mine, theirs|
|
|
217
|
-
case key
|
|
218
|
-
when :headings, :styles then mine.merge(theirs)
|
|
219
|
-
when :hidden then mine | theirs
|
|
220
|
-
else theirs
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
self.class.new(combined)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
protected
|
|
227
|
-
|
|
228
|
-
attr_reader :values
|
|
229
|
-
|
|
230
221
|
private
|
|
231
222
|
|
|
232
223
|
def validate_sections(names)
|
|
@@ -235,6 +226,48 @@ module Dry
|
|
|
235
226
|
|
|
236
227
|
names.freeze
|
|
237
228
|
end
|
|
229
|
+
|
|
230
|
+
# What a {Configuration#styles} block runs against: one method per
|
|
231
|
+
# element, each validating what it is given.
|
|
232
|
+
class StyleSheet
|
|
233
|
+
# @return [Hash{Symbol => Array<Symbol>}] the elements declared
|
|
234
|
+
attr_reader :styles
|
|
235
|
+
|
|
236
|
+
# @return [Symbol, nil] the heading case, when declared
|
|
237
|
+
attr_reader :heading_case
|
|
238
|
+
|
|
239
|
+
def initialize
|
|
240
|
+
@styles = {}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
STYLES.each_key do |element|
|
|
244
|
+
define_method(element) do |*names, **options|
|
|
245
|
+
declare(element, names.flatten, options)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def declare(element, names, options)
|
|
252
|
+
unknown = names - Colors::STYLES
|
|
253
|
+
raise ArgumentError, "unknown styles for #{element}: #{unknown.inspect}" unless unknown.empty?
|
|
254
|
+
|
|
255
|
+
@heading_case = letter_case(element, options) unless options.empty?
|
|
256
|
+
# `heading case: :as_is` alone changes the case and keeps the styles.
|
|
257
|
+
@styles[element] = names.freeze unless names.empty? && !options.empty?
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def letter_case(element, options)
|
|
261
|
+
unless element == :heading && options.keys == [:case]
|
|
262
|
+
raise ArgumentError, "#{element} takes no #{options.keys.map { "#{it}:" }.join(', ')}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
value = options.fetch(:case)
|
|
266
|
+
return value if HEADING_CASES.include?(value)
|
|
267
|
+
|
|
268
|
+
raise ArgumentError, "case cannot be #{value.inspect}; use one of #{HEADING_CASES.inspect}"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
238
271
|
end
|
|
239
272
|
end
|
|
240
273
|
end
|
|
@@ -49,12 +49,15 @@ module Dry
|
|
|
49
49
|
title(config.headings.fetch(section))
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
# A heading
|
|
52
|
+
# A heading in the configured case and style, for any text.
|
|
53
|
+
#
|
|
54
|
+
# @param text [String]
|
|
53
55
|
# @return [String]
|
|
54
56
|
def title(text)
|
|
55
57
|
cased = case config.heading_case
|
|
56
|
-
when :
|
|
57
|
-
when :
|
|
58
|
+
when :UPPERCASE then text.upcase
|
|
59
|
+
when :lowercase then text.downcase
|
|
60
|
+
when :Capitalize then text.sub(/\A\p{Ll}/, &:upcase)
|
|
58
61
|
else text
|
|
59
62
|
end
|
|
60
63
|
paint(cased, :heading)
|
|
@@ -20,7 +20,7 @@ module Dry
|
|
|
20
20
|
def help(command, prog_name)
|
|
21
21
|
screen = Screens::Command.new(
|
|
22
22
|
command:, prog_name:, top_level: !kommand.nil?,
|
|
23
|
-
config: Help.
|
|
23
|
+
config: Help.config, io: out
|
|
24
24
|
)
|
|
25
25
|
out.puts screen.render
|
|
26
26
|
exit(0)
|
|
@@ -30,7 +30,7 @@ module Dry
|
|
|
30
30
|
# all, a group without a command of its own, `-h` or `--help` at a
|
|
31
31
|
# registry level, or a typo.
|
|
32
32
|
def spell_checker(result, arguments)
|
|
33
|
-
config = Help.
|
|
33
|
+
config = Help.config
|
|
34
34
|
unmatched = arguments.drop(result.names.length)
|
|
35
35
|
|
|
36
36
|
if unmatched.empty?
|
|
@@ -50,34 +50,6 @@ module Dry
|
|
|
50
50
|
exit(status)
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
|
-
|
|
54
|
-
# Included into Dry::CLI::Registry, so every registry can describe itself.
|
|
55
|
-
module RegistryMethods
|
|
56
|
-
# Configure this registry's help. A block taking an argument receives
|
|
57
|
-
# the configuration; any other block runs against it.
|
|
58
|
-
#
|
|
59
|
-
# @example
|
|
60
|
-
# help do
|
|
61
|
-
# title "Taxlibris"
|
|
62
|
-
# width 100
|
|
63
|
-
# end
|
|
64
|
-
#
|
|
65
|
-
# @return [Configuration] this registry's settings
|
|
66
|
-
def help(&block)
|
|
67
|
-
@help_config ||= Configuration.new
|
|
68
|
-
if block&.arity == 1
|
|
69
|
-
yield @help_config
|
|
70
|
-
elsif block
|
|
71
|
-
@help_config.instance_eval(&block)
|
|
72
|
-
end
|
|
73
|
-
@help_config
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# @return [Configuration, nil] nil until {#help} is called
|
|
77
|
-
def help_config
|
|
78
|
-
@help_config
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
53
|
end
|
|
82
54
|
end
|
|
83
55
|
end
|
|
@@ -32,10 +32,9 @@ module Dry
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def render_usage
|
|
35
|
-
|
|
36
|
-
lines
|
|
37
|
-
lines
|
|
38
|
-
section(:usage, lines)
|
|
35
|
+
lines = ["#{prog_name}#{usage_arguments} [OPTIONS]"]
|
|
36
|
+
lines << "#{prog_name} COMMAND [OPTIONS]" if subcommand_rows.any?
|
|
37
|
+
section(:usage, lines.map { INDENT + format.paint(it, :usage) })
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
def render_description
|
|
@@ -59,7 +58,8 @@ module Dry
|
|
|
59
58
|
def render_examples
|
|
60
59
|
rows = command.examples.map do |example|
|
|
61
60
|
line, comment = example.split(" # ", 2)
|
|
62
|
-
Row.new(term: "#{prog_name} #{line.strip}", text: comment&.strip,
|
|
61
|
+
Row.new(term: "#{prog_name} #{line.strip}", text: comment&.strip,
|
|
62
|
+
term_style: :example, text_style: :example_comment)
|
|
63
63
|
end
|
|
64
64
|
section(:examples, format.definitions(rows, format.column_for(rows)))
|
|
65
65
|
end
|
|
@@ -72,7 +72,7 @@ module Dry
|
|
|
72
72
|
required = command.required_arguments.map { argument_name(it) }
|
|
73
73
|
optional = command.optional_arguments.map { "[#{argument_name(it)}]" }
|
|
74
74
|
names = [*required, *optional]
|
|
75
|
-
" #{
|
|
75
|
+
" #{names.join(' ')}" unless names.empty?
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def subcommand_rows
|
|
@@ -32,8 +32,8 @@ module Dry
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def render_usage
|
|
35
|
-
|
|
36
|
-
section(:usage, ["#{INDENT}#{
|
|
35
|
+
usage = format.paint("#{ProgramName.call(result.names)} COMMAND [OPTIONS]", :usage)
|
|
36
|
+
section(:usage, ["#{INDENT}#{usage}"])
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def render_commands
|
data/lib/dry/cli/help/version.rb
CHANGED
data/lib/dry/cli/help.rb
CHANGED
|
@@ -19,14 +19,22 @@ module Dry
|
|
|
19
19
|
# Configurable, wrapped and colored help screens for dry-cli.
|
|
20
20
|
#
|
|
21
21
|
# Requiring this file changes the help every Dry::CLI in the process prints.
|
|
22
|
-
#
|
|
23
|
-
#
|
|
22
|
+
# Every setting is made once, in {.configure}, so the way dry-cli declares
|
|
23
|
+
# registries, commands and options stays exactly as dry-cli ships it.
|
|
24
24
|
module Help
|
|
25
25
|
class << self
|
|
26
|
-
#
|
|
26
|
+
# Make every setting. A block taking an argument receives the
|
|
27
|
+
# configuration; any other block runs against it.
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# Dry::CLI::Help.configure do
|
|
31
|
+
# title "MyCLI"
|
|
32
|
+
# styles { heading :bold, :blue }
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
27
35
|
# @return [Configuration]
|
|
28
|
-
def configure
|
|
29
|
-
yield config
|
|
36
|
+
def configure(&block)
|
|
37
|
+
block.arity == 1 ? yield(config) : config.instance_eval(&block)
|
|
30
38
|
config
|
|
31
39
|
end
|
|
32
40
|
|
|
@@ -40,21 +48,9 @@ module Dry
|
|
|
40
48
|
def reset!
|
|
41
49
|
@config = nil
|
|
42
50
|
end
|
|
43
|
-
|
|
44
|
-
# The settings a registry renders with: its own `help` block over the
|
|
45
|
-
# process-wide settings. A single command passed to Dry::CLI.new has no
|
|
46
|
-
# `help` block, so it renders with the process-wide settings alone.
|
|
47
|
-
#
|
|
48
|
-
# @param registry [Module, Class, Dry::CLI::Command, nil]
|
|
49
|
-
# @return [Configuration]
|
|
50
|
-
def config_for(registry)
|
|
51
|
-
own = registry.help_config if registry.respond_to?(:help_config)
|
|
52
|
-
own ? config.merge(own) : config
|
|
53
|
-
end
|
|
54
51
|
end
|
|
55
52
|
end
|
|
56
53
|
end
|
|
57
54
|
end
|
|
58
55
|
|
|
59
56
|
Dry::CLI.prepend(Dry::CLI::Help::Integration::CLIMethods)
|
|
60
|
-
Dry::CLI::Registry.include(Dry::CLI::Help::Integration::RegistryMethods)
|
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
@@ -15,20 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
19
|
-
- - "<"
|
|
20
|
-
- !ruby/object:Gem::Version
|
|
21
|
-
version: '2'
|
|
18
|
+
version: '0'
|
|
22
19
|
type: :runtime
|
|
23
20
|
prerelease: false
|
|
24
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
22
|
requirements:
|
|
26
23
|
- - ">="
|
|
27
24
|
- !ruby/object:Gem::Version
|
|
28
|
-
version:
|
|
29
|
-
- - "<"
|
|
30
|
-
- !ruby/object:Gem::Version
|
|
31
|
-
version: '2'
|
|
25
|
+
version: '0'
|
|
32
26
|
- !ruby/object:Gem::Dependency
|
|
33
27
|
name: pastel
|
|
34
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -56,7 +50,6 @@ files:
|
|
|
56
50
|
- LICENSE.txt
|
|
57
51
|
- README.md
|
|
58
52
|
- Rakefile
|
|
59
|
-
- SPECIFICATION.md
|
|
60
53
|
- lib/dry-cli-help.rb
|
|
61
54
|
- lib/dry/cli/help.rb
|
|
62
55
|
- lib/dry/cli/help/colors.rb
|
data/SPECIFICATION.md
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# dry-cli-help
|
|
2
|
-
|
|
3
|
-
Enhanced help presentation for [`dry-cli`](https://github.com/dry-rb/dry-cli).
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
7
|
-
`dry-cli-help` improves the **static, human-facing documentation** generated by `dry-cli`.
|
|
8
|
-
|
|
9
|
-
It does not replace `dry-cli` or introduce another command framework. It takes the command structure already defined through `dry-cli` and provides richer, more configurable help output.
|
|
10
|
-
|
|
11
|
-
## Responsibilities
|
|
12
|
-
|
|
13
|
-
- Top-level application title and description
|
|
14
|
-
- Command descriptions
|
|
15
|
-
- Automatic line wrapping based on terminal width
|
|
16
|
-
- ANSI color and styling
|
|
17
|
-
- Configurable headings
|
|
18
|
-
- Improved spacing and indentation
|
|
19
|
-
- Usage formatting
|
|
20
|
-
- Arguments and options formatting
|
|
21
|
-
- Examples
|
|
22
|
-
- Epilogues
|
|
23
|
-
- Command grouping
|
|
24
|
-
- Section ordering
|
|
25
|
-
- Hiding or customizing sections
|
|
26
|
-
- Consistent formatting across commands
|
|
27
|
-
|
|
28
|
-
## Example
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
require "dry/cli"
|
|
32
|
-
require "dry/cli/help"
|
|
33
|
-
|
|
34
|
-
Dry::CLI::Help.configure do |config|
|
|
35
|
-
config.width = :terminal
|
|
36
|
-
config.wrap = true
|
|
37
|
-
config.color = true
|
|
38
|
-
end
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
An application could provide richer top-level help:
|
|
42
|
-
|
|
43
|
-
```ruby
|
|
44
|
-
class CLI
|
|
45
|
-
extend Dry::CLI::Registry
|
|
46
|
-
|
|
47
|
-
help do
|
|
48
|
-
title "Taxlibris"
|
|
49
|
-
|
|
50
|
-
description <<~TEXT
|
|
51
|
-
Compile, validate, and evaluate tax rules.
|
|
52
|
-
TEXT
|
|
53
|
-
|
|
54
|
-
color true
|
|
55
|
-
width :terminal
|
|
56
|
-
wrap true
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Result:
|
|
62
|
-
|
|
63
|
-
```text
|
|
64
|
-
Taxlibris
|
|
65
|
-
|
|
66
|
-
Compile, validate, and evaluate tax rules.
|
|
67
|
-
|
|
68
|
-
USAGE
|
|
69
|
-
taxlibris COMMAND [OPTIONS]
|
|
70
|
-
|
|
71
|
-
COMMANDS
|
|
72
|
-
compile Compile tax rules
|
|
73
|
-
validate Validate the rule corpus
|
|
74
|
-
evaluate Evaluate a tax return
|
|
75
|
-
|
|
76
|
-
OPTIONS
|
|
77
|
-
--help Show help
|
|
78
|
-
--version Show version
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Design Principle
|
|
82
|
-
|
|
83
|
-
`dry-cli-help` owns what the user sees **before a command runs**.
|
|
84
|
-
|
|
85
|
-
It should remain focused on documentation and presentation rather than runtime command UI.
|
|
86
|
-
|
|
87
|
-
Runtime features such as progress bars, spinners, status displays, and error panels belong in `dry-cli-ui`.
|
|
88
|
-
|
|
89
|
-
## Relationship
|
|
90
|
-
|
|
91
|
-
```text
|
|
92
|
-
dry-cli
|
|
93
|
-
│
|
|
94
|
-
└── dry-cli-help
|
|
95
|
-
│
|
|
96
|
-
├── descriptions
|
|
97
|
-
├── usage
|
|
98
|
-
├── wrapping
|
|
99
|
-
├── headings
|
|
100
|
-
├── colors
|
|
101
|
-
├── arguments/options
|
|
102
|
-
└── examples
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Requirements carried from the first draft
|
|
106
|
-
|
|
107
|
-
1. **Exit status without arguments.** dry-cli prints the command list and exits 1 when run with no command. The status is configurable. Asking for help with `-h` or `--help` always exits 0.
|
|
108
|
-
1. **Banner.** The title and description print above the command list for `mycli`, `mycli -h` and `mycli --help`. A separate setting decides whether they also print for `mycli subcommand -h`.
|
|
109
|
-
1. **Wrapping.** Three modes: no wrapping (dry-cli's behavior), wrapping at a fixed column such as 80 or 100, and wrapping at the terminal's width minus an optional margin.
|
|
110
|
-
1. **Colors.** Built on `pastel`. A `Colors` module, when included, makes each style below available as a method: `red("text")` returns decorated text, and `red.bold("text")` chains.
|
|
111
|
-
- Foreground: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, and the `bright_` form of each.
|
|
112
|
-
- Background: `on_black`, `on_red`, `on_green`, `on_yellow`, `on_blue`, `on_magenta`, `on_cyan`, `on_white`, and the `on_bright_` form of each.
|
|
113
|
-
- Styles: `clear`, `bold`, `dim`, `italic`, `underline`, `inverse`, `hidden`, `strikethrough`.
|
|
114
|
-
|
|
115
|
-
## Decisions
|
|
116
|
-
|
|
117
|
-
### Integration
|
|
118
|
-
|
|
119
|
-
Requiring `dry/cli/help` changes help output for every `Dry::CLI` in the process. It does not replace `Dry::CLI::Banner` or `Dry::CLI::Usage`. It prepends one module to `Dry::CLI` that overrides two private methods, the only two places dry-cli prints help:
|
|
120
|
-
|
|
121
|
-
| dry-cli method | When dry-cli calls it |
|
|
122
|
-
| -------------------------------------- | ---------------------------------------------------------- |
|
|
123
|
-
| `Dry::CLI#help(command, prog_name)` | `mycli deploy -h`, for any command with a class |
|
|
124
|
-
| `Dry::CLI#spell_checker(result, argv)` | `mycli`, `mycli -h`, `mycli db` for a group, `mycli bogus` |
|
|
125
|
-
|
|
126
|
-
Both are `@api private` in dry-cli 1.4.1. A spec asserts they exist, so a dry-cli release that renames them fails this gem's suite rather than a host's help screen. `Dry::CLI::Help::Integration` holds the override.
|
|
127
|
-
|
|
128
|
-
`help` is added to `Dry::CLI::Registry`, so any module or class that extends a registry can call it.
|
|
129
|
-
|
|
130
|
-
### Configuration
|
|
131
|
-
|
|
132
|
-
Two levels, one vocabulary. `Dry::CLI::Help.configure` sets process-wide values. A registry's `help` block overrides them for that registry. A setting neither level sets takes the default below.
|
|
133
|
-
|
|
134
|
-
Every setting reads and writes both ways: `title "Taxlibris"` inside a `help` block, and `config.title = "Taxlibris"` on the yielded object. A `help` block taking one argument receives the configuration instead of being evaluated against it.
|
|
135
|
-
|
|
136
|
-
| Setting | Values | Default | Effect |
|
|
137
|
-
| ----------------------------- | ------------------------------------ | --------------- | ---------------------------------------------------------------------- |
|
|
138
|
-
| `title` | String | none | First line of the banner |
|
|
139
|
-
| `description` | String | none | Paragraphs under the title, wrapped |
|
|
140
|
-
| `epilogue` | String | none | Paragraphs at the end of the top-level help |
|
|
141
|
-
| `color` | `true`, `false`, `:auto` | `:auto` | `:auto` colors a terminal and honors `NO_COLOR` |
|
|
142
|
-
| `wrap` | `true`, `false` | `true` | `false` prints descriptions as written |
|
|
143
|
-
| `width` | `:terminal`, Integer | `:terminal` | The column text wraps at |
|
|
144
|
-
| `margin` | Integer | `0` | Columns kept free at the right edge when `width` is `:terminal` |
|
|
145
|
-
| `exit_code_without_arguments` | Integer, 0 to 255 | `1` | Status for `mycli` or `mycli group` with no command |
|
|
146
|
-
| `banner_on_subcommands` | `true`, `false` | `false` | Print the title and description above `mycli command -h` |
|
|
147
|
-
| `heading_case` | `:upcase`, `:capitalize`, `:none` | `:upcase` | How every heading is cased; `:capitalize` raises only the first letter |
|
|
148
|
-
| `command_order` | `:registration`, `:alphabetical` | `:registration` | Order commands list in; dry-cli sorts alphabetically |
|
|
149
|
-
| `heading(section, text)` | Section name, String | see below | Replaces one heading's text |
|
|
150
|
-
| `style(element, *styles)` | Element name, `Colors::STYLES` names | see below | Replaces one element's styles |
|
|
151
|
-
| `group(name, *commands)` | String, command paths | none | Lists those commands under their own heading, in declaration order |
|
|
152
|
-
| `sections(*names)` | Section names | all | Order of sections; a section left out is hidden |
|
|
153
|
-
| `hide(*names)` | Section names | none | Hides sections without restating the order |
|
|
154
|
-
|
|
155
|
-
Terminal width comes from `COLUMNS`, then the console, then 80. A resolved wrap width never falls below 20 columns.
|
|
156
|
-
|
|
157
|
-
### Sections
|
|
158
|
-
|
|
159
|
-
In default order, with default headings:
|
|
160
|
-
|
|
161
|
-
| Section | Heading | Top-level help | Command help |
|
|
162
|
-
| ------------- | ----------- | -------------- | ----------------------------------- |
|
|
163
|
-
| `banner` | none | yes | when `banner_on_subcommands` is set |
|
|
164
|
-
| `usage` | Usage | yes | yes |
|
|
165
|
-
| `description` | Description | no | yes |
|
|
166
|
-
| `commands` | Commands | yes | no |
|
|
167
|
-
| `subcommands` | Subcommands | no | when the command has children |
|
|
168
|
-
| `arguments` | Arguments | no | yes |
|
|
169
|
-
| `options` | Options | yes | yes |
|
|
170
|
-
| `examples` | Examples | no | yes |
|
|
171
|
-
| `epilogue` | none | yes | only for a single command |
|
|
172
|
-
|
|
173
|
-
A single command passed to `Dry::CLI.new(SomeCommand)` is the whole program, so its command help prints the banner and the epilogue too. It has no registry, so it renders with the process-wide settings alone.
|
|
174
|
-
|
|
175
|
-
A group listing, `mycli db` where `db` has no command of its own, renders as top-level help scoped to the group, with the banner only when `banner_on_subcommands` is set, and no epilogue.
|
|
176
|
-
|
|
177
|
-
Every Options section starts with `-h, --help Show help`, because both spellings work at every level. The example above shows `--help` alone; `spec/dry/cli/help/integration_spec.rb` holds the exact output.
|
|
178
|
-
|
|
179
|
-
At any level, a command registered under a name or alias starting with `-`, such as `register "version", Version, aliases: ["--version"]`, lists under Options rather than Commands.
|
|
180
|
-
|
|
181
|
-
### Styled elements
|
|
182
|
-
|
|
183
|
-
| Element | Default styles | Applies to |
|
|
184
|
-
| ---------- | ---------------- | -------------------------------------- |
|
|
185
|
-
| `title` | `bold` | The banner title |
|
|
186
|
-
| `heading` | `bold`, `yellow` | Every section heading |
|
|
187
|
-
| `command` | `green` | Command names and the program in usage |
|
|
188
|
-
| `argument` | `cyan` | Argument names |
|
|
189
|
-
| `option` | `cyan` | Option names |
|
|
190
|
-
| `comment` | `bright_black` | The comment half of an example |
|
|
191
|
-
|
|
192
|
-
### Layout
|
|
193
|
-
|
|
194
|
-
- Every section after the first starts after one blank line. Section bodies indent two columns.
|
|
195
|
-
- A definition list (commands, arguments, options) aligns its descriptions in one column across the whole screen, two columns past the longest term that has a description. The column never passes half the wrap width, and a term longer than the column puts its description on the next line.
|
|
196
|
-
- Examples align among themselves. A full command line is longer than any option, and sharing its column would push every other description to the right.
|
|
197
|
-
- A group registered without a command has no description of its own, so it lists as `Subcommands: a, b`.
|
|
198
|
-
- An argument or option description ends with what a reader needs to supply it: `(required; one of: json, yaml; default: "json")`. An array argument reads `NAME...`.
|
|
199
|
-
- Descriptions wrap with a hanging indent under the description column. Paragraphs split on blank lines and reflow; a line starting with whitespace prints verbatim.
|
|
200
|
-
- An example written `"prod # ship to production"` renders the part after the first `#` surrounded by spaces as its description.
|