rubycli 0.1.7 → 0.2.0
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 +77 -2
- data/LICENSE +1 -1
- data/README.ja.md +357 -252
- data/README.md +386 -250
- data/examples/documentation_style_showcase.rb +110 -0
- data/examples/fallback_example.rb +14 -0
- data/examples/fallback_example_with_extra_docs.rb +13 -0
- data/examples/hello_app.rb +10 -0
- data/examples/hello_app_with_docs.rb +17 -0
- data/examples/hello_app_with_require.rb +19 -0
- data/examples/mismatched_constant_runner.rb +16 -0
- data/examples/multi_constant_runner.rb +20 -0
- data/examples/new_mode_runner.rb +46 -0
- data/examples/strict_choices_demo.rb +22 -0
- data/examples/typed_arguments_demo.rb +42 -0
- data/lib/rubycli/argument_mode_controller.rb +7 -11
- data/lib/rubycli/argument_parser.rb +295 -96
- data/lib/rubycli/cli.rb +26 -30
- data/lib/rubycli/command_line.rb +151 -86
- data/lib/rubycli/constant_capture.rb +563 -6
- data/lib/rubycli/documentation/metadata_parser.rb +108 -74
- data/lib/rubycli/environment.rb +17 -9
- data/lib/rubycli/eval_coercer.rb +27 -10
- data/lib/rubycli/help_renderer.rb +67 -62
- data/lib/rubycli/json_coercer.rb +2 -0
- data/lib/rubycli/result_emitter.rb +18 -8
- data/lib/rubycli/runner.rb +513 -0
- data/lib/rubycli/type_utils.rb +8 -6
- data/lib/rubycli/types.rb +2 -0
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +7 -456
- metadata +59 -4
data/lib/rubycli.rb
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
|
-
feature_path = File.expand_path(__FILE__)
|
|
6
|
-
$LOADED_FEATURES << feature_path unless $LOADED_FEATURES.include?(feature_path)
|
|
7
|
-
|
|
8
5
|
require_relative 'rubycli/version'
|
|
9
6
|
require_relative 'rubycli/environment'
|
|
10
7
|
require_relative 'rubycli/types'
|
|
@@ -127,8 +124,8 @@ module Rubycli
|
|
|
127
124
|
argument_mode_controller.json_mode?
|
|
128
125
|
end
|
|
129
126
|
|
|
130
|
-
def with_json_mode(enabled = true)
|
|
131
|
-
argument_mode_controller.with_json_mode(enabled)
|
|
127
|
+
def with_json_mode(enabled = true, &block)
|
|
128
|
+
argument_mode_controller.with_json_mode(enabled, &block)
|
|
132
129
|
end
|
|
133
130
|
|
|
134
131
|
def coerce_json_value(value)
|
|
@@ -143,8 +140,8 @@ module Rubycli
|
|
|
143
140
|
eval_coercer.eval_lax_mode?
|
|
144
141
|
end
|
|
145
142
|
|
|
146
|
-
def with_eval_mode(enabled = true, **options)
|
|
147
|
-
argument_mode_controller.with_eval_mode(enabled, **options)
|
|
143
|
+
def with_eval_mode(enabled = true, **options, &block)
|
|
144
|
+
argument_mode_controller.with_eval_mode(enabled, **options, &block)
|
|
148
145
|
end
|
|
149
146
|
|
|
150
147
|
def coerce_eval_value(value)
|
|
@@ -159,453 +156,7 @@ module Rubycli
|
|
|
159
156
|
apply_argument_coercions(pos_args, kw_args)
|
|
160
157
|
end
|
|
161
158
|
end
|
|
162
|
-
|
|
163
|
-
module Runner
|
|
164
|
-
class Error < Rubycli::Error; end
|
|
165
|
-
class PreScriptError < Error; end
|
|
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
|
-
|
|
205
|
-
module_function
|
|
206
|
-
|
|
207
|
-
def execute(
|
|
208
|
-
target_path,
|
|
209
|
-
class_name = nil,
|
|
210
|
-
cli_args = nil,
|
|
211
|
-
new: false,
|
|
212
|
-
new_args: nil,
|
|
213
|
-
json: false,
|
|
214
|
-
eval_args: false,
|
|
215
|
-
eval_lax: false,
|
|
216
|
-
pre_scripts: [],
|
|
217
|
-
constant_mode: nil
|
|
218
|
-
)
|
|
219
|
-
raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
|
|
220
|
-
if json && eval_args
|
|
221
|
-
raise Error, '--json-args cannot be combined with --eval-args or --eval-lax'
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
runner_target, full_path = prepare_runner_target(
|
|
225
|
-
target_path,
|
|
226
|
-
class_name,
|
|
227
|
-
new: new,
|
|
228
|
-
new_args: new_args,
|
|
229
|
-
json_mode: json,
|
|
230
|
-
eval_mode: eval_args,
|
|
231
|
-
eval_lax: eval_lax,
|
|
232
|
-
pre_scripts: pre_scripts,
|
|
233
|
-
constant_mode: constant_mode
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
original_program_name = $PROGRAM_NAME
|
|
237
|
-
original_argv = nil
|
|
238
|
-
$PROGRAM_NAME = File.basename(full_path)
|
|
239
|
-
original_argv = ARGV.dup
|
|
240
|
-
ARGV.replace(Array(cli_args).dup)
|
|
241
|
-
run_with_modes(runner_target, json: json, eval_args: eval_args, eval_lax: eval_lax)
|
|
242
|
-
ensure
|
|
243
|
-
$PROGRAM_NAME = original_program_name if original_program_name
|
|
244
|
-
ARGV.replace(original_argv) if original_argv
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def check(
|
|
248
|
-
target_path,
|
|
249
|
-
class_name = nil,
|
|
250
|
-
new: false,
|
|
251
|
-
new_args: nil,
|
|
252
|
-
pre_scripts: [],
|
|
253
|
-
constant_mode: nil,
|
|
254
|
-
json_mode: false,
|
|
255
|
-
eval_mode: false,
|
|
256
|
-
eval_lax: false
|
|
257
|
-
)
|
|
258
|
-
raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
|
|
259
|
-
previous_doc_check = Rubycli.environment.doc_check_mode?
|
|
260
|
-
Rubycli.environment.clear_documentation_issues!
|
|
261
|
-
Rubycli.environment.enable_doc_check!
|
|
262
|
-
|
|
263
|
-
runner_target, full_path = prepare_runner_target(
|
|
264
|
-
target_path,
|
|
265
|
-
class_name,
|
|
266
|
-
new: new,
|
|
267
|
-
new_args: new_args,
|
|
268
|
-
json_mode: json_mode,
|
|
269
|
-
eval_mode: eval_mode,
|
|
270
|
-
eval_lax: eval_lax,
|
|
271
|
-
pre_scripts: pre_scripts,
|
|
272
|
-
constant_mode: constant_mode
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
original_program_name = $PROGRAM_NAME
|
|
276
|
-
$PROGRAM_NAME = File.basename(full_path)
|
|
277
|
-
|
|
278
|
-
catalog = Rubycli.cli.command_catalog_for(runner_target)
|
|
279
|
-
Array(catalog&.entries).each do |entry|
|
|
280
|
-
method_obj = entry&.method
|
|
281
|
-
Rubycli.documentation_registry.metadata_for(method_obj) if method_obj
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
if catalog&.entries&.empty? && runner_target.respond_to?(:call)
|
|
285
|
-
method_obj = runner_target.method(:call) rescue nil
|
|
286
|
-
Rubycli.documentation_registry.metadata_for(method_obj) if method_obj
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
issues = Rubycli.environment.documentation_issues
|
|
290
|
-
if issues.empty?
|
|
291
|
-
puts 'rubycli documentation OK'
|
|
292
|
-
0
|
|
293
|
-
else
|
|
294
|
-
warn "[ERROR] rubycli documentation check failed (#{issues.size} issue#{issues.size == 1 ? '' : 's'})"
|
|
295
|
-
1
|
|
296
|
-
end
|
|
297
|
-
ensure
|
|
298
|
-
Rubycli.environment.disable_doc_check! unless previous_doc_check
|
|
299
|
-
$PROGRAM_NAME = original_program_name if original_program_name
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def apply_pre_scripts(sources, base_target, initial_target)
|
|
303
|
-
Array(sources).reduce(initial_target) do |current_target, source|
|
|
304
|
-
result = evaluate_pre_script(source, base_target, current_target)
|
|
305
|
-
result.nil? ? current_target : result
|
|
306
|
-
end
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def evaluate_pre_script(source, base_target, current_target)
|
|
310
|
-
code, context = read_pre_script_code(source)
|
|
311
|
-
pre_binding = Object.new.instance_eval { binding }
|
|
312
|
-
pre_binding.local_variable_set(:target, base_target)
|
|
313
|
-
pre_binding.local_variable_set(:current, current_target)
|
|
314
|
-
pre_binding.local_variable_set(:instance, current_target)
|
|
315
|
-
|
|
316
|
-
Rubycli.with_eval_mode(true) do
|
|
317
|
-
pre_binding.eval(code, context)
|
|
318
|
-
end
|
|
319
|
-
rescue Errno::ENOENT
|
|
320
|
-
raise PreScriptError, "Pre-script file not found: #{context}"
|
|
321
|
-
rescue StandardError => e
|
|
322
|
-
raise PreScriptError, "Failed to evaluate pre-script (#{context}): #{e.message}"
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
def read_pre_script_code(source)
|
|
326
|
-
value = source[:value]
|
|
327
|
-
inline_context = source[:context] || '(inline pre-script)'
|
|
328
|
-
|
|
329
|
-
if !value.nil? && File.file?(value)
|
|
330
|
-
[File.read(value), File.expand_path(value)]
|
|
331
|
-
else
|
|
332
|
-
[String(value), inline_context]
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
def find_target_path(path)
|
|
337
|
-
if File.file?(path)
|
|
338
|
-
File.expand_path(path)
|
|
339
|
-
elsif File.file?("#{path}.rb")
|
|
340
|
-
File.expand_path("#{path}.rb")
|
|
341
|
-
else
|
|
342
|
-
raise Error, "File not found: #{path}"
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
def camelize(name)
|
|
347
|
-
name.split(/[^a-zA-Z0-9]+/).reject(&:empty?).map { |part|
|
|
348
|
-
part[0].upcase + part[1..].downcase
|
|
349
|
-
}.join
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
def constantize(name, defined_constants: nil, full_path: nil)
|
|
353
|
-
parts = name.to_s.split('::').reject(&:empty?)
|
|
354
|
-
raise Error, "Unable to resolve class/module name: #{name.inspect}" if parts.empty?
|
|
355
|
-
|
|
356
|
-
parts.reduce(Object) do |context, const_name|
|
|
357
|
-
context.const_get(const_name)
|
|
358
|
-
end
|
|
359
|
-
rescue NameError
|
|
360
|
-
message = build_missing_constant_message(name, defined_constants, full_path)
|
|
361
|
-
raise Error.new(message), cause: nil
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
def instantiate_target(target, initializer_args = nil)
|
|
365
|
-
positional_args, keyword_args = Array(initializer_args || [[], {}])
|
|
366
|
-
positional_args ||= []
|
|
367
|
-
keyword_args ||= {}
|
|
368
|
-
|
|
369
|
-
case target
|
|
370
|
-
when Class
|
|
371
|
-
if keyword_args.empty?
|
|
372
|
-
target.new(*positional_args)
|
|
373
|
-
else
|
|
374
|
-
target.new(*positional_args, **keyword_args)
|
|
375
|
-
end
|
|
376
|
-
when Module
|
|
377
|
-
Object.new.extend(target)
|
|
378
|
-
else
|
|
379
|
-
target
|
|
380
|
-
end
|
|
381
|
-
rescue ArgumentError => e
|
|
382
|
-
raise Error, "Failed to instantiate target: #{e.message}"
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def run_with_modes(target, json:, eval_args:, eval_lax:)
|
|
386
|
-
runner = proc { Rubycli.run(target) }
|
|
387
|
-
|
|
388
|
-
if json
|
|
389
|
-
Rubycli.with_json_mode(true, &runner)
|
|
390
|
-
elsif eval_args
|
|
391
|
-
Rubycli.with_eval_mode(true, lax: eval_lax, &runner)
|
|
392
|
-
else
|
|
393
|
-
runner.call
|
|
394
|
-
end
|
|
395
|
-
end
|
|
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
|
-
|
|
426
|
-
def prepare_runner_target(
|
|
427
|
-
target_path,
|
|
428
|
-
class_name,
|
|
429
|
-
new: false,
|
|
430
|
-
new_args: nil,
|
|
431
|
-
json_mode: false,
|
|
432
|
-
eval_mode: false,
|
|
433
|
-
eval_lax: false,
|
|
434
|
-
pre_scripts: [],
|
|
435
|
-
constant_mode: nil
|
|
436
|
-
)
|
|
437
|
-
full_path = find_target_path(target_path)
|
|
438
|
-
capture = Rubycli.constant_capture
|
|
439
|
-
capture.capture(full_path) { load full_path }
|
|
440
|
-
constant_mode ||= Rubycli.environment.constant_resolution_mode
|
|
441
|
-
candidates = build_constant_candidates(full_path, capture.constants_for(full_path))
|
|
442
|
-
defined_constants = candidates.map(&:name)
|
|
443
|
-
|
|
444
|
-
target = if class_name
|
|
445
|
-
constantize(
|
|
446
|
-
class_name,
|
|
447
|
-
defined_constants: defined_constants,
|
|
448
|
-
full_path: full_path
|
|
449
|
-
)
|
|
450
|
-
else
|
|
451
|
-
select_constant_candidate(
|
|
452
|
-
full_path,
|
|
453
|
-
camelize(File.basename(full_path, '.rb')),
|
|
454
|
-
candidates,
|
|
455
|
-
constant_mode,
|
|
456
|
-
instantiate: new
|
|
457
|
-
)
|
|
458
|
-
end
|
|
459
|
-
|
|
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
|
|
463
|
-
runner_target = apply_pre_scripts(pre_scripts, target, runner_target)
|
|
464
|
-
[runner_target, full_path]
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
def build_constant_candidates(path, constant_names)
|
|
468
|
-
normalized = normalize_path(path)
|
|
469
|
-
Array(constant_names).each_with_object([]) do |const_name, memo|
|
|
470
|
-
constant = safe_constant_lookup(const_name)
|
|
471
|
-
next unless constant.is_a?(Module)
|
|
472
|
-
|
|
473
|
-
class_methods = collect_defined_methods(constant.singleton_class, normalized)
|
|
474
|
-
instance_methods = collect_defined_methods(constant, normalized)
|
|
475
|
-
|
|
476
|
-
memo << ConstantCandidate.new(
|
|
477
|
-
name: const_name,
|
|
478
|
-
constant: constant,
|
|
479
|
-
class_methods: class_methods,
|
|
480
|
-
instance_methods: instance_methods
|
|
481
|
-
)
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def collect_defined_methods(owner, normalized_path)
|
|
486
|
-
owner.public_instance_methods(false).each_with_object([]) do |method_name, memo|
|
|
487
|
-
method_object = owner.instance_method(method_name)
|
|
488
|
-
location = method_object.source_location
|
|
489
|
-
next unless location && normalize_path(location[0]) == normalized_path
|
|
490
|
-
|
|
491
|
-
memo << method_name
|
|
492
|
-
end
|
|
493
|
-
rescue TypeError
|
|
494
|
-
[]
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
def safe_constant_lookup(name)
|
|
498
|
-
parts = name.split('::').reject(&:empty?)
|
|
499
|
-
context = Object
|
|
500
|
-
|
|
501
|
-
parts.each do |const_name|
|
|
502
|
-
return nil unless context.const_defined?(const_name, false)
|
|
503
|
-
|
|
504
|
-
context = context.const_get(const_name)
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
context
|
|
508
|
-
rescue NameError
|
|
509
|
-
nil
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
def select_constant_candidate(path, base_const, candidates, constant_mode, instantiate: false)
|
|
513
|
-
if candidates.empty?
|
|
514
|
-
raise Error, build_missing_constant_message(
|
|
515
|
-
base_const,
|
|
516
|
-
[],
|
|
517
|
-
path,
|
|
518
|
-
details: 'Rubycli could not detect any constants in this file.'
|
|
519
|
-
)
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
matching = candidates.find { |candidate| candidate.matches?(base_const) }
|
|
523
|
-
if matching
|
|
524
|
-
return matching.constant if matching.callable?(instantiate: instantiate)
|
|
525
|
-
|
|
526
|
-
detail = if matching.instance_only?
|
|
527
|
-
"#{matching.name} only defines instance methods in this file. Run with --new to instantiate before invoking CLI commands."
|
|
528
|
-
else
|
|
529
|
-
"#{matching.name} does not define any CLI-callable methods in this file. Add a public class or instance method defined in this file."
|
|
530
|
-
end
|
|
531
|
-
raise Error, build_missing_constant_message(
|
|
532
|
-
base_const,
|
|
533
|
-
candidates.map(&:name),
|
|
534
|
-
path,
|
|
535
|
-
details: detail
|
|
536
|
-
)
|
|
537
|
-
end
|
|
538
|
-
|
|
539
|
-
callable = candidates.select { |candidate| candidate.callable?(instantiate: instantiate) }
|
|
540
|
-
if callable.empty?
|
|
541
|
-
raise Error, build_missing_constant_message(
|
|
542
|
-
base_const,
|
|
543
|
-
candidates.map(&:name),
|
|
544
|
-
path,
|
|
545
|
-
details: 'Rubycli detected constants in this file, but none define CLI-callable methods. Add a public class or instance method defined in this file.'
|
|
546
|
-
)
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
if constant_mode == :auto && callable.size == 1
|
|
550
|
-
return callable.first.constant
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
details = build_ambiguous_constant_details(callable, path)
|
|
554
|
-
raise Error, build_missing_constant_message(
|
|
555
|
-
base_const,
|
|
556
|
-
candidates.map(&:name),
|
|
557
|
-
path,
|
|
558
|
-
details: details
|
|
559
|
-
)
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
def build_ambiguous_constant_details(candidates, path)
|
|
563
|
-
command_target = File.basename(path)
|
|
564
|
-
if candidates.size == 1
|
|
565
|
-
candidate = candidates.first
|
|
566
|
-
lines = []
|
|
567
|
-
lines << "This file defines #{candidate.name}, but its name does not match #{command_target}."
|
|
568
|
-
lines << 'Re-run by specifying the constant explicitly:'
|
|
569
|
-
lines << " rubycli #{command_target} #{candidate.name} ..."
|
|
570
|
-
lines << 'Alternatively pass --auto-target (or RUBYCLI_AUTO_TARGET=auto) to auto-select it.'
|
|
571
|
-
return lines.join("\n")
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
lines = ['Multiple CLI-capable constants were found in this file:']
|
|
575
|
-
candidates.each do |candidate|
|
|
576
|
-
hint = candidate.instance_only? ? ' (instance methods only; use --new)' : ''
|
|
577
|
-
lines << " - #{candidate.name}: #{candidate.summary}#{hint}"
|
|
578
|
-
end
|
|
579
|
-
lines << "Specify one explicitly, e.g. rubycli #{command_target} MyRunner"
|
|
580
|
-
lines << 'Or pass --auto-target to allow Rubycli to auto-select a single candidate.'
|
|
581
|
-
lines.join("\n")
|
|
582
|
-
end
|
|
583
|
-
|
|
584
|
-
def normalize_path(path)
|
|
585
|
-
File.expand_path(path.to_s)
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
def build_missing_constant_message(name, defined_constants, full_path, details: nil)
|
|
589
|
-
lines = ["Could not find definition: #{name}"]
|
|
590
|
-
lines << ''
|
|
591
|
-
lines << "Loaded file: #{File.expand_path(full_path)}" if full_path
|
|
592
|
-
|
|
593
|
-
if defined_constants && !defined_constants.empty?
|
|
594
|
-
sample = defined_constants.first(5)
|
|
595
|
-
suffix = defined_constants.size > sample.size ? " ... (#{defined_constants.size} total)" : ''
|
|
596
|
-
lines << "Constants found in this file: #{sample.join(', ')}#{suffix}"
|
|
597
|
-
else
|
|
598
|
-
lines << 'Rubycli could not detect any publicly exposable constants in this file.'
|
|
599
|
-
end
|
|
600
|
-
|
|
601
|
-
if details
|
|
602
|
-
lines << ''
|
|
603
|
-
lines << details
|
|
604
|
-
end
|
|
605
|
-
|
|
606
|
-
lines << ''
|
|
607
|
-
lines << 'Hint: Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI.'
|
|
608
|
-
lines.join("\n")
|
|
609
|
-
end
|
|
610
|
-
end
|
|
611
159
|
end
|
|
160
|
+
|
|
161
|
+
# Defined after the Rubycli module so that Runner::Error can subclass Rubycli::Error.
|
|
162
|
+
require_relative 'rubycli/runner'
|
metadata
CHANGED
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubycli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- inakaegg
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
-
dependencies:
|
|
10
|
+
date: 2026-07-26 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.25'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.25'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bigdecimal
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.1'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.1'
|
|
12
54
|
description: Rubycli turns plain Ruby classes and modules into command-line interfaces
|
|
13
55
|
by reading their documentation comments, inspired by Python Fire but tailored for
|
|
14
56
|
Ruby tooling.
|
|
@@ -23,6 +65,17 @@ files:
|
|
|
23
65
|
- LICENSE
|
|
24
66
|
- README.ja.md
|
|
25
67
|
- README.md
|
|
68
|
+
- examples/documentation_style_showcase.rb
|
|
69
|
+
- examples/fallback_example.rb
|
|
70
|
+
- examples/fallback_example_with_extra_docs.rb
|
|
71
|
+
- examples/hello_app.rb
|
|
72
|
+
- examples/hello_app_with_docs.rb
|
|
73
|
+
- examples/hello_app_with_require.rb
|
|
74
|
+
- examples/mismatched_constant_runner.rb
|
|
75
|
+
- examples/multi_constant_runner.rb
|
|
76
|
+
- examples/new_mode_runner.rb
|
|
77
|
+
- examples/strict_choices_demo.rb
|
|
78
|
+
- examples/typed_arguments_demo.rb
|
|
26
79
|
- exe/rubycli
|
|
27
80
|
- lib/rubycli.rb
|
|
28
81
|
- lib/rubycli/argument_mode_controller.rb
|
|
@@ -40,6 +93,7 @@ files:
|
|
|
40
93
|
- lib/rubycli/help_renderer.rb
|
|
41
94
|
- lib/rubycli/json_coercer.rb
|
|
42
95
|
- lib/rubycli/result_emitter.rb
|
|
96
|
+
- lib/rubycli/runner.rb
|
|
43
97
|
- lib/rubycli/type_utils.rb
|
|
44
98
|
- lib/rubycli/types.rb
|
|
45
99
|
- lib/rubycli/version.rb
|
|
@@ -49,8 +103,9 @@ licenses:
|
|
|
49
103
|
metadata:
|
|
50
104
|
homepage_uri: https://github.com/inakaegg/rubycli
|
|
51
105
|
documentation_uri: https://github.com/inakaegg/rubycli#readme
|
|
52
|
-
changelog_uri: https://github.com/inakaegg/rubycli/
|
|
106
|
+
changelog_uri: https://github.com/inakaegg/rubycli/blob/main/CHANGELOG.md
|
|
53
107
|
bug_tracker_uri: https://github.com/inakaegg/rubycli/issues
|
|
108
|
+
rubygems_mfa_required: 'true'
|
|
54
109
|
rdoc_options: []
|
|
55
110
|
require_paths:
|
|
56
111
|
- lib
|