rubycli 0.1.4 → 0.1.5
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 +19 -0
- data/README.ja.md +65 -12
- data/README.md +65 -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 +160 -25
- 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 +92 -26
- metadata +2 -2
|
@@ -15,6 +15,11 @@ module Rubycli
|
|
|
15
15
|
trimmed = value.strip
|
|
16
16
|
return value if trimmed.empty?
|
|
17
17
|
|
|
18
|
+
if symbol_literal?(trimmed)
|
|
19
|
+
symbol_value = trimmed.delete_prefix(':')
|
|
20
|
+
return symbol_value.to_sym unless symbol_value.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
18
23
|
if literal_like?(trimmed)
|
|
19
24
|
literal = try_literal_parse(value)
|
|
20
25
|
return literal unless literal.equal?(LITERAL_PARSE_FAILURE)
|
|
@@ -33,6 +38,12 @@ module Rubycli
|
|
|
33
38
|
|
|
34
39
|
private
|
|
35
40
|
|
|
41
|
+
def symbol_literal?(value)
|
|
42
|
+
return false unless value
|
|
43
|
+
|
|
44
|
+
value.start_with?(':') && value.length > 1 && value[1..].match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
45
|
+
end
|
|
46
|
+
|
|
36
47
|
def integer_string?(str)
|
|
37
48
|
str =~ /\A-?\d+\z/
|
|
38
49
|
end
|
data/lib/rubycli/cli.rb
CHANGED
|
@@ -48,6 +48,9 @@ module Rubycli
|
|
|
48
48
|
else
|
|
49
49
|
execute_method(entry.method, command, args, cli_mode)
|
|
50
50
|
end
|
|
51
|
+
rescue Rubycli::ArgumentError => e
|
|
52
|
+
warn "[ERROR] #{e.message}"
|
|
53
|
+
1
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
def available_commands(target)
|
|
@@ -113,6 +116,7 @@ module Rubycli
|
|
|
113
116
|
method = target.method(:call)
|
|
114
117
|
pos_args, kw_args = @argument_parser.parse(args, method)
|
|
115
118
|
Rubycli.apply_argument_coercions(pos_args, kw_args)
|
|
119
|
+
@argument_parser.validate_inputs(method, pos_args, kw_args)
|
|
116
120
|
begin
|
|
117
121
|
result = Rubycli.call_target(target, pos_args, kw_args)
|
|
118
122
|
@result_emitter.emit(result)
|
|
@@ -151,11 +155,11 @@ module Rubycli
|
|
|
151
155
|
def execute_method_with_params(method_obj, command, args, cli_mode)
|
|
152
156
|
pos_args, kw_args = @argument_parser.parse(args, method_obj)
|
|
153
157
|
Rubycli.apply_argument_coercions(pos_args, kw_args)
|
|
154
|
-
|
|
155
158
|
if should_show_method_help?(pos_args, kw_args)
|
|
156
159
|
puts usage_for_method(command, method_obj)
|
|
157
160
|
return 0
|
|
158
161
|
end
|
|
162
|
+
@argument_parser.validate_inputs(method_obj, pos_args, kw_args)
|
|
159
163
|
|
|
160
164
|
begin
|
|
161
165
|
result = Rubycli.call_target(method_obj, pos_args, kw_args)
|
data/lib/rubycli/command_line.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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] <target-path> [<class-or-module>] [-- <cli-args>...]
|
|
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>...]
|
|
7
7
|
|
|
8
8
|
Examples:
|
|
9
9
|
rubycli scripts/sample_runner.rb echo --message hello
|
|
@@ -17,6 +17,8 @@ module Rubycli
|
|
|
17
17
|
--eval-args, -e Evaluate following arguments as Ruby code
|
|
18
18
|
--eval-lax, -E Evaluate as Ruby but fall back to raw strings when parsing fails
|
|
19
19
|
--auto-target, -a Auto-select the only callable constant when names don't match
|
|
20
|
+
--strict Enforce documented input types/choices (invalid values abort)
|
|
21
|
+
--check, -c Validate documentation/comments without executing commands
|
|
20
22
|
(Note: --json-args cannot be combined with --eval-args or --eval-lax)
|
|
21
23
|
(Note: Every option that accepts a value understands both --flag=value and --flag value forms.)
|
|
22
24
|
|
|
@@ -42,6 +44,7 @@ module Rubycli
|
|
|
42
44
|
eval_mode = false
|
|
43
45
|
eval_lax_mode = false
|
|
44
46
|
constant_mode = nil
|
|
47
|
+
check_mode = false
|
|
45
48
|
pre_script_sources = []
|
|
46
49
|
|
|
47
50
|
loop do
|
|
@@ -64,7 +67,7 @@ module Rubycli
|
|
|
64
67
|
flag = args.shift
|
|
65
68
|
src = args.shift
|
|
66
69
|
unless src
|
|
67
|
-
warn "#{flag} requires a file path or inline Ruby code"
|
|
70
|
+
warn "[ERROR] #{flag} requires a file path or inline Ruby code"
|
|
68
71
|
return 1
|
|
69
72
|
end
|
|
70
73
|
context = File.file?(src) ? File.expand_path(src) : "(inline #{flag})"
|
|
@@ -79,8 +82,19 @@ module Rubycli
|
|
|
79
82
|
eval_mode = true
|
|
80
83
|
eval_lax_mode = true
|
|
81
84
|
args.shift
|
|
85
|
+
when '--strict'
|
|
86
|
+
Rubycli.environment.enable_strict_input!
|
|
87
|
+
args.shift
|
|
88
|
+
when '--check', '-c'
|
|
89
|
+
check_mode = true
|
|
90
|
+
Rubycli.environment.enable_doc_check!
|
|
91
|
+
args.shift
|
|
82
92
|
when '--print-result'
|
|
83
|
-
|
|
93
|
+
args.shift
|
|
94
|
+
when '--debug'
|
|
95
|
+
args.shift
|
|
96
|
+
warn "[ERROR] --debug flag has been removed; set RUBYCLI_DEBUG=true instead."
|
|
97
|
+
return 1
|
|
84
98
|
when '--auto-target', '-a'
|
|
85
99
|
constant_mode = :auto
|
|
86
100
|
args.shift
|
|
@@ -106,10 +120,32 @@ module Rubycli
|
|
|
106
120
|
args.shift if args.first == '--'
|
|
107
121
|
|
|
108
122
|
if json_mode && eval_mode
|
|
109
|
-
warn '--json-args cannot be combined with --eval-args or --eval-lax'
|
|
123
|
+
warn '[ERROR] --json-args cannot be combined with --eval-args or --eval-lax'
|
|
124
|
+
return 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if check_mode && (json_mode || eval_mode)
|
|
128
|
+
warn '[ERROR] --check cannot be combined with --json-args or --eval-args'
|
|
110
129
|
return 1
|
|
111
130
|
end
|
|
112
131
|
|
|
132
|
+
if check_mode && !args.empty?
|
|
133
|
+
warn '[ERROR] --check does not accept command arguments'
|
|
134
|
+
return 1
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
Rubycli.environment.clear_documentation_issues!
|
|
138
|
+
|
|
139
|
+
if check_mode
|
|
140
|
+
return Rubycli::Runner.check(
|
|
141
|
+
target_path,
|
|
142
|
+
class_or_module,
|
|
143
|
+
new: new_flag,
|
|
144
|
+
pre_scripts: pre_script_sources,
|
|
145
|
+
constant_mode: constant_mode
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
113
149
|
Rubycli::Runner.execute(
|
|
114
150
|
target_path,
|
|
115
151
|
class_or_module,
|
|
@@ -124,10 +160,10 @@ module Rubycli
|
|
|
124
160
|
|
|
125
161
|
0
|
|
126
162
|
rescue Rubycli::Runner::PreScriptError => e
|
|
127
|
-
warn e.message
|
|
163
|
+
warn "[ERROR] #{e.message}"
|
|
128
164
|
1
|
|
129
165
|
rescue Rubycli::Runner::Error => e
|
|
130
|
-
warn e.message
|
|
166
|
+
warn "[ERROR] #{e.message}"
|
|
131
167
|
1
|
|
132
168
|
end
|
|
133
169
|
end
|
|
@@ -122,7 +122,7 @@ module Rubycli
|
|
|
122
122
|
file: source_file,
|
|
123
123
|
line: line_number
|
|
124
124
|
)
|
|
125
|
-
return nil if @environment.
|
|
125
|
+
return nil if @environment.doc_check_mode?
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
pattern = /\A@param\s+([a-zA-Z0-9_]+)(?:\s+\[([^\]]+)\])?(?:\s+\(([^)]+)\))?(?:\s+(.*))?\z/
|
|
@@ -130,12 +130,14 @@ module Rubycli
|
|
|
130
130
|
return nil unless match
|
|
131
131
|
|
|
132
132
|
param_name = match[1]
|
|
133
|
+
param_symbol = param_name.to_sym
|
|
133
134
|
type_str = match[2]
|
|
134
135
|
option_tokens = combine_bracketed_tokens(match[3]&.split(/\s+/) || [])
|
|
135
136
|
description = match[4]&.strip
|
|
136
137
|
description = nil if description&.empty?
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
raw_types = parse_type_annotation(type_str)
|
|
140
|
+
types, allowed_values = partition_type_tokens(raw_types)
|
|
139
141
|
|
|
140
142
|
long_option = nil
|
|
141
143
|
short_option = nil
|
|
@@ -179,23 +181,36 @@ module Rubycli
|
|
|
179
181
|
end
|
|
180
182
|
|
|
181
183
|
long_option ||= "--#{param_name.tr('_', '-')}"
|
|
184
|
+
role = parameter_role(method_obj, param_symbol)
|
|
185
|
+
if value_name.nil?
|
|
186
|
+
if role == :positional
|
|
187
|
+
value_name = default_placeholder_for(param_symbol)
|
|
188
|
+
elsif !types&.any? { |entry| boolean_type?(entry) }
|
|
189
|
+
# Most keywords expect a value; boolean flags should be documented with [Boolean].
|
|
190
|
+
value_name = default_placeholder_for(param_symbol)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
182
193
|
|
|
183
|
-
|
|
194
|
+
if (types.nil? || types.empty?) && type_token
|
|
195
|
+
inline_raw_types = parse_type_annotation(type_token)
|
|
196
|
+
inline_types, inline_allowed = partition_type_tokens(inline_raw_types)
|
|
197
|
+
types = inline_types
|
|
198
|
+
allowed_values = merge_allowed_values(allowed_values, inline_allowed)
|
|
199
|
+
end
|
|
184
200
|
|
|
201
|
+
# TODO: Derive primitive types from Ruby default values when explicit hints are absent.
|
|
185
202
|
option_def = build_option_definition(
|
|
186
|
-
|
|
203
|
+
param_symbol,
|
|
187
204
|
long_option,
|
|
188
205
|
short_option,
|
|
189
206
|
value_name,
|
|
190
207
|
types,
|
|
191
208
|
description,
|
|
192
209
|
inline_type_annotation: !type_token.nil?,
|
|
193
|
-
doc_format: :tagged_param
|
|
210
|
+
doc_format: :tagged_param,
|
|
211
|
+
allowed_values: allowed_values
|
|
194
212
|
)
|
|
195
213
|
|
|
196
|
-
param_symbol = param_name.to_sym
|
|
197
|
-
role = parameter_role(method_obj, param_symbol)
|
|
198
|
-
|
|
199
214
|
if role == :positional
|
|
200
215
|
placeholder = option_def.value_name || default_placeholder_for(option_def.keyword)
|
|
201
216
|
return PositionalDefinition.new(
|
|
@@ -204,7 +219,8 @@ module Rubycli
|
|
|
204
219
|
types: option_def.types,
|
|
205
220
|
description: option_def.description,
|
|
206
221
|
param_name: param_symbol,
|
|
207
|
-
doc_format: option_def.doc_format
|
|
222
|
+
doc_format: option_def.doc_format,
|
|
223
|
+
allowed_values: option_def.allowed_values
|
|
208
224
|
)
|
|
209
225
|
elsif role == :keyword
|
|
210
226
|
return option_def
|
|
@@ -218,7 +234,8 @@ module Rubycli
|
|
|
218
234
|
types: option_def.types,
|
|
219
235
|
description: option_def.description,
|
|
220
236
|
param_name: param_symbol,
|
|
221
|
-
doc_format: option_def.doc_format
|
|
237
|
+
doc_format: option_def.doc_format,
|
|
238
|
+
allowed_values: option_def.allowed_values
|
|
222
239
|
)
|
|
223
240
|
end
|
|
224
241
|
|
|
@@ -291,7 +308,8 @@ module Rubycli
|
|
|
291
308
|
|
|
292
309
|
description = description_tokens.join(' ').strip
|
|
293
310
|
description = nil if description.empty?
|
|
294
|
-
|
|
311
|
+
raw_types = parse_type_annotation(type_token)
|
|
312
|
+
types, allowed_values = partition_type_tokens(raw_types)
|
|
295
313
|
|
|
296
314
|
keyword = long_option.delete_prefix('--').tr('-', '_').to_sym
|
|
297
315
|
return nil unless method_accepts_keyword?(method_obj, keyword)
|
|
@@ -304,7 +322,8 @@ module Rubycli
|
|
|
304
322
|
types,
|
|
305
323
|
description,
|
|
306
324
|
inline_type_annotation: !type_token.nil?,
|
|
307
|
-
doc_format: :rubycli
|
|
325
|
+
doc_format: :rubycli,
|
|
326
|
+
allowed_values: allowed_values
|
|
308
327
|
)
|
|
309
328
|
end
|
|
310
329
|
|
|
@@ -326,7 +345,8 @@ module Rubycli
|
|
|
326
345
|
description = tokens.join(' ').strip
|
|
327
346
|
description = nil if description.empty?
|
|
328
347
|
|
|
329
|
-
|
|
348
|
+
raw_types = parse_type_annotation(type_token)
|
|
349
|
+
types, allowed_values = partition_type_tokens(raw_types)
|
|
330
350
|
placeholder_info = analyze_placeholder(placeholder)
|
|
331
351
|
inferred_types = infer_types_from_placeholder(
|
|
332
352
|
normalize_type_list(types),
|
|
@@ -346,7 +366,8 @@ module Rubycli
|
|
|
346
366
|
description: description,
|
|
347
367
|
inline_type_annotation: inline_annotation,
|
|
348
368
|
inline_type_text: inline_text,
|
|
349
|
-
doc_format: :rubycli
|
|
369
|
+
doc_format: :rubycli,
|
|
370
|
+
allowed_values: allowed_values
|
|
350
371
|
)
|
|
351
372
|
end
|
|
352
373
|
|
|
@@ -493,16 +514,18 @@ module Rubycli
|
|
|
493
514
|
file: source_file,
|
|
494
515
|
line: line_for_comment
|
|
495
516
|
)
|
|
496
|
-
unless @environment.
|
|
517
|
+
unless @environment.doc_check_mode?
|
|
497
518
|
fallback = PositionalDefinition.new(
|
|
498
519
|
placeholder: name.to_s,
|
|
499
520
|
label: name.to_s.upcase,
|
|
500
|
-
types: [],
|
|
521
|
+
types: ['String'],
|
|
501
522
|
description: nil,
|
|
502
523
|
param_name: name,
|
|
503
524
|
default_value: defaults[name],
|
|
504
525
|
inline_type_annotation: false,
|
|
505
|
-
inline_type_text: nil
|
|
526
|
+
inline_type_text: nil,
|
|
527
|
+
doc_format: :auto_generated,
|
|
528
|
+
allowed_values: []
|
|
506
529
|
)
|
|
507
530
|
metadata[:positionals] << fallback
|
|
508
531
|
positional_map[name] = fallback
|
|
@@ -517,7 +540,7 @@ module Rubycli
|
|
|
517
540
|
file: source_file,
|
|
518
541
|
line: line_for_comment
|
|
519
542
|
)
|
|
520
|
-
unless @environment.
|
|
543
|
+
unless @environment.doc_check_mode?
|
|
521
544
|
fallback_option = build_auto_option_definition(name)
|
|
522
545
|
ordered_options << fallback_option if fallback_option
|
|
523
546
|
end
|
|
@@ -558,6 +581,11 @@ module Rubycli
|
|
|
558
581
|
opt.value_name = nil
|
|
559
582
|
opt.types = ['Boolean']
|
|
560
583
|
end
|
|
584
|
+
elsif opt.boolean_flag
|
|
585
|
+
opt.boolean_flag = false
|
|
586
|
+
opt.requires_value = true
|
|
587
|
+
opt.value_name ||= default_placeholder_for(opt.keyword)
|
|
588
|
+
opt.types = ['String'] if opt.types.nil? || opt.types.empty?
|
|
561
589
|
end
|
|
562
590
|
end
|
|
563
591
|
end
|
|
@@ -616,6 +644,98 @@ module Rubycli
|
|
|
616
644
|
cleaned.split(/[,|]/).map { |token| normalize_type_token(token) }.reject(&:empty?)
|
|
617
645
|
end
|
|
618
646
|
|
|
647
|
+
def partition_type_tokens(tokens)
|
|
648
|
+
normalized = Array(tokens).dup
|
|
649
|
+
allowed = []
|
|
650
|
+
|
|
651
|
+
normalized.each do |token|
|
|
652
|
+
expand_annotation_token(token).each do |expanded|
|
|
653
|
+
literal_entry = literal_entry_from_token(expanded)
|
|
654
|
+
allowed << literal_entry if literal_entry
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
[normalized, allowed.compact.uniq]
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def merge_allowed_values(primary, additional)
|
|
662
|
+
return Array(additional) if primary.nil? || primary.empty?
|
|
663
|
+
return Array(primary) if additional.nil? || additional.empty?
|
|
664
|
+
|
|
665
|
+
(primary + additional).uniq
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def expand_annotation_token(token)
|
|
669
|
+
return [] unless token
|
|
670
|
+
|
|
671
|
+
stripped = token.strip
|
|
672
|
+
return [] if stripped.empty?
|
|
673
|
+
|
|
674
|
+
if stripped.start_with?('%i[') && stripped.end_with?(']')
|
|
675
|
+
inner = stripped[3..-2]
|
|
676
|
+
inner.split(/\s+/).map { |entry| ":#{entry}" }
|
|
677
|
+
elsif stripped.start_with?('%I[') && stripped.end_with?(']')
|
|
678
|
+
inner = stripped[3..-2]
|
|
679
|
+
inner.split(/\s+/).map { |entry| ":#{entry}" }
|
|
680
|
+
elsif stripped.start_with?('%w[') && stripped.end_with?(']')
|
|
681
|
+
inner = stripped[3..-2]
|
|
682
|
+
inner.split(/\s+/)
|
|
683
|
+
elsif stripped.start_with?('%W[') && stripped.end_with?(']')
|
|
684
|
+
inner = stripped[3..-2]
|
|
685
|
+
inner.split(/\s+/)
|
|
686
|
+
else
|
|
687
|
+
[stripped]
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def literal_entry_from_token(token)
|
|
692
|
+
return nil unless token
|
|
693
|
+
|
|
694
|
+
stripped = token.strip
|
|
695
|
+
return nil if stripped.empty?
|
|
696
|
+
stripped = stripped[1..] if stripped.start_with?('[') && !stripped.end_with?(']')
|
|
697
|
+
if stripped.end_with?(']') && !stripped.include?('[')
|
|
698
|
+
stripped = stripped[0...-1]
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
lowered = stripped.downcase
|
|
702
|
+
return { kind: :literal, value: nil } if %w[nil null ~].include?(lowered)
|
|
703
|
+
return { kind: :literal, value: true } if lowered == 'true'
|
|
704
|
+
return { kind: :literal, value: false } if lowered == 'false'
|
|
705
|
+
|
|
706
|
+
if stripped.start_with?(':')
|
|
707
|
+
sym_name = stripped[1..]
|
|
708
|
+
return nil if sym_name.nil? || sym_name.empty?
|
|
709
|
+
|
|
710
|
+
return { kind: :literal, value: sym_name.to_sym }
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
if stripped.start_with?('"') && stripped.end_with?('"') && stripped.length >= 2
|
|
714
|
+
return { kind: :literal, value: stripped[1..-2] }
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
if stripped.start_with?("'") && stripped.end_with?("'") && stripped.length >= 2
|
|
718
|
+
return { kind: :literal, value: stripped[1..-2] }
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
if stripped.match?(/\A-?\d+\z/)
|
|
722
|
+
return { kind: :literal, value: Integer(stripped) }
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
if stripped.match?(/\A-?\d+\.\d+\z/)
|
|
726
|
+
return { kind: :literal, value: Float(stripped) }
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
if stripped.match?(/\A[a-z0-9._-]+\z/)
|
|
730
|
+
return { kind: :literal, value: stripped }
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
nil
|
|
734
|
+
rescue ArgumentError
|
|
735
|
+
nil
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
|
|
619
739
|
def placeholder_token?(token)
|
|
620
740
|
return false unless token
|
|
621
741
|
|
|
@@ -652,11 +772,15 @@ module Rubycli
|
|
|
652
772
|
return false if stripped.empty?
|
|
653
773
|
|
|
654
774
|
return true if stripped.start_with?('@')
|
|
775
|
+
return true if stripped.start_with?('%')
|
|
776
|
+
return true if stripped.include?('::')
|
|
777
|
+
return true if stripped.start_with?('(') && stripped.end_with?(')')
|
|
778
|
+
return true if stripped.include?('[') && stripped.include?(']')
|
|
655
779
|
|
|
656
|
-
|
|
657
|
-
return false if
|
|
780
|
+
normalized = normalize_type_token(stripped)
|
|
781
|
+
return false if normalized.empty?
|
|
658
782
|
|
|
659
|
-
|
|
783
|
+
INLINE_TYPE_HINTS.include?(normalized)
|
|
660
784
|
end
|
|
661
785
|
|
|
662
786
|
def known_type_token?(token)
|
|
@@ -723,6 +847,9 @@ module Rubycli
|
|
|
723
847
|
elsif token.start_with?('(') && !token.include?(')')
|
|
724
848
|
buffer = token.dup
|
|
725
849
|
closing = ')'
|
|
850
|
+
elsif token.start_with?('%') && token.include?('[') && !token.include?(']')
|
|
851
|
+
buffer = token.dup
|
|
852
|
+
closing = ']'
|
|
726
853
|
else
|
|
727
854
|
combined << token
|
|
728
855
|
end
|
|
@@ -755,7 +882,8 @@ module Rubycli
|
|
|
755
882
|
types,
|
|
756
883
|
description,
|
|
757
884
|
inline_type_annotation: false,
|
|
758
|
-
doc_format: nil
|
|
885
|
+
doc_format: nil,
|
|
886
|
+
allowed_values: nil
|
|
759
887
|
)
|
|
760
888
|
normalized_long = normalize_long_option(long_option)
|
|
761
889
|
normalized_short = normalize_short_option(short_option)
|
|
@@ -800,10 +928,15 @@ module Rubycli
|
|
|
800
928
|
optional_value: optional_value,
|
|
801
929
|
inline_type_annotation: inline_type_annotation,
|
|
802
930
|
inline_type_text: inline_type_text,
|
|
803
|
-
doc_format: doc_format
|
|
931
|
+
doc_format: doc_format,
|
|
932
|
+
allowed_values: normalize_allowed_values(allowed_values)
|
|
804
933
|
)
|
|
805
934
|
end
|
|
806
935
|
|
|
936
|
+
def normalize_allowed_values(values)
|
|
937
|
+
Array(values).compact.uniq
|
|
938
|
+
end
|
|
939
|
+
|
|
807
940
|
def build_auto_option_definition(keyword)
|
|
808
941
|
long_option = "--#{keyword.to_s.tr('_', '-')}"
|
|
809
942
|
placeholder = default_placeholder_for(keyword)
|
|
@@ -815,7 +948,8 @@ module Rubycli
|
|
|
815
948
|
[],
|
|
816
949
|
nil,
|
|
817
950
|
inline_type_annotation: false,
|
|
818
|
-
doc_format: :auto_generated
|
|
951
|
+
doc_format: :auto_generated,
|
|
952
|
+
allowed_values: []
|
|
819
953
|
)
|
|
820
954
|
end
|
|
821
955
|
|
|
@@ -830,7 +964,8 @@ module Rubycli
|
|
|
830
964
|
default_value: option.default_value,
|
|
831
965
|
inline_type_annotation: option.inline_type_annotation,
|
|
832
966
|
inline_type_text: option.inline_type_text,
|
|
833
|
-
doc_format: option.doc_format
|
|
967
|
+
doc_format: option.doc_format,
|
|
968
|
+
allowed_values: option.allowed_values
|
|
834
969
|
)
|
|
835
970
|
end
|
|
836
971
|
end
|
|
@@ -20,7 +20,7 @@ module Rubycli
|
|
|
20
20
|
location = method_obj.source_location
|
|
21
21
|
return empty_metadata unless location
|
|
22
22
|
|
|
23
|
-
cache_key = [location[0], location[1], @environment.
|
|
23
|
+
cache_key = [location[0], location[1], @environment.doc_check_mode?, @environment.allow_param_comments?]
|
|
24
24
|
return deep_dup(@metadata_cache[cache_key]) if @metadata_cache.key?(cache_key)
|
|
25
25
|
|
|
26
26
|
comment_lines = @comment_extractor.extract(location[0], location[1])
|
data/lib/rubycli/environment.rb
CHANGED
|
@@ -6,6 +6,9 @@ module Rubycli
|
|
|
6
6
|
@argv = argv
|
|
7
7
|
@debug = env['RUBYCLI_DEBUG'] == 'true'
|
|
8
8
|
@print_result = env['RUBYCLI_PRINT_RESULT'] == 'true'
|
|
9
|
+
@doc_check_mode = false
|
|
10
|
+
@strict_input = false
|
|
11
|
+
@documentation_issues = []
|
|
9
12
|
scrub_argv_flags!
|
|
10
13
|
end
|
|
11
14
|
|
|
@@ -17,16 +20,39 @@ module Rubycli
|
|
|
17
20
|
@print_result
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
def strict_mode?
|
|
21
|
-
value = fetch_env_value('RUBYCLI_STRICT', 'OFF')
|
|
22
|
-
!%w[off 0 false].include?(value.downcase)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
23
|
def allow_param_comments?
|
|
26
24
|
value = fetch_env_value('RUBYCLI_ALLOW_PARAM_COMMENT', 'ON')
|
|
27
25
|
%w[on 1 true].include?(value)
|
|
28
26
|
end
|
|
29
27
|
|
|
28
|
+
def enable_doc_check!
|
|
29
|
+
@doc_check_mode = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def doc_check_mode?
|
|
33
|
+
@doc_check_mode
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def disable_doc_check!
|
|
37
|
+
@doc_check_mode = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def enable_strict_input!
|
|
41
|
+
@strict_input = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def strict_input?
|
|
45
|
+
@strict_input
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def documentation_issues
|
|
49
|
+
@documentation_issues.dup
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def clear_documentation_issues!
|
|
53
|
+
@documentation_issues.clear
|
|
54
|
+
end
|
|
55
|
+
|
|
30
56
|
def constant_resolution_mode
|
|
31
57
|
value = fetch_env_value('RUBYCLI_AUTO_TARGET', 'strict')
|
|
32
58
|
return :auto if %w[auto on true yes 1].include?(value)
|
|
@@ -49,7 +75,17 @@ module Rubycli
|
|
|
49
75
|
message
|
|
50
76
|
end
|
|
51
77
|
|
|
52
|
-
|
|
78
|
+
entry = { message: formatted_message, location: location }
|
|
79
|
+
@documentation_issues << entry
|
|
80
|
+
warn "[WARN] Rubycli documentation mismatch: #{formatted_message}" if doc_check_mode?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def handle_input_violation(message)
|
|
84
|
+
if strict_input?
|
|
85
|
+
raise Rubycli::ArgumentError, message
|
|
86
|
+
else
|
|
87
|
+
warn "[WARN] #{message} (use --strict to abort on invalid input)"
|
|
88
|
+
end
|
|
53
89
|
end
|
|
54
90
|
|
|
55
91
|
def enable_print_result!
|
|
@@ -65,7 +101,6 @@ module Rubycli
|
|
|
65
101
|
def scrub_argv_flags!
|
|
66
102
|
return unless @argv
|
|
67
103
|
|
|
68
|
-
remove_all_flags!(@argv, '--debug') { @debug = true }
|
|
69
104
|
remove_all_flags!(@argv, '--print-result') { @print_result = true }
|
|
70
105
|
end
|
|
71
106
|
|
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
|