cri 2.10.1 → 2.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|