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.
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.
data/exe/rubycli ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'rubycli'
5
+
6
+ exit Rubycli::CommandLine.run(ARGV)