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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.ja.md +82 -14
- data/README.md +82 -14
- data/lib/rubycli/argument_mode_controller.rb +69 -0
- data/lib/rubycli/argument_parser.rb +287 -102
- data/lib/rubycli/arguments/token_stream.rb +41 -0
- data/lib/rubycli/arguments/value_converter.rb +85 -0
- data/lib/rubycli/cli.rb +14 -7
- data/lib/rubycli/command_line.rb +58 -6
- data/lib/rubycli/constant_capture.rb +50 -0
- data/lib/rubycli/documentation/comment_extractor.rb +52 -0
- data/lib/rubycli/documentation/metadata_parser.rb +973 -0
- data/lib/rubycli/documentation_registry.rb +11 -853
- data/lib/rubycli/environment.rb +50 -8
- data/lib/rubycli/eval_coercer.rb +16 -1
- data/lib/rubycli/help_renderer.rb +64 -9
- data/lib/rubycli/types.rb +2 -2
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +265 -121
- metadata +8 -2
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
|
|
@@ -48,6 +48,9 @@ module Rubycli
|
|
|
48
48
|
else
|
|
49
49
|
execute_method(entry.method, command, args, cli_mode)
|
|
50
50
|
end
|
|
51
|
+
rescue Rubycli::ArgumentError => e
|
|
52
|
+
warn "[ERROR] #{e.message}"
|
|
53
|
+
1
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
def available_commands(target)
|
|
@@ -105,7 +108,7 @@ module Rubycli
|
|
|
105
108
|
error_msg = "Command '#{command}' is not available."
|
|
106
109
|
puts error_msg
|
|
107
110
|
@help_renderer.print_help(target, catalog)
|
|
108
|
-
|
|
111
|
+
1
|
|
109
112
|
end
|
|
110
113
|
end
|
|
111
114
|
|
|
@@ -113,9 +116,11 @@ module Rubycli
|
|
|
113
116
|
method = target.method(:call)
|
|
114
117
|
pos_args, kw_args = @argument_parser.parse(args, method)
|
|
115
118
|
Rubycli.apply_argument_coercions(pos_args, kw_args)
|
|
119
|
+
@argument_parser.validate_inputs(method, pos_args, kw_args)
|
|
116
120
|
begin
|
|
117
121
|
result = Rubycli.call_target(target, pos_args, kw_args)
|
|
118
122
|
@result_emitter.emit(result)
|
|
123
|
+
0
|
|
119
124
|
rescue StandardError => e
|
|
120
125
|
handle_execution_error(e, command, method, pos_args, kw_args, cli_mode)
|
|
121
126
|
end
|
|
@@ -132,15 +137,16 @@ module Rubycli
|
|
|
132
137
|
def execute_parameterless_method(method_obj, command, args, cli_mode)
|
|
133
138
|
if help_requested_for_parameterless?(args)
|
|
134
139
|
puts usage_for_method(command, method_obj)
|
|
135
|
-
|
|
140
|
+
return 0
|
|
136
141
|
end
|
|
137
142
|
|
|
138
143
|
begin
|
|
139
144
|
result = method_obj.call
|
|
140
145
|
debug_log "Parameterless method returned: #{result.inspect}"
|
|
141
146
|
if result
|
|
142
|
-
run(result, args, false)
|
|
147
|
+
return run(result, args, false)
|
|
143
148
|
end
|
|
149
|
+
0
|
|
144
150
|
rescue StandardError => e
|
|
145
151
|
handle_execution_error(e, command, method_obj, [], {}, cli_mode)
|
|
146
152
|
end
|
|
@@ -149,15 +155,16 @@ module Rubycli
|
|
|
149
155
|
def execute_method_with_params(method_obj, command, args, cli_mode)
|
|
150
156
|
pos_args, kw_args = @argument_parser.parse(args, method_obj)
|
|
151
157
|
Rubycli.apply_argument_coercions(pos_args, kw_args)
|
|
152
|
-
|
|
153
158
|
if should_show_method_help?(pos_args, kw_args)
|
|
154
159
|
puts usage_for_method(command, method_obj)
|
|
155
|
-
|
|
160
|
+
return 0
|
|
156
161
|
end
|
|
162
|
+
@argument_parser.validate_inputs(method_obj, pos_args, kw_args)
|
|
157
163
|
|
|
158
164
|
begin
|
|
159
165
|
result = Rubycli.call_target(method_obj, pos_args, kw_args)
|
|
160
166
|
@result_emitter.emit(result)
|
|
167
|
+
0
|
|
161
168
|
rescue StandardError => e
|
|
162
169
|
handle_execution_error(e, command, method_obj, pos_args, kw_args, cli_mode)
|
|
163
170
|
end
|
|
@@ -181,7 +188,7 @@ module Rubycli
|
|
|
181
188
|
if cli_mode && !arguments_match?(method_obj, pos_args, kw_args) && usage_error?(error)
|
|
182
189
|
puts "Error: #{error.message}"
|
|
183
190
|
puts usage_for_method(command, method_obj)
|
|
184
|
-
|
|
191
|
+
1
|
|
185
192
|
else
|
|
186
193
|
raise error
|
|
187
194
|
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=<src>] [--json-args|-j | --eval-args|-e] <target-path> [<class-or-module>] [-- <cli-args>...]
|
|
6
|
+
Usage: rubycli [--new|-n] [--pre-script=<src>] [--json-args|-j | --eval-args|-e | --eval-lax|-E] [--strict] [--check|-c] <target-path> [<class-or-module>] [-- <cli-args>...]
|
|
7
7
|
|
|
8
8
|
Examples:
|
|
9
9
|
rubycli scripts/sample_runner.rb echo --message hello
|
|
@@ -15,7 +15,11 @@ module Rubycli
|
|
|
15
15
|
--pre-script=<src> Evaluate Ruby code and use its result as the exposed target (--init alias; also accepts space-separated form)
|
|
16
16
|
--json-args, -j Parse all following arguments strictly as JSON (no YAML literals)
|
|
17
17
|
--eval-args, -e Evaluate following arguments as Ruby code
|
|
18
|
-
|
|
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
|
+
--strict Enforce documented input types/choices (invalid values abort)
|
|
21
|
+
--check, -c Validate documentation/comments without executing commands
|
|
22
|
+
(Note: --json-args cannot be combined with --eval-args or --eval-lax)
|
|
19
23
|
(Note: Every option that accepts a value understands both --flag=value and --flag value forms.)
|
|
20
24
|
|
|
21
25
|
When <class-or-module> is omitted, Rubycli infers it from the file name in CamelCase.
|
|
@@ -38,6 +42,9 @@ module Rubycli
|
|
|
38
42
|
new_flag = false
|
|
39
43
|
json_mode = false
|
|
40
44
|
eval_mode = false
|
|
45
|
+
eval_lax_mode = false
|
|
46
|
+
constant_mode = nil
|
|
47
|
+
check_mode = false
|
|
41
48
|
pre_script_sources = []
|
|
42
49
|
|
|
43
50
|
loop do
|
|
@@ -60,7 +67,7 @@ module Rubycli
|
|
|
60
67
|
flag = args.shift
|
|
61
68
|
src = args.shift
|
|
62
69
|
unless src
|
|
63
|
-
warn "#{flag} requires a file path or inline Ruby code"
|
|
70
|
+
warn "[ERROR] #{flag} requires a file path or inline Ruby code"
|
|
64
71
|
return 1
|
|
65
72
|
end
|
|
66
73
|
context = File.file?(src) ? File.expand_path(src) : "(inline #{flag})"
|
|
@@ -71,8 +78,26 @@ module Rubycli
|
|
|
71
78
|
when '--eval-args', '-e'
|
|
72
79
|
eval_mode = true
|
|
73
80
|
args.shift
|
|
81
|
+
when '--eval-lax', '-E'
|
|
82
|
+
eval_mode = true
|
|
83
|
+
eval_lax_mode = true
|
|
84
|
+
args.shift
|
|
85
|
+
when '--strict'
|
|
86
|
+
Rubycli.environment.enable_strict_input!
|
|
87
|
+
args.shift
|
|
88
|
+
when '--check', '-c'
|
|
89
|
+
check_mode = true
|
|
90
|
+
Rubycli.environment.enable_doc_check!
|
|
91
|
+
args.shift
|
|
74
92
|
when '--print-result'
|
|
75
93
|
args.shift
|
|
94
|
+
when '--debug'
|
|
95
|
+
args.shift
|
|
96
|
+
warn "[ERROR] --debug flag has been removed; set RUBYCLI_DEBUG=true instead."
|
|
97
|
+
return 1
|
|
98
|
+
when '--auto-target', '-a'
|
|
99
|
+
constant_mode = :auto
|
|
100
|
+
args.shift
|
|
76
101
|
when '--'
|
|
77
102
|
args.shift
|
|
78
103
|
break
|
|
@@ -95,10 +120,32 @@ module Rubycli
|
|
|
95
120
|
args.shift if args.first == '--'
|
|
96
121
|
|
|
97
122
|
if json_mode && eval_mode
|
|
98
|
-
warn '--json-args
|
|
123
|
+
warn '[ERROR] --json-args cannot be combined with --eval-args or --eval-lax'
|
|
124
|
+
return 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if check_mode && (json_mode || eval_mode)
|
|
128
|
+
warn '[ERROR] --check cannot be combined with --json-args or --eval-args'
|
|
129
|
+
return 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if check_mode && !args.empty?
|
|
133
|
+
warn '[ERROR] --check does not accept command arguments'
|
|
99
134
|
return 1
|
|
100
135
|
end
|
|
101
136
|
|
|
137
|
+
Rubycli.environment.clear_documentation_issues!
|
|
138
|
+
|
|
139
|
+
if check_mode
|
|
140
|
+
return Rubycli::Runner.check(
|
|
141
|
+
target_path,
|
|
142
|
+
class_or_module,
|
|
143
|
+
new: new_flag,
|
|
144
|
+
pre_scripts: pre_script_sources,
|
|
145
|
+
constant_mode: constant_mode
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
102
149
|
Rubycli::Runner.execute(
|
|
103
150
|
target_path,
|
|
104
151
|
class_or_module,
|
|
@@ -106,12 +153,17 @@ module Rubycli
|
|
|
106
153
|
new: new_flag,
|
|
107
154
|
json: json_mode,
|
|
108
155
|
eval_args: eval_mode,
|
|
109
|
-
|
|
156
|
+
eval_lax: eval_lax_mode,
|
|
157
|
+
pre_scripts: pre_script_sources,
|
|
158
|
+
constant_mode: constant_mode
|
|
110
159
|
)
|
|
111
160
|
|
|
112
161
|
0
|
|
113
162
|
rescue Rubycli::Runner::PreScriptError => e
|
|
114
|
-
warn e.message
|
|
163
|
+
warn "[ERROR] #{e.message}"
|
|
164
|
+
1
|
|
165
|
+
rescue Rubycli::Runner::Error => e
|
|
166
|
+
warn "[ERROR] #{e.message}"
|
|
115
167
|
1
|
|
116
168
|
end
|
|
117
169
|
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
|