rubycli 0.1.4 → 0.1.6
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 +24 -0
- data/README.ja.md +67 -12
- data/README.md +67 -12
- data/lib/rubycli/argument_parser.rb +255 -1
- data/lib/rubycli/arguments/value_converter.rb +11 -0
- data/lib/rubycli/cli.rb +5 -1
- data/lib/rubycli/command_line.rb +42 -6
- data/lib/rubycli/documentation/metadata_parser.rb +377 -31
- data/lib/rubycli/documentation_registry.rb +1 -1
- data/lib/rubycli/environment.rb +42 -7
- data/lib/rubycli/eval_coercer.rb +1 -1
- data/lib/rubycli/help_renderer.rb +64 -9
- data/lib/rubycli/types.rb +2 -2
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +103 -32
- metadata +2 -2
data/lib/rubycli/eval_coercer.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Rubycli
|
|
|
47
47
|
EVAL_BINDING.eval(trimmed)
|
|
48
48
|
rescue SyntaxError, NameError => e
|
|
49
49
|
if eval_lax_mode?
|
|
50
|
-
warn "[
|
|
50
|
+
warn "[WARN] Failed to evaluate argument as Ruby (#{e.message.strip}). Passing it through because --eval-lax is enabled."
|
|
51
51
|
expression
|
|
52
52
|
else
|
|
53
53
|
raise
|
|
@@ -45,7 +45,6 @@ module Rubycli
|
|
|
45
45
|
|
|
46
46
|
puts
|
|
47
47
|
puts "Detailed command help: #{File.basename($PROGRAM_NAME)} COMMAND help"
|
|
48
|
-
puts "Enable debug logging: --debug or RUBYCLI_DEBUG=true"
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
def method_description(method_obj)
|
|
@@ -152,10 +151,10 @@ module Rubycli
|
|
|
152
151
|
end
|
|
153
152
|
|
|
154
153
|
def render_positionals(positionals_in_order)
|
|
155
|
-
|
|
154
|
+
rows = positionals_in_order.map do |info|
|
|
156
155
|
definition = info[:definition]
|
|
157
156
|
label = info[:label]
|
|
158
|
-
type =
|
|
157
|
+
type = type_display(definition)
|
|
159
158
|
requirement = positional_requirement(info[:kind])
|
|
160
159
|
description_parts = []
|
|
161
160
|
description_parts << info[:description] if info[:description]
|
|
@@ -169,7 +168,7 @@ module Rubycli
|
|
|
169
168
|
def render_options(options, required_keywords)
|
|
170
169
|
rows = options.map do |opt|
|
|
171
170
|
label = option_flag_with_placeholder(opt)
|
|
172
|
-
type =
|
|
171
|
+
type = type_display(opt)
|
|
173
172
|
requirement = required_keywords.include?(opt.keyword) ? 'required' : 'optional'
|
|
174
173
|
description_parts = []
|
|
175
174
|
description_parts << opt.description if opt.description
|
|
@@ -198,10 +197,53 @@ module Rubycli
|
|
|
198
197
|
end
|
|
199
198
|
|
|
200
199
|
def formatted_types(types)
|
|
201
|
-
type_list = Array(types).compact.map
|
|
200
|
+
type_list = Array(types).compact.map { |token| token.to_s.strip }.reject(&:empty?)
|
|
201
|
+
type_list = type_list.each_with_object([]) do |token, acc|
|
|
202
|
+
acc << token unless acc.include?(token)
|
|
203
|
+
end
|
|
202
204
|
return '' if type_list.empty?
|
|
203
205
|
|
|
204
|
-
|
|
206
|
+
nil_tokens, rest = type_list.partition { |token| token.casecmp('nil').zero? }
|
|
207
|
+
ordered = rest + nil_tokens
|
|
208
|
+
"[#{ordered.join(', ')}]"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def type_display(definition)
|
|
212
|
+
return formatted_types(definition&.types) unless definition
|
|
213
|
+
|
|
214
|
+
literal_entries = Array(definition.allowed_values).map { |entry| format_allowed_value(entry[:value]) }
|
|
215
|
+
type_tokens = Array(definition.types).map { |token| token.to_s.strip }.reject(&:empty?)
|
|
216
|
+
type_tokens.reject! { |token| literal_token?(token) }
|
|
217
|
+
|
|
218
|
+
combined = (literal_entries + type_tokens).uniq
|
|
219
|
+
return formatted_types(definition.types) if combined.empty?
|
|
220
|
+
|
|
221
|
+
"[#{combined.join(', ')}]"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def format_allowed_value(value)
|
|
225
|
+
case value
|
|
226
|
+
when Symbol
|
|
227
|
+
":#{value}"
|
|
228
|
+
when String
|
|
229
|
+
%("#{value}")
|
|
230
|
+
when Integer, Float
|
|
231
|
+
value.to_s
|
|
232
|
+
when TrueClass, FalseClass
|
|
233
|
+
value.to_s
|
|
234
|
+
when NilClass
|
|
235
|
+
'nil'
|
|
236
|
+
else
|
|
237
|
+
value.inspect
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def literal_token?(token)
|
|
242
|
+
return true if token.start_with?('%')
|
|
243
|
+
return true if token.include?('[')
|
|
244
|
+
return true if token.start_with?(':')
|
|
245
|
+
|
|
246
|
+
false
|
|
205
247
|
end
|
|
206
248
|
|
|
207
249
|
def positional_requirement(kind)
|
|
@@ -268,19 +310,25 @@ module Rubycli
|
|
|
268
310
|
end
|
|
269
311
|
|
|
270
312
|
def required_placeholder(placeholder, definition, name)
|
|
271
|
-
|
|
313
|
+
unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
|
|
314
|
+
return placeholder.strip
|
|
315
|
+
end
|
|
272
316
|
|
|
273
317
|
default_positional_label(definition, name, uppercase: true)
|
|
274
318
|
end
|
|
275
319
|
|
|
276
320
|
def optional_placeholder(placeholder, definition, name)
|
|
277
|
-
|
|
321
|
+
unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
|
|
322
|
+
return placeholder.strip
|
|
323
|
+
end
|
|
278
324
|
|
|
279
325
|
"[#{default_positional_label(definition, name, uppercase: true)}]"
|
|
280
326
|
end
|
|
281
327
|
|
|
282
328
|
def rest_placeholder(placeholder, definition, name)
|
|
283
|
-
|
|
329
|
+
unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
|
|
330
|
+
return placeholder.strip
|
|
331
|
+
end
|
|
284
332
|
|
|
285
333
|
base = default_positional_label(definition, name, uppercase: true)
|
|
286
334
|
"[#{base}...]"
|
|
@@ -320,6 +368,13 @@ module Rubycli
|
|
|
320
368
|
first_non_nil_type
|
|
321
369
|
end
|
|
322
370
|
|
|
371
|
+
def auto_generated_placeholder?(placeholder, definition, name)
|
|
372
|
+
return false unless definition
|
|
373
|
+
return false unless definition.respond_to?(:doc_format) && definition.doc_format == :auto_generated
|
|
374
|
+
|
|
375
|
+
placeholder.strip.casecmp(name.to_s).zero?
|
|
376
|
+
end
|
|
377
|
+
|
|
323
378
|
def ensure_angle_bracket_placeholder(placeholder)
|
|
324
379
|
raw = placeholder.to_s.strip
|
|
325
380
|
return raw if raw.empty?
|
data/lib/rubycli/types.rb
CHANGED
|
@@ -2,13 +2,13 @@ module Rubycli
|
|
|
2
2
|
OptionDefinition = Struct.new(
|
|
3
3
|
:keyword, :long, :short, :value_name, :types, :description, :requires_value,
|
|
4
4
|
:boolean_flag, :optional_value, :default_value, :inline_type_annotation,
|
|
5
|
-
:inline_type_text, :doc_format,
|
|
5
|
+
:inline_type_text, :doc_format, :allowed_values,
|
|
6
6
|
keyword_init: true
|
|
7
7
|
)
|
|
8
8
|
|
|
9
9
|
PositionalDefinition = Struct.new(
|
|
10
10
|
:placeholder, :label, :types, :description, :param_name, :default_value,
|
|
11
|
-
:inline_type_annotation, :inline_type_text, :doc_format,
|
|
11
|
+
:inline_type_annotation, :inline_type_text, :doc_format, :allowed_values,
|
|
12
12
|
keyword_init: true
|
|
13
13
|
)
|
|
14
14
|
|
data/lib/rubycli/version.rb
CHANGED
data/lib/rubycli.rb
CHANGED
|
@@ -216,37 +216,21 @@ module Rubycli
|
|
|
216
216
|
constant_mode: nil
|
|
217
217
|
)
|
|
218
218
|
raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
|
|
219
|
-
original_program_name = $PROGRAM_NAME
|
|
220
219
|
if json && eval_args
|
|
221
220
|
raise Error, '--json-args cannot be combined with --eval-args or --eval-lax'
|
|
222
221
|
end
|
|
223
222
|
|
|
224
|
-
full_path =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
247
|
-
runner_target = new ? instantiate_target(target) : target
|
|
248
|
-
runner_target = apply_pre_scripts(pre_scripts, target, runner_target)
|
|
223
|
+
runner_target, full_path = prepare_runner_target(
|
|
224
|
+
target_path,
|
|
225
|
+
class_name,
|
|
226
|
+
new: new,
|
|
227
|
+
pre_scripts: pre_scripts,
|
|
228
|
+
constant_mode: constant_mode
|
|
229
|
+
)
|
|
249
230
|
|
|
231
|
+
original_program_name = $PROGRAM_NAME
|
|
232
|
+
original_argv = nil
|
|
233
|
+
$PROGRAM_NAME = File.basename(full_path)
|
|
250
234
|
original_argv = ARGV.dup
|
|
251
235
|
ARGV.replace(Array(cli_args).dup)
|
|
252
236
|
run_with_modes(runner_target, json: json, eval_args: eval_args, eval_lax: eval_lax)
|
|
@@ -255,6 +239,53 @@ module Rubycli
|
|
|
255
239
|
ARGV.replace(original_argv) if original_argv
|
|
256
240
|
end
|
|
257
241
|
|
|
242
|
+
def check(
|
|
243
|
+
target_path,
|
|
244
|
+
class_name = nil,
|
|
245
|
+
new: false,
|
|
246
|
+
pre_scripts: [],
|
|
247
|
+
constant_mode: nil
|
|
248
|
+
)
|
|
249
|
+
raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
|
|
250
|
+
previous_doc_check = Rubycli.environment.doc_check_mode?
|
|
251
|
+
Rubycli.environment.clear_documentation_issues!
|
|
252
|
+
Rubycli.environment.enable_doc_check!
|
|
253
|
+
|
|
254
|
+
runner_target, full_path = prepare_runner_target(
|
|
255
|
+
target_path,
|
|
256
|
+
class_name,
|
|
257
|
+
new: new,
|
|
258
|
+
pre_scripts: pre_scripts,
|
|
259
|
+
constant_mode: constant_mode
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
original_program_name = $PROGRAM_NAME
|
|
263
|
+
$PROGRAM_NAME = File.basename(full_path)
|
|
264
|
+
|
|
265
|
+
catalog = Rubycli.cli.command_catalog_for(runner_target)
|
|
266
|
+
Array(catalog&.entries).each do |entry|
|
|
267
|
+
method_obj = entry&.method
|
|
268
|
+
Rubycli.documentation_registry.metadata_for(method_obj) if method_obj
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
if catalog&.entries&.empty? && runner_target.respond_to?(:call)
|
|
272
|
+
method_obj = runner_target.method(:call) rescue nil
|
|
273
|
+
Rubycli.documentation_registry.metadata_for(method_obj) if method_obj
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
issues = Rubycli.environment.documentation_issues
|
|
277
|
+
if issues.empty?
|
|
278
|
+
puts 'rubycli documentation OK'
|
|
279
|
+
0
|
|
280
|
+
else
|
|
281
|
+
warn "[ERROR] rubycli documentation check failed (#{issues.size} issue#{issues.size == 1 ? '' : 's'})"
|
|
282
|
+
1
|
|
283
|
+
end
|
|
284
|
+
ensure
|
|
285
|
+
Rubycli.environment.disable_doc_check! unless previous_doc_check
|
|
286
|
+
$PROGRAM_NAME = original_program_name if original_program_name
|
|
287
|
+
end
|
|
288
|
+
|
|
258
289
|
def apply_pre_scripts(sources, base_target, initial_target)
|
|
259
290
|
Array(sources).reduce(initial_target) do |current_target, source|
|
|
260
291
|
result = evaluate_pre_script(source, base_target, current_target)
|
|
@@ -342,6 +373,41 @@ module Rubycli
|
|
|
342
373
|
end
|
|
343
374
|
end
|
|
344
375
|
|
|
376
|
+
def prepare_runner_target(
|
|
377
|
+
target_path,
|
|
378
|
+
class_name,
|
|
379
|
+
new: false,
|
|
380
|
+
pre_scripts: [],
|
|
381
|
+
constant_mode: nil
|
|
382
|
+
)
|
|
383
|
+
full_path = find_target_path(target_path)
|
|
384
|
+
capture = Rubycli.constant_capture
|
|
385
|
+
capture.capture(full_path) { load full_path }
|
|
386
|
+
constant_mode ||= Rubycli.environment.constant_resolution_mode
|
|
387
|
+
candidates = build_constant_candidates(full_path, capture.constants_for(full_path))
|
|
388
|
+
defined_constants = candidates.map(&:name)
|
|
389
|
+
|
|
390
|
+
target = if class_name
|
|
391
|
+
constantize(
|
|
392
|
+
class_name,
|
|
393
|
+
defined_constants: defined_constants,
|
|
394
|
+
full_path: full_path
|
|
395
|
+
)
|
|
396
|
+
else
|
|
397
|
+
select_constant_candidate(
|
|
398
|
+
full_path,
|
|
399
|
+
camelize(File.basename(full_path, '.rb')),
|
|
400
|
+
candidates,
|
|
401
|
+
constant_mode,
|
|
402
|
+
instantiate: new
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
runner_target = new ? instantiate_target(target) : target
|
|
407
|
+
runner_target = apply_pre_scripts(pre_scripts, target, runner_target)
|
|
408
|
+
[runner_target, full_path]
|
|
409
|
+
end
|
|
410
|
+
|
|
345
411
|
def build_constant_candidates(path, constant_names)
|
|
346
412
|
normalized = normalize_path(path)
|
|
347
413
|
Array(constant_names).each_with_object([]) do |const_name, memo|
|
|
@@ -465,19 +531,24 @@ module Rubycli
|
|
|
465
531
|
|
|
466
532
|
def build_missing_constant_message(name, defined_constants, full_path, details: nil)
|
|
467
533
|
lines = ["Could not find definition: #{name}"]
|
|
468
|
-
lines <<
|
|
534
|
+
lines << ''
|
|
535
|
+
lines << "Loaded file: #{File.expand_path(full_path)}" if full_path
|
|
469
536
|
|
|
470
537
|
if defined_constants && !defined_constants.empty?
|
|
471
538
|
sample = defined_constants.first(5)
|
|
472
|
-
suffix = defined_constants.size > sample.size ? "
|
|
473
|
-
lines << "
|
|
539
|
+
suffix = defined_constants.size > sample.size ? " … (#{defined_constants.size} total)" : ''
|
|
540
|
+
lines << "Constants found in this file: #{sample.join(', ')}#{suffix}"
|
|
474
541
|
else
|
|
475
|
-
lines <<
|
|
542
|
+
lines << 'Rubycli could not detect any publicly exposable constants in this file.'
|
|
476
543
|
end
|
|
477
544
|
|
|
478
|
-
|
|
545
|
+
if details
|
|
546
|
+
lines << ''
|
|
547
|
+
lines << details
|
|
548
|
+
end
|
|
479
549
|
|
|
480
|
-
lines <<
|
|
550
|
+
lines << ''
|
|
551
|
+
lines << 'Hint: Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI.'
|
|
481
552
|
lines.join("\n")
|
|
482
553
|
end
|
|
483
554
|
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.6
|
|
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-11 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
|