cri 2.11.0 → 2.12.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/Gemfile.lock +3 -3
- data/NEWS.md +6 -0
- data/README.md +27 -0
- data/lib/cri.rb +3 -0
- data/lib/cri/argument_list.rb +73 -0
- data/lib/cri/command.rb +24 -11
- data/lib/cri/command_dsl.rb +18 -25
- data/lib/cri/help_renderer.rb +24 -24
- data/lib/cri/option_definition.rb +54 -0
- data/lib/cri/option_parser.rb +45 -63
- data/lib/cri/param_definition.rb +12 -0
- data/lib/cri/version.rb +1 -1
- data/test/test_argument_list.rb +79 -0
- data/test/test_command.rb +19 -2
- data/test/test_command_dsl.rb +32 -4
- data/test/test_option_parser.rb +257 -175
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0e63fb09fa1cd1a483f34f9f535adca0ce5f543e975163584ecf43a509e4195
|
4
|
+
data.tar.gz: be17d3d95165270a73992dfa8a819704750d3b73b973a176f2d5c553a1934829
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55dc3514760d66bf2c4a7edf8e06ba37947dc278efaf339ac8de24afc5c6f8ecfa4d651557351433763800327907ec4c25859c335350a95adcb2decad9cd313f
|
7
|
+
data.tar.gz: 3bda1e11790abbe1d2187961a1868ffcb1d255cf4d82a4f514e1a966bf72280fc34a0fe9f41303d7905024b092570220615120386940c7c429bad81ac5bd29ef
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cri (2.
|
4
|
+
cri (2.12.0)
|
5
5
|
colored (~> 1.2)
|
6
6
|
|
7
7
|
GEM
|
@@ -34,7 +34,7 @@ GEM
|
|
34
34
|
rainbow (>= 2.2.2, < 4.0)
|
35
35
|
ruby-progressbar (~> 1.7)
|
36
36
|
unicode-display_width (~> 1.0, >= 1.0.1)
|
37
|
-
ruby-progressbar (1.
|
37
|
+
ruby-progressbar (1.10.0)
|
38
38
|
simplecov (0.16.1)
|
39
39
|
docile (~> 1.1)
|
40
40
|
json (>= 1.8, < 3)
|
@@ -45,7 +45,7 @@ GEM
|
|
45
45
|
thor (0.19.4)
|
46
46
|
tins (1.16.3)
|
47
47
|
unicode-display_width (1.4.0)
|
48
|
-
yard (0.9.
|
48
|
+
yard (0.9.16)
|
49
49
|
|
50
50
|
PLATFORMS
|
51
51
|
ruby
|
data/NEWS.md
CHANGED
data/README.md
CHANGED
@@ -232,6 +232,33 @@ When executing this command with `dostuff --some=value -f yes`, the `opts` hash
|
|
232
232
|
that is passed to your `run` block will be empty and the `args` array will be
|
233
233
|
`["--some=value", "-f", "yes"]`.
|
234
234
|
|
235
|
+
### Argument parsing
|
236
|
+
|
237
|
+
Cri also supports parsing arguments, outside of options. To define the
|
238
|
+
parameters of a command, use `#param`, which takes a symbol containing the name
|
239
|
+
of the parameter. For example:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
command = Cri::Command.define do
|
243
|
+
name 'publish'
|
244
|
+
usage 'publish filename'
|
245
|
+
summary 'publishes the given file'
|
246
|
+
description 'This command does a lot of stuff, but not option parsing.'
|
247
|
+
|
248
|
+
flag :q, :quick, 'publish quicker'
|
249
|
+
param :filename
|
250
|
+
|
251
|
+
run do |opts, args, cmd|
|
252
|
+
puts "Publishing #{args[:filename]}…"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
The command in this example has one parameter named `filename`. This means that
|
258
|
+
the command takes a single argument, named `filename`.
|
259
|
+
|
260
|
+
(*Why the distinction between argument and parameter?* A parameter is a name, e.g. `filename`, while an argument is a value for a parameter, e.g. `kitten.jpg`.)
|
261
|
+
|
235
262
|
### The run block
|
236
263
|
|
237
264
|
The last part of the command defines the execution itself:
|
data/lib/cri.rb
CHANGED
@@ -23,10 +23,13 @@ require 'set'
|
|
23
23
|
require 'colored'
|
24
24
|
|
25
25
|
require_relative 'cri/version'
|
26
|
+
require_relative 'cri/argument_list'
|
26
27
|
require_relative 'cri/command'
|
27
28
|
require_relative 'cri/string_formatter'
|
28
29
|
require_relative 'cri/command_dsl'
|
29
30
|
require_relative 'cri/command_runner'
|
30
31
|
require_relative 'cri/help_renderer'
|
32
|
+
require_relative 'cri/option_definition'
|
31
33
|
require_relative 'cri/option_parser'
|
34
|
+
require_relative 'cri/param_definition'
|
32
35
|
require_relative 'cri/platform'
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cri
|
4
|
+
# A list of arguments, which can be indexed using either a number or a symbol.
|
5
|
+
class ArgumentList
|
6
|
+
# Error that will be raised when an incorrect number of arguments is given.
|
7
|
+
class ArgumentCountMismatchError < Cri::Error
|
8
|
+
def initialize(expected_count, actual_count)
|
9
|
+
@expected_count = expected_count
|
10
|
+
@actual_count = actual_count
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
"incorrect number of arguments given: expected #{@expected_count}, but got #{@actual_count}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
def initialize(raw_arguments, param_defns)
|
21
|
+
@raw_arguments = raw_arguments
|
22
|
+
@param_defns = param_defns
|
23
|
+
|
24
|
+
load
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](key)
|
28
|
+
case key
|
29
|
+
when Symbol
|
30
|
+
@arguments_hash[key]
|
31
|
+
when Integer
|
32
|
+
@arguments_array[key]
|
33
|
+
else
|
34
|
+
raise ArgumentError, "argument lists can be indexed using a Symbol or an Integer, but not a #{key.class}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def each
|
39
|
+
@arguments_array.each { |e| yield(e) }
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def method_missing(sym, *args, &block)
|
44
|
+
if @arguments_array.respond_to?(sym)
|
45
|
+
@arguments_array.send(sym, *args, &block)
|
46
|
+
else
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def respond_to_missing?(sym, include_private = false)
|
52
|
+
@arguments_array.respond_to?(sym) || super
|
53
|
+
end
|
54
|
+
|
55
|
+
def load
|
56
|
+
@arguments_array = @raw_arguments.reject { |a| a == '--' }.freeze
|
57
|
+
@arguments_hash = {}
|
58
|
+
|
59
|
+
if @param_defns.empty?
|
60
|
+
# For now, don’t check arguments when no parameter definitions are given.
|
61
|
+
return
|
62
|
+
end
|
63
|
+
|
64
|
+
if @arguments_array.size != @param_defns.size
|
65
|
+
raise ArgumentCountMismatchError.new(@param_defns.size, @arguments_array.size)
|
66
|
+
end
|
67
|
+
|
68
|
+
@arguments_array.zip(@param_defns).each do |(arg, param_defn)|
|
69
|
+
@arguments_hash[param_defn.name.to_sym] = arg
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/cri/command.rb
CHANGED
@@ -87,9 +87,12 @@ module Cri
|
|
87
87
|
attr_accessor :hidden
|
88
88
|
alias hidden? hidden
|
89
89
|
|
90
|
-
# @return [Array<
|
90
|
+
# @return [Array<Cri::OptionDefinition>] The list of option definitions
|
91
91
|
attr_accessor :option_definitions
|
92
92
|
|
93
|
+
# @return [Array<Hash>] The list of parameter definitions
|
94
|
+
attr_accessor :parameter_definitions
|
95
|
+
|
93
96
|
# @return [Proc] The block that should be executed when invoking this
|
94
97
|
# command (ignored for commands with subcommands)
|
95
98
|
attr_accessor :block
|
@@ -147,6 +150,7 @@ module Cri
|
|
147
150
|
@aliases = Set.new
|
148
151
|
@commands = Set.new
|
149
152
|
@option_definitions = Set.new
|
153
|
+
@parameter_definitions = []
|
150
154
|
@default_subcommand_name = nil
|
151
155
|
end
|
152
156
|
|
@@ -167,8 +171,8 @@ module Cri
|
|
167
171
|
self
|
168
172
|
end
|
169
173
|
|
170
|
-
# @return [
|
171
|
-
# ancestors
|
174
|
+
# @return [Enumerable<Cri::OptionDefinition>] The option definitions for the
|
175
|
+
# command itself and all its ancestors
|
172
176
|
def global_option_definitions
|
173
177
|
res = Set.new
|
174
178
|
res.merge(option_definitions)
|
@@ -312,12 +316,14 @@ module Cri
|
|
312
316
|
else
|
313
317
|
# Parse
|
314
318
|
parser = Cri::OptionParser.new(
|
315
|
-
opts_and_args,
|
319
|
+
opts_and_args,
|
320
|
+
global_option_definitions,
|
321
|
+
parameter_definitions,
|
316
322
|
)
|
317
|
-
|
323
|
+
handle_errors_while { parser.run }
|
318
324
|
local_opts = parser.options
|
319
325
|
global_opts = parent_opts.merge(parser.options)
|
320
|
-
args = parser.arguments
|
326
|
+
args = handle_errors_while { parser.arguments }
|
321
327
|
|
322
328
|
# Handle options
|
323
329
|
handle_options(local_opts)
|
@@ -357,8 +363,8 @@ module Cri
|
|
357
363
|
|
358
364
|
def handle_options(opts)
|
359
365
|
opts.each_pair do |key, value|
|
360
|
-
|
361
|
-
block =
|
366
|
+
opt_defn = global_option_definitions.find { |o| (o.long || o.short) == key.to_s }
|
367
|
+
block = opt_defn.block
|
362
368
|
block&.call(value, self)
|
363
369
|
end
|
364
370
|
end
|
@@ -368,9 +374,13 @@ module Cri
|
|
368
374
|
|
369
375
|
# Parse
|
370
376
|
delegate = Cri::Command::OptionParserPartitioningDelegate.new
|
371
|
-
parser = Cri::OptionParser.new(
|
377
|
+
parser = Cri::OptionParser.new(
|
378
|
+
opts_and_args,
|
379
|
+
global_option_definitions,
|
380
|
+
parameter_definitions,
|
381
|
+
)
|
372
382
|
parser.delegate = delegate
|
373
|
-
|
383
|
+
handle_errors_while { parser.run }
|
374
384
|
|
375
385
|
# Extract
|
376
386
|
[
|
@@ -380,7 +390,7 @@ module Cri
|
|
380
390
|
]
|
381
391
|
end
|
382
392
|
|
383
|
-
def
|
393
|
+
def handle_errors_while
|
384
394
|
yield
|
385
395
|
rescue Cri::OptionParser::IllegalOptionError => e
|
386
396
|
warn "#{name}: unrecognised option -- #{e}"
|
@@ -391,6 +401,9 @@ module Cri
|
|
391
401
|
rescue Cri::OptionParser::IllegalOptionValueError => e
|
392
402
|
warn "#{name}: #{e.message}"
|
393
403
|
raise CriExitException.new(is_error: true)
|
404
|
+
rescue Cri::ArgumentList::ArgumentCountMismatchError => e
|
405
|
+
warn "#{name}: #{e.message}"
|
406
|
+
raise CriExitException.new(is_error: true)
|
394
407
|
end
|
395
408
|
end
|
396
409
|
end
|
data/lib/cri/command_dsl.rb
CHANGED
@@ -128,34 +128,27 @@ module Cri
|
|
128
128
|
#
|
129
129
|
# @return [void]
|
130
130
|
def option(short, long, desc, params = {}, &block)
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
raise ArgumentError, 'a default value cannot be specified for flag options'
|
143
|
-
end
|
144
|
-
|
145
|
-
@command.option_definitions << {
|
146
|
-
short: short.nil? ? nil : short.to_s,
|
147
|
-
long: long.nil? ? nil : long.to_s,
|
148
|
-
desc: desc,
|
149
|
-
argument: requiredness,
|
150
|
-
multiple: multiple,
|
151
|
-
block: block,
|
152
|
-
hidden: hidden,
|
153
|
-
default: default,
|
154
|
-
transform: transform,
|
155
|
-
}
|
131
|
+
@command.option_definitions << Cri::OptionDefinition.new(
|
132
|
+
short: short&.to_s,
|
133
|
+
long: long&.to_s,
|
134
|
+
desc: desc,
|
135
|
+
argument: params.fetch(:argument, :forbidden),
|
136
|
+
multiple: params.fetch(:multiple, false),
|
137
|
+
block: block,
|
138
|
+
hidden: params.fetch(:hidden, false),
|
139
|
+
default: params.fetch(:default, nil),
|
140
|
+
transform: params.fetch(:transform, nil),
|
141
|
+
)
|
156
142
|
end
|
157
143
|
alias opt option
|
158
144
|
|
145
|
+
# Defines a new parameter for the command.
|
146
|
+
#
|
147
|
+
# @param [Symbol] name The name of the parameter
|
148
|
+
def param(name)
|
149
|
+
@command.parameter_definitions << Cri::ParamDefinition.new(name: name)
|
150
|
+
end
|
151
|
+
|
159
152
|
# Adds a new option with a required argument to the command. If a block is
|
160
153
|
# given, it will be executed when the option is successfully parsed.
|
161
154
|
#
|
data/lib/cri/help_renderer.rb
CHANGED
@@ -106,18 +106,18 @@ module Cri
|
|
106
106
|
end
|
107
107
|
end
|
108
108
|
|
109
|
-
def
|
110
|
-
|
109
|
+
def length_for_opt_defns(opt_defns)
|
110
|
+
opt_defns.map do |opt_defn|
|
111
111
|
string = +''
|
112
112
|
|
113
113
|
# Always pretend there is a short option
|
114
114
|
string << '-X'
|
115
115
|
|
116
|
-
if
|
117
|
-
string << ' --' +
|
116
|
+
if opt_defn.long
|
117
|
+
string << ' --' + opt_defn.long
|
118
118
|
end
|
119
119
|
|
120
|
-
case
|
120
|
+
case opt_defn.argument
|
121
121
|
when :required
|
122
122
|
string << '=<value>'
|
123
123
|
when :optional
|
@@ -133,7 +133,7 @@ module Cri
|
|
133
133
|
if @cmd.supercommand
|
134
134
|
groups["options for #{@cmd.supercommand.name}"] = @cmd.supercommand.global_option_definitions
|
135
135
|
end
|
136
|
-
length =
|
136
|
+
length = length_for_opt_defns(groups.values.inject(&:+))
|
137
137
|
groups.keys.sort.each do |name|
|
138
138
|
defs = groups[name]
|
139
139
|
append_option_group(text, name, defs, length)
|
@@ -147,17 +147,17 @@ module Cri
|
|
147
147
|
text << fmt.format_as_title(name.to_s, @io)
|
148
148
|
text << "\n"
|
149
149
|
|
150
|
-
ordered_defs = defs.sort_by { |x| x
|
151
|
-
ordered_defs.reject
|
152
|
-
text <<
|
153
|
-
desc =
|
150
|
+
ordered_defs = defs.sort_by { |x| x.short || x.long }
|
151
|
+
ordered_defs.reject(&:hidden).each do |opt_defn|
|
152
|
+
text << format_opt_defn(opt_defn, length)
|
153
|
+
desc = opt_defn.desc + (opt_defn.default ? " (default: #{opt_defn.default})" : '')
|
154
154
|
text << fmt.wrap_and_indent(desc, LINE_WIDTH, length + OPT_DESC_SPACING + DESC_INDENT, true) << "\n"
|
155
155
|
end
|
156
156
|
end
|
157
157
|
|
158
|
-
def short_value_postfix_for(
|
158
|
+
def short_value_postfix_for(opt_defn)
|
159
159
|
value_postfix =
|
160
|
-
case
|
160
|
+
case opt_defn.argument
|
161
161
|
when :required
|
162
162
|
'<value>'
|
163
163
|
when :optional
|
@@ -165,15 +165,15 @@ module Cri
|
|
165
165
|
end
|
166
166
|
|
167
167
|
if value_postfix
|
168
|
-
|
168
|
+
opt_defn.long ? '' : ' ' + value_postfix
|
169
169
|
else
|
170
170
|
''
|
171
171
|
end
|
172
172
|
end
|
173
173
|
|
174
|
-
def long_value_postfix_for(
|
174
|
+
def long_value_postfix_for(opt_defn)
|
175
175
|
value_postfix =
|
176
|
-
case
|
176
|
+
case opt_defn.argument
|
177
177
|
when :required
|
178
178
|
'=<value>'
|
179
179
|
when :optional
|
@@ -181,30 +181,30 @@ module Cri
|
|
181
181
|
end
|
182
182
|
|
183
183
|
if value_postfix
|
184
|
-
|
184
|
+
opt_defn.long ? value_postfix : ''
|
185
185
|
else
|
186
186
|
''
|
187
187
|
end
|
188
188
|
end
|
189
189
|
|
190
|
-
def
|
191
|
-
short_value_postfix = short_value_postfix_for(
|
192
|
-
long_value_postfix = long_value_postfix_for(
|
190
|
+
def format_opt_defn(opt_defn, length)
|
191
|
+
short_value_postfix = short_value_postfix_for(opt_defn)
|
192
|
+
long_value_postfix = long_value_postfix_for(opt_defn)
|
193
193
|
|
194
194
|
opt_text = +''
|
195
195
|
opt_text_len = 0
|
196
|
-
if
|
197
|
-
opt_text << fmt.format_as_option('-' +
|
196
|
+
if opt_defn.short
|
197
|
+
opt_text << fmt.format_as_option('-' + opt_defn.short, @io)
|
198
198
|
opt_text << short_value_postfix
|
199
199
|
opt_text << ' '
|
200
|
-
opt_text_len += 1 +
|
200
|
+
opt_text_len += 1 + opt_defn.short.size + short_value_postfix.size + 1
|
201
201
|
else
|
202
202
|
opt_text << ' '
|
203
203
|
opt_text_len += 3
|
204
204
|
end
|
205
|
-
opt_text << fmt.format_as_option('--' +
|
205
|
+
opt_text << fmt.format_as_option('--' + opt_defn.long, @io) if opt_defn.long
|
206
206
|
opt_text << long_value_postfix
|
207
|
-
opt_text_len += 2 +
|
207
|
+
opt_text_len += 2 + opt_defn.long.size if opt_defn.long
|
208
208
|
opt_text_len += long_value_postfix.size
|
209
209
|
|
210
210
|
' ' + opt_text + ' ' * (length + OPT_DESC_SPACING - opt_text_len)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cri
|
4
|
+
# The definition of an option.
|
5
|
+
class OptionDefinition
|
6
|
+
attr_reader :short
|
7
|
+
attr_reader :long
|
8
|
+
attr_reader :desc
|
9
|
+
attr_reader :argument
|
10
|
+
attr_reader :multiple
|
11
|
+
attr_reader :block
|
12
|
+
attr_reader :hidden
|
13
|
+
attr_reader :default
|
14
|
+
attr_reader :transform
|
15
|
+
|
16
|
+
def initialize(params = {})
|
17
|
+
@short = params.fetch(:short)
|
18
|
+
@long = params.fetch(:long)
|
19
|
+
@desc = params.fetch(:desc)
|
20
|
+
@argument = params.fetch(:argument)
|
21
|
+
@multiple = params.fetch(:multiple)
|
22
|
+
@block = params.fetch(:block)
|
23
|
+
@hidden = params.fetch(:hidden)
|
24
|
+
@default = params.fetch(:default)
|
25
|
+
@transform = params.fetch(:transform)
|
26
|
+
|
27
|
+
if @short.nil? && @long.nil?
|
28
|
+
raise ArgumentError, 'short and long options cannot both be nil'
|
29
|
+
end
|
30
|
+
|
31
|
+
if @default && @argument == :forbidden
|
32
|
+
raise ArgumentError, 'a default value cannot be specified for flag options'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_h
|
37
|
+
{
|
38
|
+
short: @short,
|
39
|
+
long: @long,
|
40
|
+
desc: @desc,
|
41
|
+
argument: @argument,
|
42
|
+
multiple: @multiple,
|
43
|
+
block: @block,
|
44
|
+
hidden: @hidden,
|
45
|
+
default: @default,
|
46
|
+
transform: @transform,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def formatted_name
|
51
|
+
@long ? '--' + @long : '-' + @short
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|