rubycli 0.1.1 → 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 +42 -1
- data/README.ja.md +80 -43
- data/README.md +92 -42
- data/lib/rubycli/argument_mode_controller.rb +69 -0
- data/lib/rubycli/argument_parser.rb +71 -80
- 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 +29 -11
- 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 -797
- data/lib/rubycli/environment.rb +8 -1
- data/lib/rubycli/eval_coercer.rb +16 -1
- data/lib/rubycli/help_renderer.rb +162 -116
- data/lib/rubycli/type_utils.rb +28 -1
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +196 -118
- metadata +9 -3
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
|
|
@@ -53,21 +53,8 @@ module Rubycli
|
|
|
53
53
|
summary = metadata[:summary]
|
|
54
54
|
return summary if summary && !summary.empty?
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
param_desc = params.map { |type, name|
|
|
60
|
-
case type
|
|
61
|
-
when :req then "<#{name}>"
|
|
62
|
-
when :opt then "[<#{name}>]"
|
|
63
|
-
when :rest then "[<#{name}>...]"
|
|
64
|
-
when :keyreq then "--#{name.to_s.tr("_", "-")}=<value>"
|
|
65
|
-
when :key then "[--#{name.to_s.tr("_", "-")}=<value>]"
|
|
66
|
-
when :keyrest then "[--<option>...]"
|
|
67
|
-
end
|
|
68
|
-
}.compact.join(" ")
|
|
69
|
-
|
|
70
|
-
param_desc.empty? ? "(no arguments)" : param_desc
|
|
56
|
+
params_str = format_method_parameters(method_obj.parameters, metadata)
|
|
57
|
+
params_str.empty? ? "(no arguments)" : params_str
|
|
71
58
|
end
|
|
72
59
|
|
|
73
60
|
def usage_for_method(command, method)
|
|
@@ -78,59 +65,8 @@ module Rubycli
|
|
|
78
65
|
options = metadata[:options] || []
|
|
79
66
|
positionals_in_order = ordered_positionals(method, metadata)
|
|
80
67
|
|
|
81
|
-
if positionals_in_order.any?
|
|
82
|
-
|
|
83
|
-
max_label_length = labels.map(&:length).max || 0
|
|
84
|
-
|
|
85
|
-
usage_lines << ""
|
|
86
|
-
usage_lines << "Positional arguments:"
|
|
87
|
-
positionals_in_order.each do |info|
|
|
88
|
-
definition = info[:definition]
|
|
89
|
-
description_parts = []
|
|
90
|
-
if definition&.inline_type_annotation && definition.inline_type_text
|
|
91
|
-
description_parts << definition.inline_type_text
|
|
92
|
-
end
|
|
93
|
-
type_info = positional_type_display(definition)
|
|
94
|
-
if type_info && type_info_first?(definition)
|
|
95
|
-
description_parts << type_info
|
|
96
|
-
end
|
|
97
|
-
description_parts << info[:description] if info[:description]
|
|
98
|
-
description_parts << type_info if type_info && !type_info_first?(definition)
|
|
99
|
-
default_text = positional_default_display(definition)
|
|
100
|
-
description_parts << default_text if default_text
|
|
101
|
-
description_text = description_parts.join(' ')
|
|
102
|
-
line = " #{info[:label].ljust(max_label_length)}"
|
|
103
|
-
line += " #{description_text}" unless description_text.empty?
|
|
104
|
-
usage_lines << line
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
if options.any?
|
|
109
|
-
option_labels = options.map { |opt| option_flag_with_placeholder(opt) }
|
|
110
|
-
max_label_length = option_labels.map(&:length).max || 0
|
|
111
|
-
|
|
112
|
-
usage_lines << "" unless usage_lines.last == ""
|
|
113
|
-
usage_lines << "Options:"
|
|
114
|
-
options.each_with_index do |opt, idx|
|
|
115
|
-
label = option_labels[idx]
|
|
116
|
-
description_parts = []
|
|
117
|
-
if opt.inline_type_annotation && opt.inline_type_text
|
|
118
|
-
description_parts << opt.inline_type_text
|
|
119
|
-
end
|
|
120
|
-
type_info = option_type_display(opt)
|
|
121
|
-
if type_info && type_info_first?(opt)
|
|
122
|
-
description_parts << type_info
|
|
123
|
-
end
|
|
124
|
-
description_parts << opt.description if opt.description
|
|
125
|
-
description_parts << type_info if type_info && !type_info_first?(opt)
|
|
126
|
-
default_info = option_default_display(opt)
|
|
127
|
-
description_parts << default_info if default_info
|
|
128
|
-
description_text = description_parts.join(' ')
|
|
129
|
-
line = " #{label.ljust(max_label_length)}"
|
|
130
|
-
line += " #{description_text}" unless description_text.empty?
|
|
131
|
-
usage_lines << line
|
|
132
|
-
end
|
|
133
|
-
end
|
|
68
|
+
usage_lines.concat(render_positionals(positionals_in_order)) if positionals_in_order.any?
|
|
69
|
+
usage_lines.concat(render_options(options, required_keyword_names(method))) if options.any?
|
|
134
70
|
|
|
135
71
|
returns = metadata[:returns] || []
|
|
136
72
|
if returns.any?
|
|
@@ -168,14 +104,11 @@ module Rubycli
|
|
|
168
104
|
parameters.map { |type, name|
|
|
169
105
|
case type
|
|
170
106
|
when :req
|
|
171
|
-
|
|
172
|
-
label = doc&.label || name.to_s.upcase
|
|
173
|
-
"<#{label}>"
|
|
107
|
+
positional_usage_token(type, name, positional_map[name])
|
|
174
108
|
when :opt
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
when :rest then "[<#{name}>...]"
|
|
109
|
+
positional_usage_token(type, name, positional_map[name])
|
|
110
|
+
when :rest
|
|
111
|
+
positional_usage_token(type, name, positional_map[name])
|
|
179
112
|
when :keyreq
|
|
180
113
|
opt = option_map[name]
|
|
181
114
|
if opt
|
|
@@ -202,53 +135,163 @@ module Rubycli
|
|
|
202
135
|
when :keyrest then "[--<option>...]"
|
|
203
136
|
else ""
|
|
204
137
|
end
|
|
205
|
-
}.reject(&:empty?).join(" ")
|
|
138
|
+
}.compact.reject(&:empty?).join(" ")
|
|
206
139
|
end
|
|
207
140
|
|
|
208
141
|
def auto_generated_option_usage_label(name, opt)
|
|
209
142
|
base_flag = "--#{name.to_s.tr('_', '-')}"
|
|
210
143
|
return base_flag if opt.boolean_flag
|
|
211
144
|
|
|
212
|
-
|
|
145
|
+
value_name = opt.value_name
|
|
146
|
+
formatted = if value_name && !value_name.to_s.strip.empty?
|
|
147
|
+
ensure_angle_bracket_placeholder(value_name)
|
|
148
|
+
else
|
|
149
|
+
"<value>"
|
|
150
|
+
end
|
|
151
|
+
"#{base_flag}=#{formatted}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def render_positionals(positionals_in_order)
|
|
155
|
+
rows = positionals_in_order.map do |info|
|
|
156
|
+
definition = info[:definition]
|
|
157
|
+
label = info[:label]
|
|
158
|
+
type = formatted_types(definition&.types)
|
|
159
|
+
requirement = positional_requirement(info[:kind])
|
|
160
|
+
description_parts = []
|
|
161
|
+
description_parts << info[:description] if info[:description]
|
|
162
|
+
default_text = positional_default(definition)
|
|
163
|
+
description_parts << default_text if default_text
|
|
164
|
+
[label, type, requirement, description_parts.join(' ')]
|
|
165
|
+
end
|
|
166
|
+
table_block("Positional arguments:", rows)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def render_options(options, required_keywords)
|
|
170
|
+
rows = options.map do |opt|
|
|
171
|
+
label = option_flag_with_placeholder(opt)
|
|
172
|
+
type = formatted_types(opt.types)
|
|
173
|
+
requirement = required_keywords.include?(opt.keyword) ? 'required' : 'optional'
|
|
174
|
+
description_parts = []
|
|
175
|
+
description_parts << opt.description if opt.description
|
|
176
|
+
default_text = option_default(opt)
|
|
177
|
+
description_parts << default_text if default_text
|
|
178
|
+
[label, type, requirement, description_parts.join(' ').strip]
|
|
179
|
+
end
|
|
180
|
+
table_block("Options:", rows)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def table_block(header, rows)
|
|
184
|
+
return [] if rows.empty?
|
|
185
|
+
|
|
186
|
+
cols = rows.transpose
|
|
187
|
+
widths = cols.map { |col| col.map { |value| value.to_s.length }.max || 0 }
|
|
188
|
+
|
|
189
|
+
lines = ["", header]
|
|
190
|
+
rows.each do |row|
|
|
191
|
+
padded = row.each_with_index.map do |value, idx|
|
|
192
|
+
text = value.to_s
|
|
193
|
+
idx < row.length - 1 ? text.ljust(widths[idx]) : text
|
|
194
|
+
end
|
|
195
|
+
lines << " #{padded.join(' ')}".rstrip
|
|
196
|
+
end
|
|
197
|
+
lines
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def formatted_types(types)
|
|
201
|
+
type_list = Array(types).compact.map(&:to_s).reject(&:empty?).uniq
|
|
202
|
+
return '' if type_list.empty?
|
|
203
|
+
|
|
204
|
+
"[#{type_list.join(', ')}]"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def positional_requirement(kind)
|
|
208
|
+
kind == :opt ? 'optional' : 'required'
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def positional_default(definition)
|
|
212
|
+
return nil unless definition
|
|
213
|
+
value = definition.default_value
|
|
214
|
+
return nil if value.nil? || value.to_s.empty?
|
|
215
|
+
|
|
216
|
+
"(default: #{value})"
|
|
213
217
|
end
|
|
214
218
|
|
|
219
|
+
def option_default(opt)
|
|
220
|
+
value = opt.default_value
|
|
221
|
+
return nil if value.nil? || value.to_s.empty?
|
|
222
|
+
|
|
223
|
+
"(default: #{value})"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def required_keyword_names(method)
|
|
227
|
+
method.parameters.select { |type, _| type == :keyreq }.map { |_, name| name }
|
|
228
|
+
end
|
|
229
|
+
|
|
215
230
|
def ordered_positionals(method, metadata)
|
|
216
231
|
positional_map = metadata[:positionals_map] || {}
|
|
217
232
|
method.parameters.each_with_object([]) do |(type, name), memo|
|
|
218
233
|
next unless %i[req opt].include?(type)
|
|
219
234
|
|
|
220
235
|
definition = positional_map[name]
|
|
221
|
-
label =
|
|
222
|
-
type == :opt ? "[#{definition.label}]" : definition.label
|
|
223
|
-
else
|
|
224
|
-
base = name.to_s.upcase
|
|
225
|
-
type == :opt ? "[#{base}]" : base
|
|
226
|
-
end
|
|
236
|
+
label = display_label_for(definition, name)
|
|
227
237
|
description = definition&.description
|
|
228
|
-
memo << { label: label, description: description, definition: definition }
|
|
238
|
+
memo << { label: label, description: description, definition: definition, kind: type }
|
|
229
239
|
end
|
|
230
240
|
end
|
|
231
241
|
|
|
232
|
-
def
|
|
233
|
-
return
|
|
234
|
-
return nil if definition.inline_type_annotation
|
|
235
|
-
return nil if definition.types.nil? || definition.types.empty?
|
|
242
|
+
def display_label_for(definition, name)
|
|
243
|
+
return definition.label if definition&.label && !definition.label.to_s.empty?
|
|
236
244
|
|
|
237
|
-
|
|
238
|
-
|
|
245
|
+
name.to_s.upcase
|
|
246
|
+
end
|
|
239
247
|
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
def positional_usage_token(type, name, definition)
|
|
249
|
+
placeholder = extract_positional_placeholder(definition)
|
|
250
|
+
case type
|
|
251
|
+
when :req
|
|
252
|
+
required_placeholder(placeholder, definition, name)
|
|
253
|
+
when :opt
|
|
254
|
+
optional_placeholder(placeholder, definition, name)
|
|
255
|
+
when :rest
|
|
256
|
+
rest_placeholder(placeholder, definition, name)
|
|
242
257
|
else
|
|
243
|
-
|
|
258
|
+
nil
|
|
244
259
|
end
|
|
245
260
|
end
|
|
246
261
|
|
|
247
|
-
def
|
|
248
|
-
return nil unless definition
|
|
249
|
-
return nil if definition.
|
|
262
|
+
def extract_positional_placeholder(definition)
|
|
263
|
+
return nil unless definition
|
|
264
|
+
return nil if definition.doc_format.nil?
|
|
265
|
+
|
|
266
|
+
token = definition.placeholder.to_s.strip
|
|
267
|
+
token.empty? ? nil : token
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def required_placeholder(placeholder, definition, name)
|
|
271
|
+
return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
|
|
272
|
+
|
|
273
|
+
default_positional_label(definition, name, uppercase: true)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def optional_placeholder(placeholder, definition, name)
|
|
277
|
+
return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
|
|
278
|
+
|
|
279
|
+
"[#{default_positional_label(definition, name, uppercase: true)}]"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def rest_placeholder(placeholder, definition, name)
|
|
283
|
+
return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
|
|
250
284
|
|
|
251
|
-
|
|
285
|
+
base = default_positional_label(definition, name, uppercase: true)
|
|
286
|
+
"[#{base}...]"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def default_positional_label(definition, name, uppercase:)
|
|
290
|
+
label = definition&.label
|
|
291
|
+
label = label.to_s.strip unless label.nil?
|
|
292
|
+
label = nil if label.respond_to?(:empty?) && label.empty?
|
|
293
|
+
base = label || name.to_s
|
|
294
|
+
uppercase ? base.upcase : base
|
|
252
295
|
end
|
|
253
296
|
|
|
254
297
|
def option_flag_with_placeholder(opt)
|
|
@@ -257,7 +300,13 @@ module Rubycli
|
|
|
257
300
|
flag_label = flags.join(", ")
|
|
258
301
|
placeholder = option_value_placeholder(opt)
|
|
259
302
|
if placeholder
|
|
260
|
-
|
|
303
|
+
formatted = ensure_angle_bracket_placeholder(placeholder)
|
|
304
|
+
if formatted.start_with?('[') && formatted.end_with?(']')
|
|
305
|
+
inner = formatted[1..-2]
|
|
306
|
+
"#{flag_label}[=#{inner}]"
|
|
307
|
+
else
|
|
308
|
+
"#{flag_label}=#{formatted}"
|
|
309
|
+
end
|
|
261
310
|
else
|
|
262
311
|
flag_label
|
|
263
312
|
end
|
|
@@ -271,28 +320,25 @@ module Rubycli
|
|
|
271
320
|
first_non_nil_type
|
|
272
321
|
end
|
|
273
322
|
|
|
274
|
-
def
|
|
275
|
-
|
|
276
|
-
return
|
|
277
|
-
|
|
278
|
-
unique_types = opt.types.reject(&:empty?).uniq
|
|
279
|
-
return nil if unique_types.empty?
|
|
323
|
+
def ensure_angle_bracket_placeholder(placeholder)
|
|
324
|
+
raw = placeholder.to_s.strip
|
|
325
|
+
return raw if raw.empty?
|
|
280
326
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
"(type: #{unique_types.join(' | ')})"
|
|
285
|
-
end
|
|
286
|
-
end
|
|
327
|
+
optional = raw.start_with?('[') && raw.end_with?(']')
|
|
328
|
+
core = optional ? raw[1..-2].strip : raw
|
|
329
|
+
return raw if core.empty?
|
|
287
330
|
|
|
288
|
-
|
|
289
|
-
|
|
331
|
+
ellipsis = core.end_with?('...')
|
|
332
|
+
core = core[0..-4] if ellipsis
|
|
290
333
|
|
|
291
|
-
|
|
292
|
-
|
|
334
|
+
formatted_core = if core.start_with?('<') && core.end_with?('>')
|
|
335
|
+
core
|
|
336
|
+
else
|
|
337
|
+
"<#{core}>"
|
|
338
|
+
end
|
|
293
339
|
|
|
294
|
-
|
|
295
|
-
|
|
340
|
+
formatted_core = "#{formatted_core}..." if ellipsis
|
|
341
|
+
optional ? "[#{formatted_core}]" : formatted_core
|
|
296
342
|
end
|
|
297
343
|
end
|
|
298
344
|
end
|
data/lib/rubycli/type_utils.rb
CHANGED
|
@@ -19,6 +19,10 @@ module Rubycli
|
|
|
19
19
|
trimmed = trimmed.delete_prefix('@')
|
|
20
20
|
return '' if trimmed.empty?
|
|
21
21
|
|
|
22
|
+
trimmed = trimmed[1..-2].strip if trimmed.start_with?('(') && trimmed.end_with?(')')
|
|
23
|
+
trimmed = trimmed.sub(/\Atype\s*:\s*/i, '').strip
|
|
24
|
+
return '' if trimmed.empty?
|
|
25
|
+
|
|
22
26
|
if trimmed.include?('<') && trimmed.end_with?('>')
|
|
23
27
|
trimmed
|
|
24
28
|
elsif trimmed.end_with?('[]')
|
|
@@ -66,7 +70,29 @@ module Rubycli
|
|
|
66
70
|
working.unshift('Boolean') unless working.any? { |type| boolean_type?(type) }
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
if placeholder_info[:list]
|
|
74
|
+
array_type_present = false
|
|
75
|
+
|
|
76
|
+
working = working.map do |type|
|
|
77
|
+
next type if type.nil? || type.empty?
|
|
78
|
+
|
|
79
|
+
if boolean_type?(type) || nil_type?(type)
|
|
80
|
+
type
|
|
81
|
+
elsif type.end_with?('[]') || type.start_with?('Array<') || type.casecmp('Array').zero?
|
|
82
|
+
array_type_present = true
|
|
83
|
+
type
|
|
84
|
+
else
|
|
85
|
+
array_type_present = true
|
|
86
|
+
"#{type}[]"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
unless array_type_present
|
|
91
|
+
working << 'String[]'
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
working.compact.uniq
|
|
70
96
|
end
|
|
71
97
|
|
|
72
98
|
def determine_requires_value(value_placeholder:, types:, boolean_flag:, optional_value:)
|
|
@@ -101,6 +127,7 @@ module Rubycli
|
|
|
101
127
|
|
|
102
128
|
def parse_list(value)
|
|
103
129
|
return [] if value.nil?
|
|
130
|
+
return value if value.is_a?(Array)
|
|
104
131
|
|
|
105
132
|
value.to_s.split(',').map(&:strip).reject(&:empty?)
|
|
106
133
|
end
|
data/lib/rubycli/version.rb
CHANGED