rubycli 0.1.7 → 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 +77 -2
- data/LICENSE +1 -1
- data/README.ja.md +357 -252
- data/README.md +386 -250
- 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 +295 -96
- data/lib/rubycli/cli.rb +26 -30
- data/lib/rubycli/command_line.rb +151 -86
- data/lib/rubycli/constant_capture.rb +563 -6
- data/lib/rubycli/documentation/metadata_parser.rb +108 -74
- 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 -456
- 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
|
|
@@ -897,7 +936,6 @@ module Rubycli
|
|
|
897
936
|
@type_dictionary = nil
|
|
898
937
|
end
|
|
899
938
|
|
|
900
|
-
|
|
901
939
|
def placeholder_token?(token)
|
|
902
940
|
return false unless token
|
|
903
941
|
|
|
@@ -908,7 +946,7 @@ module Rubycli
|
|
|
908
946
|
candidate = candidate[1..-2].strip if optional
|
|
909
947
|
return false if candidate.empty?
|
|
910
948
|
|
|
911
|
-
candidate = candidate.gsub(/[
|
|
949
|
+
candidate = candidate.gsub(/[,|]/, '')
|
|
912
950
|
return false if candidate.empty?
|
|
913
951
|
|
|
914
952
|
ellipsis = candidate.end_with?('...')
|
|
@@ -990,8 +1028,6 @@ module Rubycli
|
|
|
990
1028
|
stripped[0..-3]
|
|
991
1029
|
elsif stripped.start_with?('Array<') && stripped.end_with?('>')
|
|
992
1030
|
stripped[6..-2].strip
|
|
993
|
-
else
|
|
994
|
-
nil
|
|
995
1031
|
end
|
|
996
1032
|
end
|
|
997
1033
|
|
|
@@ -1105,16 +1141,14 @@ module Rubycli
|
|
|
1105
1141
|
normalized_long = normalize_long_option(long_option)
|
|
1106
1142
|
normalized_short = normalize_short_option(short_option)
|
|
1107
1143
|
value_placeholder = value_name&.strip
|
|
1108
|
-
value_placeholder = nil if value_placeholder
|
|
1144
|
+
value_placeholder = nil if value_placeholder && value_placeholder.empty?
|
|
1109
1145
|
description_text = description&.strip
|
|
1110
|
-
description_text = nil if description_text
|
|
1146
|
+
description_text = nil if description_text && description_text.empty?
|
|
1111
1147
|
|
|
1112
1148
|
placeholder_info = analyze_placeholder(value_placeholder)
|
|
1113
1149
|
normalized_types = normalize_type_list(types)
|
|
1114
1150
|
inferred_types = infer_types_from_placeholder(normalized_types, placeholder_info)
|
|
1115
|
-
if inferred_types.empty? && value_placeholder.nil?
|
|
1116
|
-
inferred_types = ['Boolean']
|
|
1117
|
-
end
|
|
1151
|
+
inferred_types = ['Boolean'] if inferred_types.empty? && value_placeholder.nil?
|
|
1118
1152
|
|
|
1119
1153
|
optional_value = placeholder_info[:optional]
|
|
1120
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
|