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
data/lib/rubycli/cli.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'set'
|
|
2
4
|
|
|
3
5
|
module Rubycli
|
|
@@ -76,6 +78,15 @@ module Rubycli
|
|
|
76
78
|
command_catalog(target)
|
|
77
79
|
end
|
|
78
80
|
|
|
81
|
+
# True when the method can be exposed as a CLI command. Rubycli::Runner uses
|
|
82
|
+
# this to lint the same methods the CLI would expose.
|
|
83
|
+
def exposable_method?(method_obj)
|
|
84
|
+
return false unless method_obj&.source_location
|
|
85
|
+
return false if accessor_generated_method?(method_obj)
|
|
86
|
+
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
79
90
|
private
|
|
80
91
|
|
|
81
92
|
def debug_log(message)
|
|
@@ -101,13 +112,12 @@ module Rubycli
|
|
|
101
112
|
|
|
102
113
|
def handle_missing_method(target, catalog, command, args, cli_mode)
|
|
103
114
|
if target.respond_to?(:call)
|
|
104
|
-
debug_log
|
|
115
|
+
debug_log 'Target is callable, treating as lambda/proc'
|
|
105
116
|
args.unshift(command)
|
|
106
117
|
execute_callable(target, args, command, cli_mode)
|
|
107
118
|
else
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@help_renderer.print_help(target, catalog)
|
|
119
|
+
warn "Command '#{command}' is not available."
|
|
120
|
+
@help_renderer.print_help(target, catalog, io: $stderr)
|
|
111
121
|
1
|
|
112
122
|
end
|
|
113
123
|
end
|
|
@@ -134,22 +144,15 @@ module Rubycli
|
|
|
134
144
|
end
|
|
135
145
|
end
|
|
136
146
|
|
|
137
|
-
def execute_parameterless_method(method_obj, command, args,
|
|
147
|
+
def execute_parameterless_method(method_obj, command, args, _cli_mode)
|
|
138
148
|
if help_requested_for_parameterless?(args)
|
|
139
149
|
puts usage_for_method(command, method_obj)
|
|
140
150
|
return 0
|
|
141
151
|
end
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if result
|
|
147
|
-
return run(result, args, false)
|
|
148
|
-
end
|
|
149
|
-
0
|
|
150
|
-
rescue StandardError => e
|
|
151
|
-
handle_execution_error(e, command, method_obj, [], {}, cli_mode)
|
|
152
|
-
end
|
|
153
|
+
warn "Command '#{command}' does not accept arguments."
|
|
154
|
+
warn usage_for_method(command, method_obj)
|
|
155
|
+
1
|
|
153
156
|
end
|
|
154
157
|
|
|
155
158
|
def execute_method_with_params(method_obj, command, args, cli_mode)
|
|
@@ -185,13 +188,11 @@ module Rubycli
|
|
|
185
188
|
end
|
|
186
189
|
|
|
187
190
|
def handle_execution_error(error, command, method_obj, pos_args, kw_args, cli_mode)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
raise error
|
|
194
|
-
end
|
|
191
|
+
raise error unless cli_mode && !arguments_match?(method_obj, pos_args, kw_args) && usage_error?(error)
|
|
192
|
+
|
|
193
|
+
warn "Error: #{error.message}"
|
|
194
|
+
warn usage_for_method(command, method_obj)
|
|
195
|
+
1
|
|
195
196
|
end
|
|
196
197
|
|
|
197
198
|
def usage_error?(error)
|
|
@@ -284,7 +285,9 @@ module Rubycli
|
|
|
284
285
|
|
|
285
286
|
def collect_class_methods(target)
|
|
286
287
|
klass = target.class
|
|
287
|
-
collect_method_map(klass.singleton_class.public_instance_methods(false))
|
|
288
|
+
collect_method_map(klass.singleton_class.public_instance_methods(false)) do |name|
|
|
289
|
+
safe_method_lookup(klass, name)
|
|
290
|
+
end
|
|
288
291
|
end
|
|
289
292
|
|
|
290
293
|
def collect_method_map(method_names)
|
|
@@ -302,13 +305,6 @@ module Rubycli
|
|
|
302
305
|
nil
|
|
303
306
|
end
|
|
304
307
|
|
|
305
|
-
def exposable_method?(method_obj)
|
|
306
|
-
return false unless method_obj&.source_location
|
|
307
|
-
return false if accessor_generated_method?(method_obj)
|
|
308
|
-
|
|
309
|
-
true
|
|
310
|
-
end
|
|
311
|
-
|
|
312
308
|
def build_alias_map(entries)
|
|
313
309
|
entries.each_with_object({}) do |entry, memo|
|
|
314
310
|
entry.all_commands.each do |command|
|
data/lib/rubycli/command_line.rb
CHANGED
|
@@ -6,20 +6,22 @@ module Rubycli
|
|
|
6
6
|
Usage: rubycli [--new|-n[=<value>]] [--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
|
-
rubycli
|
|
10
|
-
rubycli
|
|
11
|
-
rubycli --
|
|
12
|
-
rubycli --
|
|
9
|
+
rubycli path/to/greeter.rb greet Hanako
|
|
10
|
+
rubycli path/to/multi_runner.rb AlternateRunner greet --name Ruby
|
|
11
|
+
rubycli --auto-target path/to/mismatched_runner.rb greet Hanako
|
|
12
|
+
rubycli --new='["a","b"]' path/to/collection_runner.rb run --mode summary
|
|
13
|
+
rubycli --json-args --new='["a","b"]' path/to/collection_runner.rb run --mode '"summary"'
|
|
13
14
|
|
|
14
15
|
Options:
|
|
15
|
-
--new, -n [<value>] Instantiate the class/module before invoking CLI commands; optional
|
|
16
|
+
--new, -n [<value>] Instantiate the class/module before invoking CLI commands; one optional literal can follow and is bound to the constructor's first parameter (respects --json-args/--eval-args/--eval-lax; use --pre-script for several or keyword arguments)
|
|
16
17
|
--pre-script=<src> Evaluate Ruby code and use its result as the exposed target (--init alias; also accepts space-separated form)
|
|
17
|
-
--json-args, -j Parse all following arguments strictly as JSON (no YAML literals)
|
|
18
|
-
--eval-args, -e Evaluate following arguments as Ruby code
|
|
18
|
+
--json-args, -j Parse all following arguments strictly as JSON (no YAML literals; even plain words need quoting)
|
|
19
|
+
--eval-args, -e Evaluate following arguments as Ruby code (every argument must be valid Ruby)
|
|
19
20
|
--eval-lax, -E Evaluate as Ruby but fall back to raw strings when parsing fails
|
|
20
21
|
--auto-target, -a Auto-select the only callable constant when names don't match
|
|
21
22
|
--strict Enforce documented input types/choices (invalid values abort)
|
|
22
23
|
--check, -c Validate documentation/comments without executing commands
|
|
24
|
+
--help, -h Show this message (the bare word `help` works too)
|
|
23
25
|
(Note: --json-args cannot be combined with --eval-args or --eval-lax)
|
|
24
26
|
(Note: Every option that accepts a value understands both --flag=value and --flag value forms.)
|
|
25
27
|
|
|
@@ -29,90 +31,104 @@ module Rubycli
|
|
|
29
31
|
<cli-args> are forwarded to Rubycli unchanged.
|
|
30
32
|
USAGE
|
|
31
33
|
|
|
34
|
+
# Flags collected before the target path; forwarded to Rubycli::Runner.
|
|
35
|
+
Options = Struct.new(
|
|
36
|
+
:new_flag,
|
|
37
|
+
:new_args,
|
|
38
|
+
:json_mode,
|
|
39
|
+
:eval_mode,
|
|
40
|
+
:eval_lax,
|
|
41
|
+
:constant_mode,
|
|
42
|
+
:check_mode,
|
|
43
|
+
:pre_scripts,
|
|
44
|
+
keyword_init: true
|
|
45
|
+
) do
|
|
46
|
+
def self.defaults
|
|
47
|
+
new(
|
|
48
|
+
new_flag: false,
|
|
49
|
+
new_args: nil,
|
|
50
|
+
json_mode: false,
|
|
51
|
+
eval_mode: false,
|
|
52
|
+
eval_lax: false,
|
|
53
|
+
constant_mode: nil,
|
|
54
|
+
check_mode: false,
|
|
55
|
+
pre_scripts: []
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
32
60
|
module_function
|
|
33
61
|
|
|
34
62
|
def run(argv = ARGV)
|
|
63
|
+
previous_state = environment_state
|
|
35
64
|
args = Array(argv).dup
|
|
36
65
|
Rubycli.environment.enable_print_result!
|
|
66
|
+
return print_usage(1) if args.empty?
|
|
37
67
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
options = Options.defaults
|
|
69
|
+
status = parse_flags!(args, options)
|
|
70
|
+
return status if status
|
|
71
|
+
return print_usage(1) if args.empty?
|
|
42
72
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
eval_mode = false
|
|
47
|
-
eval_lax_mode = false
|
|
48
|
-
constant_mode = nil
|
|
49
|
-
check_mode = false
|
|
50
|
-
pre_script_sources = []
|
|
73
|
+
target_path, class_or_module = extract_target!(args)
|
|
74
|
+
status = reject_conflicting_flags(options, args)
|
|
75
|
+
return status if status
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
77
|
+
Rubycli.environment.clear_documentation_issues!
|
|
78
|
+
dispatch(target_path, class_or_module, args, options)
|
|
79
|
+
rescue Rubycli::Runner::Error => e
|
|
80
|
+
warn "[ERROR] #{e.message}"
|
|
81
|
+
1
|
|
82
|
+
ensure
|
|
83
|
+
restore_environment(previous_state)
|
|
84
|
+
end
|
|
55
85
|
|
|
56
|
-
|
|
86
|
+
# Consumes leading flags from +args+ and fills +options+.
|
|
87
|
+
# Returns an exit status when the CLI should stop, otherwise nil.
|
|
88
|
+
def parse_flags!(args, options)
|
|
89
|
+
until args.empty?
|
|
90
|
+
case args.first
|
|
57
91
|
when '-h', '--help', 'help'
|
|
58
|
-
|
|
59
|
-
return 0
|
|
92
|
+
return print_usage(0)
|
|
60
93
|
when /\A--new(?:=(.+))?\z/
|
|
61
|
-
|
|
62
|
-
new_args_expr = Regexp.last_match(1)
|
|
94
|
+
inline_value = Regexp.last_match(1)
|
|
63
95
|
args.shift
|
|
64
|
-
|
|
65
|
-
possible = args.first
|
|
66
|
-
if possible && !possible.start_with?('-') && likely_new_args_value?(possible)
|
|
67
|
-
new_args_expr = args.shift
|
|
68
|
-
end
|
|
69
|
-
end
|
|
96
|
+
consume_new_arguments!(args, options, inline_value)
|
|
70
97
|
when '-n'
|
|
71
|
-
new_flag = true
|
|
72
98
|
args.shift
|
|
73
|
-
|
|
74
|
-
if possible && !possible.start_with?('-') && likely_new_args_value?(possible)
|
|
75
|
-
new_args_expr = args.shift
|
|
76
|
-
end
|
|
99
|
+
consume_new_arguments!(args, options, nil)
|
|
77
100
|
when /\A--pre-script=(.+)\z/, /\A--init=(.+)\z/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
pre_script_sources << { value: expr, context: "(inline #{label})" }
|
|
101
|
+
flag = Regexp.last_match(0).start_with?('--pre-script') ? '--pre-script' : '--init'
|
|
102
|
+
options.pre_scripts << { value: Regexp.last_match(1), context: "(inline #{flag})" }
|
|
81
103
|
args.shift
|
|
82
104
|
when '--pre-script', '--init'
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
unless src
|
|
86
|
-
warn "[ERROR] #{flag} requires a file path or inline Ruby code"
|
|
87
|
-
return 1
|
|
88
|
-
end
|
|
89
|
-
context = File.file?(src) ? File.expand_path(src) : "(inline #{flag})"
|
|
90
|
-
pre_script_sources << { value: src, context: context }
|
|
105
|
+
status = consume_pre_script_source!(args, options)
|
|
106
|
+
return status if status
|
|
91
107
|
when '--json-args', '-j'
|
|
92
|
-
json_mode = true
|
|
108
|
+
options.json_mode = true
|
|
93
109
|
args.shift
|
|
94
110
|
when '--eval-args', '-e'
|
|
95
|
-
eval_mode = true
|
|
111
|
+
options.eval_mode = true
|
|
96
112
|
args.shift
|
|
97
113
|
when '--eval-lax', '-E'
|
|
98
|
-
eval_mode = true
|
|
99
|
-
|
|
114
|
+
options.eval_mode = true
|
|
115
|
+
options.eval_lax = true
|
|
100
116
|
args.shift
|
|
101
117
|
when '--strict'
|
|
102
118
|
Rubycli.environment.enable_strict_input!
|
|
103
119
|
args.shift
|
|
104
120
|
when '--check', '-c'
|
|
105
|
-
check_mode = true
|
|
121
|
+
options.check_mode = true
|
|
106
122
|
Rubycli.environment.enable_doc_check!
|
|
107
123
|
args.shift
|
|
108
124
|
when '--print-result'
|
|
109
125
|
args.shift
|
|
110
126
|
when '--debug'
|
|
111
127
|
args.shift
|
|
112
|
-
warn
|
|
128
|
+
warn '[ERROR] --debug flag has been removed; set RUBYCLI_DEBUG=true instead.'
|
|
113
129
|
return 1
|
|
114
130
|
when '--auto-target', '-a'
|
|
115
|
-
constant_mode = :auto
|
|
131
|
+
options.constant_mode = :auto
|
|
116
132
|
args.shift
|
|
117
133
|
when '--'
|
|
118
134
|
args.shift
|
|
@@ -122,67 +138,116 @@ module Rubycli
|
|
|
122
138
|
end
|
|
123
139
|
end
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def consume_new_arguments!(args, options, inline_value)
|
|
145
|
+
options.new_flag = true
|
|
146
|
+
options.new_args = inline_value
|
|
147
|
+
return unless inline_value.nil?
|
|
148
|
+
|
|
149
|
+
candidate = args.first
|
|
150
|
+
return unless candidate && !candidate.start_with?('-') && likely_new_args_value?(candidate)
|
|
151
|
+
|
|
152
|
+
options.new_args = args.shift
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def consume_pre_script_source!(args, options)
|
|
156
|
+
flag = args.shift
|
|
157
|
+
source = args.shift
|
|
158
|
+
unless source
|
|
159
|
+
warn "[ERROR] #{flag} requires a file path or inline Ruby code"
|
|
127
160
|
return 1
|
|
128
161
|
end
|
|
129
162
|
|
|
163
|
+
context = File.file?(source) ? File.expand_path(source) : "(inline #{flag})"
|
|
164
|
+
options.pre_scripts << { value: source, context: context }
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Removes the target path and the optional explicit constant from +args+.
|
|
169
|
+
def extract_target!(args)
|
|
130
170
|
target_path = args.shift
|
|
131
171
|
class_or_module = nil
|
|
132
|
-
|
|
133
|
-
if
|
|
134
|
-
class_or_module = args.shift
|
|
135
|
-
end
|
|
172
|
+
candidate = args.first
|
|
173
|
+
class_or_module = args.shift if candidate && candidate != '--' && candidate.match?(/\A[A-Z]/)
|
|
136
174
|
args.shift if args.first == '--'
|
|
175
|
+
[target_path, class_or_module]
|
|
176
|
+
end
|
|
137
177
|
|
|
138
|
-
|
|
178
|
+
# Returns an exit status when the flag combination is unusable, otherwise nil.
|
|
179
|
+
def reject_conflicting_flags(options, args)
|
|
180
|
+
if options.json_mode && options.eval_mode
|
|
139
181
|
warn '[ERROR] --json-args cannot be combined with --eval-args or --eval-lax'
|
|
140
182
|
return 1
|
|
141
183
|
end
|
|
142
184
|
|
|
143
|
-
|
|
185
|
+
return nil unless options.check_mode
|
|
186
|
+
|
|
187
|
+
if options.json_mode || options.eval_mode
|
|
144
188
|
warn '[ERROR] --check cannot be combined with --json-args or --eval-args'
|
|
145
189
|
return 1
|
|
146
190
|
end
|
|
147
191
|
|
|
148
|
-
|
|
192
|
+
unless args.empty?
|
|
149
193
|
warn '[ERROR] --check does not accept command arguments'
|
|
150
194
|
return 1
|
|
151
195
|
end
|
|
152
196
|
|
|
153
|
-
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
154
199
|
|
|
155
|
-
|
|
200
|
+
def dispatch(target_path, class_or_module, args, options)
|
|
201
|
+
if options.check_mode
|
|
156
202
|
return Rubycli::Runner.check(
|
|
157
203
|
target_path,
|
|
158
204
|
class_or_module,
|
|
159
|
-
new: new_flag,
|
|
160
|
-
new_args:
|
|
161
|
-
pre_scripts:
|
|
162
|
-
constant_mode: constant_mode
|
|
205
|
+
new: options.new_flag,
|
|
206
|
+
new_args: options.new_args,
|
|
207
|
+
pre_scripts: options.pre_scripts,
|
|
208
|
+
constant_mode: options.constant_mode
|
|
163
209
|
)
|
|
164
210
|
end
|
|
165
211
|
|
|
166
|
-
Rubycli::Runner.execute(
|
|
212
|
+
status = Rubycli::Runner.execute(
|
|
167
213
|
target_path,
|
|
168
214
|
class_or_module,
|
|
169
215
|
args,
|
|
170
|
-
new: new_flag,
|
|
171
|
-
new_args:
|
|
172
|
-
json: json_mode,
|
|
173
|
-
eval_args: eval_mode,
|
|
174
|
-
eval_lax:
|
|
175
|
-
pre_scripts:
|
|
176
|
-
constant_mode: constant_mode
|
|
216
|
+
new: options.new_flag,
|
|
217
|
+
new_args: options.new_args,
|
|
218
|
+
json: options.json_mode,
|
|
219
|
+
eval_args: options.eval_mode,
|
|
220
|
+
eval_lax: options.eval_lax,
|
|
221
|
+
pre_scripts: options.pre_scripts,
|
|
222
|
+
constant_mode: options.constant_mode
|
|
177
223
|
)
|
|
178
224
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
225
|
+
# The runner reports the CLI status; treat anything else as success so the
|
|
226
|
+
# process always exits with a defined status.
|
|
227
|
+
status.is_a?(Integer) ? status : 0
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Usage requested with --help goes to stdout; usage shown because the
|
|
231
|
+
# invocation was wrong is a diagnostic and belongs on stderr.
|
|
232
|
+
def print_usage(status)
|
|
233
|
+
(status.zero? ? $stdout : $stderr).puts(USAGE)
|
|
234
|
+
status
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def environment_state
|
|
238
|
+
environment = Rubycli.environment
|
|
239
|
+
{
|
|
240
|
+
doc_check: environment.doc_check_mode?,
|
|
241
|
+
strict_input: environment.strict_input?,
|
|
242
|
+
print_result: environment.print_result?
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def restore_environment(state)
|
|
247
|
+
environment = Rubycli.environment
|
|
248
|
+
state[:doc_check] ? environment.enable_doc_check! : environment.disable_doc_check!
|
|
249
|
+
state[:strict_input] ? environment.enable_strict_input! : environment.disable_strict_input!
|
|
250
|
+
state[:print_result] ? environment.enable_print_result! : environment.disable_print_result!
|
|
186
251
|
end
|
|
187
252
|
|
|
188
253
|
def likely_new_args_value?(token)
|