rubycli 0.1.1
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 +7 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE +21 -0
- data/README.ja.md +404 -0
- data/README.md +399 -0
- data/exe/rubycli +6 -0
- data/lib/rubycli/argument_parser.rb +343 -0
- data/lib/rubycli/cli.rb +341 -0
- data/lib/rubycli/command_line.rb +116 -0
- data/lib/rubycli/documentation_registry.rb +836 -0
- data/lib/rubycli/environment.rb +77 -0
- data/lib/rubycli/eval_coercer.rb +42 -0
- data/lib/rubycli/help_renderer.rb +298 -0
- data/lib/rubycli/json_coercer.rb +32 -0
- data/lib/rubycli/result_emitter.rb +41 -0
- data/lib/rubycli/type_utils.rb +128 -0
- data/lib/rubycli/types.rb +16 -0
- data/lib/rubycli/version.rb +5 -0
- data/lib/rubycli.rb +406 -0
- metadata +65 -0
data/README.md
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Rubycli β Python Fire-inspired CLI for Ruby
|
|
2
|
+
|
|
3
|
+
Rubycli turns existing Ruby classes and modules into CLIs by reading their documentation comments. It is inspired by [Python Fire](https://github.com/google/python-fire) but is not a drop-in port or an official project; the focus here is Rubyβs documentation conventions and type annotations.
|
|
4
|
+
|
|
5
|
+
> π―π΅ Japanese documentation is available in [README.ja.md](README.ja.md).
|
|
6
|
+
|
|
7
|
+
### 1. Existing Ruby script (Rubycli unaware)
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# hello_app.rb
|
|
11
|
+
module HelloApp
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def greet(name)
|
|
15
|
+
puts "Hello, #{name}!"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> Try it yourself: this repository ships with `examples/hello_app.rb`, so from the project root you can run `rubycli examples/hello_app.rb` to explore the generated commands.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
rubycli examples/hello_app.rb
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
Usage: hello_app.rb COMMAND [arguments]
|
|
28
|
+
|
|
29
|
+
Available commands:
|
|
30
|
+
Class methods:
|
|
31
|
+
greet <name>
|
|
32
|
+
|
|
33
|
+
Detailed command help: hello_app.rb COMMAND help
|
|
34
|
+
Enable debug logging: --debug or RUBYCLI_DEBUG=true
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
rubycli examples/hello_app.rb greet
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
Error: wrong number of arguments (given 0, expected 1)
|
|
43
|
+
Usage: hello_app.rb greet <NAME>
|
|
44
|
+
|
|
45
|
+
Positional arguments:
|
|
46
|
+
NAME
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
rubycli examples/hello_app.rb greet Hanako
|
|
51
|
+
#=> Hello, Hanako!
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Running `rubycli examples/hello_app.rb --help` prints the same summary as invoking it without a command.
|
|
55
|
+
|
|
56
|
+
### 2. Add documentation hints for richer flags
|
|
57
|
+
|
|
58
|
+
> Still no `require "rubycli"` needed; comments alone drive option parsing and help text.
|
|
59
|
+
|
|
60
|
+
**Concise placeholder style**
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# hello_app.rb
|
|
64
|
+
module HelloApp
|
|
65
|
+
module_function
|
|
66
|
+
|
|
67
|
+
# NAME [String] Name to greet
|
|
68
|
+
# --shout [Boolean] Print in uppercase
|
|
69
|
+
def greet(name, shout: false)
|
|
70
|
+
message = "Hello, #{name}!"
|
|
71
|
+
message = message.upcase if shout
|
|
72
|
+
puts message
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**YARD-style tags work too**
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# hello_app.rb
|
|
81
|
+
module HelloApp
|
|
82
|
+
module_function
|
|
83
|
+
|
|
84
|
+
# @param name [String] Name to greet
|
|
85
|
+
# @param shout [Boolean] Print in uppercase
|
|
86
|
+
def greet(name, shout: false)
|
|
87
|
+
message = "Hello, #{name}!"
|
|
88
|
+
message = message.upcase if shout
|
|
89
|
+
puts message
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> The documented variant lives at `examples/hello_app_with_docs.rb` if you want to follow along locally.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
rubycli examples/hello_app_with_docs.rb
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
Usage: hello_app_with_docs.rb COMMAND [arguments]
|
|
102
|
+
|
|
103
|
+
Available commands:
|
|
104
|
+
Class methods:
|
|
105
|
+
greet <name> [--shout=<value>]
|
|
106
|
+
|
|
107
|
+
Detailed command help: hello_app_with_docs.rb COMMAND help
|
|
108
|
+
Enable debug logging: --debug or RUBYCLI_DEBUG=true
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
rubycli examples/hello_app_with_docs.rb greet --help
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
Usage: hello_app_with_docs.rb greet <NAME> [--shout]
|
|
117
|
+
|
|
118
|
+
Positional arguments:
|
|
119
|
+
NAME [String] Name to greet
|
|
120
|
+
|
|
121
|
+
Options:
|
|
122
|
+
--shout [Boolean] Print in uppercase (default: false)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
rubycli examples/hello_app_with_docs.rb greet --shout Hanako
|
|
127
|
+
#=> HELLO, HANAKO!
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Need to keep a helper off the CLI? Define it as `private` on the singleton class:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
module HelloApp
|
|
134
|
+
class << self
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def internal_ping(url)
|
|
138
|
+
# not exposed as a CLI command
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 3. (Optional) Embed the runner inside your script
|
|
145
|
+
|
|
146
|
+
Prefer to launch via `ruby hello_app.rb ...`? Require the gem and delegate to `Rubycli.run` (see Quick start below).
|
|
147
|
+
|
|
148
|
+
## Project Philosophy
|
|
149
|
+
|
|
150
|
+
- **Convenience first** β The goal is to wrap existing Ruby scripts in a CLI with almost no manual plumbing. Fidelity with Python Fire is not a requirement.
|
|
151
|
+
- **Inspired, not a port** β We borrow ideas from Python Fire, but we do not aim for feature parity. Missing Fire features are generally βby design.β
|
|
152
|
+
- **Comments enrich, code decides** β Method signatures remain the source of truth; optional documentation comments add richer help output and can surface warnings when you opt into strict mode (`RUBYCLI_STRICT=ON`).
|
|
153
|
+
- **Lightweight maintenance** β Much of the implementation was generated with AI assistance; contributions that diverge into deep Ruby metaprogramming are out of scope. Please discuss expectations before opening parity PRs.
|
|
154
|
+
|
|
155
|
+
## Features
|
|
156
|
+
|
|
157
|
+
- Comment-aware CLI generation with both YARD-style tags and concise placeholders
|
|
158
|
+
- Automatic option signature inference (`NAME [Type] Descriptionβ¦`) without extra DSLs
|
|
159
|
+
- Optional JSON coercion for arguments passed via `--json-args`
|
|
160
|
+
- Optional pre-script hook (`--pre-script` / `--init`) to evaluate Ruby and expose the resulting object
|
|
161
|
+
- Opt-in strict mode (`RUBYCLI_STRICT=ON`) that emits warnings whenever comments contradict method signatures
|
|
162
|
+
|
|
163
|
+
## How it differs from Python Fire
|
|
164
|
+
|
|
165
|
+
- **Comment-aware help** β Rubycli leans on doc comments when present but still reflects the live method signature, keeping code as the ultimate authority.
|
|
166
|
+
- **Type-aware parsing** β Placeholder syntax (`NAME [String]`) and YARD tags let Rubycli coerce arguments to booleans, arrays, numerics, etc. without additional code.
|
|
167
|
+
- **Strict validation** β Opt-in strict mode surfaces warnings when comments fall out of sync with method signatures, helping teams keep help text accurate.
|
|
168
|
+
- **Ruby-centric tooling** β Supports Ruby-specific conventions such as optional keyword arguments, block documentation (`@yield*` tags), and `RUBYCLI_*` environment toggles.
|
|
169
|
+
|
|
170
|
+
| Capability | Python Fire | Rubycli |
|
|
171
|
+
| ---------- | ----------- | -------- |
|
|
172
|
+
| Attribute traversal | Recursively exposes attributes/properties on demand | Exposes public methods defined on the target; no implicit traversal |
|
|
173
|
+
| Constructor handling | Automatically prompts for `__init__` args when instantiating classes | Requires explicit `--new` plus comment docs; no automatic prompting |
|
|
174
|
+
| Interactive shell | Offers Fire-specific REPL when invoked without command | No interactive shell mode; strictly command execution |
|
|
175
|
+
| Input discovery | Pure reflection, no doc comments required | Doc comments drive option names, placeholders, and validation |
|
|
176
|
+
| Data structures | Dictionaries / lists become subcommands by default | Focused on class or module methods; no automatic dict/list expansion |
|
|
177
|
+
|
|
178
|
+
## Installation
|
|
179
|
+
|
|
180
|
+
The library is not published on RubyGems yet. Clone the repository and point Bundler to the local path, or build a `.gem` once the `.gemspec` is added.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
git clone https://github.com/inakaegg/rubycli.git
|
|
184
|
+
cd rubycli
|
|
185
|
+
# gem build rubycli.gemspec
|
|
186
|
+
gem build rubycli.gemspec
|
|
187
|
+
gem install rubycli-<version>.gem
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Bundler example:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# Gemfile
|
|
194
|
+
gem "rubycli", path: "path/to/rubycli"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Quick start (embed Rubycli in the script)
|
|
198
|
+
|
|
199
|
+
Step 3 adds `require "rubycli"` so the script can invoke the CLI directly:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# hello_app.rb
|
|
203
|
+
require "rubycli"
|
|
204
|
+
|
|
205
|
+
module HelloApp
|
|
206
|
+
module_function
|
|
207
|
+
|
|
208
|
+
# NAME [String] Name to greet
|
|
209
|
+
# --shout [Boolean] Print in uppercase
|
|
210
|
+
# => [String] Printed message
|
|
211
|
+
def greet(name, shout: false)
|
|
212
|
+
message = "Hello, #{name}!"
|
|
213
|
+
message = message.upcase if shout
|
|
214
|
+
puts message
|
|
215
|
+
message
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
Rubycli.run(HelloApp)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Run it:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
ruby hello_app.rb greet Taro
|
|
226
|
+
#=> Hello, Taro!
|
|
227
|
+
|
|
228
|
+
ruby hello_app.rb greet Taro --shout
|
|
229
|
+
#=> HELLO, TARO!
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
To launch the same file without adding `require "rubycli"`, use the bundled executable:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
rubycli path/to/hello_app.rb greet --shout Hanako
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
When you omit `CLASS_OR_MODULE`, Rubycli now infers it from the file name and even locates nested constants such as `Module1::Inner::Runner`. Return values are printed by default when you run the bundled CLI.
|
|
239
|
+
|
|
240
|
+
Need to target a different constant explicitly? Provide it after the file path:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
rubycli scripts/multi_runner.rb Admin::Runner list --active
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
This is useful when a file defines multiple candidates or when you want a nested constant that does not match the file name.
|
|
247
|
+
|
|
248
|
+
## Comment syntax
|
|
249
|
+
|
|
250
|
+
Rubycli parses a hybrid format β you can stick to familiar YARD tags or use short forms.
|
|
251
|
+
|
|
252
|
+
| Purpose | YARD-compatible | Concise form |
|
|
253
|
+
| ------- | --------------- | ------------ |
|
|
254
|
+
| Positional argument | `@param name [Type] Description` | `NAME [Type] Description` (`NAME` must be uppercase) |
|
|
255
|
+
| Keyword option | Same as above | `--flag -f FLAG [Type] Description` |
|
|
256
|
+
| Return value | `@return [Type] Description` | `=> [Type] Description` |
|
|
257
|
+
|
|
258
|
+
Types accept `String`, `Integer`, `String[]`, `Array<String>`, union `String | nil`, etc. Optional placeholders like `[VALUE]` or `[VALUE...]` let Rubycli infer boolean flags, optional values, and list coercion. When you omit the type on an uppercase placeholder (for example `--quiet`), Rubycli infers a Boolean flag automatically.
|
|
259
|
+
|
|
260
|
+
Common inference rules:
|
|
261
|
+
|
|
262
|
+
- Writing a bare uppercase placeholder such as `ARG1` (without `[String]`) makes Rubycli treat it as a `String`.
|
|
263
|
+
- Using that placeholder in an option line (`--name ARG1`) also infers a `String`.
|
|
264
|
+
- Omitting the placeholder entirely (`--verbose`) produces a Boolean flag.
|
|
265
|
+
|
|
266
|
+
Other YARD tags such as `@example`, `@raise`, `@see`, and `@deprecated` are currently ignored by the CLI renderer.
|
|
267
|
+
|
|
268
|
+
YARD-style `@param` annotations continue to work out of the box. If you want to enforce the concise placeholder syntax exclusively, set `RUBYCLI_ALLOW_PARAM_COMMENT=OFF` (strict mode still applies either way).
|
|
269
|
+
|
|
270
|
+
### When docs are missing or incomplete
|
|
271
|
+
|
|
272
|
+
Rubycli always trusts the live method signature. If a parameter (or option) is undocumented, the CLI still exposes it using the parameter name and default values inferred from the method definition:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
# fallback_example.rb
|
|
276
|
+
module FallbackExample
|
|
277
|
+
module_function
|
|
278
|
+
|
|
279
|
+
# AMOUNT [Integer] Base amount to process
|
|
280
|
+
def scale(amount, factor = 2, clamp: nil, notify: false)
|
|
281
|
+
result = amount * factor
|
|
282
|
+
result = [result, clamp].min if clamp
|
|
283
|
+
puts "Scaled to #{result}" if notify
|
|
284
|
+
result
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
rubycli examples/fallback_example.rb
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```text
|
|
294
|
+
Usage: fallback_example.rb COMMAND [arguments]
|
|
295
|
+
|
|
296
|
+
Available commands:
|
|
297
|
+
Class methods:
|
|
298
|
+
scale <amount> [<factor>] [--clamp=<value>] [--notify=<value>]
|
|
299
|
+
|
|
300
|
+
Detailed command help: fallback_example.rb COMMAND help
|
|
301
|
+
Enable debug logging: --debug or RUBYCLI_DEBUG=true
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
rubycli examples/fallback_example.rb scale --help
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```text
|
|
309
|
+
Usage: fallback_example.rb scale <AMOUNT> [<FACTOR>] [--clamp=<value>] [--notify]
|
|
310
|
+
|
|
311
|
+
Positional arguments:
|
|
312
|
+
AMOUNT [Integer] Base amount to process
|
|
313
|
+
[FACTOR] (default: 2)
|
|
314
|
+
|
|
315
|
+
Options:
|
|
316
|
+
--clamp CLAMP (type: String) (default: nil)
|
|
317
|
+
--notify (type: Boolean) (default: false)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Here only `AMOUNT` is documented, yet `factor`, `clamp`, and `notify` are still presented with sensible defaults and inferred types. Enable strict mode (`RUBYCLI_STRICT=ON`) if you want mismatches between comments and signatures to surface as warnings during development.
|
|
321
|
+
|
|
322
|
+
#### What if the docs mention arguments that do not exist?
|
|
323
|
+
|
|
324
|
+
- **Out-of-sync lines fall back to plain text** β Comments that reference non-existent options (for example `--ghost`) or positionals (such as `EXTRA`) are emitted verbatim in the helpβs detail section. They do not materialize as real arguments, and strict mode still warns about positional mismatches (`Extra positional argument comments were found: EXTRA`) so you can reconcile the docs.
|
|
325
|
+
|
|
326
|
+
> Want to see this behaviour? Try `rubycli examples/fallback_example_with_extra_docs.rb scale --help` for a runnable mismatch demo.
|
|
327
|
+
|
|
328
|
+
In short, comments never add live parameters by themselves; they enrich or describe what your method already supports.
|
|
329
|
+
|
|
330
|
+
## JSON mode
|
|
331
|
+
|
|
332
|
+
Supply `--json-args` when invoking the runner and Rubycli will parse subsequent arguments as JSON before passing them to your method:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
rubycli --json-args my_cli.rb MyCLI run '["--config", "{\"foo\":1}"]'
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Programmatically you can call `Rubycli.with_json_mode(true) { β¦ }`.
|
|
339
|
+
|
|
340
|
+
## Eval mode
|
|
341
|
+
|
|
342
|
+
Use `--eval-args` to evaluate Ruby expressions before they are forwarded to your CLI. This is handy when you want to pass rich objects that are awkward to express as JSON:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
rubycli --eval-args scripts/data_cli.rb DataCLI run '(1..10).to_a'
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Under the hood Rubycli evaluates each argument inside an isolated binding (`Object.new.instance_eval { binding }`). Treat this as unsafe input: do not enable it for untrusted callers. The mode can also be toggled programmatically via `Rubycli.with_eval_mode(true) { β¦ }`.
|
|
349
|
+
|
|
350
|
+
`--eval-args` and `--json-args` are mutually exclusive; Rubycli will raise an error if both are present.
|
|
351
|
+
|
|
352
|
+
## Pre-script bootstrap
|
|
353
|
+
|
|
354
|
+
Add `--pre-script SRC` (alias: `--init`) when launching the bundled CLI to run arbitrary Ruby code before exposing methods. The code runs inside an isolated binding where the following locals are pre-populated:
|
|
355
|
+
|
|
356
|
+
- `target` β the original class or module (before `--new` instantiation)
|
|
357
|
+
- `current` / `instance` β the object that would otherwise be exposed (after `--new` if specified)
|
|
358
|
+
|
|
359
|
+
The last evaluated value becomes the new public target. Returning `nil` keeps the previous object.
|
|
360
|
+
|
|
361
|
+
Inline example:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
rubycli --pre-script 'InitArgRunner.new(source: "cli", retries: 2)' \
|
|
365
|
+
lib/init_arg_runner.rb summarize --verbose
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
File example:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# scripts/bootstrap_runner.rb
|
|
372
|
+
instance = InitArgRunner.new(source: "preset")
|
|
373
|
+
instance.logger = Logger.new($stdout)
|
|
374
|
+
instance
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
rubycli --pre-script scripts/bootstrap_runner.rb \
|
|
379
|
+
lib/init_arg_runner.rb summarize --verbose
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
This keeps `--new` available for quick zero-argument instantiation while allowing richer bootstrapping when needed.
|
|
383
|
+
|
|
384
|
+
## Environment variables & flags
|
|
385
|
+
|
|
386
|
+
| Flag / Env | Description | Default |
|
|
387
|
+
| ---------- | ----------- | ------- |
|
|
388
|
+
| `--debug` / `RUBYCLI_DEBUG=true` | Print debug logs | `false` |
|
|
389
|
+
| `RUBYCLI_STRICT=ON` | Enable strict mode validation (prints warnings on comment/signature drift) | `OFF` |
|
|
390
|
+
| `RUBYCLI_ALLOW_PARAM_COMMENT=OFF` | Disable legacy `@param` lines (defaults to on today for compatibility) | `ON` |
|
|
391
|
+
|
|
392
|
+
## Library helpers
|
|
393
|
+
|
|
394
|
+
- `Rubycli.parse_arguments(argv, method)` β parse argv with comment metadata
|
|
395
|
+
- `Rubycli.available_commands(target)` β list CLI exposable methods
|
|
396
|
+
- `Rubycli.usage_for_method(name, method)` β render usage for a single method
|
|
397
|
+
- `Rubycli.method_description(method)` β fetch structured documentation info
|
|
398
|
+
|
|
399
|
+
Feedback and issues are welcome while we prepare the public release.
|