rubycli 0.1.6 → 0.2.0
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 +88 -0
- data/LICENSE +1 -1
- data/README.ja.md +357 -235
- data/README.md +386 -233
- data/examples/documentation_style_showcase.rb +110 -0
- data/examples/fallback_example.rb +14 -0
- data/examples/fallback_example_with_extra_docs.rb +13 -0
- data/examples/hello_app.rb +10 -0
- data/examples/hello_app_with_docs.rb +17 -0
- data/examples/hello_app_with_require.rb +19 -0
- data/examples/mismatched_constant_runner.rb +16 -0
- data/examples/multi_constant_runner.rb +20 -0
- data/examples/new_mode_runner.rb +46 -0
- data/examples/strict_choices_demo.rb +22 -0
- data/examples/typed_arguments_demo.rb +42 -0
- data/lib/rubycli/argument_mode_controller.rb +7 -11
- data/lib/rubycli/argument_parser.rb +338 -80
- data/lib/rubycli/cli.rb +26 -30
- data/lib/rubycli/command_line.rb +159 -72
- data/lib/rubycli/constant_capture.rb +563 -6
- data/lib/rubycli/documentation/metadata_parser.rb +114 -74
- data/lib/rubycli/documentation_registry.rb +1 -0
- data/lib/rubycli/environment.rb +17 -9
- data/lib/rubycli/eval_coercer.rb +27 -10
- data/lib/rubycli/help_renderer.rb +67 -62
- data/lib/rubycli/json_coercer.rb +2 -0
- data/lib/rubycli/result_emitter.rb +18 -8
- data/lib/rubycli/runner.rb +513 -0
- data/lib/rubycli/type_utils.rb +8 -6
- data/lib/rubycli/types.rb +2 -0
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +7 -400
- metadata +59 -4
|
@@ -32,11 +32,12 @@ module Rubycli
|
|
|
32
32
|
summary_display_lines = []
|
|
33
33
|
detail_lines = []
|
|
34
34
|
summary_phase = true
|
|
35
|
+
deferred_positional = nil
|
|
35
36
|
|
|
36
37
|
comment_lines.each do |content|
|
|
37
38
|
stripped = content.strip
|
|
38
39
|
if summary_phase && stripped.empty?
|
|
39
|
-
summary_display_lines <<
|
|
40
|
+
summary_display_lines << ''
|
|
40
41
|
next
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -69,7 +70,14 @@ module Rubycli
|
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
if (positional = parse_positional_line(stripped, method_obj))
|
|
72
|
-
|
|
73
|
+
# An untyped uppercase line opening a doc block ("JSON output
|
|
74
|
+
# formatter for reports.") reads exactly like a placeholder. Hold on
|
|
75
|
+
# to it and decide once the whole block is known.
|
|
76
|
+
if summary_phase && deferred_positional.nil? && !positional.inline_type_annotation
|
|
77
|
+
deferred_positional = { content: content, stripped: stripped, definition: positional }
|
|
78
|
+
else
|
|
79
|
+
metadata[:positionals] << positional
|
|
80
|
+
end
|
|
73
81
|
summary_phase = false
|
|
74
82
|
next
|
|
75
83
|
end
|
|
@@ -81,6 +89,15 @@ module Rubycli
|
|
|
81
89
|
end
|
|
82
90
|
end
|
|
83
91
|
|
|
92
|
+
if deferred_positional
|
|
93
|
+
if positional_documentation_needed?(method_obj, metadata[:positionals].size)
|
|
94
|
+
metadata[:positionals].unshift(deferred_positional[:definition])
|
|
95
|
+
else
|
|
96
|
+
summary_display_lines << deferred_positional[:content].rstrip
|
|
97
|
+
summary_compact_lines << deferred_positional[:stripped]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
84
101
|
summary_text = summary_compact_lines.join(' ')
|
|
85
102
|
summary_text = nil if summary_text.empty?
|
|
86
103
|
metadata[:summary] = summary_text
|
|
@@ -93,6 +110,15 @@ module Rubycli
|
|
|
93
110
|
metadata
|
|
94
111
|
end
|
|
95
112
|
|
|
113
|
+
# True while the documented placeholders do not yet cover every positional
|
|
114
|
+
# parameter, which is what tells a real placeholder line from prose.
|
|
115
|
+
def positional_documentation_needed?(method_obj, documented_count)
|
|
116
|
+
return true unless method_obj.respond_to?(:parameters)
|
|
117
|
+
|
|
118
|
+
positional_count = method_obj.parameters.count { |type, _| %i[req opt rest].include?(type) }
|
|
119
|
+
documented_count < positional_count
|
|
120
|
+
end
|
|
121
|
+
|
|
96
122
|
def trim_blank_edges(lines)
|
|
97
123
|
return [] if lines.nil? || lines.empty?
|
|
98
124
|
|
|
@@ -108,15 +134,13 @@ module Rubycli
|
|
|
108
134
|
def parse_tagged_param_line(line, method_obj)
|
|
109
135
|
return nil unless line.start_with?('@param')
|
|
110
136
|
|
|
111
|
-
|
|
137
|
+
nil
|
|
112
138
|
source_line = nil
|
|
113
|
-
if method_obj.respond_to?(:source_location)
|
|
114
|
-
source_file, source_line = method_obj.source_location
|
|
115
|
-
end
|
|
139
|
+
_, source_line = method_obj.source_location if method_obj.respond_to?(:source_location)
|
|
116
140
|
line_number = source_line ? [source_line - 1, 1].max : nil
|
|
117
141
|
|
|
118
142
|
unless @environment.allow_param_comments?
|
|
119
|
-
source_file,
|
|
143
|
+
source_file, = method_obj.source_location
|
|
120
144
|
@environment.handle_documentation_issue(
|
|
121
145
|
'@param notation is disabled. Enable it via ENV RUBYCLI_ALLOW_PARAM_COMMENT=ON.',
|
|
122
146
|
file: source_file,
|
|
@@ -134,7 +158,7 @@ module Rubycli
|
|
|
134
158
|
type_str = match[2]
|
|
135
159
|
option_tokens = combine_bracketed_tokens(match[3]&.split(/\s+/) || [])
|
|
136
160
|
description = match[4]&.strip
|
|
137
|
-
description = nil if description
|
|
161
|
+
description = nil if description && description.empty?
|
|
138
162
|
|
|
139
163
|
raw_types = parse_type_annotation(type_str)
|
|
140
164
|
audit_type_annotation_tokens(raw_types, method_obj)
|
|
@@ -248,13 +272,13 @@ module Rubycli
|
|
|
248
272
|
return nil unless line.start_with?('--') || line.start_with?('-')
|
|
249
273
|
|
|
250
274
|
raw_tokens = combine_bracketed_tokens(line.split(/\s+/))
|
|
251
|
-
tokens = raw_tokens.flat_map
|
|
275
|
+
tokens = raw_tokens.flat_map do |token|
|
|
252
276
|
if token.include?('/') && !token.start_with?('[')
|
|
253
277
|
token.split('/')
|
|
254
278
|
else
|
|
255
279
|
[token]
|
|
256
280
|
end
|
|
257
|
-
|
|
281
|
+
end
|
|
258
282
|
|
|
259
283
|
long_option = nil
|
|
260
284
|
short_option = nil
|
|
@@ -341,9 +365,13 @@ module Rubycli
|
|
|
341
365
|
return nil unless placeholder_token?(clean_placeholder)
|
|
342
366
|
|
|
343
367
|
type_token = nil
|
|
344
|
-
if tokens.first && type_token_candidate?(tokens.first)
|
|
345
|
-
|
|
346
|
-
|
|
368
|
+
type_token = tokens.shift if tokens.first && type_token_candidate?(tokens.first)
|
|
369
|
+
|
|
370
|
+
# Summaries such as "A command that ..." or "I keep ..." start with an
|
|
371
|
+
# all-uppercase single letter and would otherwise take over the first
|
|
372
|
+
# positional argument. Require a type annotation to use one as a
|
|
373
|
+
# placeholder; "A [String] ..." keeps working.
|
|
374
|
+
return nil if type_token.nil? && clean_placeholder.match?(/\A[A-Z]\z/)
|
|
347
375
|
|
|
348
376
|
description = tokens.join(' ').strip
|
|
349
377
|
description = nil if description.empty?
|
|
@@ -392,14 +420,14 @@ module Rubycli
|
|
|
392
420
|
return ReturnDefinition.new(types: types, description: description)
|
|
393
421
|
end
|
|
394
422
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
423
|
+
return unless line.start_with?('return ')
|
|
424
|
+
|
|
425
|
+
stripped = line.sub(/\Areturn\s+/, '')
|
|
426
|
+
type_token, description = stripped.split(/\s+/, 2)
|
|
427
|
+
types = parse_type_annotation(type_token)
|
|
428
|
+
audit_type_annotation_tokens(types, method_obj)
|
|
429
|
+
description = description&.strip
|
|
430
|
+
ReturnDefinition.new(types: types, description: description)
|
|
403
431
|
end
|
|
404
432
|
|
|
405
433
|
def doc_issue_location(method_obj)
|
|
@@ -437,11 +465,10 @@ module Rubycli
|
|
|
437
465
|
end
|
|
438
466
|
|
|
439
467
|
literal_entry = literal_entry_from_token(normalized)
|
|
440
|
-
if literal_entry
|
|
441
|
-
return
|
|
442
|
-
end
|
|
468
|
+
return if literal_entry
|
|
443
469
|
|
|
444
|
-
if literal_context && literal_token_candidate?(normalized,
|
|
470
|
+
if literal_context && literal_token_candidate?(normalized,
|
|
471
|
+
include_uppercase: true) && !known_type_token?(normalized)
|
|
445
472
|
warn_unknown_allowed_value(normalized, source_file, line_number)
|
|
446
473
|
return
|
|
447
474
|
end
|
|
@@ -478,6 +505,7 @@ module Rubycli
|
|
|
478
505
|
line = lines[index]
|
|
479
506
|
signature << line
|
|
480
507
|
break if balanced_signature?(signature)
|
|
508
|
+
|
|
481
509
|
index += 1
|
|
482
510
|
end
|
|
483
511
|
|
|
@@ -520,12 +548,13 @@ module Rubycli
|
|
|
520
548
|
|
|
521
549
|
def extract_params_from_signature(signature)
|
|
522
550
|
return nil unless (def_match = signature.match(/\bdef\b\s+[^(\s]+\s*(\((.*)\))?/m))
|
|
551
|
+
|
|
523
552
|
if def_match[1]
|
|
524
|
-
|
|
525
|
-
|
|
553
|
+
def_match[1][1..-2]
|
|
554
|
+
|
|
526
555
|
else
|
|
527
556
|
signature_after_def = signature.sub(/.*\bdef\b\s+[^(\s]+\s*/m, '')
|
|
528
|
-
signature_after_def.split(
|
|
557
|
+
signature_after_def.split("\n").first&.strip
|
|
529
558
|
end
|
|
530
559
|
end
|
|
531
560
|
|
|
@@ -564,18 +593,16 @@ module Rubycli
|
|
|
564
593
|
|
|
565
594
|
source_file = nil
|
|
566
595
|
source_line = nil
|
|
567
|
-
if method_obj.respond_to?(:source_location)
|
|
568
|
-
source_file, source_line = method_obj.source_location
|
|
569
|
-
end
|
|
596
|
+
source_file, source_line = method_obj.source_location if method_obj.respond_to?(:source_location)
|
|
570
597
|
line_for_comment = source_line ? [source_line - 1, 1].max : nil
|
|
571
598
|
|
|
572
599
|
method_obj.parameters.each do |type, name|
|
|
573
600
|
case type
|
|
574
|
-
when :req, :opt
|
|
575
|
-
doc = positional_defs
|
|
601
|
+
when :req, :opt, :rest
|
|
602
|
+
doc = take_positional_definition(positional_defs, name)
|
|
576
603
|
if doc
|
|
577
604
|
doc.param_name = name
|
|
578
|
-
doc.default_value = defaults[name]
|
|
605
|
+
doc.default_value = type == :rest ? [] : defaults[name]
|
|
579
606
|
positional_map[name] = doc
|
|
580
607
|
else
|
|
581
608
|
@environment.handle_documentation_issue(
|
|
@@ -587,10 +614,10 @@ module Rubycli
|
|
|
587
614
|
fallback = PositionalDefinition.new(
|
|
588
615
|
placeholder: name.to_s,
|
|
589
616
|
label: name.to_s.upcase,
|
|
590
|
-
types: ['String'],
|
|
617
|
+
types: [type == :rest ? 'String[]' : 'String'],
|
|
591
618
|
description: nil,
|
|
592
619
|
param_name: name,
|
|
593
|
-
default_value: defaults[name],
|
|
620
|
+
default_value: type == :rest ? [] : defaults[name],
|
|
594
621
|
inline_type_annotation: false,
|
|
595
622
|
inline_type_text: nil,
|
|
596
623
|
doc_format: :auto_generated,
|
|
@@ -641,25 +668,46 @@ module Rubycli
|
|
|
641
668
|
metadata[:positionals_map] = positional_map
|
|
642
669
|
|
|
643
670
|
metadata[:options].each do |opt|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
elsif opt.boolean_flag
|
|
654
|
-
opt.boolean_flag = false
|
|
655
|
-
opt.requires_value = true
|
|
656
|
-
opt.value_name ||= default_placeholder_for(opt.keyword)
|
|
657
|
-
opt.types = ['String'] if opt.types.nil? || opt.types.empty?
|
|
671
|
+
next unless defaults.key?(opt.keyword)
|
|
672
|
+
|
|
673
|
+
opt.default_value = defaults[opt.keyword]
|
|
674
|
+
if boolean_default?(opt.default_value)
|
|
675
|
+
opt.boolean_flag = true
|
|
676
|
+
opt.requires_value = false
|
|
677
|
+
if opt.doc_format == :auto_generated
|
|
678
|
+
opt.value_name = nil
|
|
679
|
+
opt.types = ['Boolean']
|
|
658
680
|
end
|
|
681
|
+
elsif opt.boolean_flag && !TypeUtils.boolean_string?(opt.default_value)
|
|
682
|
+
opt.boolean_flag = false
|
|
683
|
+
opt.requires_value = true
|
|
684
|
+
opt.value_name ||= default_placeholder_for(opt.keyword)
|
|
685
|
+
opt.types = ['String'] if opt.types.nil? || opt.types.empty?
|
|
659
686
|
end
|
|
660
687
|
end
|
|
661
688
|
end
|
|
662
689
|
|
|
690
|
+
# Only a literal true/false default turns an option into a boolean flag.
|
|
691
|
+
# Values such as 0/1 are truthy for TypeUtils.boolean_string?, but they are
|
|
692
|
+
# ordinary defaults for numeric options, so they must keep accepting a value.
|
|
693
|
+
def boolean_default?(default_value)
|
|
694
|
+
return true if [true, false].include?(default_value)
|
|
695
|
+
|
|
696
|
+
%w[true false].include?(default_value.to_s.strip.downcase)
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def take_positional_definition(positional_defs, parameter_name)
|
|
700
|
+
tagged_index = positional_defs.index do |definition|
|
|
701
|
+
definition.doc_format == :tagged_param && definition.param_name == parameter_name
|
|
702
|
+
end
|
|
703
|
+
return positional_defs.delete_at(tagged_index) if tagged_index
|
|
704
|
+
|
|
705
|
+
tagless_index = positional_defs.index { |definition| definition.doc_format != :tagged_param }
|
|
706
|
+
return positional_defs.delete_at(tagless_index) if tagless_index
|
|
707
|
+
|
|
708
|
+
nil
|
|
709
|
+
end
|
|
710
|
+
|
|
663
711
|
def detail_line_for_extra_positional(doc)
|
|
664
712
|
return nil unless doc
|
|
665
713
|
|
|
@@ -669,9 +717,7 @@ module Rubycli
|
|
|
669
717
|
parts << placeholder unless placeholder.empty?
|
|
670
718
|
|
|
671
719
|
type_text = doc.inline_type_text
|
|
672
|
-
if (!type_text || type_text.empty?) && doc.types && !doc.types.empty?
|
|
673
|
-
type_text = "[#{doc.types.join(', ')}]"
|
|
674
|
-
end
|
|
720
|
+
type_text = "[#{doc.types.join(', ')}]" if (!type_text || type_text.empty?) && doc.types && !doc.types.empty?
|
|
675
721
|
parts << type_text if type_text && !type_text.empty?
|
|
676
722
|
|
|
677
723
|
description = doc.description.to_s.strip
|
|
@@ -762,10 +808,9 @@ module Rubycli
|
|
|
762
808
|
|
|
763
809
|
stripped = token.strip
|
|
764
810
|
return nil if stripped.empty?
|
|
811
|
+
|
|
765
812
|
stripped = stripped[1..] if stripped.start_with?('[') && !stripped.end_with?(']')
|
|
766
|
-
if stripped.end_with?(']') && !stripped.include?('[')
|
|
767
|
-
stripped = stripped[0...-1]
|
|
768
|
-
end
|
|
813
|
+
stripped = stripped[0...-1] if stripped.end_with?(']') && !stripped.include?('[')
|
|
769
814
|
|
|
770
815
|
lowered = stripped.downcase
|
|
771
816
|
return { kind: :literal, value: nil } if %w[nil null ~].include?(lowered)
|
|
@@ -787,17 +832,11 @@ module Rubycli
|
|
|
787
832
|
return { kind: :literal, value: stripped[1..-2] }
|
|
788
833
|
end
|
|
789
834
|
|
|
790
|
-
if stripped.match?(/\A-?\d+\z/)
|
|
791
|
-
return { kind: :literal, value: Integer(stripped) }
|
|
792
|
-
end
|
|
835
|
+
return { kind: :literal, value: Integer(stripped) } if stripped.match?(/\A-?\d+\z/)
|
|
793
836
|
|
|
794
|
-
if stripped.match?(/\A-?\d+\.\d+\z/)
|
|
795
|
-
return { kind: :literal, value: Float(stripped) }
|
|
796
|
-
end
|
|
837
|
+
return { kind: :literal, value: Float(stripped) } if stripped.match?(/\A-?\d+\.\d+\z/)
|
|
797
838
|
|
|
798
|
-
if stripped.match?(/\A[a-z0-9._-]+\z/)
|
|
799
|
-
return { kind: :literal, value: stripped }
|
|
800
|
-
end
|
|
839
|
+
return { kind: :literal, value: stripped } if stripped.match?(/\A[a-z0-9._-]+\z/)
|
|
801
840
|
|
|
802
841
|
nil
|
|
803
842
|
rescue ArgumentError
|
|
@@ -873,6 +912,8 @@ module Rubycli
|
|
|
873
912
|
end
|
|
874
913
|
|
|
875
914
|
def type_dictionary
|
|
915
|
+
return @type_dictionary if defined?(@type_dictionary) && @type_dictionary
|
|
916
|
+
|
|
876
917
|
builtins = (INLINE_TYPE_HINTS + %w[NilClass Fixnum Decimal Struct]).uniq
|
|
877
918
|
constant_names = []
|
|
878
919
|
begin
|
|
@@ -888,9 +929,12 @@ module Rubycli
|
|
|
888
929
|
constant_names = Object.constants.map(&:to_s)
|
|
889
930
|
end
|
|
890
931
|
|
|
891
|
-
(builtins + constant_names).map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
932
|
+
@type_dictionary = (builtins + constant_names).map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
892
933
|
end
|
|
893
934
|
|
|
935
|
+
def reset_type_dictionary_cache!
|
|
936
|
+
@type_dictionary = nil
|
|
937
|
+
end
|
|
894
938
|
|
|
895
939
|
def placeholder_token?(token)
|
|
896
940
|
return false unless token
|
|
@@ -902,7 +946,7 @@ module Rubycli
|
|
|
902
946
|
candidate = candidate[1..-2].strip if optional
|
|
903
947
|
return false if candidate.empty?
|
|
904
948
|
|
|
905
|
-
candidate = candidate.gsub(/[
|
|
949
|
+
candidate = candidate.gsub(/[,|]/, '')
|
|
906
950
|
return false if candidate.empty?
|
|
907
951
|
|
|
908
952
|
ellipsis = candidate.end_with?('...')
|
|
@@ -984,8 +1028,6 @@ module Rubycli
|
|
|
984
1028
|
stripped[0..-3]
|
|
985
1029
|
elsif stripped.start_with?('Array<') && stripped.end_with?('>')
|
|
986
1030
|
stripped[6..-2].strip
|
|
987
|
-
else
|
|
988
|
-
nil
|
|
989
1031
|
end
|
|
990
1032
|
end
|
|
991
1033
|
|
|
@@ -1099,16 +1141,14 @@ module Rubycli
|
|
|
1099
1141
|
normalized_long = normalize_long_option(long_option)
|
|
1100
1142
|
normalized_short = normalize_short_option(short_option)
|
|
1101
1143
|
value_placeholder = value_name&.strip
|
|
1102
|
-
value_placeholder = nil if value_placeholder
|
|
1144
|
+
value_placeholder = nil if value_placeholder && value_placeholder.empty?
|
|
1103
1145
|
description_text = description&.strip
|
|
1104
|
-
description_text = nil if description_text
|
|
1146
|
+
description_text = nil if description_text && description_text.empty?
|
|
1105
1147
|
|
|
1106
1148
|
placeholder_info = analyze_placeholder(value_placeholder)
|
|
1107
1149
|
normalized_types = normalize_type_list(types)
|
|
1108
1150
|
inferred_types = infer_types_from_placeholder(normalized_types, placeholder_info)
|
|
1109
|
-
if inferred_types.empty? && value_placeholder.nil?
|
|
1110
|
-
inferred_types = ['Boolean']
|
|
1111
|
-
end
|
|
1151
|
+
inferred_types = ['Boolean'] if inferred_types.empty? && value_placeholder.nil?
|
|
1112
1152
|
|
|
1113
1153
|
optional_value = placeholder_info[:optional]
|
|
1114
1154
|
boolean_flag = !optional_value && inferred_types.any? { |type| boolean_type?(type) }
|
data/lib/rubycli/environment.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Rubycli
|
|
2
4
|
# Environment captures runtime flags and configuration derived from ENV / argv.
|
|
3
5
|
class Environment
|
|
@@ -45,6 +47,10 @@ module Rubycli
|
|
|
45
47
|
@strict_input
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
def disable_strict_input!
|
|
51
|
+
@strict_input = false
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
def documentation_issues
|
|
49
55
|
@documentation_issues.dup
|
|
50
56
|
end
|
|
@@ -70,10 +76,10 @@ module Rubycli
|
|
|
70
76
|
end
|
|
71
77
|
|
|
72
78
|
formatted_message = if location
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
"#{location} #{message}"
|
|
80
|
+
else
|
|
81
|
+
message
|
|
82
|
+
end
|
|
77
83
|
|
|
78
84
|
entry = { message: formatted_message, location: location }
|
|
79
85
|
@documentation_issues << entry
|
|
@@ -81,17 +87,19 @@ module Rubycli
|
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
def handle_input_violation(message)
|
|
84
|
-
if strict_input?
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
warn "[WARN] #{message} (use --strict to abort on invalid input)"
|
|
88
|
-
end
|
|
90
|
+
raise Rubycli::ArgumentError, message if strict_input?
|
|
91
|
+
|
|
92
|
+
warn "[WARN] #{message} (use --strict to abort on invalid input)"
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
def enable_print_result!
|
|
92
96
|
@print_result = true
|
|
93
97
|
end
|
|
94
98
|
|
|
99
|
+
def disable_print_result!
|
|
100
|
+
@print_result = false
|
|
101
|
+
end
|
|
102
|
+
|
|
95
103
|
private
|
|
96
104
|
|
|
97
105
|
def fetch_env_value(key, default)
|
data/lib/rubycli/eval_coercer.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Rubycli
|
|
2
4
|
class EvalCoercer
|
|
3
5
|
THREAD_KEY = :rubycli_eval_mode
|
|
4
6
|
LAX_THREAD_KEY = :rubycli_eval_lax_mode
|
|
5
|
-
|
|
7
|
+
BINDING_THREAD_KEY = :rubycli_eval_binding
|
|
6
8
|
|
|
7
9
|
def eval_mode?
|
|
8
10
|
Thread.current[THREAD_KEY] == true
|
|
@@ -12,15 +14,28 @@ module Rubycli
|
|
|
12
14
|
Thread.current[LAX_THREAD_KEY] == true
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
def with_eval_mode(enabled = true, lax: false)
|
|
17
|
+
def with_eval_mode(enabled = true, lax: false, reuse_binding: false)
|
|
16
18
|
previous = Thread.current[THREAD_KEY]
|
|
17
19
|
previous_lax = Thread.current[LAX_THREAD_KEY]
|
|
20
|
+
previous_binding = Thread.current[BINDING_THREAD_KEY]
|
|
18
21
|
Thread.current[THREAD_KEY] = enabled
|
|
19
22
|
Thread.current[LAX_THREAD_KEY] = enabled && lax
|
|
23
|
+
if enabled
|
|
24
|
+
Thread.current[BINDING_THREAD_KEY] = reuse_binding && previous_binding ? previous_binding : isolated_binding
|
|
25
|
+
end
|
|
20
26
|
yield
|
|
21
27
|
ensure
|
|
22
28
|
Thread.current[THREAD_KEY] = previous
|
|
23
29
|
Thread.current[LAX_THREAD_KEY] = previous_lax
|
|
30
|
+
Thread.current[BINDING_THREAD_KEY] = previous_binding
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def with_eval_binding
|
|
34
|
+
previous_binding = Thread.current[BINDING_THREAD_KEY]
|
|
35
|
+
Thread.current[BINDING_THREAD_KEY] = isolated_binding
|
|
36
|
+
yield
|
|
37
|
+
ensure
|
|
38
|
+
Thread.current[BINDING_THREAD_KEY] = previous_binding
|
|
24
39
|
end
|
|
25
40
|
|
|
26
41
|
def coerce_eval_value(value)
|
|
@@ -34,7 +49,7 @@ module Rubycli
|
|
|
34
49
|
else
|
|
35
50
|
value
|
|
36
51
|
end
|
|
37
|
-
rescue StandardError => e
|
|
52
|
+
rescue SyntaxError, StandardError => e
|
|
38
53
|
raise Rubycli::ArgumentError, "Failed to evaluate Ruby code: #{e.message}"
|
|
39
54
|
end
|
|
40
55
|
|
|
@@ -44,14 +59,16 @@ module Rubycli
|
|
|
44
59
|
trimmed = expression.strip
|
|
45
60
|
return trimmed if trimmed.empty?
|
|
46
61
|
|
|
47
|
-
|
|
62
|
+
(Thread.current[BINDING_THREAD_KEY] || isolated_binding).eval(trimmed)
|
|
48
63
|
rescue SyntaxError, NameError => e
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
raise unless eval_lax_mode?
|
|
65
|
+
|
|
66
|
+
warn "[WARN] Failed to evaluate argument as Ruby (#{e.message.strip}). Passing it through because --eval-lax is enabled."
|
|
67
|
+
expression
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def isolated_binding
|
|
71
|
+
Object.new.instance_eval { binding }
|
|
55
72
|
end
|
|
56
73
|
end
|
|
57
74
|
end
|