cri 2.10.1 → 2.11.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 +5 -5
- data/Gemfile +2 -0
- data/Gemfile.lock +24 -23
- data/NEWS.md +6 -0
- data/{README.adoc → README.md} +105 -64
- data/Rakefile +2 -0
- data/cri.gemspec +6 -5
- data/lib/cri.rb +11 -11
- data/lib/cri/command.rb +7 -2
- data/lib/cri/command_dsl.rb +8 -0
- data/lib/cri/command_runner.rb +2 -0
- data/lib/cri/commands/basic_help.rb +8 -6
- data/lib/cri/commands/basic_root.rb +2 -0
- data/lib/cri/help_renderer.rb +9 -7
- data/lib/cri/option_parser.rb +45 -15
- data/lib/cri/platform.rb +6 -0
- data/lib/cri/string_formatter.rb +18 -14
- data/lib/cri/version.rb +3 -1
- data/test/helper.rb +2 -0
- data/test/test_base.rb +2 -0
- data/test/test_basic_help.rb +2 -0
- data/test/test_basic_root.rb +2 -0
- data/test/test_command.rb +30 -6
- data/test/test_command_dsl.rb +18 -16
- data/test/test_command_runner.rb +2 -0
- data/test/test_help_renderer.rb +18 -16
- data/test/test_option_parser.rb +76 -2
- data/test/test_string_formatter.rb +2 -0
- metadata +7 -11
- data/lib/cri/argument_array.rb +0 -21
- data/lib/cri/core_ext.rb +0 -10
- data/lib/cri/core_ext/string.rb +0 -33
- data/test/test_argument_array.rb +0 -11
data/lib/cri.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# The namespace for Cri, a library for building easy-to-use command-line tools
|
4
4
|
# with support for nested commands.
|
@@ -16,17 +16,17 @@ module Cri
|
|
16
16
|
# command has no supercommand for which to show help.
|
17
17
|
class NoHelpAvailableError < Error
|
18
18
|
end
|
19
|
-
|
20
|
-
autoload 'Command', 'cri/command'
|
21
|
-
autoload 'StringFormatter', 'cri/string_formatter'
|
22
|
-
autoload 'CommandDSL', 'cri/command_dsl'
|
23
|
-
autoload 'CommandRunner', 'cri/command_runner'
|
24
|
-
autoload 'HelpRenderer', 'cri/help_renderer'
|
25
|
-
autoload 'OptionParser', 'cri/option_parser'
|
26
|
-
autoload 'Platform', 'cri/platform'
|
27
19
|
end
|
28
20
|
|
29
21
|
require 'set'
|
30
22
|
|
31
|
-
require '
|
32
|
-
|
23
|
+
require 'colored'
|
24
|
+
|
25
|
+
require_relative 'cri/version'
|
26
|
+
require_relative 'cri/command'
|
27
|
+
require_relative 'cri/string_formatter'
|
28
|
+
require_relative 'cri/command_dsl'
|
29
|
+
require_relative 'cri/command_runner'
|
30
|
+
require_relative 'cri/help_renderer'
|
31
|
+
require_relative 'cri/option_parser'
|
32
|
+
require_relative 'cri/platform'
|
data/lib/cri/command.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cri
|
2
4
|
# Cri::Command represents a command that can be executed on the command line.
|
3
5
|
# It is also used for the command-line tool itself.
|
@@ -357,7 +359,7 @@ module Cri
|
|
357
359
|
opts.each_pair do |key, value|
|
358
360
|
opt_def = global_option_definitions.find { |o| (o[:long] || o[:short]) == key.to_s }
|
359
361
|
block = opt_def[:block]
|
360
|
-
block
|
362
|
+
block&.call(value, self)
|
361
363
|
end
|
362
364
|
end
|
363
365
|
|
@@ -381,11 +383,14 @@ module Cri
|
|
381
383
|
def handle_parser_errors_while
|
382
384
|
yield
|
383
385
|
rescue Cri::OptionParser::IllegalOptionError => e
|
384
|
-
warn "#{name}:
|
386
|
+
warn "#{name}: unrecognised option -- #{e}"
|
385
387
|
raise CriExitException.new(is_error: true)
|
386
388
|
rescue Cri::OptionParser::OptionRequiresAnArgumentError => e
|
387
389
|
warn "#{name}: option requires an argument -- #{e}"
|
388
390
|
raise CriExitException.new(is_error: true)
|
391
|
+
rescue Cri::OptionParser::IllegalOptionValueError => e
|
392
|
+
warn "#{name}: #{e.message}"
|
393
|
+
raise CriExitException.new(is_error: true)
|
389
394
|
end
|
390
395
|
end
|
391
396
|
end
|
data/lib/cri/command_dsl.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cri
|
2
4
|
# The command DSL is a class that is used for building and modifying
|
3
5
|
# commands.
|
@@ -130,6 +132,7 @@ module Cri
|
|
130
132
|
multiple = params.fetch(:multiple, false)
|
131
133
|
hidden = params.fetch(:hidden, false)
|
132
134
|
default = params.fetch(:default, nil)
|
135
|
+
transform = params.fetch(:transform, nil)
|
133
136
|
|
134
137
|
if short.nil? && long.nil?
|
135
138
|
raise ArgumentError, 'short and long options cannot both be nil'
|
@@ -148,6 +151,7 @@ module Cri
|
|
148
151
|
block: block,
|
149
152
|
hidden: hidden,
|
150
153
|
default: default,
|
154
|
+
transform: transform,
|
151
155
|
}
|
152
156
|
end
|
153
157
|
alias opt option
|
@@ -169,6 +173,8 @@ module Cri
|
|
169
173
|
#
|
170
174
|
# @return [void]
|
171
175
|
#
|
176
|
+
# @deprecated
|
177
|
+
#
|
172
178
|
# @see {#option}
|
173
179
|
def required(short, long, desc, params = {}, &block)
|
174
180
|
params = params.merge(argument: :required)
|
@@ -216,6 +222,8 @@ module Cri
|
|
216
222
|
#
|
217
223
|
# @return [void]
|
218
224
|
#
|
225
|
+
# @deprecated
|
226
|
+
#
|
219
227
|
# @see {#option}
|
220
228
|
def optional(short, long, desc, params = {}, &block)
|
221
229
|
params = params.merge(argument: :optional)
|
data/lib/cri/command_runner.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
name 'help'
|
2
4
|
usage 'help [command_name]'
|
3
5
|
summary 'show help'
|
4
|
-
description
|
5
|
-
Show help for the given command, or show general help. When no command is
|
6
|
-
given, a list of available commands is displayed, as well as a list of global
|
7
|
-
command-line options. When a command is given, a command description, as well
|
8
|
-
as command-specific command-line options, are shown.
|
9
|
-
|
6
|
+
description <<~DESC
|
7
|
+
Show help for the given command, or show general help. When no command is
|
8
|
+
given, a list of available commands is displayed, as well as a list of global
|
9
|
+
command-line options. When a command is given, a command description, as well
|
10
|
+
as command-specific command-line options, are shown.
|
11
|
+
DESC
|
10
12
|
|
11
13
|
flag :v, :verbose, 'show more detailed help'
|
12
14
|
|
data/lib/cri/help_renderer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cri
|
2
4
|
# The {HelpRenderer} class is responsible for generating a string containing
|
3
5
|
# the help for a given command, intended to be printed on the command line.
|
@@ -25,7 +27,7 @@ module Cri
|
|
25
27
|
|
26
28
|
# @return [String] The help text for this command
|
27
29
|
def render
|
28
|
-
text = ''
|
30
|
+
text = +''
|
29
31
|
|
30
32
|
append_summary(text)
|
31
33
|
append_usage(text)
|
@@ -39,7 +41,7 @@ module Cri
|
|
39
41
|
private
|
40
42
|
|
41
43
|
def fmt
|
42
|
-
@
|
44
|
+
@fmt ||= Cri::StringFormatter.new
|
43
45
|
end
|
44
46
|
|
45
47
|
def append_summary(text)
|
@@ -87,9 +89,9 @@ module Cri
|
|
87
89
|
shown_subcommands.sort_by(&:name).each do |cmd|
|
88
90
|
text <<
|
89
91
|
format(
|
90
|
-
"
|
91
|
-
fmt.format_as_command(cmd.name, @io),
|
92
|
-
cmd.summary,
|
92
|
+
" %<name>-#{length + DESC_INDENT}s %<summary>s\n",
|
93
|
+
name: fmt.format_as_command(cmd.name, @io),
|
94
|
+
summary: cmd.summary,
|
93
95
|
)
|
94
96
|
end
|
95
97
|
|
@@ -106,7 +108,7 @@ module Cri
|
|
106
108
|
|
107
109
|
def length_for_opt_defs(opt_defs)
|
108
110
|
opt_defs.map do |opt_def|
|
109
|
-
string = ''
|
111
|
+
string = +''
|
110
112
|
|
111
113
|
# Always pretend there is a short option
|
112
114
|
string << '-X'
|
@@ -189,7 +191,7 @@ module Cri
|
|
189
191
|
short_value_postfix = short_value_postfix_for(opt_def)
|
190
192
|
long_value_postfix = long_value_postfix_for(opt_def)
|
191
193
|
|
192
|
-
opt_text = ''
|
194
|
+
opt_text = +''
|
193
195
|
opt_text_len = 0
|
194
196
|
if opt_def[:short]
|
195
197
|
opt_text << fmt.format_as_option('-' + opt_def[:short], @io)
|
data/lib/cri/option_parser.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cri
|
2
4
|
# Cri::OptionParser is used for parsing command-line options.
|
3
5
|
#
|
@@ -57,6 +59,23 @@ module Cri
|
|
57
59
|
class IllegalOptionError < Cri::Error
|
58
60
|
end
|
59
61
|
|
62
|
+
# Error that will be raised when an option with an invalid or
|
63
|
+
# non-transformable value is encountered.
|
64
|
+
class IllegalOptionValueError < Cri::Error
|
65
|
+
attr_reader :definition
|
66
|
+
attr_reader :value
|
67
|
+
|
68
|
+
def initialize(definition, value)
|
69
|
+
@definition = definition
|
70
|
+
@value = value
|
71
|
+
end
|
72
|
+
|
73
|
+
def message
|
74
|
+
name = @definition[:long] ? '--' + @definition[:long] : '-' + @definition[:short]
|
75
|
+
"invalid value #{value.inspect} for #{name} option"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
60
79
|
# Error that will be raised when an option without argument is
|
61
80
|
# encountered.
|
62
81
|
class OptionRequiresAnArgumentError < Cri::Error
|
@@ -128,7 +147,7 @@ module Cri
|
|
128
147
|
#
|
129
148
|
# @return [Array] The already parsed arguments.
|
130
149
|
def arguments
|
131
|
-
|
150
|
+
@raw_arguments.reject { |a| a == '--' }.freeze
|
132
151
|
end
|
133
152
|
|
134
153
|
# @return [Boolean] true if the parser is running, false otherwise.
|
@@ -187,18 +206,18 @@ module Cri
|
|
187
206
|
@definitions.each { |d| add_default_option(d) }
|
188
207
|
end
|
189
208
|
|
190
|
-
def handle_dashdash(
|
191
|
-
add_argument(
|
209
|
+
def handle_dashdash(elem)
|
210
|
+
add_argument(elem)
|
192
211
|
@no_more_options = true
|
193
212
|
end
|
194
213
|
|
195
|
-
def handle_dashdash_option(
|
214
|
+
def handle_dashdash_option(elem)
|
196
215
|
# Get option key, and option value if included
|
197
|
-
if
|
216
|
+
if elem =~ /^--([^=]+)=(.+)$/
|
198
217
|
option_key = Regexp.last_match[1]
|
199
218
|
option_value = Regexp.last_match[2]
|
200
219
|
else
|
201
|
-
option_key =
|
220
|
+
option_key = elem[2..-1]
|
202
221
|
option_value = nil
|
203
222
|
end
|
204
223
|
|
@@ -220,9 +239,9 @@ module Cri
|
|
220
239
|
end
|
221
240
|
end
|
222
241
|
|
223
|
-
def handle_dash_option(
|
242
|
+
def handle_dash_option(elem)
|
224
243
|
# Get option keys
|
225
|
-
option_keys =
|
244
|
+
option_keys = elem[1..-1].scan(/./)
|
226
245
|
|
227
246
|
# For each key
|
228
247
|
option_keys.each do |option_key|
|
@@ -258,9 +277,11 @@ module Cri
|
|
258
277
|
option_value
|
259
278
|
end
|
260
279
|
|
261
|
-
def add_option(definition, value)
|
280
|
+
def add_option(definition, value, transform: true)
|
262
281
|
key = key_for(definition)
|
263
282
|
|
283
|
+
value = transform ? transform_value(definition, value) : value
|
284
|
+
|
264
285
|
if definition[:multiple]
|
265
286
|
options[key] ||= []
|
266
287
|
options[key] << value
|
@@ -268,7 +289,7 @@ module Cri
|
|
268
289
|
options[key] = value
|
269
290
|
end
|
270
291
|
|
271
|
-
delegate
|
292
|
+
delegate&.option_added(key, value, self)
|
272
293
|
end
|
273
294
|
|
274
295
|
def add_default_option(definition)
|
@@ -278,11 +299,20 @@ module Cri
|
|
278
299
|
value = definition[:default]
|
279
300
|
return unless value
|
280
301
|
|
281
|
-
|
282
|
-
|
283
|
-
|
302
|
+
add_option(definition, value, transform: false)
|
303
|
+
end
|
304
|
+
|
305
|
+
def transform_value(definition, value)
|
306
|
+
transformer = definition[:transform]
|
307
|
+
|
308
|
+
if transformer
|
309
|
+
begin
|
310
|
+
transformer.call(value)
|
311
|
+
rescue StandardError
|
312
|
+
raise IllegalOptionValueError.new(definition, value)
|
313
|
+
end
|
284
314
|
else
|
285
|
-
|
315
|
+
value
|
286
316
|
end
|
287
317
|
end
|
288
318
|
|
@@ -294,7 +324,7 @@ module Cri
|
|
294
324
|
@raw_arguments << value
|
295
325
|
|
296
326
|
unless value == '--'
|
297
|
-
delegate
|
327
|
+
delegate&.argument_added(value, self)
|
298
328
|
end
|
299
329
|
end
|
300
330
|
end
|
data/lib/cri/platform.rb
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cri
|
4
|
+
# Provides tools to detect platform and environment configuration (e.g. is
|
5
|
+
# color support available?)
|
6
|
+
#
|
7
|
+
# @api private
|
2
8
|
module Platform
|
3
9
|
# @return [Boolean] true if the current platform is Windows, false
|
4
10
|
# otherwise.
|
data/lib/cri/string_formatter.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Cri
|
4
|
+
# Used for formatting strings (e.g. converting to paragraphs, wrapping,
|
5
|
+
# formatting as title)
|
6
|
+
#
|
7
|
+
# @api private
|
4
8
|
class StringFormatter
|
5
9
|
# Extracts individual paragraphs (separated by two newlines).
|
6
10
|
#
|
7
11
|
# @param [String] s The string to format
|
8
12
|
#
|
9
13
|
# @return [Array<String>] A list of paragraphs in the string
|
10
|
-
def to_paragraphs(
|
11
|
-
lines =
|
14
|
+
def to_paragraphs(str)
|
15
|
+
lines = str.scan(/([^\n]+\n|[^\n]*$)/).map { |l| l[0].strip }
|
12
16
|
|
13
17
|
paragraphs = [[]]
|
14
18
|
lines.each do |line|
|
@@ -36,11 +40,11 @@ module Cri
|
|
36
40
|
# line is already indented
|
37
41
|
#
|
38
42
|
# @return [String] The word-wrapped and indented string
|
39
|
-
def wrap_and_indent(
|
43
|
+
def wrap_and_indent(str, width, indentation, first_line_already_indented = false)
|
40
44
|
indented_width = width - indentation
|
41
45
|
indent = ' ' * indentation
|
42
46
|
# Split into paragraphs
|
43
|
-
paragraphs = to_paragraphs(
|
47
|
+
paragraphs = to_paragraphs(str)
|
44
48
|
|
45
49
|
# Wrap and indent each paragraph
|
46
50
|
text = paragraphs.map do |paragraph|
|
@@ -76,11 +80,11 @@ module Cri
|
|
76
80
|
#
|
77
81
|
# @return [String] The string, formatted to be used as a title in a section
|
78
82
|
# in the help
|
79
|
-
def format_as_title(
|
83
|
+
def format_as_title(str, io)
|
80
84
|
if Cri::Platform.color?(io)
|
81
|
-
|
85
|
+
str.upcase.red.bold
|
82
86
|
else
|
83
|
-
|
87
|
+
str.upcase
|
84
88
|
end
|
85
89
|
end
|
86
90
|
|
@@ -88,11 +92,11 @@ module Cri
|
|
88
92
|
#
|
89
93
|
# @return [String] The string, formatted to be used as the name of a command
|
90
94
|
# in the help
|
91
|
-
def format_as_command(
|
95
|
+
def format_as_command(str, io)
|
92
96
|
if Cri::Platform.color?(io)
|
93
|
-
|
97
|
+
str.green
|
94
98
|
else
|
95
|
-
|
99
|
+
str
|
96
100
|
end
|
97
101
|
end
|
98
102
|
|
@@ -100,11 +104,11 @@ module Cri
|
|
100
104
|
#
|
101
105
|
# @return [String] The string, formatted to be used as an option definition
|
102
106
|
# of a command in the help
|
103
|
-
def format_as_option(
|
107
|
+
def format_as_option(str, io)
|
104
108
|
if Cri::Platform.color?(io)
|
105
|
-
|
109
|
+
str.yellow
|
106
110
|
else
|
107
|
-
|
111
|
+
str
|
108
112
|
end
|
109
113
|
end
|
110
114
|
end
|
data/lib/cri/version.rb
CHANGED
data/test/helper.rb
CHANGED
data/test/test_base.rb
CHANGED
data/test/test_basic_help.rb
CHANGED
data/test/test_basic_root.rb
CHANGED
data/test/test_command.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'helper'
|
2
4
|
|
3
5
|
module Cri
|
@@ -16,6 +18,7 @@ module Cri
|
|
16
18
|
optional :c, :ccc, 'opt c'
|
17
19
|
flag :d, :ddd, 'opt d'
|
18
20
|
forbidden :e, :eee, 'opt e'
|
21
|
+
required :t, :transform, 'opt t', transform: method(:Integer)
|
19
22
|
|
20
23
|
run do |opts, args, c|
|
21
24
|
$stdout.puts "Awesome #{c.name}!"
|
@@ -175,7 +178,7 @@ module Cri
|
|
175
178
|
end
|
176
179
|
|
177
180
|
assert_equal [], lines(out)
|
178
|
-
assert_equal ['moo:
|
181
|
+
assert_equal ['moo: unrecognised option -- z'], lines(err)
|
179
182
|
end
|
180
183
|
|
181
184
|
def test_invoke_simple_with_illegal_opt_no_exit
|
@@ -184,7 +187,28 @@ module Cri
|
|
184
187
|
end
|
185
188
|
|
186
189
|
assert_equal [], lines(out)
|
187
|
-
assert_equal ['moo:
|
190
|
+
assert_equal ['moo: unrecognised option -- z'], lines(err)
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_invoke_simple_with_invalid_value_for_opt
|
194
|
+
out, err = capture_io_while do
|
195
|
+
err = assert_raises SystemExit do
|
196
|
+
simple_cmd.run(%w[-t nope])
|
197
|
+
end
|
198
|
+
assert_equal 1, err.status
|
199
|
+
end
|
200
|
+
|
201
|
+
assert_equal [], lines(out)
|
202
|
+
assert_equal ['moo: invalid value "nope" for --transform option'], lines(err)
|
203
|
+
end
|
204
|
+
|
205
|
+
def test_invoke_simple_with_invalid_value_for_opt_no_exit
|
206
|
+
out, err = capture_io_while do
|
207
|
+
simple_cmd.run(%w[-t nope], {}, hard_exit: false)
|
208
|
+
end
|
209
|
+
|
210
|
+
assert_equal [], lines(out)
|
211
|
+
assert_equal ['moo: invalid value "nope" for --transform option'], lines(err)
|
188
212
|
end
|
189
213
|
|
190
214
|
def test_invoke_simple_with_opt_with_block
|
@@ -568,14 +592,14 @@ module Cri
|
|
568
592
|
cmd = Cri::Command.define do
|
569
593
|
name 'moo'
|
570
594
|
run do |_opts, args|
|
571
|
-
puts "args=#{args.join(',')}
|
595
|
+
puts "args=#{args.join(',')}"
|
572
596
|
end
|
573
597
|
end
|
574
598
|
|
575
599
|
out, _err = capture_io_while do
|
576
600
|
cmd.run(%w[foo -- bar])
|
577
601
|
end
|
578
|
-
assert_equal "args=foo,bar
|
602
|
+
assert_equal "args=foo,bar\n", out
|
579
603
|
end
|
580
604
|
|
581
605
|
def test_run_without_block
|
@@ -593,7 +617,7 @@ module Cri
|
|
593
617
|
name 'moo'
|
594
618
|
runner(Class.new(Cri::CommandRunner) do
|
595
619
|
def run
|
596
|
-
puts "args=#{arguments.join(',')}
|
620
|
+
puts "args=#{arguments.join(',')}"
|
597
621
|
end
|
598
622
|
end)
|
599
623
|
end
|
@@ -601,7 +625,7 @@ module Cri
|
|
601
625
|
out, _err = capture_io_while do
|
602
626
|
cmd.run(%w[foo -- bar])
|
603
627
|
end
|
604
|
-
assert_equal "args=foo,bar
|
628
|
+
assert_equal "args=foo,bar\n", out
|
605
629
|
end
|
606
630
|
|
607
631
|
def test_compare
|