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