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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubycli
|
|
4
|
+
# Coordinates json/eval argument modes and enforces mutual exclusion.
|
|
5
|
+
class ArgumentModeController
|
|
6
|
+
def initialize(json_coercer:, eval_coercer:)
|
|
7
|
+
@json_coercer = json_coercer
|
|
8
|
+
@eval_coercer = eval_coercer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def json_mode?
|
|
12
|
+
@json_coercer.json_mode?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def eval_mode?
|
|
16
|
+
@eval_coercer.eval_mode?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with_json_mode(enabled = true, &block)
|
|
20
|
+
enforce_mutual_exclusion!(:json, enabled)
|
|
21
|
+
@json_coercer.with_json_mode(enabled, &block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def with_eval_mode(enabled = true, **options, &block)
|
|
25
|
+
enforce_mutual_exclusion!(:eval, enabled)
|
|
26
|
+
@eval_coercer.with_eval_mode(enabled, **options, &block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def apply_argument_coercions(positional_args, keyword_args)
|
|
30
|
+
ensure_modes_compatible!
|
|
31
|
+
|
|
32
|
+
if json_mode?
|
|
33
|
+
coerce_values!(positional_args, keyword_args) { |value| @json_coercer.coerce_json_value(value) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if eval_mode?
|
|
37
|
+
coerce_values!(positional_args, keyword_args) { |value| @eval_coercer.coerce_eval_value(value) }
|
|
38
|
+
end
|
|
39
|
+
rescue ::ArgumentError => e
|
|
40
|
+
raise Rubycli::ArgumentError, e.message
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def enforce_mutual_exclusion!(mode, enabled)
|
|
46
|
+
return unless enabled
|
|
47
|
+
|
|
48
|
+
case mode
|
|
49
|
+
when :json
|
|
50
|
+
raise Rubycli::ArgumentError, '--json-args cannot be combined with --eval-args or --eval-lax' if eval_mode?
|
|
51
|
+
when :eval
|
|
52
|
+
raise Rubycli::ArgumentError, '--json-args cannot be combined with --eval-args or --eval-lax' if json_mode?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ensure_modes_compatible!
|
|
57
|
+
if json_mode? && eval_mode?
|
|
58
|
+
raise Rubycli::ArgumentError, '--json-args cannot be combined with --eval-args or --eval-lax'
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def coerce_values!(positional_args, keyword_args)
|
|
63
|
+
positional_args.map! { |value| yield(value) }
|
|
64
|
+
keyword_args.keys.each do |key|
|
|
65
|
+
keyword_args[key] = yield(keyword_args[key])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require_relative 'type_utils'
|
|
2
|
+
require_relative 'arguments/token_stream'
|
|
3
|
+
require_relative 'arguments/value_converter'
|
|
2
4
|
|
|
3
5
|
module Rubycli
|
|
4
6
|
class ArgumentParser
|
|
@@ -9,6 +11,7 @@ module Rubycli
|
|
|
9
11
|
@documentation_registry = documentation_registry
|
|
10
12
|
@json_coercer = json_coercer
|
|
11
13
|
@debug_logger = debug_logger
|
|
14
|
+
@value_converter = Arguments::ValueConverter.new
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def parse(args, method = nil)
|
|
@@ -24,21 +27,21 @@ module Rubycli
|
|
|
24
27
|
option_lookup = build_option_lookup(option_defs)
|
|
25
28
|
type_converters = build_type_converter_map(option_defs)
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
stream = Arguments::TokenStream.new(args)
|
|
31
|
+
|
|
32
|
+
until stream.finished?
|
|
33
|
+
token = stream.current
|
|
30
34
|
|
|
31
35
|
if token == '--'
|
|
32
|
-
|
|
36
|
+
stream.advance
|
|
37
|
+
rest_tokens = stream.consume_remaining.map { |value| convert_arg(value) }
|
|
33
38
|
pos_args.concat(rest_tokens)
|
|
34
39
|
break
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
i = process_option_token(
|
|
40
|
+
elsif option_token?(token)
|
|
41
|
+
stream.advance
|
|
42
|
+
process_option_token(
|
|
39
43
|
token,
|
|
40
|
-
|
|
41
|
-
i,
|
|
44
|
+
stream,
|
|
42
45
|
kw_param_names,
|
|
43
46
|
kw_args,
|
|
44
47
|
cli_aliases,
|
|
@@ -46,12 +49,12 @@ module Rubycli
|
|
|
46
49
|
type_converters
|
|
47
50
|
)
|
|
48
51
|
elsif assignment_token?(token)
|
|
52
|
+
stream.advance
|
|
49
53
|
process_assignment_token(token, kw_args)
|
|
50
54
|
else
|
|
51
55
|
pos_args << convert_arg(token)
|
|
56
|
+
stream.advance
|
|
52
57
|
end
|
|
53
|
-
|
|
54
|
-
i += 1
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
debug_log "Final parsed - pos_args: #{pos_args.inspect}, kw_args: #{kw_args.inspect}"
|
|
@@ -84,8 +87,7 @@ module Rubycli
|
|
|
84
87
|
|
|
85
88
|
def process_option_token(
|
|
86
89
|
token,
|
|
87
|
-
|
|
88
|
-
current_index,
|
|
90
|
+
stream,
|
|
89
91
|
kw_param_names,
|
|
90
92
|
kw_args,
|
|
91
93
|
cli_aliases,
|
|
@@ -107,21 +109,19 @@ module Rubycli
|
|
|
107
109
|
requires_value = option_meta ? option_meta[:requires_value] : nil
|
|
108
110
|
option_label = option_meta&.long || "--#{final_key.tr('_', '-')}"
|
|
109
111
|
|
|
110
|
-
value_capture
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
['true', current_index]
|
|
124
|
-
end
|
|
112
|
+
value_capture = if embedded_value
|
|
113
|
+
embedded_value
|
|
114
|
+
elsif option_meta
|
|
115
|
+
capture_option_value(
|
|
116
|
+
option_meta,
|
|
117
|
+
stream,
|
|
118
|
+
requires_value
|
|
119
|
+
)
|
|
120
|
+
elsif (next_token = stream.current) && !looks_like_option?(next_token)
|
|
121
|
+
stream.consume
|
|
122
|
+
else
|
|
123
|
+
'true'
|
|
124
|
+
end
|
|
125
125
|
|
|
126
126
|
if requires_value && (value_capture.nil? || value_capture == 'true')
|
|
127
127
|
raise ArgumentError, "Option '#{option_label}' requires a value"
|
|
@@ -135,40 +135,30 @@ module Rubycli
|
|
|
135
135
|
)
|
|
136
136
|
|
|
137
137
|
kw_args[final_key_sym] = converted_value
|
|
138
|
-
current_index
|
|
139
138
|
end
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
new_index += 1
|
|
164
|
-
args[new_index]
|
|
165
|
-
elsif new_index + 1 < args.size && !looks_like_option?(args[new_index + 1])
|
|
166
|
-
new_index += 1
|
|
167
|
-
args[new_index]
|
|
168
|
-
else
|
|
169
|
-
'true'
|
|
170
|
-
end
|
|
171
|
-
[value, new_index]
|
|
139
|
+
def capture_option_value(option_meta, stream, requires_value)
|
|
140
|
+
if option_meta[:boolean_flag]
|
|
141
|
+
if (next_token = stream.current) && TypeUtils.boolean_string?(next_token)
|
|
142
|
+
return stream.consume
|
|
143
|
+
end
|
|
144
|
+
return 'true'
|
|
145
|
+
elsif option_meta[:optional_value]
|
|
146
|
+
if (next_token = stream.current) && !looks_like_option?(next_token)
|
|
147
|
+
return stream.consume
|
|
148
|
+
end
|
|
149
|
+
return true
|
|
150
|
+
elsif requires_value == false
|
|
151
|
+
return 'true'
|
|
152
|
+
elsif requires_value
|
|
153
|
+
next_token = stream.current
|
|
154
|
+
raise ArgumentError, "Option '#{option_meta.long}' requires a value" unless next_token
|
|
155
|
+
|
|
156
|
+
return stream.consume
|
|
157
|
+
elsif (next_token = stream.current) && !looks_like_option?(next_token)
|
|
158
|
+
return stream.consume
|
|
159
|
+
else
|
|
160
|
+
return 'true'
|
|
161
|
+
end
|
|
172
162
|
end
|
|
173
163
|
|
|
174
164
|
def process_assignment_token(token, kw_args)
|
|
@@ -203,21 +193,7 @@ module Rubycli
|
|
|
203
193
|
end
|
|
204
194
|
|
|
205
195
|
def convert_arg(arg)
|
|
206
|
-
|
|
207
|
-
return true if arg.casecmp('true').zero?
|
|
208
|
-
return false if arg.casecmp('false').zero?
|
|
209
|
-
return arg.to_i if integer_string?(arg)
|
|
210
|
-
return arg.to_f if float_string?(arg)
|
|
211
|
-
|
|
212
|
-
arg
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def integer_string?(str)
|
|
216
|
-
str =~ /\A-?\d+\z/
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def float_string?(str)
|
|
220
|
-
str =~ /\A-?\d+\.\d+\z/
|
|
196
|
+
@value_converter.convert(arg)
|
|
221
197
|
end
|
|
222
198
|
|
|
223
199
|
|
|
@@ -286,7 +262,7 @@ module Rubycli
|
|
|
286
262
|
when 'String'
|
|
287
263
|
->(value) { value }
|
|
288
264
|
when 'Integer', 'Fixnum'
|
|
289
|
-
->(value) { Integer(value
|
|
265
|
+
->(value) { Integer(value) }
|
|
290
266
|
when 'Float'
|
|
291
267
|
->(value) { Float(value) }
|
|
292
268
|
when 'Numeric'
|
|
@@ -324,10 +300,25 @@ module Rubycli
|
|
|
324
300
|
end
|
|
325
301
|
|
|
326
302
|
def convert_option_value(keyword, value, option_meta, type_converters)
|
|
303
|
+
if Rubycli.eval_mode? || Rubycli.json_mode?
|
|
304
|
+
return convert_arg(value)
|
|
305
|
+
end
|
|
306
|
+
|
|
327
307
|
converter = type_converters[keyword]
|
|
328
|
-
|
|
308
|
+
converted_value = convert_arg(value)
|
|
309
|
+
return converted_value unless converter
|
|
310
|
+
|
|
311
|
+
original_input = value if value.is_a?(String)
|
|
312
|
+
expects_list = option_meta && option_meta.types.any? { |type|
|
|
313
|
+
type.to_s.end_with?('[]') || type.to_s.start_with?('Array<')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
value_for_converter = converted_value
|
|
317
|
+
if expects_list && original_input && converted_value.is_a?(Numeric) && original_input.include?(',')
|
|
318
|
+
value_for_converter = original_input
|
|
319
|
+
end
|
|
329
320
|
|
|
330
|
-
converter.call(
|
|
321
|
+
converter.call(value_for_converter)
|
|
331
322
|
rescue StandardError => e
|
|
332
323
|
option_label = option_meta&.long || option_meta&.short || keyword
|
|
333
324
|
raise ArgumentError, "Value '#{value}' for option '#{option_label}' is invalid: #{e.message}"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubycli
|
|
4
|
+
module Arguments
|
|
5
|
+
# Lightweight mutable cursor over CLI tokens.
|
|
6
|
+
class TokenStream
|
|
7
|
+
def initialize(tokens)
|
|
8
|
+
@tokens = Array(tokens).dup
|
|
9
|
+
@index = 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def current
|
|
13
|
+
@tokens[@index]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def peek(offset = 1)
|
|
17
|
+
@tokens[@index + offset]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def advance(count = 1)
|
|
21
|
+
@index += count
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def consume
|
|
25
|
+
value = current
|
|
26
|
+
advance
|
|
27
|
+
value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def consume_remaining
|
|
31
|
+
remaining = @tokens[@index..] || []
|
|
32
|
+
@index = @tokens.length
|
|
33
|
+
remaining
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def finished?
|
|
37
|
+
@index >= @tokens.length
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'psych'
|
|
4
|
+
|
|
5
|
+
module Rubycli
|
|
6
|
+
module Arguments
|
|
7
|
+
# Converts raw CLI tokens into Ruby primitives when safe to do so.
|
|
8
|
+
class ValueConverter
|
|
9
|
+
LITERAL_PARSE_FAILURE = Object.new
|
|
10
|
+
|
|
11
|
+
def convert(value)
|
|
12
|
+
return value if Rubycli.eval_mode? || Rubycli.json_mode?
|
|
13
|
+
return value unless value.is_a?(String)
|
|
14
|
+
|
|
15
|
+
trimmed = value.strip
|
|
16
|
+
return value if trimmed.empty?
|
|
17
|
+
|
|
18
|
+
if literal_like?(trimmed)
|
|
19
|
+
literal = try_literal_parse(value)
|
|
20
|
+
return literal unless literal.equal?(LITERAL_PARSE_FAILURE)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return nil if null_literal?(trimmed)
|
|
24
|
+
|
|
25
|
+
lower = trimmed.downcase
|
|
26
|
+
return true if lower == 'true'
|
|
27
|
+
return false if lower == 'false'
|
|
28
|
+
return value.to_i if integer_string?(trimmed)
|
|
29
|
+
return value.to_f if float_string?(trimmed)
|
|
30
|
+
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def integer_string?(str)
|
|
37
|
+
str =~ /\A-?\d+\z/
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def float_string?(str)
|
|
41
|
+
str =~ /\A-?\d+\.\d+\z/
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def try_literal_parse(value)
|
|
45
|
+
return LITERAL_PARSE_FAILURE unless value.is_a?(String)
|
|
46
|
+
|
|
47
|
+
trimmed = value.strip
|
|
48
|
+
return value if trimmed.empty?
|
|
49
|
+
|
|
50
|
+
literal = Psych.safe_load(trimmed, aliases: false)
|
|
51
|
+
return literal unless literal.nil? && !null_literal?(trimmed)
|
|
52
|
+
|
|
53
|
+
LITERAL_PARSE_FAILURE
|
|
54
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::Exception
|
|
55
|
+
LITERAL_PARSE_FAILURE
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def null_literal?(value)
|
|
59
|
+
return false unless value
|
|
60
|
+
|
|
61
|
+
%w[null ~].include?(value.downcase)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def literal_like?(value)
|
|
65
|
+
return false unless value
|
|
66
|
+
return true if value.start_with?('[', '{', '"', "'")
|
|
67
|
+
return true if value.start_with?('---')
|
|
68
|
+
return true if value.match?(/\A(?:true|false|null|nil)\z/i)
|
|
69
|
+
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/rubycli/cli.rb
CHANGED
|
@@ -37,7 +37,7 @@ module Rubycli
|
|
|
37
37
|
|
|
38
38
|
if should_show_help?(args)
|
|
39
39
|
@help_renderer.print_help(target, catalog)
|
|
40
|
-
|
|
40
|
+
return 0
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
command = args.shift
|
|
@@ -105,7 +105,7 @@ module Rubycli
|
|
|
105
105
|
error_msg = "Command '#{command}' is not available."
|
|
106
106
|
puts error_msg
|
|
107
107
|
@help_renderer.print_help(target, catalog)
|
|
108
|
-
|
|
108
|
+
1
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
@@ -116,6 +116,7 @@ module Rubycli
|
|
|
116
116
|
begin
|
|
117
117
|
result = Rubycli.call_target(target, pos_args, kw_args)
|
|
118
118
|
@result_emitter.emit(result)
|
|
119
|
+
0
|
|
119
120
|
rescue StandardError => e
|
|
120
121
|
handle_execution_error(e, command, method, pos_args, kw_args, cli_mode)
|
|
121
122
|
end
|
|
@@ -132,15 +133,16 @@ module Rubycli
|
|
|
132
133
|
def execute_parameterless_method(method_obj, command, args, cli_mode)
|
|
133
134
|
if help_requested_for_parameterless?(args)
|
|
134
135
|
puts usage_for_method(command, method_obj)
|
|
135
|
-
|
|
136
|
+
return 0
|
|
136
137
|
end
|
|
137
138
|
|
|
138
139
|
begin
|
|
139
140
|
result = method_obj.call
|
|
140
141
|
debug_log "Parameterless method returned: #{result.inspect}"
|
|
141
142
|
if result
|
|
142
|
-
run(result, args, false)
|
|
143
|
+
return run(result, args, false)
|
|
143
144
|
end
|
|
145
|
+
0
|
|
144
146
|
rescue StandardError => e
|
|
145
147
|
handle_execution_error(e, command, method_obj, [], {}, cli_mode)
|
|
146
148
|
end
|
|
@@ -152,12 +154,13 @@ module Rubycli
|
|
|
152
154
|
|
|
153
155
|
if should_show_method_help?(pos_args, kw_args)
|
|
154
156
|
puts usage_for_method(command, method_obj)
|
|
155
|
-
|
|
157
|
+
return 0
|
|
156
158
|
end
|
|
157
159
|
|
|
158
160
|
begin
|
|
159
161
|
result = Rubycli.call_target(method_obj, pos_args, kw_args)
|
|
160
162
|
@result_emitter.emit(result)
|
|
163
|
+
0
|
|
161
164
|
rescue StandardError => e
|
|
162
165
|
handle_execution_error(e, command, method_obj, pos_args, kw_args, cli_mode)
|
|
163
166
|
end
|
|
@@ -181,7 +184,7 @@ module Rubycli
|
|
|
181
184
|
if cli_mode && !arguments_match?(method_obj, pos_args, kw_args) && usage_error?(error)
|
|
182
185
|
puts "Error: #{error.message}"
|
|
183
186
|
puts usage_for_method(command, method_obj)
|
|
184
|
-
|
|
187
|
+
1
|
|
185
188
|
else
|
|
186
189
|
raise error
|
|
187
190
|
end
|
data/lib/rubycli/command_line.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Rubycli
|
|
4
4
|
module CommandLine
|
|
5
5
|
USAGE = <<~USAGE
|
|
6
|
-
Usage: rubycli [--new|-n] [--pre-script
|
|
6
|
+
Usage: rubycli [--new|-n] [--pre-script=<src>] [--json-args|-j | --eval-args|-e | --eval-lax|-E] <target-path> [<class-or-module>] [-- <cli-args>...]
|
|
7
7
|
|
|
8
8
|
Examples:
|
|
9
9
|
rubycli scripts/sample_runner.rb echo --message hello
|
|
@@ -12,14 +12,18 @@ module Rubycli
|
|
|
12
12
|
|
|
13
13
|
Options:
|
|
14
14
|
--new, -n Instantiate the class/module before invoking CLI commands
|
|
15
|
-
--pre-script
|
|
16
|
-
--json-args
|
|
17
|
-
--eval-args
|
|
18
|
-
|
|
15
|
+
--pre-script=<src> Evaluate Ruby code and use its result as the exposed target (--init alias; also accepts space-separated form)
|
|
16
|
+
--json-args, -j Parse all following arguments strictly as JSON (no YAML literals)
|
|
17
|
+
--eval-args, -e Evaluate following arguments as Ruby code
|
|
18
|
+
--eval-lax, -E Evaluate as Ruby but fall back to raw strings when parsing fails
|
|
19
|
+
--auto-target, -a Auto-select the only callable constant when names don't match
|
|
20
|
+
(Note: --json-args cannot be combined with --eval-args or --eval-lax)
|
|
21
|
+
(Note: Every option that accepts a value understands both --flag=value and --flag value forms.)
|
|
19
22
|
|
|
20
|
-
When
|
|
23
|
+
When <class-or-module> is omitted, Rubycli infers it from the file name in CamelCase.
|
|
24
|
+
Arguments are parsed as safe literals by default; pick a mode above if you need strict JSON or Ruby eval.
|
|
21
25
|
Method return values are printed to STDOUT by default.
|
|
22
|
-
|
|
26
|
+
<cli-args> are forwarded to Rubycli unchanged.
|
|
23
27
|
USAGE
|
|
24
28
|
|
|
25
29
|
module_function
|
|
@@ -36,6 +40,8 @@ module Rubycli
|
|
|
36
40
|
new_flag = false
|
|
37
41
|
json_mode = false
|
|
38
42
|
eval_mode = false
|
|
43
|
+
eval_lax_mode = false
|
|
44
|
+
constant_mode = nil
|
|
39
45
|
pre_script_sources = []
|
|
40
46
|
|
|
41
47
|
loop do
|
|
@@ -63,13 +69,20 @@ module Rubycli
|
|
|
63
69
|
end
|
|
64
70
|
context = File.file?(src) ? File.expand_path(src) : "(inline #{flag})"
|
|
65
71
|
pre_script_sources << { value: src, context: context }
|
|
66
|
-
when '--json-args'
|
|
72
|
+
when '--json-args', '-j'
|
|
67
73
|
json_mode = true
|
|
68
74
|
args.shift
|
|
69
|
-
when '--eval-args'
|
|
75
|
+
when '--eval-args', '-e'
|
|
70
76
|
eval_mode = true
|
|
71
77
|
args.shift
|
|
78
|
+
when '--eval-lax', '-E'
|
|
79
|
+
eval_mode = true
|
|
80
|
+
eval_lax_mode = true
|
|
81
|
+
args.shift
|
|
72
82
|
when '--print-result'
|
|
83
|
+
args.shift
|
|
84
|
+
when '--auto-target', '-a'
|
|
85
|
+
constant_mode = :auto
|
|
73
86
|
args.shift
|
|
74
87
|
when '--'
|
|
75
88
|
args.shift
|
|
@@ -93,7 +106,7 @@ module Rubycli
|
|
|
93
106
|
args.shift if args.first == '--'
|
|
94
107
|
|
|
95
108
|
if json_mode && eval_mode
|
|
96
|
-
warn '--json-args
|
|
109
|
+
warn '--json-args cannot be combined with --eval-args or --eval-lax'
|
|
97
110
|
return 1
|
|
98
111
|
end
|
|
99
112
|
|
|
@@ -104,13 +117,18 @@ module Rubycli
|
|
|
104
117
|
new: new_flag,
|
|
105
118
|
json: json_mode,
|
|
106
119
|
eval_args: eval_mode,
|
|
107
|
-
|
|
120
|
+
eval_lax: eval_lax_mode,
|
|
121
|
+
pre_scripts: pre_script_sources,
|
|
122
|
+
constant_mode: constant_mode
|
|
108
123
|
)
|
|
109
124
|
|
|
110
125
|
0
|
|
111
126
|
rescue Rubycli::Runner::PreScriptError => e
|
|
112
127
|
warn e.message
|
|
113
128
|
1
|
|
129
|
+
rescue Rubycli::Runner::Error => e
|
|
130
|
+
warn e.message
|
|
131
|
+
1
|
|
114
132
|
end
|
|
115
133
|
end
|
|
116
134
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubycli
|
|
4
|
+
# Observes constants defined while loading a file.
|
|
5
|
+
class ConstantCapture
|
|
6
|
+
def initialize
|
|
7
|
+
@captured = Hash.new { |hash, key| hash[key] = [] }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def capture(file)
|
|
11
|
+
trace = TracePoint.new(:class) do |tp|
|
|
12
|
+
location = tp.path
|
|
13
|
+
next unless location && same_file?(file, location)
|
|
14
|
+
|
|
15
|
+
constant_name = qualified_name_for(tp.self)
|
|
16
|
+
next unless constant_name
|
|
17
|
+
|
|
18
|
+
@captured[file] << constant_name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
trace.enable
|
|
22
|
+
yield
|
|
23
|
+
ensure
|
|
24
|
+
trace&.disable
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def constants_for(file)
|
|
28
|
+
Array(@captured[normalize(file)]).uniq
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def same_file?(target, candidate)
|
|
34
|
+
normalize(target) == normalize(candidate)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def normalize(file)
|
|
38
|
+
File.expand_path(file.to_s)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def qualified_name_for(target)
|
|
42
|
+
return nil unless target.respond_to?(:name)
|
|
43
|
+
|
|
44
|
+
name = target.name
|
|
45
|
+
return nil unless name && !name.empty? && !name.start_with?('#<')
|
|
46
|
+
|
|
47
|
+
name
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubycli
|
|
4
|
+
module Documentation
|
|
5
|
+
# Extracts contiguous comment blocks that appear immediately before a method.
|
|
6
|
+
class CommentExtractor
|
|
7
|
+
def initialize
|
|
8
|
+
@file_cache = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def extract(file, line_number)
|
|
12
|
+
return [] unless file && line_number
|
|
13
|
+
|
|
14
|
+
lines = cached_lines_for(file)
|
|
15
|
+
index = line_number - 2
|
|
16
|
+
block = []
|
|
17
|
+
|
|
18
|
+
while index >= 0
|
|
19
|
+
line = lines[index]
|
|
20
|
+
break unless comment_line?(line)
|
|
21
|
+
|
|
22
|
+
block << line
|
|
23
|
+
index -= 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
block.reverse.map { |line| strip_comment_prefix(line) }
|
|
27
|
+
rescue Errno::ENOENT
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reset!
|
|
32
|
+
@file_cache.clear
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def cached_lines_for(file)
|
|
38
|
+
@file_cache[file] ||= File.readlines(file, chomp: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def comment_line?(line)
|
|
42
|
+
return false unless line
|
|
43
|
+
|
|
44
|
+
line.lstrip.start_with?('#')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def strip_comment_prefix(line)
|
|
48
|
+
line.lstrip.sub(/^#/, '').lstrip
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|