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
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'type_utils'
|
|
2
4
|
|
|
3
5
|
module Rubycli
|
|
4
6
|
class HelpRenderer
|
|
@@ -8,43 +10,42 @@ module Rubycli
|
|
|
8
10
|
@documentation_registry = documentation_registry
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
# +io+ is $stderr when the help is shown because a command failed, so that
|
|
14
|
+
# piping stdout to another program never mixes diagnostics into the payload.
|
|
15
|
+
def print_help(target, catalog, io: $stdout)
|
|
16
|
+
io.puts(help_text(target, catalog))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def help_text(_target, catalog)
|
|
20
|
+
lines = ["Usage: #{File.basename($PROGRAM_NAME)} COMMAND [arguments]", '']
|
|
14
21
|
|
|
15
22
|
instance_entries = catalog.entries_for(:instance)
|
|
16
23
|
class_entries = catalog.entries_for(:class)
|
|
17
24
|
groups = []
|
|
18
|
-
groups << { label:
|
|
19
|
-
groups << { label:
|
|
25
|
+
groups << { label: 'Instance methods', entries: instance_entries } unless instance_entries.empty?
|
|
26
|
+
groups << { label: 'Class methods', entries: class_entries } unless class_entries.empty?
|
|
20
27
|
|
|
21
|
-
if groups.empty?
|
|
22
|
-
puts "No commands available."
|
|
23
|
-
return
|
|
24
|
-
end
|
|
28
|
+
return (lines << 'No commands available.').join("\n") if groups.empty?
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
lines << 'Available commands:'
|
|
27
31
|
groups.each do |group|
|
|
28
|
-
|
|
32
|
+
lines << " #{group[:label]}:"
|
|
29
33
|
group[:entries].each do |entry|
|
|
30
34
|
description = method_description(entry.method)
|
|
31
35
|
line = " #{entry.command.ljust(20)}"
|
|
32
36
|
line += " #{description}" unless description.empty?
|
|
33
|
-
|
|
37
|
+
lines << line.rstrip
|
|
34
38
|
|
|
35
|
-
unless entry.aliases.empty?
|
|
36
|
-
puts " Aliases: #{entry.aliases.join(", ")}"
|
|
37
|
-
end
|
|
39
|
+
lines << " Aliases: #{entry.aliases.join(', ')}" unless entry.aliases.empty?
|
|
38
40
|
end
|
|
39
|
-
|
|
41
|
+
lines << '' unless group.equal?(groups.last)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
if catalog.duplicates.any?
|
|
43
|
-
puts "Methods with the same name can be invoked via instance::NAME / class::NAME."
|
|
44
|
-
end
|
|
44
|
+
lines << 'Methods with the same name can be invoked via instance::NAME / class::NAME.' if catalog.duplicates.any?
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
lines << ''
|
|
47
|
+
lines << "Detailed command help: #{File.basename($PROGRAM_NAME)} COMMAND help"
|
|
48
|
+
lines.join("\n")
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def method_description(method_obj)
|
|
@@ -53,7 +54,7 @@ module Rubycli
|
|
|
53
54
|
return summary if summary && !summary.empty?
|
|
54
55
|
|
|
55
56
|
params_str = format_method_parameters(method_obj.parameters, metadata)
|
|
56
|
-
params_str.empty? ?
|
|
57
|
+
params_str.empty? ? '(no arguments)' : params_str
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def usage_for_method(command, method)
|
|
@@ -69,17 +70,17 @@ module Rubycli
|
|
|
69
70
|
|
|
70
71
|
returns = metadata[:returns] || []
|
|
71
72
|
if returns.any?
|
|
72
|
-
usage_lines <<
|
|
73
|
-
usage_lines <<
|
|
73
|
+
usage_lines << '' unless usage_lines.last == ''
|
|
74
|
+
usage_lines << 'Return values:'
|
|
74
75
|
returns.each do |ret|
|
|
75
|
-
type_label = (ret.types || []).join(
|
|
76
|
+
type_label = (ret.types || []).join(' | ')
|
|
76
77
|
line = " #{type_label}"
|
|
77
78
|
line += " #{ret.description}" if ret.description && !ret.description.empty?
|
|
78
79
|
usage_lines << line
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
usage_lines.pop while usage_lines.last ==
|
|
83
|
+
usage_lines.pop while usage_lines.last == ''
|
|
83
84
|
usage_block = usage_lines.join("\n")
|
|
84
85
|
|
|
85
86
|
sections = []
|
|
@@ -100,7 +101,7 @@ module Rubycli
|
|
|
100
101
|
option_map = (metadata[:options] || []).each_with_object({}) { |opt, h| h[opt[:keyword]] = opt }
|
|
101
102
|
positional_map = metadata[:positionals_map] || {}
|
|
102
103
|
|
|
103
|
-
parameters.map
|
|
104
|
+
parameters.map do |type, name|
|
|
104
105
|
case type
|
|
105
106
|
when :req
|
|
106
107
|
positional_usage_token(type, name, positional_map[name])
|
|
@@ -122,19 +123,19 @@ module Rubycli
|
|
|
122
123
|
when :key
|
|
123
124
|
opt = option_map[name]
|
|
124
125
|
label = if opt
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
if opt.doc_format == :auto_generated
|
|
127
|
+
auto_generated_option_usage_label(name, opt)
|
|
128
|
+
else
|
|
129
|
+
option_flag_with_placeholder(opt)
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
"--#{name.to_s.tr('_', '-')}=<value>"
|
|
133
|
+
end
|
|
133
134
|
"[#{label}]"
|
|
134
|
-
when :keyrest then
|
|
135
|
-
else
|
|
135
|
+
when :keyrest then '[--<option>...]'
|
|
136
|
+
else ''
|
|
136
137
|
end
|
|
137
|
-
|
|
138
|
+
end.compact.reject(&:empty?).join(' ')
|
|
138
139
|
end
|
|
139
140
|
|
|
140
141
|
def auto_generated_option_usage_label(name, opt)
|
|
@@ -145,13 +146,13 @@ module Rubycli
|
|
|
145
146
|
formatted = if value_name && !value_name.to_s.strip.empty?
|
|
146
147
|
ensure_angle_bracket_placeholder(value_name)
|
|
147
148
|
else
|
|
148
|
-
|
|
149
|
+
'<value>'
|
|
149
150
|
end
|
|
150
151
|
"#{base_flag}=#{formatted}"
|
|
151
152
|
end
|
|
152
153
|
|
|
153
154
|
def render_positionals(positionals_in_order)
|
|
154
|
-
|
|
155
|
+
rows = positionals_in_order.map do |info|
|
|
155
156
|
definition = info[:definition]
|
|
156
157
|
label = info[:label]
|
|
157
158
|
type = type_display(definition)
|
|
@@ -162,7 +163,7 @@ module Rubycli
|
|
|
162
163
|
description_parts << default_text if default_text
|
|
163
164
|
[label, type, requirement, description_parts.join(' ')]
|
|
164
165
|
end
|
|
165
|
-
table_block(
|
|
166
|
+
table_block('Positional arguments:', rows)
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
def render_options(options, required_keywords)
|
|
@@ -176,7 +177,7 @@ module Rubycli
|
|
|
176
177
|
description_parts << default_text if default_text
|
|
177
178
|
[label, type, requirement, description_parts.join(' ').strip]
|
|
178
179
|
end
|
|
179
|
-
table_block(
|
|
180
|
+
table_block('Options:', rows)
|
|
180
181
|
end
|
|
181
182
|
|
|
182
183
|
def table_block(header, rows)
|
|
@@ -185,7 +186,7 @@ module Rubycli
|
|
|
185
186
|
cols = rows.transpose
|
|
186
187
|
widths = cols.map { |col| col.map { |value| value.to_s.length }.max || 0 }
|
|
187
188
|
|
|
188
|
-
lines = [
|
|
189
|
+
lines = ['', header]
|
|
189
190
|
rows.each do |row|
|
|
190
191
|
padded = row.each_with_index.map do |value, idx|
|
|
191
192
|
text = value.to_s
|
|
@@ -247,32 +248,33 @@ module Rubycli
|
|
|
247
248
|
end
|
|
248
249
|
|
|
249
250
|
def positional_requirement(kind)
|
|
250
|
-
|
|
251
|
+
%i[opt rest].include?(kind) ? 'optional' : 'required'
|
|
251
252
|
end
|
|
252
253
|
|
|
253
254
|
def positional_default(definition)
|
|
254
255
|
return nil unless definition
|
|
256
|
+
|
|
255
257
|
value = definition.default_value
|
|
256
258
|
return nil if value.nil? || value.to_s.empty?
|
|
257
259
|
|
|
258
260
|
"(default: #{value})"
|
|
259
261
|
end
|
|
260
262
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
def option_default(opt)
|
|
264
|
+
value = opt.default_value
|
|
265
|
+
return nil if value.nil? || value.to_s.empty?
|
|
264
266
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
"(default: #{value})"
|
|
268
|
+
end
|
|
267
269
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
def required_keyword_names(method)
|
|
271
|
+
method.parameters.select { |type, _| type == :keyreq }.map { |_, name| name }
|
|
272
|
+
end
|
|
271
273
|
|
|
272
274
|
def ordered_positionals(method, metadata)
|
|
273
275
|
positional_map = metadata[:positionals_map] || {}
|
|
274
276
|
method.parameters.each_with_object([]) do |(type, name), memo|
|
|
275
|
-
next unless %i[req opt].include?(type)
|
|
277
|
+
next unless %i[req opt rest].include?(type)
|
|
276
278
|
|
|
277
279
|
definition = positional_map[name]
|
|
278
280
|
label = display_label_for(definition, name)
|
|
@@ -296,8 +298,6 @@ module Rubycli
|
|
|
296
298
|
optional_placeholder(placeholder, definition, name)
|
|
297
299
|
when :rest
|
|
298
300
|
rest_placeholder(placeholder, definition, name)
|
|
299
|
-
else
|
|
300
|
-
nil
|
|
301
301
|
end
|
|
302
302
|
end
|
|
303
303
|
|
|
@@ -319,7 +319,10 @@ module Rubycli
|
|
|
319
319
|
|
|
320
320
|
def optional_placeholder(placeholder, definition, name)
|
|
321
321
|
unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
|
|
322
|
-
|
|
322
|
+
documented = placeholder.strip
|
|
323
|
+
return documented if documented.start_with?('[') && documented.end_with?(']')
|
|
324
|
+
|
|
325
|
+
return "[#{documented}]"
|
|
323
326
|
end
|
|
324
327
|
|
|
325
328
|
"[#{default_positional_label(definition, name, uppercase: true)}]"
|
|
@@ -327,7 +330,10 @@ module Rubycli
|
|
|
327
330
|
|
|
328
331
|
def rest_placeholder(placeholder, definition, name)
|
|
329
332
|
unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
|
|
330
|
-
|
|
333
|
+
documented = placeholder.strip
|
|
334
|
+
documented = documented[1..-2].strip if documented.start_with?('[') && documented.end_with?(']')
|
|
335
|
+
documented = "#{documented}..." unless documented.end_with?('...')
|
|
336
|
+
return "[#{documented}]"
|
|
331
337
|
end
|
|
332
338
|
|
|
333
339
|
base = default_positional_label(definition, name, uppercase: true)
|
|
@@ -344,8 +350,8 @@ module Rubycli
|
|
|
344
350
|
|
|
345
351
|
def option_flag_with_placeholder(opt)
|
|
346
352
|
flags = [opt.short, opt.long].compact
|
|
347
|
-
flags = ["--#{opt.keyword.to_s.tr(
|
|
348
|
-
flag_label = flags.join(
|
|
353
|
+
flags = ["--#{opt.keyword.to_s.tr('_', '-')}"] if flags.empty?
|
|
354
|
+
flag_label = flags.join(', ')
|
|
349
355
|
placeholder = option_value_placeholder(opt)
|
|
350
356
|
if placeholder
|
|
351
357
|
formatted = ensure_angle_bracket_placeholder(placeholder)
|
|
@@ -364,8 +370,7 @@ module Rubycli
|
|
|
364
370
|
return nil if opt.boolean_flag
|
|
365
371
|
return opt.value_name if opt.value_name && !opt.value_name.empty?
|
|
366
372
|
|
|
367
|
-
|
|
368
|
-
first_non_nil_type
|
|
373
|
+
opt.types&.find { |type| !nil_type?(type) && !boolean_type?(type) }
|
|
369
374
|
end
|
|
370
375
|
|
|
371
376
|
def auto_generated_placeholder?(placeholder, definition, name)
|
data/lib/rubycli/json_coercer.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Rubycli
|
|
2
4
|
class ResultEmitter
|
|
3
5
|
def initialize(environment:)
|
|
@@ -26,15 +28,23 @@ module Rubycli
|
|
|
26
28
|
when Array, Hash
|
|
27
29
|
JSON.pretty_generate(result)
|
|
28
30
|
else
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
format_convertible_result(result)
|
|
32
|
+
end
|
|
33
|
+
rescue JSON::GeneratorError, JSON::NestingError
|
|
34
|
+
result.inspect
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# to_h / to_ary are duck-typed: every Enumerable answers to_h, but Set,
|
|
38
|
+
# Range and Enumerator raise TypeError unless their elements are pairs.
|
|
39
|
+
def format_convertible_result(result)
|
|
40
|
+
if result.respond_to?(:to_h)
|
|
41
|
+
JSON.pretty_generate(result.to_h)
|
|
42
|
+
elsif result.respond_to?(:to_ary)
|
|
43
|
+
JSON.pretty_generate(result.to_ary)
|
|
44
|
+
else
|
|
45
|
+
result.inspect
|
|
36
46
|
end
|
|
37
|
-
rescue
|
|
47
|
+
rescue TypeError
|
|
38
48
|
result.inspect
|
|
39
49
|
end
|
|
40
50
|
end
|