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.
@@ -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.downcase)
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)
@@ -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 with_eval_mode(enabled = true)
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
- params = method_obj.parameters
57
- return "(no arguments)" if params.empty?
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
- labels = positionals_in_order.map { |info| info[:label] }
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
- doc = positional_map[name]
172
- label = doc&.label || name.to_s.upcase
173
- "<#{label}>"
107
+ positional_usage_token(type, name, positional_map[name])
174
108
  when :opt
175
- doc = positional_map[name]
176
- label = doc&.label || name.to_s.upcase
177
- "[<#{label}>]"
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
- "#{base_flag}=<value>"
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 = if definition
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 positional_type_display(definition)
233
- return nil unless definition
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
- unique_types = definition.types.reject(&:empty?).uniq
238
- return nil if unique_types.empty?
245
+ name.to_s.upcase
246
+ end
239
247
 
240
- if definition.respond_to?(:doc_format) && definition.doc_format == :rubycli
241
- "[#{unique_types.join(', ')}]"
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
- "(type: #{unique_types.join(' | ')})"
258
+ nil
244
259
  end
245
260
  end
246
261
 
247
- def positional_default_display(definition)
248
- return nil unless definition && definition.default_value
249
- return nil if definition.default_value.to_s.empty?
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
- "(default: #{definition.default_value})"
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
- "#{flag_label} #{placeholder}"
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 option_type_display(opt)
275
- return nil if opt.inline_type_annotation
276
- return nil if opt.types.nil? || opt.types.empty?
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
- if opt.respond_to?(:doc_format) && opt.doc_format == :rubycli
282
- "[#{unique_types.join(', ')}]"
283
- else
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
- def option_default_display(opt)
289
- return nil if opt.default_value.nil? || opt.default_value.to_s.empty?
331
+ ellipsis = core.end_with?('...')
332
+ core = core[0..-4] if ellipsis
290
333
 
291
- "(default: #{opt.default_value})"
292
- end
334
+ formatted_core = if core.start_with?('<') && core.end_with?('>')
335
+ core
336
+ else
337
+ "<#{core}>"
338
+ end
293
339
 
294
- def type_info_first?(definition)
295
- definition.respond_to?(:doc_format) && definition.doc_format == :rubycli
340
+ formatted_core = "#{formatted_core}..." if ellipsis
341
+ optional ? "[#{formatted_core}]" : formatted_core
296
342
  end
297
343
  end
298
344
  end
@@ -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
- working.uniq
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubycli
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.4'
5
5
  end