rubycli 0.1.2 → 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 +44 -0
- data/README.ja.md +82 -14
- data/README.md +82 -14
- data/lib/rubycli/argument_mode_controller.rb +69 -0
- data/lib/rubycli/argument_parser.rb +287 -102
- data/lib/rubycli/arguments/token_stream.rb +41 -0
- data/lib/rubycli/arguments/value_converter.rb +85 -0
- data/lib/rubycli/cli.rb +14 -7
- data/lib/rubycli/command_line.rb +58 -6
- data/lib/rubycli/constant_capture.rb +50 -0
- data/lib/rubycli/documentation/comment_extractor.rb +52 -0
- data/lib/rubycli/documentation/metadata_parser.rb +973 -0
- data/lib/rubycli/documentation_registry.rb +11 -853
- data/lib/rubycli/environment.rb +50 -8
- data/lib/rubycli/eval_coercer.rb +16 -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 +265 -121
- metadata +8 -2
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,14 +20,44 @@ 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
|
-
%w[on 1 true].include?(value
|
|
25
|
+
%w[on 1 true].include?(value)
|
|
26
|
+
end
|
|
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
|
+
|
|
56
|
+
def constant_resolution_mode
|
|
57
|
+
value = fetch_env_value('RUBYCLI_AUTO_TARGET', 'strict')
|
|
58
|
+
return :auto if %w[auto on true yes 1].include?(value)
|
|
59
|
+
|
|
60
|
+
:strict
|
|
28
61
|
end
|
|
29
62
|
|
|
30
63
|
def handle_documentation_issue(message, file: nil, line: nil)
|
|
@@ -42,7 +75,17 @@ module Rubycli
|
|
|
42
75
|
message
|
|
43
76
|
end
|
|
44
77
|
|
|
45
|
-
|
|
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
|
|
46
89
|
end
|
|
47
90
|
|
|
48
91
|
def enable_print_result!
|
|
@@ -58,7 +101,6 @@ module Rubycli
|
|
|
58
101
|
def scrub_argv_flags!
|
|
59
102
|
return unless @argv
|
|
60
103
|
|
|
61
|
-
remove_all_flags!(@argv, '--debug') { @debug = true }
|
|
62
104
|
remove_all_flags!(@argv, '--print-result') { @print_result = true }
|
|
63
105
|
end
|
|
64
106
|
|
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 "[WARN] 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
|
|
@@ -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