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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc4daab3e3ba10d21d0545fab6a7ab99dc8ad125b3ca3acd3691326b164ba4ca
4
- data.tar.gz: 686ac2c2324893fbaabc650fe42858355f643eea90af8b6acff986207fb183ca
3
+ metadata.gz: e0e63fb09fa1cd1a483f34f9f535adca0ce5f543e975163584ecf43a509e4195
4
+ data.tar.gz: be17d3d95165270a73992dfa8a819704750d3b73b973a176f2d5c553a1934829
5
5
  SHA512:
6
- metadata.gz: 0cc8b4aadbde4cae477feb33d2578287d23dc77aea4700dae5115ee06d19e1234ba5625db2f3cd053bfa941e425cc50d0ac0c6836ffcb1b807b92f3aa3055470
7
- data.tar.gz: 55c6c0dd76ab840f11f73568de6fa5105dfd934b4602d1fb55bc9f506d4bd001b9bb890110fee0b68bc23ca6824547f33b757855fda126bca27dd72f656d6164
6
+ metadata.gz: 55dc3514760d66bf2c4a7edf8e06ba37947dc278efaf339ac8de24afc5c6f8ecfa4d651557351433763800327907ec4c25859c335350a95adcb2decad9cd313f
7
+ data.tar.gz: 3bda1e11790abbe1d2187961a1868ffcb1d255cf4d82a4f514e1a966bf72280fc34a0fe9f41303d7905024b092570220615120386940c7c429bad81ac5bd29ef
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cri (2.11.0)
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.9.0)
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.15)
48
+ yard (0.9.16)
49
49
 
50
50
  PLATFORMS
51
51
  ruby
data/NEWS.md CHANGED
@@ -1,6 +1,12 @@
1
1
  Cri News
2
2
  ========
3
3
 
4
+ ## 2.12.0
5
+
6
+ Features:
7
+
8
+ * Added support for parameter naming and validation (#70)
9
+
4
10
  ## 2.11.0
5
11
 
6
12
  Features:
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
@@ -87,9 +87,12 @@ module Cri
87
87
  attr_accessor :hidden
88
88
  alias hidden? hidden
89
89
 
90
- # @return [Array<Hash>] The list of option definitions
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 [Hash] The option definitions for the command itself and all its
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, global_option_definitions
319
+ opts_and_args,
320
+ global_option_definitions,
321
+ parameter_definitions,
316
322
  )
317
- handle_parser_errors_while { parser.run }
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
- opt_def = global_option_definitions.find { |o| (o[:long] || o[:short]) == key.to_s }
361
- block = opt_def[: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(opts_and_args, global_option_definitions)
377
+ parser = Cri::OptionParser.new(
378
+ opts_and_args,
379
+ global_option_definitions,
380
+ parameter_definitions,
381
+ )
372
382
  parser.delegate = delegate
373
- handle_parser_errors_while { parser.run }
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 handle_parser_errors_while
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
@@ -128,34 +128,27 @@ module Cri
128
128
  #
129
129
  # @return [void]
130
130
  def option(short, long, desc, params = {}, &block)
131
- requiredness = params.fetch(:argument, :forbidden)
132
- multiple = params.fetch(:multiple, false)
133
- hidden = params.fetch(:hidden, false)
134
- default = params.fetch(:default, nil)
135
- transform = params.fetch(:transform, nil)
136
-
137
- if short.nil? && long.nil?
138
- raise ArgumentError, 'short and long options cannot both be nil'
139
- end
140
-
141
- if default && requiredness == :forbidden
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
  #
@@ -106,18 +106,18 @@ module Cri
106
106
  end
107
107
  end
108
108
 
109
- def length_for_opt_defs(opt_defs)
110
- opt_defs.map do |opt_def|
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 opt_def[:long]
117
- string << ' --' + opt_def[:long]
116
+ if opt_defn.long
117
+ string << ' --' + opt_defn.long
118
118
  end
119
119
 
120
- case opt_def[:argument]
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 = length_for_opt_defs(groups.values.inject(&:+))
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[:short] || x[:long] }
151
- ordered_defs.reject { |opt_def| opt_def[:hidden] }.each do |opt_def|
152
- text << format_opt_def(opt_def, length)
153
- desc = opt_def[:desc] + (opt_def[:default] ? " (default: #{opt_def[:default]})" : '')
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(opt_def)
158
+ def short_value_postfix_for(opt_defn)
159
159
  value_postfix =
160
- case opt_def[:argument]
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
- opt_def[:long] ? '' : ' ' + value_postfix
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(opt_def)
174
+ def long_value_postfix_for(opt_defn)
175
175
  value_postfix =
176
- case opt_def[:argument]
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
- opt_def[:long] ? value_postfix : ''
184
+ opt_defn.long ? value_postfix : ''
185
185
  else
186
186
  ''
187
187
  end
188
188
  end
189
189
 
190
- def format_opt_def(opt_def, length)
191
- short_value_postfix = short_value_postfix_for(opt_def)
192
- long_value_postfix = long_value_postfix_for(opt_def)
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 opt_def[:short]
197
- opt_text << fmt.format_as_option('-' + opt_def[:short], @io)
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 + opt_def[:short].size + short_value_postfix.size + 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('--' + opt_def[:long], @io) if opt_def[:long]
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 + opt_def[:long].size if opt_def[:long]
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