rubycli 0.1.5 → 0.1.7

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: 9287ea2ca51772eb53c50fe717e648c7d23a9fb1e5cb0e25990eef7f07cde225
4
- data.tar.gz: aab6e71f2beafb924bb4bc29689c6ce64391d55bc43936a789278ad764e91592
3
+ metadata.gz: '07113181085c0df0c8f4bc34aca661d4c29fb9bbc309012ace52b36e8bdf9ead'
4
+ data.tar.gz: 773d7de6a820fd1702e4e618c595237e2293c74dca6cc83f3ea57ba9eaaa18db
5
5
  SHA512:
6
- metadata.gz: 98baa8a62366c9babedda68174dbc1fadaafbdf1ea4354393bf1a525b9b1d1f2ce0459f20ef8aff81cc4a3448f0686da9bf7940a8fde2526a3b42caf3d2feb63
7
- data.tar.gz: ac5044eec17148e3bb26ef1aa2fd67b78e6578acd6693747bb9a95e8d043e1842f9ebc853e6e50297130d0351fdfb9cfad04c1d217738c6462eda94c1c5b039f
6
+ metadata.gz: dadd19c4c2081e6fd7f8d6e40750c3cbc727a2c7f0452a5eb81b6feb80ce878e50fac111589ea87415d7656db4d288591bf7352fd4ee80031998e5835e84e732
7
+ data.tar.gz: 0aef161e5d6fd30fb475de5afc908ecc94f1b5d4d1c96c0d735399c90b615e865e91286fa100008804d846d22e37874528b192dcbcea74b52484e45ef56273b6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ # Changelog
4
+
5
+ ## [0.1.7] - 2025-11-12
6
+
7
+ ### Added
8
+ - `--new` now optionally accepts constructor arguments inline (e.g., `--new=[...]`); YAML/JSON-like literals are safely parsed, and `--json-args` / `--eval-args` / `--eval-lax` still apply. Arrays become positional args, hashes become keyword args.
9
+ - Positional argument coercion now runs through the same type-conversion pipeline as options/`--new`, including comment-driven array/element coercion and strict-mode validation; new tests cover arrays, hashes, booleans, and `--new` with JSON/eval modes.
10
+ - Added `examples/new_mode_runner.rb` to showcase `--new` with constructor args, eval/JSON modes, and pre-script initialization for instance-only classes.
11
+ - Strengthened tests for `--eval-lax` success/fallback with `--new` and end-to-end positional Hash coercion to guard against regressions.
12
+
13
+ ### Changed
14
+ - `--new` arguments now flow through the same type-coercion pipeline as regular CLI arguments (including comment-driven type hints) before being passed to `initialize`.
15
+
16
+ ## [0.1.6] - 2025-11-11
17
+
18
+ ### Changed
19
+ - `rubycli --check` now reports unknown type tokens and enumerated allowed values (with DidYouMean suggestions) instead of silently treating them as strings, while `--strict` continues to enforce the surviving annotations at runtime.
20
+
3
21
  ## [0.1.5] - 2025-11-10
4
22
 
5
23
  ### Added
data/README.ja.md CHANGED
@@ -156,13 +156,15 @@ Rubycli は「ファイル名を CamelCase にした定数」を公開対象だ
156
156
 
157
157
  大規模なコードベースでも安全側を保ちながら、どうしても自動選択したいときだけ 1 フラグで切り替えられます。
158
158
 
159
- > **インスタンスメソッド専用のクラスについて** – 公開メソッドがインスタンス側(`def greet` など)にしか無い場合は、`--new` を付けて事前にインスタンス化しないと CLI から呼び出せません。クラスメソッドを 1 つ用意するか、`--new` を明示して実行してください。
159
+ > **インスタンスメソッド専用のクラスについて** – 公開メソッドがインスタンス側(`def greet` など)にしか無い場合は、`--new` を付けて事前にインスタンス化しないと CLI から呼び出せません。クラスメソッドを 1 つ用意するか、`--new` を明示して実行してください。`--new` を付ければ `rubycli --help` でもインスタンスメソッドが一覧に現れ、`rubycli --check --new` でコメントの lint も実行できます。初期化時に引数が必要なら `--new=VALUE` のように続けて指定できます(通常の引数と同様に YAML/JSON ライクな安全パースに加え、`--json-args` / `--eval-args` / `--eval-lax` も適用可能)。`initialize` に書いたコメントも通常の CLI メソッドと同様に型変換に反映されます。
160
+
161
+ > 補足: `--new 1` のようにスペース区切りで 1 つだけ値を渡すと、後続トークンがパス扱いされやすいため `--new=VALUE` のように `=` 付きで指定するのが確実です。
160
162
 
161
163
  ## 開発方針
162
164
 
163
165
  - **便利さが最優先** – 既存の Ruby スクリプトを最小の手間で CLI 化できることを目的にしており、Python Fire の完全移植は目指していません。
164
166
  - **インスパイアであってポートではない** – アイデアの出自は Fire ですが、同等機能を揃える予定は基本的にありません。Fire 由来の未実装機能は仕様です。
165
- - **メソッド定義が土台、コメントが挙動を補強** – 公開メソッドのシグネチャが CLI に露出する範囲と必須/任意を決めますが、コメントに `TAG...` や `[Integer]` を書くと同じ引数でも配列化や型変換が行われます。さらに Rubycli は `--names='["Alice","Bob"]'` のような JSON/YAML らしい入力を自動的に安全なリテラルとして評価します。`rubycli --check パス/対象.rb` でコメントと実装のズレを検査しつつ、通常実行時に `--strict` を付ければドキュメント通りでない入力をその場でエラーにできます。(現状の `--check` は「コメントが揃っているか」を中心にチェックしており、`Booalean` のように型トークンを誤記しても検出できません。)
167
+ - **メソッド定義が土台、コメントが挙動を補強** – 公開メソッドのシグネチャが CLI に露出する範囲と必須/任意を決めますが、コメントに `TAG...` や `[Integer]` を書くと同じ引数でも配列化や型変換が行われます。さらに Rubycli は `--names='["Alice","Bob"]'` のような JSON/YAML らしい入力を自動的に安全なリテラルとして評価します。`rubycli --check パス/対象.rb` でコメントと実装のズレ(未定義の型ラベルや列挙値の誤記を含む)を DidYouMean の候補付きで検査し、通常実行時に `--strict` を付ければドキュメント通りでない入力をその場でエラーにできます。
166
168
  - **軽量メンテナンス** – 実装の多くは AI 支援で作られており、深い Ruby メタプログラミングを伴う大規模拡張は想定外です。Fire 互換を求める PR は事前相談をお願いします。
167
169
 
168
170
  ## 特徴
@@ -172,18 +174,35 @@ Rubycli は「ファイル名を CamelCase にした定数」を公開対象だ
172
174
  - 引数はデフォルトで安全なリテラルとして解釈し、必要に応じて厳格 JSON モードや Ruby eval モードを切り替え可能
173
175
  - `--pre-script`(エイリアス: `--init`)で任意の Ruby コードを評価し、その結果オブジェクトを公開
174
176
  - `--check` でコメント整合性を lint、`--strict` で入力値をドキュメント通りに強制する二段構えのガード
177
+ - `examples/new_mode_runner.rb` ではインスタンス専用クラスを `--new=VALUE` で初期化し、eval/JSON モードや pre-script を組み合わせる例を示しています。
178
+
179
+ ### サンプル / 付属例
180
+
181
+ - `examples/hello_app.rb` / `examples/hello_app_with_docs.rb`: 最小のモジュール関数とドキュメント付きの版
182
+ - `examples/typed_arguments_demo.rb`: 標準ライブラリ型 (Date/Time/BigDecimal/Pathname) の coercion
183
+ - `examples/strict_choices_demo.rb`: リテラル列挙と `--strict` の組み合わせ
184
+ - `examples/new_mode_runner.rb`: インスタンス専用クラスを `--new=VALUE` で初期化し、eval/JSON/pre-script を組み合わせる例
185
+
186
+ #### サンプルコマンド
187
+
188
+ - `rubycli examples/new_mode_runner.rb run --new='["a","b","c"]' --mode reverse`
189
+ - `rubycli --json-args --new='["x","y"]' examples/new_mode_runner.rb run --mode summary --options '{"source":"json"}'`
190
+ - `rubycli --eval-args --new='["x","y"]' examples/new_mode_runner.rb run --mode summary --options '{tags: [:a, :b]}'`
191
+ - `rubycli --pre-script 'NewModeRunner.new(%w[a b c], options: {from: :pre})' examples/new_mode_runner.rb run --mode summary`
192
+
193
+ > 補足: `--strict` はコメントに書かれた型/許可値をそのまま信頼して検証するため、コメントが誤記だと実行時には検出できません。CI では必ず `rubycli --check` を走らせ、`--strict` は「 lint を通過したドキュメントを本番で厳密に守る」用途に使ってください。
175
194
 
176
195
  ## Python Fire との違い
177
196
 
178
197
  - **コメント対応のヘルプ生成**: コメントがあればヘルプに反映しつつ、最終的な判断は常にライブなメソッド定義に基づきます。
179
198
  - **型に基づく解析**: `NAME [String]` や YARD タグから型を推論し、真偽値・配列・数値などを自動変換します。
180
- - **厳密な整合性チェック**: `rubycli --check` でコメントと実装のズレを実行前に検査し、通常実行時に `--strict` を付ければドキュメントで宣言した型・許可値以外の入力を拒否できます。
199
+ - **厳密な整合性チェック**: `rubycli --check` でコメントと実装のズレ(未定義の型ラベルや列挙値の誤記など)をコード実行前に検査し、通常実行時に `--strict` を付ければドキュメントで宣言した型・許可値以外の入力を拒否できます。
181
200
  - **Ruby 向け拡張**: キーワード引数やブロック (`@yield*`) といった Ruby 固有の構文に合わせたパーサや `RUBYCLI_*` 環境変数を用意しています。
182
201
 
183
202
  | 機能 | Python Fire | Rubycli |
184
203
  | ---- | ----------- | -------- |
185
204
  | 属性の辿り方 | オブジェクトを辿ってプロパティ/属性を自動公開 | 対象オブジェクトの公開メソッドをそのまま公開(暗黙の辿りは無し) |
186
- | クラス初期化 | `__init__` 引数を CLI で自動受け取りインスタンス化 | `--new` 指定時だけ引数なしで明示的に初期化(初期化引数の CLI 受け渡しは未対応なので、必要なら pre-script や自前ファクトリで注入) |
205
+ | クラス初期化 | `__init__` 引数を CLI で自動受け取りインスタンス化 | `--new` 指定時だけ初期化(コンストラクタ引数は `--new=VALUE` で渡せる。YAML/JSON らしいリテラルは安全にパース、`--json-args` / `--eval-args` / `--eval-lax` も適用可能。より複雑なら pre-script や自前ファクトリを利用) |
187
206
  | インタラクティブシェル | コマンド未指定時に Fire REPL を提供 | インタラクティブモード無し。コマンド実行専用 |
188
207
  | 情報源 | 反射で引数・プロパティを解析 | ライブなメソッド定義を基点にしつつコメントをヘルプへ反映 |
189
208
  | 辞書/配列 | dict/list を自動でサブコマンド化 | クラス/モジュールのメソッドに特化(辞書自動展開なし) |
data/README.md CHANGED
@@ -163,13 +163,15 @@ Rubycli assumes that the file name (CamelCased) matches the class or module you
163
163
 
164
164
  This keeps large projects safe by default but still provides a one-flag escape hatch when you prefer the fully automatic behaviour.
165
165
 
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`.
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`. Passing `--new` also makes those instance methods appear in `rubycli --help` output and allows `rubycli --check --new` to lint their documentation. When your constructor needs arguments, pass them inline with `--new=VALUE` (safe YAML/JSON-like parsing by default; `--json-args` for strict JSON, `--eval-args` / `--eval-lax` for Ruby literals). Any comments on `initialize` are respected for type coercion just like regular CLI methods.
167
+
168
+ > Hint: Single values should be passed as `--new=value` so they aren’t mistaken for the next path/command. Space-separated single tokens like `--new 1` may be treated as the following path unless they look obviously structured.
167
169
 
168
170
  ## Project Philosophy
169
171
 
170
172
  - **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.
171
173
  - **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.”
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.)
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. 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—including undefined type labels or enumerated values, with DidYouMean suggestions for `Booalean`-style typos—and pass `--strict` during normal runs when you want invalid input to abort instead of merely warning.
173
175
  - **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.
174
176
 
175
177
  ## Features
@@ -179,22 +181,39 @@ This keeps large projects safe by default but still provides a one-flag escape h
179
181
  - Safe literal parsing out of the box (arrays / hashes / booleans) with opt-in strict JSON and Ruby eval modes
180
182
  - Optional pre-script hook (`--pre-script` / `--init`) to evaluate Ruby and expose the resulting object
181
183
  - Dedicated CLI flags for quality gates: `--check` lints documentation/comments without running commands, and `--strict` treats documented types/choices as hard requirements
184
+ - Example `examples/new_mode_runner.rb` demonstrates instance-only classes with `--new=VALUE` constructor arguments, eval/JSON modes, and a pre-script initialization pattern.
185
+
186
+ ### Examples
187
+
188
+ - `examples/hello_app.rb` / `examples/hello_app_with_docs.rb`: minimal module-function variants, with and without docs
189
+ - `examples/typed_arguments_demo.rb`: stdlib type coercions (Date/Time/BigDecimal/Pathname)
190
+ - `examples/strict_choices_demo.rb`: literal enumerations and `--strict`
191
+ - `examples/new_mode_runner.rb`: instance-only class initialized via `--new=VALUE` with eval/JSON/pre-script combinations
192
+
193
+ > Tip: `--strict` trusts whatever types/choices your comments spell out—if the annotations are misspelled, runtime enforcement has nothing reliable to compare against. Keep `rubycli --check` in CI so documentation typos are caught before production runs that rely on `--strict`.
182
194
 
183
195
  ## How it differs from Python Fire
184
196
 
185
197
  - **Comment-aware help** – Rubycli leans on doc comments when present but still reflects the live method signature, keeping code as the ultimate authority.
186
198
  - **Type-aware parsing** – Placeholder syntax (`NAME [String]`) and YARD tags let Rubycli coerce arguments to booleans, arrays, numerics, etc. without additional code.
187
- - **Strict validation** – `rubycli --check` lint runs catch documentation drift before execution, while runtime `--strict` runs turn documented types/choices into enforceable contracts.
199
+ - **Strict validation** – `rubycli --check` lint runs catch documentation drift (including undefined type labels or enumerated values) without executing commands, while runtime `--strict` runs turn those documented types/choices into enforceable contracts.
188
200
  - **Ruby-centric tooling** – Supports Ruby-specific conventions such as optional keyword arguments, block documentation (`@yield*` tags), and `RUBYCLI_*` environment toggles.
189
201
 
190
202
  | Capability | Python Fire | Rubycli |
191
203
  | ---------- | ----------- | -------- |
192
204
  | Attribute traversal | Recursively exposes attributes/properties on demand | Exposes public methods defined on the target; no implicit traversal |
193
- | Constructor handling | Automatically prompts for `__init__` args when instantiating classes | `--new` simply instantiates without passing CLI args (use pre-scripts or your own factories if you need injected dependencies) |
205
+ | Constructor handling | Automatically prompts for `__init__` args when instantiating classes | `--new` instantiates and accepts constructor arguments via `--new=VALUE` (safe YAML/JSON-like parsing by default; `--json-args` for strict JSON, `--eval-args` / `--eval-lax` for Ruby literals). Use pre-scripts or your own factories for more complex wiring. |
194
206
  | Interactive shell | Offers Fire-specific REPL when invoked without command | No interactive shell mode; strictly command execution |
195
207
  | Input discovery | Pure reflection, no doc comments required | Doc comments drive option names, placeholders, and validation |
196
208
  | Data structures | Dictionaries / lists become subcommands by default | Focused on class or module methods; no automatic dict/list expansion |
197
209
 
210
+ #### Example commands
211
+
212
+ - `rubycli examples/new_mode_runner.rb run --new='["a","b","c"]' --mode reverse`
213
+ - `rubycli --json-args --new='["x","y"]' examples/new_mode_runner.rb run --mode summary --options '{"source":"json"}'`
214
+ - `rubycli --eval-args --new='["x","y"]' examples/new_mode_runner.rb run --mode summary --options '{tags: [:a, :b]}'`
215
+ - `rubycli --pre-script 'NewModeRunner.new(%w[a b c], options: {from: :pre})' examples/new_mode_runner.rb run --mode summary`
216
+
198
217
  ## Installation
199
218
 
200
219
  Rubycli is published on RubyGems.
@@ -59,6 +59,7 @@ module Rubycli
59
59
  end
60
60
  end
61
61
 
62
+ pos_args = convert_positional_arguments(pos_args, method, metadata)
62
63
  debug_log "Final parsed - pos_args: #{pos_args.inspect}, kw_args: #{kw_args.inspect}"
63
64
  [pos_args, kw_args]
64
65
  end
@@ -186,6 +187,60 @@ module Rubycli
186
187
  [key, value]
187
188
  end
188
189
 
190
+ def convert_positional_arguments(pos_args, method, metadata)
191
+ return pos_args unless method
192
+
193
+ positional_map = metadata[:positionals_map] || {}
194
+ return pos_args if positional_map.empty?
195
+
196
+ converted = pos_args.dup
197
+ method.parameters.each_with_index do |(type, name), index|
198
+ next unless %i[req opt].include?(type)
199
+ definition = positional_map[name]
200
+ next unless definition
201
+ next if index >= converted.size
202
+
203
+ converter = converter_for_definition(definition)
204
+ next unless converter
205
+
206
+ begin
207
+ converted[index] = converter.call(converted[index])
208
+ rescue StandardError => e
209
+ label = definition.label || definition.placeholder || name.to_s.upcase
210
+ raise ArgumentError, "Value '#{converted[index]}' for #{label} is invalid: #{e.message}"
211
+ end
212
+ end
213
+
214
+ converted
215
+ end
216
+
217
+ def converter_for_definition(definition)
218
+ types = Array(definition.types).compact
219
+ return nil if types.empty?
220
+
221
+ types.each do |type|
222
+ normalized = type.to_s.strip
223
+ next if normalized.empty?
224
+
225
+ if normalized.start_with?('Array<') && normalized.end_with?('>')
226
+ inner = normalized[6..-2].strip
227
+ element_converter = converter_for_single_type(inner)
228
+ return ->(value) { TypeUtils.parse_list(value).map { |item| element_converter ? element_converter.call(item) : item } }
229
+ elsif normalized.end_with?('[]')
230
+ inner = normalized[0..-3]
231
+ element_converter = converter_for_single_type(inner)
232
+ return ->(value) { TypeUtils.parse_list(value).map { |item| element_converter ? element_converter.call(item) : item } }
233
+ elsif normalized.casecmp('Array').zero?
234
+ return ->(value) { TypeUtils.parse_list(value) }
235
+ else
236
+ single = converter_for_single_type(normalized)
237
+ return single if single
238
+ end
239
+ end
240
+
241
+ nil
242
+ end
243
+
189
244
  def resolve_keyword_parameter(cli_key, kw_param_names)
190
245
  exact_match = kw_param_names.find { |name| name == cli_key }
191
246
  return exact_match if exact_match
@@ -528,7 +583,11 @@ module Rubycli
528
583
  require 'time'
529
584
  ->(value) { Time.parse(value) }
530
585
  when 'JSON', 'Hash'
531
- ->(value) { JSON.parse(value) }
586
+ ->(value) {
587
+ return value if value.is_a?(Hash)
588
+
589
+ JSON.parse(value)
590
+ }
532
591
  when 'Pathname'
533
592
  require 'pathname'
534
593
  ->(value) {
@@ -3,15 +3,16 @@
3
3
  module Rubycli
4
4
  module CommandLine
5
5
  USAGE = <<~USAGE
6
- Usage: rubycli [--new|-n] [--pre-script=<src>] [--json-args|-j | --eval-args|-e | --eval-lax|-E] [--strict] [--check|-c] <target-path> [<class-or-module>] [-- <cli-args>...]
6
+ Usage: rubycli [--new|-n[=<value>]] [--pre-script=<src>] [--json-args|-j | --eval-args|-e | --eval-lax|-E] [--strict] [--check|-c] <target-path> [<class-or-module>] [-- <cli-args>...]
7
7
 
8
8
  Examples:
9
9
  rubycli scripts/sample_runner.rb echo --message hello
10
10
  rubycli scripts/sample_runner.rb AlternateRunner greet --name Ruby
11
11
  rubycli --new lib/akiya_fetcher.rb fetch_simplified_html https://example.com
12
+ rubycli --json-args --new='["a","b"]' examples/new_mode_runner.rb run --mode summary --options '{"source":"json"}'
12
13
 
13
14
  Options:
14
- --new, -n Instantiate the class/module before invoking CLI commands
15
+ --new, -n [<value>] Instantiate the class/module before invoking CLI commands; optional constructor arguments can follow (array/hash recommended; respects --json-args/--eval-args/--eval-lax)
15
16
  --pre-script=<src> Evaluate Ruby code and use its result as the exposed target (--init alias; also accepts space-separated form)
16
17
  --json-args, -j Parse all following arguments strictly as JSON (no YAML literals)
17
18
  --eval-args, -e Evaluate following arguments as Ruby code
@@ -40,6 +41,7 @@ module Rubycli
40
41
  end
41
42
 
42
43
  new_flag = false
44
+ new_args_expr = nil
43
45
  json_mode = false
44
46
  eval_mode = false
45
47
  eval_lax_mode = false
@@ -55,9 +57,23 @@ module Rubycli
55
57
  when '-h', '--help', 'help'
56
58
  $stdout.puts(USAGE)
57
59
  return 0
58
- when '--new', '-n'
60
+ when /\A--new(?:=(.+))?\z/
59
61
  new_flag = true
62
+ new_args_expr = Regexp.last_match(1)
60
63
  args.shift
64
+ if new_args_expr.nil?
65
+ possible = args.first
66
+ if possible && !possible.start_with?('-') && likely_new_args_value?(possible)
67
+ new_args_expr = args.shift
68
+ end
69
+ end
70
+ when '-n'
71
+ new_flag = true
72
+ args.shift
73
+ possible = args.first
74
+ if possible && !possible.start_with?('-') && likely_new_args_value?(possible)
75
+ new_args_expr = args.shift
76
+ end
61
77
  when /\A--pre-script=(.+)\z/, /\A--init=(.+)\z/
62
78
  label = Regexp.last_match(0).start_with?('--pre-script') ? '--pre-script' : '--init'
63
79
  expr = Regexp.last_match(1)
@@ -141,6 +157,7 @@ module Rubycli
141
157
  target_path,
142
158
  class_or_module,
143
159
  new: new_flag,
160
+ new_args: new_args_expr,
144
161
  pre_scripts: pre_script_sources,
145
162
  constant_mode: constant_mode
146
163
  )
@@ -151,6 +168,7 @@ module Rubycli
151
168
  class_or_module,
152
169
  args,
153
170
  new: new_flag,
171
+ new_args: new_args_expr,
154
172
  json: json_mode,
155
173
  eval_args: eval_mode,
156
174
  eval_lax: eval_lax_mode,
@@ -166,5 +184,9 @@ module Rubycli
166
184
  warn "[ERROR] #{e.message}"
167
185
  1
168
186
  end
187
+
188
+ def likely_new_args_value?(token)
189
+ token.include?(',') || token.start_with?('[', '{')
190
+ end
169
191
  end
170
192
  end
@@ -55,7 +55,7 @@ module Rubycli
55
55
  next
56
56
  end
57
57
 
58
- if (return_meta = parse_return_metadata(stripped))
58
+ if (return_meta = parse_return_metadata(stripped, method_obj))
59
59
  metadata[:returns] << return_meta
60
60
  summary_phase = false
61
61
  next
@@ -68,7 +68,7 @@ module Rubycli
68
68
  next
69
69
  end
70
70
 
71
- if (positional = parse_positional_line(stripped))
71
+ if (positional = parse_positional_line(stripped, method_obj))
72
72
  metadata[:positionals] << positional
73
73
  summary_phase = false
74
74
  next
@@ -137,6 +137,7 @@ module Rubycli
137
137
  description = nil if description&.empty?
138
138
 
139
139
  raw_types = parse_type_annotation(type_str)
140
+ audit_type_annotation_tokens(raw_types, method_obj)
140
141
  types, allowed_values = partition_type_tokens(raw_types)
141
142
 
142
143
  long_option = nil
@@ -193,6 +194,7 @@ module Rubycli
193
194
 
194
195
  if (types.nil? || types.empty?) && type_token
195
196
  inline_raw_types = parse_type_annotation(type_token)
197
+ audit_type_annotation_tokens(inline_raw_types, method_obj)
196
198
  inline_types, inline_allowed = partition_type_tokens(inline_raw_types)
197
199
  types = inline_types
198
200
  allowed_values = merge_allowed_values(allowed_values, inline_allowed)
@@ -309,6 +311,7 @@ module Rubycli
309
311
  description = description_tokens.join(' ').strip
310
312
  description = nil if description.empty?
311
313
  raw_types = parse_type_annotation(type_token)
314
+ audit_type_annotation_tokens(raw_types, method_obj)
312
315
  types, allowed_values = partition_type_tokens(raw_types)
313
316
 
314
317
  keyword = long_option.delete_prefix('--').tr('-', '_').to_sym
@@ -327,7 +330,7 @@ module Rubycli
327
330
  )
328
331
  end
329
332
 
330
- def parse_positional_line(line)
333
+ def parse_positional_line(line, method_obj)
331
334
  return nil if line.start_with?('--') || line.start_with?('-')
332
335
 
333
336
  tokens = combine_bracketed_tokens(line.split(/\s+/))
@@ -346,6 +349,7 @@ module Rubycli
346
349
  description = nil if description.empty?
347
350
 
348
351
  raw_types = parse_type_annotation(type_token)
352
+ audit_type_annotation_tokens(raw_types, method_obj)
349
353
  types, allowed_values = partition_type_tokens(raw_types)
350
354
  placeholder_info = analyze_placeholder(placeholder)
351
355
  inferred_types = infer_types_from_placeholder(
@@ -371,10 +375,11 @@ module Rubycli
371
375
  )
372
376
  end
373
377
 
374
- def parse_return_metadata(line)
378
+ def parse_return_metadata(line, method_obj)
375
379
  yard_match = /\A@return\s+\[([^\]]+)\](?:\s+(.*))?\z/.match(line)
376
380
  if yard_match
377
381
  types = parse_type_annotation(yard_match[1])
382
+ audit_type_annotation_tokens(types, method_obj)
378
383
  description = yard_match[2]&.strip
379
384
  return ReturnDefinition.new(types: types, description: description)
380
385
  end
@@ -382,6 +387,7 @@ module Rubycli
382
387
  shorthand_match = /\A=>\s+(\[[^\]]+\]|[^\s]+)(?:\s+(.*))?\z/.match(line)
383
388
  if shorthand_match
384
389
  types = parse_type_annotation(shorthand_match[1])
390
+ audit_type_annotation_tokens(types, method_obj)
385
391
  description = shorthand_match[2]&.strip
386
392
  return ReturnDefinition.new(types: types, description: description)
387
393
  end
@@ -390,11 +396,74 @@ module Rubycli
390
396
  stripped = line.sub(/\Areturn\s+/, '')
391
397
  type_token, description = stripped.split(/\s+/, 2)
392
398
  types = parse_type_annotation(type_token)
399
+ audit_type_annotation_tokens(types, method_obj)
393
400
  description = description&.strip
394
401
  return ReturnDefinition.new(types: types, description: description)
395
402
  end
396
403
  end
397
404
 
405
+ def doc_issue_location(method_obj)
406
+ return [nil, nil] unless method_obj.respond_to?(:source_location)
407
+
408
+ source_file, source_line = method_obj.source_location
409
+ line_number = source_line ? [source_line - 1, 1].max : nil
410
+ [source_file, line_number]
411
+ end
412
+
413
+ def audit_type_annotation_tokens(tokens, method_obj)
414
+ return if tokens.nil? || tokens.empty?
415
+ return unless @environment.doc_check_mode?
416
+
417
+ source_file, line_number = doc_issue_location(method_obj)
418
+ literal_tokens_present = Array(tokens).any? do |token|
419
+ literal_entry = literal_entry_from_token(token)
420
+ literal_entry || literal_hint_token?(token)
421
+ end
422
+
423
+ Array(tokens).each do |token|
424
+ audit_single_token(token, source_file, line_number, literal_tokens_present)
425
+ end
426
+ end
427
+
428
+ def audit_single_token(token, source_file, line_number, literal_context)
429
+ normalized = token.to_s.strip
430
+ return if normalized.empty?
431
+
432
+ if literal_hint_token?(normalized)
433
+ expand_annotation_token(normalized).each do |entry|
434
+ audit_literal_entry(entry, source_file, line_number)
435
+ end
436
+ return
437
+ end
438
+
439
+ literal_entry = literal_entry_from_token(normalized)
440
+ if literal_entry
441
+ return
442
+ end
443
+
444
+ if literal_context && literal_token_candidate?(normalized, include_uppercase: true) && !known_type_token?(normalized)
445
+ warn_unknown_allowed_value(normalized, source_file, line_number)
446
+ return
447
+ end
448
+
449
+ if literal_token_candidate?(normalized, include_uppercase: false)
450
+ warn_unknown_allowed_value(normalized, source_file, line_number)
451
+ return
452
+ end
453
+
454
+ return if inline_type_hint?(normalized)
455
+ return if known_type_token?(normalized)
456
+
457
+ warn_unknown_type_token(normalized, source_file, line_number)
458
+ end
459
+
460
+ def audit_literal_entry(token, source_file, line_number)
461
+ literal_entry = literal_entry_from_token(token)
462
+ return if literal_entry
463
+
464
+ warn_unknown_allowed_value(token, source_file, line_number)
465
+ end
466
+
398
467
  def extract_parameter_defaults(method_obj)
399
468
  location = method_obj.source_location
400
469
  return {} unless location
@@ -735,6 +804,99 @@ module Rubycli
735
804
  nil
736
805
  end
737
806
 
807
+ def literal_token_candidate?(token, include_uppercase: false)
808
+ return false unless token
809
+
810
+ stripped = token.strip
811
+ return false if stripped.empty?
812
+
813
+ lowered = stripped.downcase
814
+ return true if %w[true false nil null ~].include?(lowered)
815
+ return true if stripped.start_with?(':', '"', "'")
816
+ return true if stripped.match?(/\A-?\d/)
817
+
818
+ pattern = include_uppercase ? /\A[a-z0-9._-]+\z/i : /\A[a-z0-9._-]+\z/
819
+ stripped.match?(pattern)
820
+ end
821
+
822
+ def warn_unknown_type_token(token, source_file, line_number)
823
+ return if token.nil? || token.empty?
824
+
825
+ suggestions = type_token_suggestions(token)
826
+ message = "Unknown type token '#{token}'"
827
+ if suggestions.any?
828
+ hint = suggestions.first(2).join(' or ')
829
+ message = "#{message} (did you mean #{hint}?)"
830
+ end
831
+ @environment.handle_documentation_issue(message, file: source_file, line: line_number)
832
+ end
833
+
834
+ def warn_unknown_allowed_value(token, source_file, line_number)
835
+ return if token.nil? || token.empty?
836
+
837
+ suggestions = allowed_value_suggestions(token)
838
+ message = "Unknown allowed value token '#{token}'"
839
+ if suggestions.any?
840
+ hint = suggestions.first(2).join(' or ')
841
+ message = "#{message} (did you mean #{hint}?)"
842
+ end
843
+ @environment.handle_documentation_issue(message, file: source_file, line: line_number)
844
+ end
845
+
846
+ def type_token_suggestions(token)
847
+ dictionary = type_dictionary
848
+ return [] if dictionary.empty?
849
+
850
+ require 'did_you_mean'
851
+ checker = DidYouMean::SpellChecker.new(dictionary: dictionary)
852
+ checker.correct(token.to_s).take(3)
853
+ rescue LoadError, NameError
854
+ []
855
+ end
856
+
857
+ def allowed_value_suggestions(token)
858
+ stripped = token.to_s.strip
859
+ return [] if stripped.empty?
860
+
861
+ candidates = []
862
+ if stripped.match?(/\A[a-z0-9._-]+\z/i)
863
+ candidates << ":#{stripped.downcase}"
864
+ candidates << stripped.downcase.inspect
865
+ end
866
+ return [] if candidates.empty?
867
+
868
+ require 'did_you_mean'
869
+ checker = DidYouMean::SpellChecker.new(dictionary: candidates)
870
+ checker.correct(stripped).take(2)
871
+ rescue LoadError, NameError
872
+ candidates.first(1)
873
+ end
874
+
875
+ def type_dictionary
876
+ return @type_dictionary if defined?(@type_dictionary) && @type_dictionary
877
+
878
+ builtins = (INLINE_TYPE_HINTS + %w[NilClass Fixnum Decimal Struct]).uniq
879
+ constant_names = []
880
+ begin
881
+ ObjectSpace.each_object(Module) do |mod|
882
+ name = mod.name
883
+ next unless name && !name.empty?
884
+
885
+ constant_names << name
886
+ parts = name.split('::')
887
+ constant_names << parts.last if parts.size > 1
888
+ end
889
+ rescue StandardError
890
+ constant_names = Object.constants.map(&:to_s)
891
+ end
892
+
893
+ @type_dictionary = (builtins + constant_names).map(&:to_s).map(&:strip).reject(&:empty?).uniq
894
+ end
895
+
896
+ def reset_type_dictionary_cache!
897
+ @type_dictionary = nil
898
+ end
899
+
738
900
 
739
901
  def placeholder_token?(token)
740
902
  return false unless token
@@ -786,8 +948,16 @@ module Rubycli
786
948
  def known_type_token?(token)
787
949
  return false unless token
788
950
 
789
- candidate = token.start_with?('@') ? token[1..] : token
790
- candidate =~ /\A[A-Z][A-Za-z0-9_:<>\[\]]*\z/
951
+ normalized = normalize_type_token(token)
952
+ return false if normalized.empty?
953
+
954
+ return true if primitive_type_token?(normalized)
955
+
956
+ if (inner = array_inner_type_token(normalized))
957
+ return known_type_token?(inner)
958
+ end
959
+
960
+ !safe_constant_lookup(normalized).nil?
791
961
  end
792
962
 
793
963
  def inline_type_hint?(token)
@@ -805,6 +975,53 @@ module Rubycli
805
975
  INLINE_TYPE_HINTS.include?(base)
806
976
  end
807
977
 
978
+ def primitive_type_token?(token)
979
+ return false if token.nil? || token.empty?
980
+
981
+ base = token.to_s
982
+ INLINE_TYPE_HINTS.include?(base) || %w[NilClass Fixnum Decimal Struct].include?(base)
983
+ end
984
+
985
+ def array_inner_type_token(token)
986
+ return nil unless token
987
+
988
+ stripped = token.to_s.strip
989
+ if stripped.end_with?('[]')
990
+ stripped[0..-3]
991
+ elsif stripped.start_with?('Array<') && stripped.end_with?('>')
992
+ stripped[6..-2].strip
993
+ else
994
+ nil
995
+ end
996
+ end
997
+
998
+ def safe_constant_lookup(name)
999
+ parts = name.to_s.split('::').reject(&:empty?)
1000
+ return nil if parts.empty?
1001
+
1002
+ context = Object
1003
+ parts.each do |const_name|
1004
+ return nil unless context.const_defined?(const_name, false)
1005
+
1006
+ context = context.const_get(const_name)
1007
+ end
1008
+ context
1009
+ rescue NameError
1010
+ nil
1011
+ end
1012
+
1013
+ def literal_hint_token?(token)
1014
+ return false unless token
1015
+
1016
+ stripped = token.to_s.strip
1017
+ return false if stripped.empty?
1018
+
1019
+ stripped.start_with?('%i[') ||
1020
+ stripped.start_with?('%I[') ||
1021
+ stripped.start_with?('%w[') ||
1022
+ stripped.start_with?('%W[')
1023
+ end
1024
+
808
1025
  def parameter_role(method_obj, keyword)
809
1026
  return nil unless method_obj.respond_to?(:parameters)
810
1027
 
@@ -34,6 +34,7 @@ module Rubycli
34
34
  def reset!
35
35
  @metadata_cache.clear
36
36
  @comment_extractor.reset!
37
+ @parser.reset_type_dictionary_cache! if @parser.respond_to?(:reset_type_dictionary_cache!)
37
38
  end
38
39
 
39
40
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubycli
4
- VERSION = '0.1.5'
4
+ VERSION = '0.1.7'
5
5
  end
data/lib/rubycli.rb CHANGED
@@ -209,6 +209,7 @@ module Rubycli
209
209
  class_name = nil,
210
210
  cli_args = nil,
211
211
  new: false,
212
+ new_args: nil,
212
213
  json: false,
213
214
  eval_args: false,
214
215
  eval_lax: false,
@@ -224,6 +225,10 @@ module Rubycli
224
225
  target_path,
225
226
  class_name,
226
227
  new: new,
228
+ new_args: new_args,
229
+ json_mode: json,
230
+ eval_mode: eval_args,
231
+ eval_lax: eval_lax,
227
232
  pre_scripts: pre_scripts,
228
233
  constant_mode: constant_mode
229
234
  )
@@ -243,8 +248,12 @@ module Rubycli
243
248
  target_path,
244
249
  class_name = nil,
245
250
  new: false,
251
+ new_args: nil,
246
252
  pre_scripts: [],
247
- constant_mode: nil
253
+ constant_mode: nil,
254
+ json_mode: false,
255
+ eval_mode: false,
256
+ eval_lax: false
248
257
  )
249
258
  raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
250
259
  previous_doc_check = Rubycli.environment.doc_check_mode?
@@ -255,6 +264,10 @@ module Rubycli
255
264
  target_path,
256
265
  class_name,
257
266
  new: new,
267
+ new_args: new_args,
268
+ json_mode: json_mode,
269
+ eval_mode: eval_mode,
270
+ eval_lax: eval_lax,
258
271
  pre_scripts: pre_scripts,
259
272
  constant_mode: constant_mode
260
273
  )
@@ -348,10 +361,18 @@ module Rubycli
348
361
  raise Error.new(message), cause: nil
349
362
  end
350
363
 
351
- def instantiate_target(target)
364
+ def instantiate_target(target, initializer_args = nil)
365
+ positional_args, keyword_args = Array(initializer_args || [[], {}])
366
+ positional_args ||= []
367
+ keyword_args ||= {}
368
+
352
369
  case target
353
370
  when Class
354
- target.new
371
+ if keyword_args.empty?
372
+ target.new(*positional_args)
373
+ else
374
+ target.new(*positional_args, **keyword_args)
375
+ end
355
376
  when Module
356
377
  Object.new.extend(target)
357
378
  else
@@ -373,10 +394,43 @@ module Rubycli
373
394
  end
374
395
  end
375
396
 
397
+ def parse_initializer_arguments(raw_value, target, json_mode:, eval_mode:, eval_lax:)
398
+ return [[], {}] if raw_value.nil?
399
+ raise Rubycli::ArgumentError, '--json-args cannot be combined with --eval-args or --eval-lax' if json_mode && eval_mode
400
+
401
+ initializer_method = initializer_method_for(target)
402
+ tokens = Array(raw_value)
403
+
404
+ positional_args = []
405
+ keyword_args = {}
406
+
407
+ Rubycli.argument_mode_controller.with_json_mode(json_mode) do
408
+ Rubycli.argument_mode_controller.with_eval_mode(eval_mode, lax: eval_lax) do
409
+ positional_args, keyword_args = Rubycli.argument_parser.parse(tokens.dup, initializer_method)
410
+ Rubycli.apply_argument_coercions(positional_args, keyword_args)
411
+ Rubycli.argument_parser.validate_inputs(initializer_method, positional_args, keyword_args)
412
+ end
413
+ end
414
+
415
+ [positional_args || [], keyword_args || {}]
416
+ rescue Rubycli::ArgumentError => e
417
+ raise Runner::Error, "Failed to parse --new arguments: #{e.message}"
418
+ end
419
+
420
+ def initializer_method_for(target)
421
+ return nil unless target.is_a?(Class)
422
+
423
+ target.instance_method(:initialize) rescue nil
424
+ end
425
+
376
426
  def prepare_runner_target(
377
427
  target_path,
378
428
  class_name,
379
429
  new: false,
430
+ new_args: nil,
431
+ json_mode: false,
432
+ eval_mode: false,
433
+ eval_lax: false,
380
434
  pre_scripts: [],
381
435
  constant_mode: nil
382
436
  )
@@ -403,7 +457,9 @@ module Rubycli
403
457
  )
404
458
  end
405
459
 
406
- runner_target = new ? instantiate_target(target) : target
460
+ initializer_args = new ? parse_initializer_arguments(new_args, target, json_mode: json_mode, eval_mode: eval_mode, eval_lax: eval_lax) : nil
461
+
462
+ runner_target = new ? instantiate_target(target, initializer_args) : target
407
463
  runner_target = apply_pre_scripts(pre_scripts, target, runner_target)
408
464
  [runner_target, full_path]
409
465
  end
@@ -531,19 +587,24 @@ module Rubycli
531
587
 
532
588
  def build_missing_constant_message(name, defined_constants, full_path, details: nil)
533
589
  lines = ["Could not find definition: #{name}"]
534
- lines << " Loaded file: #{File.expand_path(full_path)}" if full_path
590
+ lines << ''
591
+ lines << "Loaded file: #{File.expand_path(full_path)}" if full_path
535
592
 
536
593
  if defined_constants && !defined_constants.empty?
537
594
  sample = defined_constants.first(5)
538
595
  suffix = defined_constants.size > sample.size ? " ... (#{defined_constants.size} total)" : ''
539
- lines << " Constants found in this file: #{sample.join(', ')}#{suffix}"
596
+ lines << "Constants found in this file: #{sample.join(', ')}#{suffix}"
540
597
  else
541
- lines << " Rubycli could not detect any publicly exposable constants in this file."
598
+ lines << 'Rubycli could not detect any publicly exposable constants in this file.'
542
599
  end
543
600
 
544
- lines << " #{details}" if details
601
+ if details
602
+ lines << ''
603
+ lines << details
604
+ end
545
605
 
546
- lines << " Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI."
606
+ lines << ''
607
+ lines << 'Hint: Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI.'
547
608
  lines.join("\n")
548
609
  end
549
610
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubycli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - inakaegg
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-11-09 00:00:00.000000000 Z
10
+ date: 2025-11-16 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Rubycli turns plain Ruby classes and modules into command-line interfaces
13
13
  by reading their documentation comments, inspired by Python Fire but tailored for