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 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