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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.ja.md +23 -4
- data/README.md +23 -4
- data/lib/rubycli/argument_parser.rb +60 -1
- data/lib/rubycli/command_line.rb +25 -3
- data/lib/rubycli/documentation/metadata_parser.rb +223 -6
- data/lib/rubycli/documentation_registry.rb +1 -0
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +70 -9
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '07113181085c0df0c8f4bc34aca661d4c29fb9bbc309012ace52b36e8bdf9ead'
|
|
4
|
+
data.tar.gz: 773d7de6a820fd1702e4e618c595237e2293c74dca6cc83f3ea57ba9eaaa18db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`
|
|
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`
|
|
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`
|
|
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.
|
|
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
|
|
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`
|
|
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) {
|
|
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) {
|
data/lib/rubycli/command_line.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
790
|
-
|
|
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
|
|
data/lib/rubycli/version.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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 <<
|
|
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 << "
|
|
596
|
+
lines << "Constants found in this file: #{sample.join(', ')}#{suffix}"
|
|
540
597
|
else
|
|
541
|
-
lines <<
|
|
598
|
+
lines << 'Rubycli could not detect any publicly exposable constants in this file.'
|
|
542
599
|
end
|
|
543
600
|
|
|
544
|
-
|
|
601
|
+
if details
|
|
602
|
+
lines << ''
|
|
603
|
+
lines << details
|
|
604
|
+
end
|
|
545
605
|
|
|
546
|
-
lines <<
|
|
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.
|
|
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-
|
|
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
|