cri 2.10.1 → 2.11.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.
data/lib/cri.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'cri/version'
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 'cri/core_ext'
32
- require 'cri/argument_array'
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'
@@ -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.call(value, self) if 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}: illegal option -- #{e}"
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
@@ -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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Cri
2
4
  # A command runner is responsible for the execution of a command. Using it
3
5
  # is optional, but it is useful for commands whose execution block is large.
@@ -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 <<-EOS
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
- EOS
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
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  flag :h, :help, 'show help for this command' do |_value, cmd|
2
4
  puts cmd.help
3
5
  raise CriExitException.new(is_error: false)
@@ -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
- @_formatter ||= Cri::StringFormatter.new
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
- " %-#{length + DESC_INDENT}s %s\n",
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)
@@ -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
- ArgumentArray.new(@raw_arguments).freeze
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(e)
191
- add_argument(e)
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(e)
214
+ def handle_dashdash_option(elem)
196
215
  # Get option key, and option value if included
197
- if e =~ /^--([^=]+)=(.+)$/
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 = e[2..-1]
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(e)
242
+ def handle_dash_option(elem)
224
243
  # Get option keys
225
- option_keys = e[1..-1].scan(/./)
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.option_added(key, value, self) unless delegate.nil?
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
- if definition[:multiple]
282
- options[key] ||= []
283
- options[key] << value
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
- options[key] = value
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.argument_added(value, self) unless delegate.nil?
327
+ delegate&.argument_added(value, self)
298
328
  end
299
329
  end
300
330
  end
@@ -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.
@@ -1,14 +1,18 @@
1
- require 'colored'
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(s)
11
- lines = s.scan(/([^\n]+\n|[^\n]*$)/).map { |l| l[0].strip }
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(s, width, indentation, first_line_already_indented = false)
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(s)
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(s, io)
83
+ def format_as_title(str, io)
80
84
  if Cri::Platform.color?(io)
81
- s.upcase.red.bold
85
+ str.upcase.red.bold
82
86
  else
83
- s.upcase
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(s, io)
95
+ def format_as_command(str, io)
92
96
  if Cri::Platform.color?(io)
93
- s.green
97
+ str.green
94
98
  else
95
- s
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(s, io)
107
+ def format_as_option(str, io)
104
108
  if Cri::Platform.color?(io)
105
- s.yellow
109
+ str.yellow
106
110
  else
107
- s
111
+ str
108
112
  end
109
113
  end
110
114
  end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Cri
2
4
  # The current Cri version.
3
- VERSION = '2.10.1'.freeze
5
+ VERSION = '2.11.0'
4
6
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'coveralls'
2
4
  Coveralls.wear!
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'helper'
2
4
 
3
5
  module Cri
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'helper'
2
4
 
3
5
  module Cri
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'helper'
2
4
 
3
5
  module Cri
@@ -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: illegal option -- z'], lines(err)
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: illegal option -- z'], lines(err)
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(',')} args.raw=#{args.raw.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 args.raw=foo,--,bar\n", out
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(',')} args.raw=#{arguments.raw.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 args.raw=foo,--,bar\n", out
628
+ assert_equal "args=foo,bar\n", out
605
629
  end
606
630
 
607
631
  def test_compare