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.
@@ -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.downcase)
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
- warn "[WARN] Rubycli documentation mismatch: #{formatted_message}" if strict_mode?
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
 
@@ -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 "[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
- rows = positionals_in_order.map do |info|
154
+ rows = positionals_in_order.map do |info|
156
155
  definition = info[:definition]
157
156
  label = info[:label]
158
- type = formatted_types(definition&.types)
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 = formatted_types(opt.types)
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(&:to_s).reject(&:empty?).uniq
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
- "[#{type_list.join(', ')}]"
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
- return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
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
- return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
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
- return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubycli
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.5'
5
5
  end