rubycli 0.1.4 → 0.1.5

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: a71a438637363ea54e22377b645dc99a26a0c91692571bf57a68368403e32308
4
- data.tar.gz: 367b354b2ffde5ee80e7ee3ee5b2858f15cbf8097cc8346fd812b780f41443e2
3
+ metadata.gz: 9287ea2ca51772eb53c50fe717e648c7d23a9fb1e5cb0e25990eef7f07cde225
4
+ data.tar.gz: aab6e71f2beafb924bb4bc29689c6ce64391d55bc43936a789278ad764e91592
5
5
  SHA512:
6
- metadata.gz: 5eb055a5cbaa3daf0d311b67922a0c9bb7dbb8a75c2844524dc21f5faed232f560c8f1aaea6caf96737fea0b979625813b1ea5cc6c589ecee405b994b07ee16f
7
- data.tar.gz: f64107c28083a96674970f8414090e47b77a749f8d3c9393ff9ca972a747ebe36a5da128d1c75a313ca570631436509392985477d1eefe271722eb4b046c69a8
6
+ metadata.gz: 98baa8a62366c9babedda68174dbc1fadaafbdf1ea4354393bf1a525b9b1d1f2ce0459f20ef8aff81cc4a3448f0686da9bf7940a8fde2526a3b42caf3d2feb63
7
+ data.tar.gz: ac5044eec17148e3bb26ef1aa2fd67b78e6578acd6693747bb9a95e8d043e1842f9ebc853e6e50297130d0351fdfb9cfad04c1d217738c6462eda94c1c5b039f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.5] - 2025-11-10
4
+
5
+ ### Added
6
+ - `rubycli --check` gained a short `-c` alias and refuses to run target commands while linting, making documentation checks easier to script.
7
+ - Bundled `examples/strict_choices_demo.rb` and `examples/typed_arguments_demo.rb` now illustrate literal choice validation and stdlib type coercions.
8
+
9
+ ### Changed
10
+ - Runtime validation now reads the documented literal choices and inferred types for both positional and keyword arguments; invalid values emit `[WARN] …` guidance by default and raise `Rubycli::ArgumentError` under `--strict` with friendly suggestions.
11
+ - Help output renders positional arguments and options as structured tables showing requirement level, types, defaults, and descriptions for quicker scanning.
12
+ - Documentation comments are aligned with the actual method signature, and mismatches surface with file/line context during `rubycli --check`.
13
+ - CLI warnings/errors are prefixed with `[WARN]` / `[ERROR]`, and the deprecated `--debug` flag was removed in favor of `RUBYCLI_DEBUG=true`.
14
+
15
+ ### Fixed
16
+ - `--check` now rejects forwarded CLI arguments as well as JSON/Eval modes, ensuring documentation linting never executes user code and always starts from a clean issue list.
17
+ - Placeholder parsing keeps option descriptions such as `--prefix` in the documentation showcase so README snippets and live behavior stay in sync.
18
+
3
19
  ## [0.1.4] - 2025-11-08
4
20
 
5
21
  ### Changed
@@ -7,6 +23,9 @@
7
23
 
8
24
  > _Note:_ 0.1.3 was yanked before general availability; consumers should upgrade directly to 0.1.4.
9
25
 
26
+ ### Documentation
27
+ - Clarified repeated-value guidance (type enforcement, eval mode workflows) and updated both READMEs to reflect the retirement of `(type: …)` annotations while preserving the historical changelog entry.
28
+
10
29
  ## [0.1.3] - 2025-11-08
11
30
 
12
31
  ### Added
data/README.ja.md CHANGED
@@ -33,7 +33,6 @@ Available commands:
33
33
  greet <NAME>
34
34
 
35
35
  Detailed command help: hello_app.rb COMMAND help
36
- Enable debug logging: --debug or RUBYCLI_DEBUG=true
37
36
  ```
38
37
 
39
38
  ```bash
@@ -107,7 +106,6 @@ Available commands:
107
106
  greet <NAME> [--shout]
108
107
 
109
108
  Detailed command help: hello_app_with_docs.rb COMMAND help
110
- Enable debug logging: --debug or RUBYCLI_DEBUG=true
111
109
  ```
112
110
 
113
111
  ```bash
@@ -158,13 +156,13 @@ Rubycli は「ファイル名を CamelCase にした定数」を公開対象だ
158
156
 
159
157
  大規模なコードベースでも安全側を保ちながら、どうしても自動選択したいときだけ 1 フラグで切り替えられます。
160
158
 
161
- > **インスタンスメソッド専用のクラスについて** – 公開メソッドがインスタンス側(`attr_reader` `def greet`)にしか無い場合は、`--new` を付けて事前にインスタンス化しないと CLI から呼び出せません。クラスメソッドを 1 つ用意するか、`--new` を明示して実行してください。
159
+ > **インスタンスメソッド専用のクラスについて** – 公開メソッドがインスタンス側(`def greet` など)にしか無い場合は、`--new` を付けて事前にインスタンス化しないと CLI から呼び出せません。クラスメソッドを 1 つ用意するか、`--new` を明示して実行してください。
162
160
 
163
161
  ## 開発方針
164
162
 
165
163
  - **便利さが最優先** – 既存の Ruby スクリプトを最小の手間で CLI 化できることを目的にしており、Python Fire の完全移植は目指していません。
166
164
  - **インスパイアであってポートではない** – アイデアの出自は Fire ですが、同等機能を揃える予定は基本的にありません。Fire 由来の未実装機能は仕様です。
167
- - **メソッド定義が土台、コメントが挙動を補強** – 公開メソッドのシグネチャが CLI に露出する範囲と必須/任意を決めますが、コメントに `TAG...` や `[Integer]` を書くと同じ引数でも配列化や型変換が行われます。コメントと実装のズレを観測したいときだけ `RUBYCLI_STRICT=ON` で厳格モードを有効化し、警告を受け取ります。
165
+ - **メソッド定義が土台、コメントが挙動を補強** – 公開メソッドのシグネチャが CLI に露出する範囲と必須/任意を決めますが、コメントに `TAG...` や `[Integer]` を書くと同じ引数でも配列化や型変換が行われます。さらに Rubycli は `--names='["Alice","Bob"]'` のような JSON/YAML らしい入力を自動的に安全なリテラルとして評価します。`rubycli --check パス/対象.rb` でコメントと実装のズレを検査しつつ、通常実行時に `--strict` を付ければドキュメント通りでない入力をその場でエラーにできます。(現状の `--check` は「コメントが揃っているか」を中心にチェックしており、`Booalean` のように型トークンを誤記しても検出できません。)
168
166
  - **軽量メンテナンス** – 実装の多くは AI 支援で作られており、深い Ruby メタプログラミングを伴う大規模拡張は想定外です。Fire 互換を求める PR は事前相談をお願いします。
169
167
 
170
168
  ## 特徴
@@ -173,13 +171,13 @@ Rubycli は「ファイル名を CamelCase にした定数」を公開対象だ
173
171
  - YARD 形式と `NAME [Type] 説明…` の簡潔記法を同時サポート
174
172
  - 引数はデフォルトで安全なリテラルとして解釈し、必要に応じて厳格 JSON モードや Ruby eval モードを切り替え可能
175
173
  - `--pre-script`(エイリアス: `--init`)で任意の Ruby コードを評価し、その結果オブジェクトを公開
176
- - `RUBYCLI_STRICT=ON` で有効化できる厳格モードにより、コメントとシグネチャの矛盾を警告として検知可能
174
+ - `--check` でコメント整合性を lint、`--strict` で入力値をドキュメント通りに強制する二段構えのガード
177
175
 
178
176
  ## Python Fire との違い
179
177
 
180
178
  - **コメント対応のヘルプ生成**: コメントがあればヘルプに反映しつつ、最終的な判断は常にライブなメソッド定義に基づきます。
181
179
  - **型に基づく解析**: `NAME [String]` や YARD タグから型を推論し、真偽値・配列・数値などを自動変換します。
182
- - **厳密な整合性チェック**: 厳格モードを有効にすれば、コメントとメソッド定義が食い違う際に警告を出して保守性を高められます。
180
+ - **厳密な整合性チェック**: `rubycli --check` でコメントと実装のズレを実行前に検査し、通常実行時に `--strict` を付ければドキュメントで宣言した型・許可値以外の入力を拒否できます。
183
181
  - **Ruby 向け拡張**: キーワード引数やブロック (`@yield*`) といった Ruby 固有の構文に合わせたパーサや `RUBYCLI_*` 環境変数を用意しています。
184
182
 
185
183
  | 機能 | Python Fire | Rubycli |
@@ -284,15 +282,62 @@ README のサンプルは既定スタイルとして大文字プレースホル
284
282
 
285
283
  > 補足: コメント内で任意引数を角括弧で表す必要はありません。Ruby 側のメソッドシグネチャから必須/任意は自動判定され、ヘルプ出力では Rubycli が適切に角括弧を追加します。
286
284
 
287
- 型ヒントは `[String]`, `(String)`, `(type: String)` のように角括弧・丸括弧・`type:` プレフィックスのいずれでも指定できます。複数型は `(String, nil)` `(type: String, nil)` のように列挙してください。
285
+ 型ヒントは `[String]` `(String)` のように角括弧/丸括弧で指定できます。複数型は `(String, nil)` のように列挙してください。
288
286
 
289
- `VALUE...` のような繰り返し指定(`TAG...` など)や、`[String[]]` / `Array<String>` といった配列型の注釈が付いたオプションは配列として扱われます。JSON/YAML 形式のリスト(例: `--tags '["build","test"]'`)を渡すか、カンマ区切り文字列(`--tags "build,test"`)を渡すことで配列に変換されます。スペース区切りの複数値入力(`--tags build test`)にはまだ対応しておらず、繰り返し注記のないオプションは従来どおりスカラーとして扱われます。
287
+ `VALUE...` のような繰り返し指定(`TAG...` など)や、`[String[]]` / `Array<String>` といった配列型の注釈が付いたオプションは配列として扱われます。JSON/YAML 形式のリスト(例: `--tags '["build","test"]'`)を渡すか、カンマ区切り文字列(`--tags "build,test"`)を渡すことで配列に変換されます。スペース区切りの複数値入力(`--tags build test`)にはまだ対応しておらず、繰り返し注記のないオプションは従来どおりスカラーとして扱われます。`--strict` 実行時は各要素の型も検証されるため、`[String[]]` と書かれているのに `--tags [1,2]` のような数値配列を渡すと即エラーになります。
288
+
289
+ JSON やカンマ区切りで表現しづらいシンボル配列・ハッシュなどを渡したい場合は eval モード(`--eval-args`/`-e` または `--eval-lax`/`-E`)を有効にし、ドキュメントで宣言した型に合わせた Ruby リテラルを渡してください。スペース区切りが未対応でも、安全に複数選択を指定できます(後述の eval 例を参照)。
290
290
 
291
291
  代表的な推論例:
292
292
 
293
293
  - `ARG1` のように型ラベルを省略したプレースホルダは既定で `String` として扱われます。
294
294
  - `--name ARG1` のようにオプションへプレースホルダだけを指定しても同じく `String` が推論されます。
295
295
  - `--verbose` のように値プレースホルダを省略したオプションは Boolean フラグとして扱われます。
296
+ - 位置引数を Boolean にしたい場合は必ず `[Boolean]` を明示してください。`NAME 説明` や `@param name 説明` のように型を省略すると、Ruby 側のデフォルト値に関わらず `String` とみなされます。
297
+
298
+ ### リテラル列挙による制約
299
+
300
+ `--format MODE [:json, :yaml, :auto]` や `LEVEL [:info, :warn]` のように型注釈内へ許容リテラルを列挙すると、ヘルプに選択肢を表示しつつ Rubycli が入力制約として解釈します。シンボル・文字列(裸の単語も可)・真偽値・数値・`nil` に対応し、型ヒントと混在させて `--channel TARGET [:stdout, :stderr, Boolean]` のような宣言も書けます。`%i[info warn]` / `%w[debug info]` などの短縮記法も展開されるため、`LEVEL %i[info warn]` でも同じ効果になります。通常実行では許可外の入力に警告を表示して続行し、`--strict` を付けた場合は `Rubycli::ArgumentError` を送出して即座に停止します。
301
+
302
+ > シンボルと文字列は厳密に区別されます。`[:info, :warn]` と書いた場合は `:info` のようにコロン付きで入力してください。`["info", "warn"]` を選んだ場合はプレーンな文字列のみ受け付けます。
303
+
304
+ > 列挙は各スカラー値に適用されます。`[Symbol[]]` のような配列注釈に対して「許可される組み合わせ」をリテラルで書く構文(例: `[%i[foo bar][]]`)は未サポートなので、必要に応じて文章で説明するか、eval モードで Ruby の配列を渡してください。
305
+
306
+ ```bash
307
+ # literal choice デモ (examples/strict_choices_demo.rb)
308
+ ruby examples/strict_choices_demo.rb report warn --format json
309
+ #=> [WARN] format=json
310
+
311
+ # --strict を付けると仕様外の値で即エラー
312
+ ruby -Ilib exe/rubycli --strict examples/strict_choices_demo.rb report debug
313
+ #=> Rubycli::ArgumentError: Value "debug" for LEVEL is not allowed: allowed values are :info, :warn, :error
314
+ ```
315
+
316
+ ```bash
317
+ # シンボル入力はコロンを付ける
318
+ ruby -Ilib exe/rubycli --strict examples/strict_choices_demo.rb report :warn
319
+ #=> [WARN] format=text
320
+
321
+ ruby -Ilib exe/rubycli --strict examples/strict_choices_demo.rb report warn
322
+ #=> Rubycli::ArgumentError: Value "warn" for LEVEL is not allowed: allowed values are :info, :warn, :error
323
+ ```
324
+
325
+ ### 標準ライブラリ型ヒント
326
+
327
+ コメントに `Date` や `Time`, `BigDecimal`, `Pathname` など標準ライブラリの型名を書けば、Rubycli が必要な `require` を行った上で CLI 引数をその型へ変換します。
328
+
329
+ ```bash
330
+ # examples/typed_arguments_demo.rb より
331
+ ruby examples/typed_arguments_demo.rb ingest \
332
+ --date 2024-12-25 \
333
+ --moment 2024-12-25T10:00:00Z \
334
+ --budget 123.45 \
335
+ --input ./data/input.csv
336
+ ```
337
+
338
+ ハンドラ側には `Date` / `Time` / `BigDecimal` / `Pathname` のインスタンスがそのまま渡るため、追加のパース処理は不要です。
339
+
340
+ 各オプションには既定値があるため、`ruby examples/typed_arguments_demo.rb ingest --budget 999.99` のように個別の型だけ試すこともできます。
296
341
 
297
342
  `@example` や `@raise`, `@see`, `@deprecated` などその他の YARD タグは、現状ヘルプ出力には反映されません。
298
343
 
@@ -331,7 +376,6 @@ Available commands:
331
376
  scale AMOUNT [<FACTOR>] [--clamp=<value>] [--notify]
332
377
 
333
378
  Detailed command help: fallback_example.rb COMMAND help
334
- Enable debug logging: --debug or RUBYCLI_DEBUG=true
335
379
  ```
336
380
 
337
381
  ```bash
@@ -350,7 +394,7 @@ Options:
350
394
  --notify [Boolean] optional (default: false)
351
395
  ```
352
396
 
353
- `AMOUNT` だけがドキュメント化されていますが、`factor` や `clamp`, `notify` も自動的に補完され、既定値や型が推論されていることがわかります。コメントとシグネチャの矛盾を早期に検知したい場合は `RUBYCLI_STRICT=ON` で厳格モードを有効化してください。
397
+ `AMOUNT` だけがドキュメント化されていますが、`factor` や `clamp`, `notify` も自動的に補完され、既定値や型が推論されていることがわかります。開発時は `rubycli --check 対象.rb` でコメントとシグネチャの矛盾を検出し、本番実行で `--strict` を付ければ仕様外の入力をその場で弾けます。
354
398
 
355
399
  #### 存在しない引数やオプションをコメントに書いた場合
356
400
 
@@ -387,6 +431,14 @@ YAML 固有の書き方は拒否され、無効な JSON であれば `JSON::Pars
387
431
 
388
432
  `--eval-args`(短縮形 `-e`)を使うと、後続の引数を Ruby コードとして評価した結果を CLI に渡せます。JSON や YAML では表現しづらいオブジェクトを扱いたいときに便利ですが、評価は `Object.new.instance_eval { binding }` 上で行われるため、信頼できる入力に限定してください。コード内では `Rubycli.with_eval_mode(true) { … }` で切り替えられます。
389
433
 
434
+ Ruby 評価はシンボルや配列/ハッシュもそのまま扱えるため、列挙値の組み合わせをオプションへ渡すときにも役立ちます。
435
+
436
+ ```bash
437
+ rubycli -E scripts/report_runner.rb publish \
438
+ --targets '[:marketing, :sales]' \
439
+ --channels '[:email, :slack]'
440
+ ```
441
+
390
442
  Ruby 評価を使いつつ、構文エラーが出たときは元の文字列にフォールバックさせたい場合は `--eval-lax`(短縮形 `-E`)を指定します。`--eval-args` と同じく eval モードを有効にしますが、Ruby として解釈できなかったトークン(例: 素の `https://example.com`)は警告を出した上でそのまま渡すため、`60*60*24*14` のような式と文字列を気軽に混在させられます。
391
443
 
392
444
  `--json-args`/`-j` は `--eval-args`/`-e` および `--eval-lax`/`-E` と同時指定できません。どのモードも既定のリテラル解析を拡張する位置づけなので、用途に応じて厳格な JSON か Ruby eval(通常/lax)のいずれかを選択してください。
@@ -427,8 +479,9 @@ rubycli --pre-script scripts/bootstrap_runner.rb \
427
479
 
428
480
  | 変数 / フラグ | 説明 | 既定値 |
429
481
  | ------------- | ---- | ------ |
430
- | `--debug` / `RUBYCLI_DEBUG=true` | デバッグログ表示 | `false` |
431
- | `RUBYCLI_STRICT=ON` | 厳格モードを有効化(コメントとシグネチャの矛盾を警告) | `OFF` |
482
+ | `RUBYCLI_DEBUG=true` | デバッグログ表示 | `false` |
483
+ | `--check` | コメント/実装のズレを検査し、コマンドは実行しない | `off` |
484
+ | `--strict` | ドキュメントで許可した型・値以外をエラーとして拒否 | `off` |
432
485
  | `RUBYCLI_ALLOW_PARAM_COMMENT=OFF` | レガシーな `@param` 記法を無効化(互換性のため既定では ON) | `ON` |
433
486
 
434
487
  ## Rubycli API
data/README.md CHANGED
@@ -35,7 +35,6 @@ Available commands:
35
35
  greet <NAME>
36
36
 
37
37
  Detailed command help: hello_app.rb COMMAND help
38
- Enable debug logging: --debug or RUBYCLI_DEBUG=true
39
38
  ```
40
39
 
41
40
  ```bash
@@ -109,7 +108,6 @@ Available commands:
109
108
  greet <NAME> [--shout]
110
109
 
111
110
  Detailed command help: hello_app_with_docs.rb COMMAND help
112
- Enable debug logging: --debug or RUBYCLI_DEBUG=true
113
111
  ```
114
112
 
115
113
  ```bash
@@ -165,13 +163,13 @@ Rubycli assumes that the file name (CamelCased) matches the class or module you
165
163
 
166
164
  This keeps large projects safe by default but still provides a one-flag escape hatch when you prefer the fully automatic behaviour.
167
165
 
168
- > **Instance-only classes** – If a class only defines public *instance* methods (for example, it exposes functionality via `attr_reader` or `def greet` on the instance), you must run Rubycli with `--new` so the class is instantiated before commands are resolved. Otherwise Rubycli cannot see any CLI-callable methods. Add at least one public class method when you do not want to rely on `--new`.
166
+ > **Instance-only classes** – If a class only defines public *instance* methods (for example, it exposes functionality via `def greet` on the instance), you must run Rubycli with `--new` so the class is instantiated before commands are resolved. Otherwise Rubycli cannot see any CLI-callable methods. Add at least one public class method when you do not want to rely on `--new`.
169
167
 
170
168
  ## Project Philosophy
171
169
 
172
170
  - **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.
173
171
  - **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.”
174
- - **Method definitions first, comments augment behavior** – Public method signatures determine what gets exposed (and which arguments are required), while doc comments like `TAG...` or `[Integer]` can turn the very same CLI value into arrays, integers, booleans, etc. Enable strict mode (`RUBYCLI_STRICT=ON`) when you want warnings about mismatches.
172
+ - **Method definitions first, comments augment behavior** – Public method signatures determine what gets exposed (and which arguments are required), while doc comments like `TAG...` or `[Integer]` can turn the very same CLI value into arrays, integers, booleans, etc. Rubycli also auto-parses inputs that look like JSON/YAML literals (for example `--names='["Alice","Bob"]'`) before enforcing the documented type. Run `rubycli --check path/to/script.rb` to lint documentation mismatches, and pass `--strict` during normal runs when you want invalid input to abort instead of merely warning. (Current limitation: `--check` verifies that comments cover every argument, but it does not yet validate the spelling of type tokens—`Booalean` will be treated as a plain string.)
175
173
  - **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.
176
174
 
177
175
  ## Features
@@ -180,13 +178,13 @@ This keeps large projects safe by default but still provides a one-flag escape h
180
178
  - Automatic option signature inference (`NAME [Type] Description…`) without extra DSLs
181
179
  - Safe literal parsing out of the box (arrays / hashes / booleans) with opt-in strict JSON and Ruby eval modes
182
180
  - Optional pre-script hook (`--pre-script` / `--init`) to evaluate Ruby and expose the resulting object
183
- - Opt-in strict mode (`RUBYCLI_STRICT=ON`) that emits warnings whenever comments contradict method signatures
181
+ - Dedicated CLI flags for quality gates: `--check` lints documentation/comments without running commands, and `--strict` treats documented types/choices as hard requirements
184
182
 
185
183
  ## How it differs from Python Fire
186
184
 
187
185
  - **Comment-aware help** – Rubycli leans on doc comments when present but still reflects the live method signature, keeping code as the ultimate authority.
188
186
  - **Type-aware parsing** – Placeholder syntax (`NAME [String]`) and YARD tags let Rubycli coerce arguments to booleans, arrays, numerics, etc. without additional code.
189
- - **Strict validation** – Opt-in strict mode surfaces warnings when comments fall out of sync with method signatures, helping teams keep help text accurate.
187
+ - **Strict validation** – `rubycli --check` lint runs catch documentation drift before execution, while runtime `--strict` runs turn documented types/choices into enforceable contracts.
190
188
  - **Ruby-centric tooling** – Supports Ruby-specific conventions such as optional keyword arguments, block documentation (`@yield*` tags), and `RUBYCLI_*` environment toggles.
191
189
 
192
190
  | Capability | Python Fire | Rubycli |
@@ -293,15 +291,62 @@ The CLI treats `--flag VALUE`, `--flag <value>`, and `--flag=<value>` identicall
293
291
 
294
292
  > Tip: You do not need to wrap optional arguments in brackets inside the comment. Rubycli already knows which parameters are optional from the Ruby signature and will introduce the brackets in generated help.
295
293
 
296
- You can annotate types using `[String]`, `(String)`, or `(type: String)`—they all convey the same hint, and you can list multiple types such as `(String, nil)` or `(type: String, nil)`.
294
+ You can annotate types using `[String]` or `(String)`—they both convey the same hint, and you can list multiple types such as `(String, nil)`.
297
295
 
298
- Repeated values (`VALUE...`) now materialize as arrays automatically whenever the option is documented with an ellipsis (for example `TAG...`) or an explicit array type hint (`[String[]]`, `Array<String>`). Supply either JSON/YAML list syntax (`--tags "[\"build\",\"test\"]"`) or a comma-delimited string (`--tags "build,test"`); Rubycli will coerce both forms to arrays. Space-separated multi-value flags (`--tags build test`) are still not supported, and options without a repeated/array hint continue to be parsed as scalars.
296
+ Repeated values (`VALUE...`) now materialize as arrays automatically whenever the option is documented with an ellipsis (for example `TAG...`) or an explicit array type hint (`[String[]]`, `Array<String>`). Supply either JSON/YAML list syntax (`--tags "[\"build\",\"test\"]"`) or a comma-delimited string (`--tags "build,test"`); Rubycli will coerce both forms to arrays. Space-separated multi-value flags (`--tags build test`) are still not supported, and options without a repeated/array hint continue to be parsed as scalars. Strict mode still verifies each element against the documented type, so `--tags [1,2]` will fail when the docs say `[String[]]`.
297
+
298
+ Need to pass structures that are awkward to express as JSON (for example symbol arrays or hashes)? Enable eval mode (`--eval-args`/`-e` or `--eval-lax`/`-E`) and supply a Ruby literal that matches the documented type; the example in the eval section below shows how to pass multiple enum selections safely even though space-separated syntax remains unsupported.
299
299
 
300
300
  Common inference rules:
301
301
 
302
302
  - Writing a placeholder such as `ARG1` (without `[String]`) makes Rubycli treat it as a `String`.
303
303
  - Using that placeholder in an option line (`--name ARG1`) also infers a `String`.
304
304
  - Omitting the placeholder entirely (`--verbose`) produces a Boolean flag.
305
+ - Positional arguments only become booleans when you annotate `[Boolean]`; a bare `NAME Description` (or `@param name Description`) falls back to `String`, regardless of the Ruby default value.
306
+
307
+ ### Literal choices and enums
308
+
309
+ You can express a finite set of accepted values directly inside the type annotation, for example `--format MODE [:json, :yaml, :auto]` or `LEVEL [:info, :warn]`. Symbols, strings (including barewords), booleans, numbers, and `nil` are supported, and you can mix literal entries with broader types such as `--channel TARGET [:stdout, :stderr, Boolean]`. `%i[info warn]` / `%w[debug info]` short-hands expand as expected, so `LEVEL %i[info warn]` works the same as the explicit array form. Rubycli always records these choices in the generated help; when you run with `--strict`, any value outside the documented set results in `Rubycli::ArgumentError`, otherwise a warning is printed and execution proceeds.
310
+
311
+ > Symbols and strings are compared strictly. `[:info, :warn]` requires symbol inputs such as `:info`, while `["info", "warn"]` only accepts plain strings. Prefix a value with `:` at the CLI to pass a symbol.
312
+
313
+ > Literal enums currently apply to each scalar argument. If an option is documented as an array (for example `[Symbol[]]`), spell out the allowed members in prose for now—combined literal arrays such as `[%i[foo bar][]]` are not supported.
314
+
315
+ ```bash
316
+ # literal choices + booleans (see examples/strict_choices_demo.rb)
317
+ ruby examples/strict_choices_demo.rb report warn --format json
318
+ #=> [WARN] format=json
319
+
320
+ # the same command with --strict will abort when values drift
321
+ ruby -Ilib exe/rubycli --strict examples/strict_choices_demo.rb report debug
322
+ #=> Rubycli::ArgumentError: Value "debug" for LEVEL is not allowed: allowed values are :info, :warn, :error
323
+ ```
324
+
325
+ ```bash
326
+ # symbol values stay distinct
327
+ ruby -Ilib exe/rubycli --strict examples/strict_choices_demo.rb report :warn
328
+ #=> [WARN] format=text
329
+
330
+ ruby -Ilib exe/rubycli --strict examples/strict_choices_demo.rb report warn
331
+ #=> Rubycli::ArgumentError: Value "warn" for LEVEL is not allowed: allowed values are :info, :warn, :error
332
+ ```
333
+
334
+ ### Standard library type hints
335
+
336
+ Doc comments can reference standard classes such as `Date`, `Time`, `BigDecimal`, or `Pathname`. Rubycli loads the necessary stdlib files on demand and coerces CLI inputs using the documented types.
337
+
338
+ ```bash
339
+ # see examples/typed_arguments_demo.rb
340
+ ruby examples/typed_arguments_demo.rb ingest \
341
+ --date 2024-12-25 \
342
+ --moment 2024-12-25T10:00:00Z \
343
+ --budget 123.45 \
344
+ --input ./data/input.csv
345
+ ```
346
+
347
+ This command prints a normalized summary and the handler receives real `Date`, `Time`, `BigDecimal`, and `Pathname` objects without manual parsing.
348
+
349
+ Each option has a sensible default, so you can also experiment one at a time (for example `ruby examples/typed_arguments_demo.rb ingest --budget 999.99`).
305
350
 
306
351
  Other YARD tags such as `@example`, `@raise`, `@see`, and `@deprecated` are currently ignored by the CLI renderer.
307
352
 
@@ -340,7 +385,6 @@ Available commands:
340
385
  scale AMOUNT [<FACTOR>] [--clamp=<value>] [--notify]
341
386
 
342
387
  Detailed command help: fallback_example.rb COMMAND help
343
- Enable debug logging: --debug or RUBYCLI_DEBUG=true
344
388
  ```
345
389
 
346
390
  ```bash
@@ -359,7 +403,7 @@ Options:
359
403
  --notify [Boolean] optional (default: false)
360
404
  ```
361
405
 
362
- 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.
406
+ Here only `AMOUNT` is documented, yet `factor`, `clamp`, and `notify` are still presented with sensible defaults and inferred types. Run `rubycli --check path/to/script.rb` during development to surface mismatches between comments and signatures, and pass `--strict` when executing commands to enforce the documented types/choices.
363
407
 
364
408
  #### What if the docs mention arguments that do not exist?
365
409
 
@@ -395,6 +439,14 @@ rubycli -e scripts/data_cli.rb DataCLI run '(1..10).to_a'
395
439
 
396
440
  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) { … }`.
397
441
 
442
+ Because Ruby evaluation understands symbols, arrays, and hashes, it’s a convenient way to pass literal enum combinations to options that expect arrays:
443
+
444
+ ```bash
445
+ rubycli -E scripts/report_runner.rb publish \
446
+ --targets '[:marketing, :sales]' \
447
+ --channels '[:email, :slack]'
448
+ ```
449
+
398
450
  Need Ruby evaluation plus a safety net? Pass `--eval-lax` (or `-E`). It flips on eval mode just like `--eval-args`, but if Ruby fails to parse a token (for example, a bare `https://example.com`), Rubycli emits a warning and forwards the original string unchanged. This lets you mix inline math (`60*60*24*14`) with literal values without constantly juggling quotes.
399
451
 
400
452
  `--json-args`/`-j` cannot be combined with either `--eval-args`/`-e` or `--eval-lax`/`-E`; Rubycli will raise an error if both are present. Both modes augment the default literal parsing, so you can pick either strict JSON or one of the Ruby eval variants when the defaults are not enough.
@@ -435,8 +487,9 @@ This keeps `--new` available for quick zero-argument instantiation while allowin
435
487
 
436
488
  | Flag / Env | Description | Default |
437
489
  | ---------- | ----------- | ------- |
438
- | `--debug` / `RUBYCLI_DEBUG=true` | Print debug logs | `false` |
439
- | `RUBYCLI_STRICT=ON` | Enable strict mode validation (prints warnings on comment/signature drift) | `OFF` |
490
+ | `RUBYCLI_DEBUG=true` | Print debug logs | `false` |
491
+ | `--check` | Validate documentation/comments without executing commands | `off` |
492
+ | `--strict` | Enforce documented choices/types; invalid input aborts | `off` |
440
493
  | `RUBYCLI_ALLOW_PARAM_COMMENT=OFF` | Disable legacy `@param` lines (defaults to on today for compatibility) | `ON` |
441
494
 
442
495
  ## Library helpers
@@ -1,3 +1,5 @@
1
+ require 'did_you_mean'
2
+
1
3
  require_relative 'type_utils'
2
4
  require_relative 'arguments/token_stream'
3
5
  require_relative 'arguments/value_converter'
@@ -61,6 +63,14 @@ module Rubycli
61
63
  [pos_args, kw_args]
62
64
  end
63
65
 
66
+ def validate_inputs(method_obj, positional_args, keyword_args)
67
+ return unless method_obj
68
+
69
+ metadata = @documentation_registry.metadata_for(method_obj)
70
+ validate_positional_arguments(method_obj, metadata, positional_args)
71
+ validate_keyword_arguments(metadata, keyword_args)
72
+ end
73
+
64
74
  private
65
75
 
66
76
  def debug_log(message)
@@ -196,6 +206,235 @@ module Rubycli
196
206
  @value_converter.convert(arg)
197
207
  end
198
208
 
209
+ def validate_positional_arguments(method_obj, metadata, positional_args)
210
+ return if positional_args.nil? || positional_args.empty?
211
+
212
+ positional_map = metadata[:positionals_map] || {}
213
+ ordered_params = method_obj.parameters.select { |type, _| %i[req opt].include?(type) }
214
+
215
+ ordered_params.each_with_index do |(_, name), index|
216
+ definition = positional_map[name]
217
+ next unless definition
218
+ next if index >= positional_args.size
219
+
220
+ label = definition.label || definition.placeholder || name.to_s.upcase
221
+ enforce_value_against_definition(definition, positional_args[index], label)
222
+ end
223
+ end
224
+
225
+ def validate_keyword_arguments(metadata, keyword_args)
226
+ return if keyword_args.nil? || keyword_args.empty?
227
+
228
+ option_lookup = build_option_lookup(metadata[:options] || [])
229
+ keyword_args.each do |key, value|
230
+ definition = option_lookup[key.to_sym]
231
+ next unless definition
232
+
233
+ label = definition.long || "--#{key.to_s.tr('_', '-')}"
234
+ enforce_value_against_definition(definition, value, label)
235
+ end
236
+ end
237
+
238
+ def enforce_value_against_definition(definition, value, label)
239
+ return unless definition
240
+
241
+ return if type_allowed?(definition.types, value)
242
+
243
+ Array(value.is_a?(Array) ? value : [value]).each do |entry|
244
+ next if literal_allowed?(definition.allowed_values, entry)
245
+ next if type_allowed?(definition.types, entry)
246
+
247
+ message = build_invalid_value_message(definition, entry, label)
248
+ @environment.handle_input_violation(message)
249
+ end
250
+ end
251
+
252
+ def build_invalid_value_message(definition, entry, label)
253
+ description = allowed_value_description(definition)
254
+ formatted_value = format_literal_value(entry)
255
+
256
+ message = if description
257
+ "#{label} must be #{description} (received #{formatted_value})"
258
+ else
259
+ "Value #{formatted_value} for #{label} is not allowed"
260
+ end
261
+
262
+ suggestions = literal_suggestions(definition, entry)
263
+ return message if suggestions.empty?
264
+
265
+ suggestion_text = suggestions.size == 1 ? suggestions.first : suggestions.join(', ')
266
+ "#{message}. Did you mean #{suggestion_text}?"
267
+ end
268
+
269
+ def literal_allowed?(allowed_entries, value)
270
+ entries = Array(allowed_entries).compact
271
+ return false if entries.empty?
272
+
273
+ entries.any? { |entry| literal_match?(entry[:value], value) }
274
+ end
275
+
276
+ def literal_match?(candidate, value)
277
+ case candidate
278
+ when Symbol
279
+ value.is_a?(Symbol) && value == candidate
280
+ when String
281
+ value.is_a?(String) && value == candidate
282
+ when Integer
283
+ value.is_a?(Integer) && value == candidate
284
+ when Float
285
+ value.is_a?(Float) && value == candidate
286
+ when TrueClass, FalseClass
287
+ value == candidate
288
+ when NilClass
289
+ value.nil?
290
+ else
291
+ value == candidate
292
+ end
293
+ end
294
+
295
+ def type_allowed?(types, value)
296
+ tokens = Array(types).compact
297
+ return false if tokens.empty?
298
+
299
+ tokens.any? { |token| matches_type_token?(token, value) }
300
+ end
301
+
302
+ def allowed_value_description(definition)
303
+ literal_descriptions = Array(definition.allowed_values).map { |entry| format_literal_value(entry[:value]) }.reject(&:empty?)
304
+ type_descriptions = Array(definition.types)
305
+ .map { |token| token.to_s.strip }
306
+ .reject { |token| token.empty? || literal_hint_token?(token) }
307
+ combined = (literal_descriptions + type_descriptions).uniq.reject(&:empty?)
308
+ return nil if combined.empty?
309
+
310
+ return combined.first if combined.size == 1
311
+
312
+ "one of #{combined.join(', ')}"
313
+ end
314
+
315
+ def literal_suggestions(definition, entry)
316
+ literals = Array(definition.allowed_values).map { |allowed| allowed[:value] }.compact
317
+ return [] if literals.empty?
318
+ return [] unless entry.is_a?(String) || entry.is_a?(Symbol)
319
+
320
+ candidates = literals.select { |value| value.is_a?(String) || value.is_a?(Symbol) }
321
+ return [] if candidates.empty?
322
+
323
+ lookup = {}
324
+ dictionary = candidates.each_with_object([]) do |candidate, memo|
325
+ key = candidate.to_s
326
+ lookup[key] ||= candidate
327
+ memo << key unless memo.include?(key)
328
+ end
329
+
330
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: dictionary)
331
+ matches = spell_checker.correct(entry.to_s)
332
+ return [] if matches.empty?
333
+
334
+ matches.take(3).map { |match| format_literal_value(lookup[match]) }
335
+ rescue LoadError, NameError
336
+ []
337
+ end
338
+
339
+ def matches_type_token?(token, value)
340
+ normalized = token.to_s.strip
341
+ return true if normalized.empty?
342
+
343
+ if (inner = array_inner_type(normalized))
344
+ return false unless value.is_a?(Array)
345
+ return value.all? { |element| matches_type_token?(inner, element) }
346
+ end
347
+
348
+ case normalized
349
+ when 'Boolean'
350
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
351
+ when 'JSON'
352
+ value.is_a?(Hash) || value.is_a?(Array)
353
+ when 'nil', 'NilClass'
354
+ value.nil?
355
+ else
356
+ klass = constant_for_token(normalized)
357
+ return value.is_a?(klass) if klass
358
+
359
+ false
360
+ end
361
+ end
362
+
363
+ def array_inner_type(token)
364
+ if token.end_with?('[]')
365
+ token[0..-3]
366
+ elsif token.start_with?('Array<') && token.end_with?('>')
367
+ token[6..-2].strip
368
+ else
369
+ nil
370
+ end
371
+ end
372
+
373
+ def nil_type_token?(token)
374
+ token.to_s.strip.casecmp('nil').zero? || token.to_s.strip.casecmp('NilClass').zero?
375
+ end
376
+
377
+ def safe_constant_lookup(name)
378
+ parts = name.to_s.split('::').reject(&:empty?)
379
+ return nil if parts.empty?
380
+
381
+ context = Object
382
+ parts.each do |const_name|
383
+ return nil unless context.const_defined?(const_name, false)
384
+
385
+ context = context.const_get(const_name)
386
+ end
387
+ context
388
+ rescue NameError
389
+ nil
390
+ end
391
+
392
+ def constant_for_token(token)
393
+ normalized = token.to_s
394
+ case normalized
395
+ when 'Fixnum'
396
+ return Integer
397
+ when 'Date', 'DateTime'
398
+ require 'date'
399
+ when 'Time'
400
+ require 'time'
401
+ when 'BigDecimal', 'Decimal'
402
+ require 'bigdecimal'
403
+ when 'Pathname'
404
+ require 'pathname'
405
+ when 'Struct'
406
+ return Struct
407
+ end
408
+
409
+ safe_constant_lookup(normalized)
410
+ rescue LoadError
411
+ safe_constant_lookup(normalized)
412
+ end
413
+
414
+ def format_literal_value(value)
415
+ case value
416
+ when Symbol
417
+ ":#{value}"
418
+ when String
419
+ value.inspect
420
+ when Integer, Float
421
+ value.to_s
422
+ when TrueClass, FalseClass
423
+ value.to_s
424
+ when NilClass
425
+ 'nil'
426
+ else
427
+ value.inspect
428
+ end
429
+ end
430
+
431
+ def literal_hint_token?(token)
432
+ token = token.to_s.strip
433
+ return false if token.empty?
434
+
435
+ token.start_with?('%i[', '%I[', '%w[', '%W[')
436
+ end
437
+
199
438
 
200
439
  def build_cli_alias_map(option_defs)
201
440
  option_defs.each_with_object({}) do |opt, memo|
@@ -273,7 +512,15 @@ module Rubycli
273
512
  ->(value) { value.to_sym }
274
513
  when 'BigDecimal', 'Decimal'
275
514
  require 'bigdecimal'
276
- ->(value) { BigDecimal(value) }
515
+ ->(value) {
516
+ return value if value.is_a?(BigDecimal)
517
+
518
+ if value.is_a?(String)
519
+ BigDecimal(value)
520
+ else
521
+ BigDecimal(value.to_s)
522
+ end
523
+ }
277
524
  when 'Date'
278
525
  require 'date'
279
526
  ->(value) { Date.parse(value) }
@@ -282,6 +529,13 @@ module Rubycli
282
529
  ->(value) { Time.parse(value) }
283
530
  when 'JSON', 'Hash'
284
531
  ->(value) { JSON.parse(value) }
532
+ when 'Pathname'
533
+ require 'pathname'
534
+ ->(value) {
535
+ return value if value.is_a?(Pathname)
536
+
537
+ Pathname.new(value.to_s)
538
+ }
285
539
  else
286
540
  if normalized.start_with?('Array<') && normalized.end_with?('>')
287
541
  inner = normalized[6..-2].strip