rubycli 0.1.2 → 0.1.4
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 +25 -0
- data/README.ja.md +19 -4
- data/README.md +19 -4
- data/lib/rubycli/argument_mode_controller.rb +69 -0
- data/lib/rubycli/argument_parser.rb +53 -122
- data/lib/rubycli/arguments/token_stream.rb +41 -0
- data/lib/rubycli/arguments/value_converter.rb +74 -0
- data/lib/rubycli/cli.rb +9 -6
- data/lib/rubycli/command_line.rb +20 -4
- data/lib/rubycli/constant_capture.rb +50 -0
- data/lib/rubycli/documentation/comment_extractor.rb +52 -0
- data/lib/rubycli/documentation/metadata_parser.rb +838 -0
- data/lib/rubycli/documentation_registry.rb +10 -852
- data/lib/rubycli/environment.rb +8 -1
- data/lib/rubycli/eval_coercer.rb +16 -1
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +196 -118
- metadata +8 -2
data/lib/rubycli/environment.rb
CHANGED
|
@@ -24,7 +24,14 @@ module Rubycli
|
|
|
24
24
|
|
|
25
25
|
def allow_param_comments?
|
|
26
26
|
value = fetch_env_value('RUBYCLI_ALLOW_PARAM_COMMENT', 'ON')
|
|
27
|
-
%w[on 1 true].include?(value
|
|
27
|
+
%w[on 1 true].include?(value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def constant_resolution_mode
|
|
31
|
+
value = fetch_env_value('RUBYCLI_AUTO_TARGET', 'strict')
|
|
32
|
+
return :auto if %w[auto on true yes 1].include?(value)
|
|
33
|
+
|
|
34
|
+
:strict
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def handle_documentation_issue(message, file: nil, line: nil)
|
data/lib/rubycli/eval_coercer.rb
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
module Rubycli
|
|
2
2
|
class EvalCoercer
|
|
3
3
|
THREAD_KEY = :rubycli_eval_mode
|
|
4
|
+
LAX_THREAD_KEY = :rubycli_eval_lax_mode
|
|
4
5
|
EVAL_BINDING = Object.new.instance_eval { binding }
|
|
5
6
|
|
|
6
7
|
def eval_mode?
|
|
7
8
|
Thread.current[THREAD_KEY] == true
|
|
8
9
|
end
|
|
9
10
|
|
|
10
|
-
def
|
|
11
|
+
def eval_lax_mode?
|
|
12
|
+
Thread.current[LAX_THREAD_KEY] == true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def with_eval_mode(enabled = true, lax: false)
|
|
11
16
|
previous = Thread.current[THREAD_KEY]
|
|
17
|
+
previous_lax = Thread.current[LAX_THREAD_KEY]
|
|
12
18
|
Thread.current[THREAD_KEY] = enabled
|
|
19
|
+
Thread.current[LAX_THREAD_KEY] = enabled && lax
|
|
13
20
|
yield
|
|
14
21
|
ensure
|
|
15
22
|
Thread.current[THREAD_KEY] = previous
|
|
23
|
+
Thread.current[LAX_THREAD_KEY] = previous_lax
|
|
16
24
|
end
|
|
17
25
|
|
|
18
26
|
def coerce_eval_value(value)
|
|
@@ -37,6 +45,13 @@ module Rubycli
|
|
|
37
45
|
return trimmed if trimmed.empty?
|
|
38
46
|
|
|
39
47
|
EVAL_BINDING.eval(trimmed)
|
|
48
|
+
rescue SyntaxError, NameError => e
|
|
49
|
+
if eval_lax_mode?
|
|
50
|
+
warn "[rubycli] Failed to evaluate argument as Ruby (#{e.message.strip}). Passing it through because --eval-lax is enabled."
|
|
51
|
+
expression
|
|
52
|
+
else
|
|
53
|
+
raise
|
|
54
|
+
end
|
|
40
55
|
end
|
|
41
56
|
end
|
|
42
57
|
end
|
data/lib/rubycli/version.rb
CHANGED
data/lib/rubycli.rb
CHANGED
|
@@ -12,11 +12,15 @@ require_relative 'rubycli/type_utils'
|
|
|
12
12
|
require_relative 'rubycli/documentation_registry'
|
|
13
13
|
require_relative 'rubycli/json_coercer'
|
|
14
14
|
require_relative 'rubycli/eval_coercer'
|
|
15
|
+
require_relative 'rubycli/arguments/token_stream'
|
|
16
|
+
require_relative 'rubycli/arguments/value_converter'
|
|
17
|
+
require_relative 'rubycli/argument_mode_controller'
|
|
15
18
|
require_relative 'rubycli/argument_parser'
|
|
16
19
|
require_relative 'rubycli/help_renderer'
|
|
17
20
|
require_relative 'rubycli/result_emitter'
|
|
18
21
|
require_relative 'rubycli/cli'
|
|
19
22
|
require_relative 'rubycli/command_line'
|
|
23
|
+
require_relative 'rubycli/constant_capture'
|
|
20
24
|
|
|
21
25
|
module Rubycli
|
|
22
26
|
class Error < StandardError; end
|
|
@@ -40,6 +44,13 @@ module Rubycli
|
|
|
40
44
|
@eval_coercer ||= EvalCoercer.new
|
|
41
45
|
end
|
|
42
46
|
|
|
47
|
+
def argument_mode_controller
|
|
48
|
+
@argument_mode_controller ||= ArgumentModeController.new(
|
|
49
|
+
json_coercer: json_coercer,
|
|
50
|
+
eval_coercer: eval_coercer
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
43
54
|
def argument_parser
|
|
44
55
|
@argument_parser ||= ArgumentParser.new(
|
|
45
56
|
environment: environment,
|
|
@@ -57,6 +68,10 @@ module Rubycli
|
|
|
57
68
|
@result_emitter ||= ResultEmitter.new(environment: environment)
|
|
58
69
|
end
|
|
59
70
|
|
|
71
|
+
def constant_capture
|
|
72
|
+
@constant_capture ||= ConstantCapture.new
|
|
73
|
+
end
|
|
74
|
+
|
|
60
75
|
def cli
|
|
61
76
|
@cli ||= CLI.new(
|
|
62
77
|
environment: environment,
|
|
@@ -68,7 +83,10 @@ module Rubycli
|
|
|
68
83
|
end
|
|
69
84
|
|
|
70
85
|
def run(target, args = ARGV, cli_mode = true)
|
|
71
|
-
cli.run(target, args.dup, cli_mode)
|
|
86
|
+
status = cli.run(target, args.dup, cli_mode)
|
|
87
|
+
return status unless cli_mode
|
|
88
|
+
|
|
89
|
+
exit(status.to_i)
|
|
72
90
|
end
|
|
73
91
|
|
|
74
92
|
def parse_arguments(args, method = nil)
|
|
@@ -106,15 +124,11 @@ module Rubycli
|
|
|
106
124
|
end
|
|
107
125
|
|
|
108
126
|
def json_mode?
|
|
109
|
-
|
|
127
|
+
argument_mode_controller.json_mode?
|
|
110
128
|
end
|
|
111
129
|
|
|
112
130
|
def with_json_mode(enabled = true)
|
|
113
|
-
|
|
114
|
-
raise Rubycli::ArgumentError, '--json-args and --eval-args cannot be used together'
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
json_coercer.with_json_mode(enabled) { yield }
|
|
131
|
+
argument_mode_controller.with_json_mode(enabled) { yield }
|
|
118
132
|
end
|
|
119
133
|
|
|
120
134
|
def coerce_json_value(value)
|
|
@@ -122,15 +136,15 @@ module Rubycli
|
|
|
122
136
|
end
|
|
123
137
|
|
|
124
138
|
def eval_mode?
|
|
125
|
-
|
|
139
|
+
argument_mode_controller.eval_mode?
|
|
126
140
|
end
|
|
127
141
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
end
|
|
142
|
+
def eval_lax_mode?
|
|
143
|
+
eval_coercer.eval_lax_mode?
|
|
144
|
+
end
|
|
132
145
|
|
|
133
|
-
|
|
146
|
+
def with_eval_mode(enabled = true, **options)
|
|
147
|
+
argument_mode_controller.with_eval_mode(enabled, **options) { yield }
|
|
134
148
|
end
|
|
135
149
|
|
|
136
150
|
def coerce_eval_value(value)
|
|
@@ -138,25 +152,7 @@ module Rubycli
|
|
|
138
152
|
end
|
|
139
153
|
|
|
140
154
|
def apply_argument_coercions(pos_args, kw_args)
|
|
141
|
-
|
|
142
|
-
raise Rubycli::ArgumentError, '--json-args and --eval-args cannot be used together'
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
if json_mode?
|
|
146
|
-
pos_args.map! { |value| coerce_json_value(value) }
|
|
147
|
-
kw_args.keys.each do |key|
|
|
148
|
-
kw_args[key] = coerce_json_value(kw_args[key])
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
return unless eval_mode?
|
|
153
|
-
|
|
154
|
-
pos_args.map! { |value| coerce_eval_value(value) }
|
|
155
|
-
kw_args.keys.each do |key|
|
|
156
|
-
kw_args[key] = coerce_eval_value(kw_args[key])
|
|
157
|
-
end
|
|
158
|
-
rescue ArgumentError => e
|
|
159
|
-
raise Rubycli::ArgumentError, e.message
|
|
155
|
+
argument_mode_controller.apply_argument_coercions(pos_args, kw_args)
|
|
160
156
|
end
|
|
161
157
|
|
|
162
158
|
def apply_json_coercion(pos_args, kw_args)
|
|
@@ -168,6 +164,44 @@ module Rubycli
|
|
|
168
164
|
class Error < Rubycli::Error; end
|
|
169
165
|
class PreScriptError < Error; end
|
|
170
166
|
|
|
167
|
+
ConstantCandidate = Struct.new(
|
|
168
|
+
:name,
|
|
169
|
+
:constant,
|
|
170
|
+
:class_methods,
|
|
171
|
+
:instance_methods,
|
|
172
|
+
keyword_init: true
|
|
173
|
+
) do
|
|
174
|
+
def callable?(instantiate: false)
|
|
175
|
+
return true if class_methods.any?
|
|
176
|
+
|
|
177
|
+
instantiate && instance_methods.any?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def matches?(base_name)
|
|
181
|
+
name.split('::').last == base_name
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def instance_only?
|
|
185
|
+
instance_methods.any? && class_methods.empty?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def summary
|
|
189
|
+
parts = []
|
|
190
|
+
parts << "class: #{format_methods(class_methods)}" if class_methods.any?
|
|
191
|
+
parts << "instance: #{format_methods(instance_methods)}" if instance_methods.any?
|
|
192
|
+
parts << 'no CLI methods' if parts.empty?
|
|
193
|
+
parts.join(' | ')
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def format_methods(methods)
|
|
199
|
+
list = methods.first(3).map(&:to_s)
|
|
200
|
+
list << '...' if methods.size > 3
|
|
201
|
+
list.join(', ')
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
171
205
|
module_function
|
|
172
206
|
|
|
173
207
|
def execute(
|
|
@@ -177,31 +211,45 @@ module Rubycli
|
|
|
177
211
|
new: false,
|
|
178
212
|
json: false,
|
|
179
213
|
eval_args: false,
|
|
180
|
-
|
|
214
|
+
eval_lax: false,
|
|
215
|
+
pre_scripts: [],
|
|
216
|
+
constant_mode: nil
|
|
181
217
|
)
|
|
182
218
|
raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
|
|
183
219
|
original_program_name = $PROGRAM_NAME
|
|
184
220
|
if json && eval_args
|
|
185
|
-
raise Error, '--json-args
|
|
221
|
+
raise Error, '--json-args cannot be combined with --eval-args or --eval-lax'
|
|
186
222
|
end
|
|
187
223
|
|
|
188
224
|
full_path = find_target_path(target_path)
|
|
189
|
-
|
|
225
|
+
capture = Rubycli.constant_capture
|
|
226
|
+
capture.capture(full_path) { load full_path }
|
|
190
227
|
$PROGRAM_NAME = File.basename(full_path)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
228
|
+
constant_mode ||= Rubycli.environment.constant_resolution_mode
|
|
229
|
+
candidates = build_constant_candidates(full_path, capture.constants_for(full_path))
|
|
230
|
+
defined_constants = candidates.map(&:name)
|
|
231
|
+
|
|
232
|
+
target = if class_name
|
|
233
|
+
constantize(
|
|
234
|
+
class_name,
|
|
235
|
+
defined_constants: defined_constants,
|
|
236
|
+
full_path: full_path
|
|
237
|
+
)
|
|
238
|
+
else
|
|
239
|
+
select_constant_candidate(
|
|
240
|
+
full_path,
|
|
241
|
+
camelize(File.basename(full_path, '.rb')),
|
|
242
|
+
candidates,
|
|
243
|
+
constant_mode,
|
|
244
|
+
instantiate: new
|
|
245
|
+
)
|
|
246
|
+
end
|
|
199
247
|
runner_target = new ? instantiate_target(target) : target
|
|
200
248
|
runner_target = apply_pre_scripts(pre_scripts, target, runner_target)
|
|
201
249
|
|
|
202
250
|
original_argv = ARGV.dup
|
|
203
251
|
ARGV.replace(Array(cli_args).dup)
|
|
204
|
-
run_with_modes(runner_target, json: json, eval_args: eval_args)
|
|
252
|
+
run_with_modes(runner_target, json: json, eval_args: eval_args, eval_lax: eval_lax)
|
|
205
253
|
ensure
|
|
206
254
|
$PROGRAM_NAME = original_program_name if original_program_name
|
|
207
255
|
ARGV.replace(original_argv) if original_argv
|
|
@@ -251,12 +299,6 @@ module Rubycli
|
|
|
251
299
|
end
|
|
252
300
|
end
|
|
253
301
|
|
|
254
|
-
def infer_class_name(path)
|
|
255
|
-
base = File.basename(path, '.rb')
|
|
256
|
-
base_const = camelize(base)
|
|
257
|
-
detect_constant_for_file(path, base_const) || base_const
|
|
258
|
-
end
|
|
259
|
-
|
|
260
302
|
def camelize(name)
|
|
261
303
|
name.split(/[^a-zA-Z0-9]+/).reject(&:empty?).map { |part|
|
|
262
304
|
part[0].upcase + part[1..].downcase
|
|
@@ -288,106 +330,140 @@ module Rubycli
|
|
|
288
330
|
raise Error, "Failed to instantiate target: #{e.message}"
|
|
289
331
|
end
|
|
290
332
|
|
|
291
|
-
def run_with_modes(target, json:, eval_args:)
|
|
333
|
+
def run_with_modes(target, json:, eval_args:, eval_lax:)
|
|
292
334
|
runner = proc { Rubycli.run(target) }
|
|
293
335
|
|
|
294
336
|
if json
|
|
295
337
|
Rubycli.with_json_mode(true, &runner)
|
|
296
338
|
elsif eval_args
|
|
297
|
-
Rubycli.with_eval_mode(true, &runner)
|
|
339
|
+
Rubycli.with_eval_mode(true, lax: eval_lax, &runner)
|
|
298
340
|
else
|
|
299
341
|
runner.call
|
|
300
342
|
end
|
|
301
343
|
end
|
|
302
344
|
|
|
303
|
-
def
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
345
|
+
def build_constant_candidates(path, constant_names)
|
|
346
|
+
normalized = normalize_path(path)
|
|
347
|
+
Array(constant_names).each_with_object([]) do |const_name, memo|
|
|
348
|
+
constant = safe_constant_lookup(const_name)
|
|
349
|
+
next unless constant.is_a?(Module)
|
|
308
350
|
|
|
309
|
-
|
|
310
|
-
|
|
351
|
+
class_methods = collect_defined_methods(constant.singleton_class, normalized)
|
|
352
|
+
instance_methods = collect_defined_methods(constant, normalized)
|
|
311
353
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
354
|
+
memo << ConstantCandidate.new(
|
|
355
|
+
name: const_name,
|
|
356
|
+
constant: constant,
|
|
357
|
+
class_methods: class_methods,
|
|
358
|
+
instance_methods: instance_methods
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
316
362
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
363
|
+
def collect_defined_methods(owner, normalized_path)
|
|
364
|
+
owner.public_instance_methods(false).each_with_object([]) do |method_name, memo|
|
|
365
|
+
method_object = owner.instance_method(method_name)
|
|
366
|
+
location = method_object.source_location
|
|
367
|
+
next unless location && normalize_path(location[0]) == normalized_path
|
|
321
368
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
369
|
+
memo << method_name
|
|
370
|
+
end
|
|
371
|
+
rescue TypeError
|
|
372
|
+
[]
|
|
325
373
|
end
|
|
326
374
|
|
|
327
|
-
def
|
|
328
|
-
|
|
375
|
+
def safe_constant_lookup(name)
|
|
376
|
+
parts = name.split('::').reject(&:empty?)
|
|
377
|
+
context = Object
|
|
329
378
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
mod_name = module_name_for(mod)
|
|
333
|
-
next unless mod_name
|
|
334
|
-
next unless safe_const_defined?(mod, base_const)
|
|
379
|
+
parts.each do |const_name|
|
|
380
|
+
return nil unless context.const_defined?(const_name, false)
|
|
335
381
|
|
|
336
|
-
|
|
337
|
-
|
|
382
|
+
context = context.const_get(const_name)
|
|
383
|
+
end
|
|
338
384
|
|
|
339
|
-
|
|
340
|
-
|
|
385
|
+
context
|
|
386
|
+
rescue NameError
|
|
387
|
+
nil
|
|
341
388
|
end
|
|
342
389
|
|
|
343
|
-
def
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
390
|
+
def select_constant_candidate(path, base_const, candidates, constant_mode, instantiate: false)
|
|
391
|
+
if candidates.empty?
|
|
392
|
+
raise Error, build_missing_constant_message(
|
|
393
|
+
base_const,
|
|
394
|
+
[],
|
|
395
|
+
path,
|
|
396
|
+
details: 'Rubycli could not detect any constants in this file.'
|
|
397
|
+
)
|
|
398
|
+
end
|
|
348
399
|
|
|
349
|
-
|
|
350
|
-
|
|
400
|
+
matching = candidates.find { |candidate| candidate.matches?(base_const) }
|
|
401
|
+
if matching
|
|
402
|
+
return matching.constant if matching.callable?(instantiate: instantiate)
|
|
403
|
+
|
|
404
|
+
detail = if matching.instance_only?
|
|
405
|
+
"#{matching.name} only defines instance methods in this file. Run with --new to instantiate before invoking CLI commands."
|
|
406
|
+
else
|
|
407
|
+
"#{matching.name} does not define any CLI-callable methods in this file. Add a public class or instance method defined in this file."
|
|
408
|
+
end
|
|
409
|
+
raise Error, build_missing_constant_message(
|
|
410
|
+
base_const,
|
|
411
|
+
candidates.map(&:name),
|
|
412
|
+
path,
|
|
413
|
+
details: detail
|
|
414
|
+
)
|
|
415
|
+
end
|
|
351
416
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
417
|
+
callable = candidates.select { |candidate| candidate.callable?(instantiate: instantiate) }
|
|
418
|
+
if callable.empty?
|
|
419
|
+
raise Error, build_missing_constant_message(
|
|
420
|
+
base_const,
|
|
421
|
+
candidates.map(&:name),
|
|
422
|
+
path,
|
|
423
|
+
details: 'Rubycli detected constants in this file, but none define CLI-callable methods. Add a public class or instance method defined in this file.'
|
|
424
|
+
)
|
|
425
|
+
end
|
|
355
426
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
[]
|
|
360
|
-
end
|
|
427
|
+
if constant_mode == :auto && callable.size == 1
|
|
428
|
+
return callable.first.constant
|
|
429
|
+
end
|
|
361
430
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
431
|
+
details = build_ambiguous_constant_details(callable, path)
|
|
432
|
+
raise Error, build_missing_constant_message(
|
|
433
|
+
base_const,
|
|
434
|
+
candidates.map(&:name),
|
|
435
|
+
path,
|
|
436
|
+
details: details
|
|
437
|
+
)
|
|
366
438
|
end
|
|
367
439
|
|
|
368
|
-
def
|
|
369
|
-
|
|
440
|
+
def build_ambiguous_constant_details(candidates, path)
|
|
441
|
+
command_target = File.basename(path)
|
|
442
|
+
if candidates.size == 1
|
|
443
|
+
candidate = candidates.first
|
|
444
|
+
lines = []
|
|
445
|
+
lines << "This file defines #{candidate.name}, but its name does not match #{command_target}."
|
|
446
|
+
lines << 'Re-run by specifying the constant explicitly:'
|
|
447
|
+
lines << " rubycli #{command_target} #{candidate.name} ..."
|
|
448
|
+
lines << 'Alternatively pass --auto-target (or RUBYCLI_AUTO_TARGET=auto) to auto-select it.'
|
|
449
|
+
return lines.join("\n")
|
|
450
|
+
end
|
|
370
451
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
452
|
+
lines = ['Multiple CLI-capable constants were found in this file:']
|
|
453
|
+
candidates.each do |candidate|
|
|
454
|
+
hint = candidate.instance_only? ? ' (instance methods only; use --new)' : ''
|
|
455
|
+
lines << " - #{candidate.name}: #{candidate.summary}#{hint}"
|
|
456
|
+
end
|
|
457
|
+
lines << "Specify one explicitly, e.g. rubycli #{command_target} MyRunner"
|
|
458
|
+
lines << 'Or pass --auto-target to allow Rubycli to auto-select a single candidate.'
|
|
459
|
+
lines.join("\n")
|
|
374
460
|
end
|
|
375
461
|
|
|
376
|
-
def
|
|
377
|
-
|
|
378
|
-
context = Object
|
|
379
|
-
|
|
380
|
-
parts.each do |const_name|
|
|
381
|
-
return false unless context.const_defined?(const_name, false)
|
|
382
|
-
|
|
383
|
-
context = context.const_get(const_name)
|
|
384
|
-
end
|
|
385
|
-
true
|
|
386
|
-
rescue NameError
|
|
387
|
-
false
|
|
462
|
+
def normalize_path(path)
|
|
463
|
+
File.expand_path(path.to_s)
|
|
388
464
|
end
|
|
389
465
|
|
|
390
|
-
def build_missing_constant_message(name, defined_constants, full_path)
|
|
466
|
+
def build_missing_constant_message(name, defined_constants, full_path, details: nil)
|
|
391
467
|
lines = ["Could not find definition: #{name}"]
|
|
392
468
|
lines << " Loaded file: #{File.expand_path(full_path)}" if full_path
|
|
393
469
|
|
|
@@ -399,6 +475,8 @@ module Rubycli
|
|
|
399
475
|
lines << " Rubycli could not detect any publicly exposable constants in this file."
|
|
400
476
|
end
|
|
401
477
|
|
|
478
|
+
lines << " #{details}" if details
|
|
479
|
+
|
|
402
480
|
lines << " Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI."
|
|
403
481
|
lines.join("\n")
|
|
404
482
|
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.4
|
|
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-07 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
|
|
@@ -25,9 +25,15 @@ files:
|
|
|
25
25
|
- README.md
|
|
26
26
|
- exe/rubycli
|
|
27
27
|
- lib/rubycli.rb
|
|
28
|
+
- lib/rubycli/argument_mode_controller.rb
|
|
28
29
|
- lib/rubycli/argument_parser.rb
|
|
30
|
+
- lib/rubycli/arguments/token_stream.rb
|
|
31
|
+
- lib/rubycli/arguments/value_converter.rb
|
|
29
32
|
- lib/rubycli/cli.rb
|
|
30
33
|
- lib/rubycli/command_line.rb
|
|
34
|
+
- lib/rubycli/constant_capture.rb
|
|
35
|
+
- lib/rubycli/documentation/comment_extractor.rb
|
|
36
|
+
- lib/rubycli/documentation/metadata_parser.rb
|
|
31
37
|
- lib/rubycli/documentation_registry.rb
|
|
32
38
|
- lib/rubycli/environment.rb
|
|
33
39
|
- lib/rubycli/eval_coercer.rb
|