cri 2.4.1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NmEyNjRhYjcyOTg2MzdiNWM1MDA5OWJiYTM0ZWNiMGIzYWM1ZjAxNw==
4
+ YTY4ZDU4NjdmNGI4NDNhM2UxMTA1YTA1MDBjZmEzYWZiNDgwOTY0ZA==
5
5
  data.tar.gz: !binary |-
6
- MGUyZjJjNjdkMGY2ZjAxYWZjYzdjMzgzMTAzOWZiMzgwZWYzYTkzOA==
6
+ MjkxMmY1ZDA1M2ExNDUxYmUxOTYyNjVjZTE3OWExM2Y1NmVhODg2Mg==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MjhlMjE2ODU5OTNiODMxZTY4ZGFlMzA3YjM0OGY3MDRiYmI2MzUwMjNhMTY1
10
- YmY2YTZkODk2NGEyNjYwMmZiOTM0ZjQ2ZDQ3YWE5NDIxMWNkZGZhY2UzNjVk
11
- MmJmNDI0ZjYwMDQ5ODYyNzE4N2QwNjQ3NzVlMTJmZmZlZTFlODg=
9
+ MTNjMmU4ZTViYjg2YzFiYjI5MjZiZTcyNzViNGYyYTEzZjdiMzY5MTc4Mzlk
10
+ MWIwYWNlNzI4Njk3Y2JlYTFkNDI4NDkyNGQ0YmVhMWE5Mzk1YjFhM2VjZDBm
11
+ ZjMxZmYzMDVkMzM2MGQ1OTVhYmI4NDUxMTFiNWEyZDU5MGQ5ZWE=
12
12
  data.tar.gz: !binary |-
13
- OGZjNzRmYzk5ZmYzMmVhZDlhOTcyM2IzOGY2OTZhOWQ5YmFmMzdjODZmYzk2
14
- MjhmMDkzOTQ3Yjc2Mzc4MjExMTg4ZjU4ZTM5N2YzNTgwZWViMTgxZTdjZjJl
15
- NTYxYWJjOGE5ZDM2MWY3NzQ2M2FjYWU0NjEyNGY3YmZmOGQ5MmI=
13
+ MmEwYjAzNDVhYzg0MWExM2MyODhmMzJlMDE1ZDkwY2U4Y2U2ZGFkNjYxZjk5
14
+ YjYxNGMxNTJlYjk5N2U0MDRkYzYyNDg3Mjc0NjlhODg5MDkyNGFmNWZlYTU2
15
+ YjRlY2EyNGU0NjlhYWM2NTMzZmE1MGU1MjRkMDRkNjZiODFiYjc=
@@ -1,16 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cri (2.4.1)
4
+ cri (2.5.0)
5
5
  colored (>= 1.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
10
  colored (1.2)
11
- minitest (5.0.6)
12
- rake (10.1.0)
13
- yard (0.8.7)
11
+ minitest (5.3.0)
12
+ rake (10.1.1)
13
+ yard (0.8.7.3)
14
14
 
15
15
  PLATFORMS
16
16
  ruby
data/NEWS.md CHANGED
@@ -1,6 +1,12 @@
1
1
  Cri News
2
2
  ========
3
3
 
4
+ 2.5.0
5
+ -----
6
+
7
+ * Made the default help command handle subcommands
8
+ * Added `#raw` method to argument arrays, returning all arguments including `--`
9
+
4
10
  2.4.1
5
11
  -----
6
12
 
@@ -0,0 +1,158 @@
1
+ = Cri =
2
+
3
+ Cri is a library for building easy-to-use commandline tools with support for
4
+ nested commands.
5
+
6
+ == Usage ==
7
+
8
+ The central concept in Cri is the _command_, which has option definitions as
9
+ well as code for actually executing itself. In Cri, the commandline tool
10
+ itself is a command as well.
11
+
12
+ Here’s a sample command definition:
13
+
14
+ [source,ruby]
15
+ --------------------------------------------------------------------------------
16
+ command = Cri::Command.define do
17
+ name 'dostuff'
18
+ usage 'dostuff [options]'
19
+ aliases :ds, :stuff
20
+ summary 'does stuff'
21
+ description 'This command does a lot of stuff. I really mean a lot.'
22
+
23
+ flag :h, :help, 'show help for this command' do |value, cmd|
24
+ puts cmd.help
25
+ exit 0
26
+ end
27
+ flag nil, :more, 'do even more stuff'
28
+ option :s, :stuff, 'specify stuff to do', :argument => :required
29
+
30
+ run do |opts, args, cmd|
31
+ stuff = opts.fetch(:stuff, 'generic stuff')
32
+ puts "Doing #{stuff}!"
33
+
34
+ if opts[:more]
35
+ puts 'Doing it even more!'
36
+ end
37
+ end
38
+ end
39
+ --------------------------------------------------------------------------------
40
+
41
+ To run this command, invoke the `#run` method with the raw arguments. For
42
+ example, for a root command (the commandline tool itself), the command could
43
+ be called like this:
44
+
45
+ [source,ruby]
46
+ --------------------------------------------------------------------------------
47
+ command.run(ARGV)
48
+ --------------------------------------------------------------------------------
49
+
50
+ Each command has automatically generated help. This help can be printed using
51
+ `Cri::Command#help`; something like this will be shown:
52
+
53
+ --------------------------------------------------------------------------------
54
+ usage: dostuff [options]
55
+
56
+ does stuff
57
+
58
+ This command does a lot of stuff. I really mean a lot.
59
+
60
+ options:
61
+
62
+ -h --help show help for this command
63
+ --more do even more stuff
64
+ -s --stuff specify stuff to do
65
+ --------------------------------------------------------------------------------
66
+
67
+ Let’s disect the command definition and start with the first five lines:
68
+
69
+ [source,ruby]
70
+ --------------------------------------------------------------------------------
71
+ name 'dostuff'
72
+ usage 'dostuff [options]'
73
+ aliases :ds, :stuff
74
+ summary 'does stuff'
75
+ description 'This command does a lot of stuff. I really mean a lot.'
76
+ --------------------------------------------------------------------------------
77
+
78
+ These lines of the command definition specify the name of the command (or the
79
+ commandline tool, if the command is the root command), the usage, a list of
80
+ aliases that can be used to call this command, a one-line summary and a (long)
81
+ description. The usage should not include a “usage:” prefix nor the name of
82
+ the supercommand, because the latter will be automatically prepended.
83
+
84
+ Aliases don’t make sense for root commands, but for subcommands they do.
85
+
86
+ The next few lines contain the command’s option definitions:
87
+
88
+ [source,ruby]
89
+ --------------------------------------------------------------------------------
90
+ flag :h, :help, 'show help for this command' do |value, cmd|
91
+ puts cmd.help
92
+ exit 0
93
+ end
94
+ flag nil, :more, 'do even more stuff'
95
+ option :s, :stuff, 'specify stuff to do', :argument => :required
96
+ --------------------------------------------------------------------------------
97
+
98
+ Options can be defined using the following methods:
99
+
100
+ * `Cri::CommandDSL#option` or `Cri::CommandDSL#opt`
101
+ * `Cri::CommandDSL#flag` (implies no arguments passed to option)
102
+ * `Cri::CommandDSL#required` (implies required argument)
103
+ * `Cri::CommandDSL#optional` (implies optional argument)
104
+
105
+ All these methods take the short option form as their first argument, and a
106
+ long option form as their second argument. Either the short or the long form
107
+ can be nil, but not both (because that would not make any sense). In the
108
+ example above, the `--more` option has no short form.
109
+
110
+ Each of the above methods also take a block, which will be executed when the
111
+ option is found. The argument to the block are the option value (`true` in
112
+ case the option does not have an argument) and the command.
113
+
114
+ The last part of the command defines the execution itself:
115
+
116
+ [source,ruby]
117
+ --------------------------------------------------------------------------------
118
+ run do |opts, args, cmd|
119
+ stuff = opts.fetch(:stuff, 'generic stuff')
120
+ puts "Doing #{stuff}!"
121
+
122
+ if opts[:more]
123
+ puts 'Doing it even more!'
124
+ end
125
+ end
126
+ --------------------------------------------------------------------------------
127
+
128
+ The +Cri::CommandDSL#run+ method takes a block with the actual code to
129
+ execute. This block takes three arguments: the options, any arguments passed
130
+ to the command, and the command itself.
131
+
132
+ Instead of defining a run block, it is possible to declare a class, the
133
+ _command runner_ class (`Cri::CommandRunner`) that will perform the actual
134
+ execution of the command. This makes it easier to break up large run blocks
135
+ into manageable pieces.
136
+
137
+ Commands can have subcommands. For example, the `git` commandline tool would be
138
+ represented by a command that has subcommands named `commit`, `add`, and so on.
139
+ Commands with subcommands do not use a run block; execution will always be
140
+ dispatched to a subcommand (or none, if no subcommand is found).
141
+
142
+ To add a command as a subcommand to another command, use the
143
+ `Cri::Command#add_command` method, like this:
144
+
145
+ [source,ruby]
146
+ --------------------------------------------------------------------------------
147
+ root_cmd.add_command(cmd_add)
148
+ root_cmd.add_command(cmd_commit)
149
+ root.cmd.add_command(cmd_init)
150
+ --------------------------------------------------------------------------------
151
+
152
+ == Contributors ==
153
+
154
+ * Toon Willems
155
+ * Ken Coar
156
+
157
+ Thanks for Lee “injekt” Jarvis for link:https://github.com/injekt/slop[Slop],
158
+ which has inspired the design of Cri 2.0.
@@ -19,12 +19,12 @@ Gem::Specification.new do |s|
19
19
  [ 'cri.gemspec' ]
20
20
  s.require_paths = [ 'lib' ]
21
21
 
22
- s.add_dependency('colored', '>= 1.2')
22
+ s.add_dependency('colored', '~> 1.2')
23
23
 
24
- s.add_development_dependency('rake')
25
- s.add_development_dependency('minitest')
26
- s.add_development_dependency('yard')
24
+ s.add_development_dependency('rake', '~> 10.1')
25
+ s.add_development_dependency('minitest', '~> 5.3')
26
+ s.add_development_dependency('yard', '~> 0.8')
27
27
 
28
- s.rdoc_options = [ '--main', 'README.md' ]
29
- s.extra_rdoc_files = [ 'LICENSE', 'README.md', 'NEWS.md' ]
28
+ s.rdoc_options = [ '--main', 'README.adoc' ]
29
+ s.extra_rdoc_files = [ 'LICENSE', 'README.adoc', 'NEWS.md' ]
30
30
  end
data/lib/cri.rb CHANGED
@@ -20,6 +20,7 @@ module Cri
20
20
  autoload 'Command', 'cri/command'
21
21
  autoload 'CommandDSL', 'cri/command_dsl'
22
22
  autoload 'CommandRunner', 'cri/command_runner'
23
+ autoload 'HelpRenderer', 'cri/help_renderer'
23
24
  autoload 'OptionParser', 'cri/option_parser'
24
25
 
25
26
  end
@@ -27,3 +28,4 @@ end
27
28
  require 'set'
28
29
 
29
30
  require 'cri/core_ext'
31
+ require 'cri/argument_array'
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ module Cri
4
+
5
+ # Represents an array of arguments. It is an array that strips separator
6
+ # arguments (`--`) but provides a `#raw` method to get the raw arguments
7
+ # array, i.e. an array that includes the separator `--` arguments.
8
+ class ArgumentArray < Array
9
+
10
+ # Initializes the array using the given raw arguments.
11
+ #
12
+ # @param [Array<String>] raw_arguments A list of raw arguments, i.e.
13
+ # including any separator arguments (`--`).
14
+ def initialize(raw_arguments)
15
+ super(raw_arguments.reject { |a| '--' == a })
16
+ @raw_arguments = raw_arguments
17
+ end
18
+
19
+ # @return [Array<String>] The arguments, including any separator arguments
20
+ # (`--`)
21
+ def raw
22
+ @raw_arguments
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -300,92 +300,7 @@ module Cri
300
300
 
301
301
  # @return [String] The help text for this command
302
302
  def help(params={})
303
- is_verbose = params.fetch(:verbose, false)
304
-
305
- text = ''
306
-
307
- # Append name and summary
308
- if summary
309
- text << "name".formatted_as_title << "\n"
310
- text << " #{name.formatted_as_command} - #{summary}" << "\n"
311
- unless aliases.empty?
312
- text << " aliases: " << aliases.map { |a| a.formatted_as_command }.join(' ') << "\n"
313
- end
314
- end
315
-
316
- # Append usage
317
- if usage
318
- path = [ self.supercommand ]
319
- path.unshift(path[0].supercommand) until path[0].nil?
320
- formatted_usage = usage.gsub(/^([^\s]+)/) { |m| m.formatted_as_command }
321
- full_usage = path[1..-1].map { |c| c.name.formatted_as_command + ' ' }.join + formatted_usage
322
-
323
- text << "\n"
324
- text << "usage".formatted_as_title << "\n"
325
- text << full_usage.wrap_and_indent(78, 4) << "\n"
326
- end
327
-
328
- # Append long description
329
- if description
330
- text << "\n"
331
- text << "description".formatted_as_title << "\n"
332
- text << description.wrap_and_indent(78, 4) + "\n"
333
- end
334
-
335
- # Append subcommands
336
- unless self.subcommands.empty?
337
- text << "\n"
338
- text << (self.supercommand ? 'subcommands' : 'commands').formatted_as_title
339
- text << "\n"
340
-
341
- shown_subcommands = self.subcommands.select { |c| !c.hidden? || is_verbose }
342
- length = shown_subcommands.map { |c| c.name.formatted_as_command.size }.max
343
-
344
- # Command
345
- shown_subcommands.sort_by { |cmd| cmd.name }.each do |cmd|
346
- text << sprintf(" %-#{length+4}s %s\n",
347
- cmd.name.formatted_as_command,
348
- cmd.summary)
349
- end
350
-
351
- # Hidden notice
352
- if !is_verbose
353
- diff = self.subcommands.size - shown_subcommands.size
354
- case diff
355
- when 0
356
- when 1
357
- text << " (1 hidden command omitted; show it with --verbose)\n"
358
- else
359
- text << " (#{diff} hidden commands omitted; show them with --verbose)\n"
360
- end
361
- end
362
- end
363
-
364
- # Append options
365
- groups = { 'options' => self.option_definitions }
366
- if self.supercommand
367
- groups["options for #{self.supercommand.name}"] = self.supercommand.global_option_definitions
368
- end
369
- length = groups.values.inject(&:+).map { |o| o[:long].to_s.size }.max
370
- groups.keys.sort.each do |name|
371
- defs = groups[name]
372
- unless defs.empty?
373
- text << "\n"
374
- text << "#{name}".formatted_as_title
375
- text << "\n"
376
- ordered_defs = defs.sort_by { |x| x[:short] || x[:long] }
377
- ordered_defs.each do |opt_def|
378
- text << sprintf(
379
- " %-2s %-#{length+6}s",
380
- opt_def[:short] ? ('-' + opt_def[:short]) : '',
381
- opt_def[:long] ? ('--' + opt_def[:long]) : '').formatted_as_option
382
-
383
- text << opt_def[:desc] << "\n"
384
- end
385
- end
386
- end
387
-
388
- text
303
+ HelpRenderer.new(self, params).render
389
304
  end
390
305
 
391
306
  # Compares this command's name to the other given command's name.
@@ -20,12 +20,8 @@ run do |opts, args, cmd|
20
20
 
21
21
  is_verbose = opts.fetch(:verbose, false)
22
22
 
23
- if args.empty?
24
- puts cmd.supercommand.help(:verbose => is_verbose)
25
- elsif args.size == 1
26
- puts cmd.supercommand.command_named(args[0]).help(:verbose => is_verbose)
27
- else
28
- $stderr.puts cmd.usage
29
- exit 1
23
+ resolved_cmd = args.inject(cmd.supercommand) do |acc, name|
24
+ acc.command_named(name)
30
25
  end
26
+ puts resolved_cmd.help(:verbose => is_verbose)
31
27
  end
@@ -0,0 +1,123 @@
1
+ # encoding: utf-8
2
+
3
+ module Cri
4
+
5
+ class HelpRenderer
6
+
7
+ def initialize(cmd, params={})
8
+ @cmd = cmd
9
+ @is_verbose = params.fetch(:verbose, false)
10
+ end
11
+
12
+ def render
13
+ text = ''
14
+
15
+ append_summary(text)
16
+ append_usage(text)
17
+ append_description(text)
18
+ append_subcommands(text)
19
+ append_options(text)
20
+
21
+ text
22
+ end
23
+
24
+ private
25
+
26
+ def append_summary(text)
27
+ return if @cmd.summary.nil?
28
+
29
+ text << "name".formatted_as_title << "\n"
30
+ text << " #{@cmd.name.formatted_as_command} - #{@cmd.summary}" << "\n"
31
+ unless @cmd.aliases.empty?
32
+ text << " aliases: " << @cmd.aliases.map { |a| a.formatted_as_command }.join(' ') << "\n"
33
+ end
34
+ end
35
+
36
+ def append_usage(text)
37
+ return if @cmd.usage.nil?
38
+
39
+ path = [ @cmd.supercommand ]
40
+ path.unshift(path[0].supercommand) until path[0].nil?
41
+ formatted_usage = @cmd.usage.gsub(/^([^\s]+)/) { |m| m.formatted_as_command }
42
+ full_usage = path[1..-1].map { |c| c.name.formatted_as_command + ' ' }.join + formatted_usage
43
+
44
+ text << "\n"
45
+ text << "usage".formatted_as_title << "\n"
46
+ text << full_usage.wrap_and_indent(78, 4) << "\n"
47
+ end
48
+
49
+ def append_description(text)
50
+ return if @cmd.description.nil?
51
+
52
+ text << "\n"
53
+ text << "description".formatted_as_title << "\n"
54
+ text << @cmd.description.wrap_and_indent(78, 4) + "\n"
55
+ end
56
+
57
+ def append_subcommands(text)
58
+ return if @cmd.subcommands.empty?
59
+
60
+ text << "\n"
61
+ text << (@cmd.supercommand ? 'subcommands' : 'commands').formatted_as_title
62
+ text << "\n"
63
+
64
+ shown_subcommands = @cmd.subcommands.select { |c| !c.hidden? || @is_verbose }
65
+ length = shown_subcommands.map { |c| c.name.formatted_as_command.size }.max
66
+
67
+ # Command
68
+ shown_subcommands.sort_by { |cmd| cmd.name }.each do |cmd|
69
+ text << sprintf(" %-#{length+4}s %s\n",
70
+ cmd.name.formatted_as_command,
71
+ cmd.summary)
72
+ end
73
+
74
+ # Hidden notice
75
+ if !@is_verbose
76
+ diff = @cmd.subcommands.size - shown_subcommands.size
77
+ case diff
78
+ when 0
79
+ when 1
80
+ text << " (1 hidden command omitted; show it with --verbose)\n"
81
+ else
82
+ text << " (#{diff} hidden commands omitted; show them with --verbose)\n"
83
+ end
84
+ end
85
+ end
86
+
87
+ def append_options(text)
88
+ groups = { 'options' => @cmd.option_definitions }
89
+ if @cmd.supercommand
90
+ groups["options for #{@cmd.supercommand.name}"] = @cmd.supercommand.global_option_definitions
91
+ end
92
+ length = groups.values.inject(&:+).map { |o| o[:long].to_s.size }.max
93
+ groups.keys.sort.each do |name|
94
+ defs = groups[name]
95
+ append_option_group(text, name, defs, length)
96
+ end
97
+ end
98
+
99
+ def append_option_group(text, name, defs, length)
100
+ return if defs.empty?
101
+
102
+ text << "\n"
103
+ text << "#{name}".formatted_as_title
104
+ text << "\n"
105
+
106
+ ordered_defs = defs.sort_by { |x| x[:short] || x[:long] }
107
+ ordered_defs.each do |opt_def|
108
+ text << format_opt_def(opt_def, length)
109
+ text << opt_def[:desc] << "\n"
110
+ end
111
+ end
112
+
113
+ def format_opt_def(opt_def, length)
114
+ opt_text = sprintf(
115
+ " %-2s %-#{length+6}s",
116
+ opt_def[:short] ? ('-' + opt_def[:short]) : '',
117
+ opt_def[:long] ? ('--' + opt_def[:long]) : '')
118
+ opt_text.formatted_as_option
119
+ end
120
+
121
+ end
122
+
123
+ end
@@ -84,14 +84,9 @@ module Cri
84
84
  # @return [Hash] The already parsed options.
85
85
  attr_reader :options
86
86
 
87
- # The arguments that have already been parsed.
88
- #
89
- # If the parser was stopped before it finished, this will not contain all
90
- # options and `unprocessed_arguments_and_options` will contain what is
91
- # left to be processed.
92
- #
93
- # @return [Array] The already parsed arguments.
94
- attr_reader :arguments
87
+ # @return [Array] The arguments that have already been parsed, including
88
+ # the -- separator.
89
+ attr_reader :raw_arguments
95
90
 
96
91
  # The options and arguments that have not yet been processed. If the
97
92
  # parser wasn’t stopped (using {#stop}), this list will be empty.
@@ -122,13 +117,24 @@ module Cri
122
117
  @unprocessed_arguments_and_options = arguments_and_options.dup
123
118
  @definitions = definitions
124
119
 
125
- @options = {}
126
- @arguments = []
120
+ @options = {}
121
+ @raw_arguments = []
127
122
 
128
123
  @running = false
129
124
  @no_more_options = false
130
125
  end
131
126
 
127
+ # Returns the arguments that have already been parsed.
128
+ #
129
+ # If the parser was stopped before it finished, this will not contain all
130
+ # options and `unprocessed_arguments_and_options` will contain what is
131
+ # left to be processed.
132
+ #
133
+ # @return [Array] The already parsed arguments.
134
+ def arguments
135
+ ArgumentArray.new(@raw_arguments).freeze
136
+ end
137
+
132
138
  # @return [Boolean] true if the parser is running, false otherwise.
133
139
  def running?
134
140
  @running
@@ -161,78 +167,12 @@ module Cri
161
167
  e = @unprocessed_arguments_and_options.shift
162
168
  break if e.nil?
163
169
 
164
- # Handle end-of-options marker
165
170
  if e == '--'
166
- @no_more_options = true
167
- # Handle incomplete options
171
+ handle_dashdash(e)
168
172
  elsif e =~ /^--./ and !@no_more_options
169
- # Get option key, and option value if included
170
- if e =~ /^--([^=]+)=(.+)$/
171
- option_key = $1
172
- option_value = $2
173
- else
174
- option_key = e[2..-1]
175
- option_value = nil
176
- end
177
-
178
- # Find definition
179
- definition = @definitions.find { |d| d[:long] == option_key }
180
- raise IllegalOptionError.new(option_key) if definition.nil?
181
-
182
- if [ :required, :optional ].include?(definition[:argument])
183
- # Get option value if necessary
184
- if option_value.nil?
185
- option_value = @unprocessed_arguments_and_options.shift
186
- if option_value.nil? || option_value =~ /^-/
187
- if definition[:argument] == :required
188
- raise OptionRequiresAnArgumentError.new(option_key)
189
- else
190
- @unprocessed_arguments_and_options.unshift(option_value)
191
- option_value = true
192
- end
193
- end
194
- end
195
-
196
- # Store option
197
- add_option(definition, option_value)
198
- else
199
- # Store option
200
- add_option(definition, true)
201
- end
202
- # Handle -xyz options
173
+ handle_dashdash_option(e)
203
174
  elsif e =~ /^-./ and !@no_more_options
204
- # Get option keys
205
- option_keys = e[1..-1].scan(/./)
206
-
207
- # For each key
208
- option_keys.each do |option_key|
209
- # Find definition
210
- definition = @definitions.find { |d| d[:short] == option_key }
211
- raise IllegalOptionError.new(option_key) if definition.nil?
212
-
213
- if option_keys.length > 1 and definition[:argument] == :required
214
- # This is a combined option and it requires an argument, so complain
215
- raise OptionRequiresAnArgumentError.new(option_key)
216
- elsif [ :required, :optional ].include?(definition[:argument])
217
- # Get option value
218
- option_value = @unprocessed_arguments_and_options.shift
219
- if option_value.nil? || option_value =~ /^-/
220
- if definition[:argument] == :required
221
- raise OptionRequiresAnArgumentError.new(option_key)
222
- else
223
- @unprocessed_arguments_and_options.unshift(option_value)
224
- option_value = true
225
- end
226
- end
227
-
228
- # Store option
229
- add_option(definition, option_value)
230
- else
231
- # Store option
232
- add_option(definition, true)
233
- end
234
- end
235
- # Handle normal arguments
175
+ handle_dash_option(e)
236
176
  else
237
177
  add_argument(e)
238
178
  end
@@ -244,6 +184,78 @@ module Cri
244
184
 
245
185
  private
246
186
 
187
+ def handle_dashdash(e)
188
+ add_argument(e)
189
+ @no_more_options = true
190
+ end
191
+
192
+ def handle_dashdash_option(e)
193
+ # Get option key, and option value if included
194
+ if e =~ /^--([^=]+)=(.+)$/
195
+ option_key = $1
196
+ option_value = $2
197
+ else
198
+ option_key = e[2..-1]
199
+ option_value = nil
200
+ end
201
+
202
+ # Find definition
203
+ definition = @definitions.find { |d| d[:long] == option_key }
204
+ raise IllegalOptionError.new(option_key) if definition.nil?
205
+
206
+ if [ :required, :optional ].include?(definition[:argument])
207
+ # Get option value if necessary
208
+ if option_value.nil?
209
+ option_value = find_option_value(definition, option_key)
210
+ end
211
+
212
+ # Store option
213
+ add_option(definition, option_value)
214
+ else
215
+ # Store option
216
+ add_option(definition, true)
217
+ end
218
+ end
219
+
220
+ def handle_dash_option(e)
221
+ # Get option keys
222
+ option_keys = e[1..-1].scan(/./)
223
+
224
+ # For each key
225
+ option_keys.each do |option_key|
226
+ # Find definition
227
+ definition = @definitions.find { |d| d[:short] == option_key }
228
+ raise IllegalOptionError.new(option_key) if definition.nil?
229
+
230
+ if option_keys.length > 1 and definition[:argument] == :required
231
+ # This is a combined option and it requires an argument, so complain
232
+ raise OptionRequiresAnArgumentError.new(option_key)
233
+ elsif [ :required, :optional ].include?(definition[:argument])
234
+ # Get option value
235
+ option_value = find_option_value(definition, option_key)
236
+
237
+ # Store option
238
+ add_option(definition, option_value)
239
+ else
240
+ # Store option
241
+ add_option(definition, true)
242
+ end
243
+ end
244
+ end
245
+
246
+ def find_option_value(definition, option_key)
247
+ option_value = @unprocessed_arguments_and_options.shift
248
+ if option_value.nil? || option_value =~ /^-/
249
+ if definition[:argument] == :required
250
+ raise OptionRequiresAnArgumentError.new(option_key)
251
+ else
252
+ @unprocessed_arguments_and_options.unshift(option_value)
253
+ option_value = true
254
+ end
255
+ end
256
+ option_value
257
+ end
258
+
247
259
  def add_option(definition, value)
248
260
  key = (definition[:long] || definition[:short]).to_sym
249
261
  options[key] = value
@@ -251,8 +263,11 @@ module Cri
251
263
  end
252
264
 
253
265
  def add_argument(value)
254
- arguments << value
255
- delegate.argument_added(value, self) unless delegate.nil?
266
+ @raw_arguments << value
267
+
268
+ unless '--' == value
269
+ delegate.argument_added(value, self) unless delegate.nil?
270
+ end
256
271
  end
257
272
 
258
273
  end
@@ -1,6 +1,6 @@
1
1
  module Cri
2
2
 
3
3
  # The current Cri version.
4
- VERSION = '2.4.1'
4
+ VERSION = '2.5.0'
5
5
 
6
6
  end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ class Cri::ArgumentArrayTestCase < Cri::TestCase
4
+
5
+ def test_initialize
6
+ arr = Cri::ArgumentArray.new([ 'foo', 'bar', '--', 'baz' ])
7
+ assert_equal [ 'foo', 'bar', 'baz' ], arr
8
+ assert_equal [ 'foo', 'bar', '--', 'baz' ], arr.raw
9
+ end
10
+
11
+ end
@@ -21,4 +21,47 @@ class Cri::BasicHelpTestCase < Cri::TestCase
21
21
  help_cmd.run([])
22
22
  end
23
23
 
24
+ def test_run_with_chain_of_commands
25
+ cmd = Cri::Command.define do
26
+ name 'root'
27
+ summary 'I am root!'
28
+
29
+ subcommand do
30
+ name 'foo'
31
+ summary 'I am foo!'
32
+
33
+ subcommand do
34
+ name 'subsubby'
35
+ summary 'I am subsubby!'
36
+ end
37
+ end
38
+ end
39
+
40
+ help_cmd = Cri::Command.new_basic_help
41
+ cmd.add_command(help_cmd)
42
+
43
+ # Simple call
44
+ stdout, stderr = capture_io_while do
45
+ help_cmd.run([ 'foo' ])
46
+ end
47
+ assert_match(/I am foo!/m, stdout)
48
+ assert_equal('', stderr)
49
+
50
+ # Subcommand
51
+ stdout, stderr = capture_io_while do
52
+ help_cmd.run([ 'foo', 'subsubby' ])
53
+ end
54
+ assert_match(/I am subsubby!/m, stdout)
55
+ assert_equal('', stderr)
56
+
57
+ # Non-existing subcommand
58
+ stdout, stderr = capture_io_while do
59
+ assert_raises SystemExit do
60
+ help_cmd.run([ 'foo', 'mysterycmd' ])
61
+ end
62
+ end
63
+ assert_equal '', stdout
64
+ assert_match(/foo: unknown command 'mysterycmd'/, stderr)
65
+ end
66
+
24
67
  end
@@ -424,4 +424,34 @@ class Cri::CommandTestCase < Cri::TestCase
424
424
  assert_match(pattern, cmd.help(:verbose => true))
425
425
  end
426
426
 
427
+ def test_run_with_raw_args
428
+ cmd = Cri::Command.define do
429
+ name 'moo'
430
+ run do |opts, args|
431
+ puts "args=#{args.join(',')} args.raw=#{args.raw.join(',')}"
432
+ end
433
+ end
434
+
435
+ out, err = capture_io_while do
436
+ cmd.run(%w( foo -- bar ))
437
+ end
438
+ assert_equal "args=foo,bar args.raw=foo,--,bar\n", out
439
+ end
440
+
441
+ def test_runner_with_raw_args
442
+ cmd = Cri::Command.define do
443
+ name 'moo'
444
+ runner(Class.new(Cri::CommandRunner) do
445
+ def run
446
+ puts "args=#{arguments.join(',')} args.raw=#{arguments.raw.join(',')}"
447
+ end
448
+ end)
449
+ end
450
+
451
+ out, err = capture_io_while do
452
+ cmd.run(%w( foo -- bar ))
453
+ end
454
+ assert_equal "args=foo,bar args.raw=foo,--,bar\n", out
455
+ end
456
+
427
457
  end
@@ -263,8 +263,9 @@ class Cri::OptionParserTestCase < Cri::TestCase
263
263
 
264
264
  parser = Cri::OptionParser.parse(input, definitions)
265
265
 
266
- assert_equal({}, parser.options)
267
- assert_equal([ 'foo', 'bar', '-x', '--yyy', '-abc' ], parser.arguments)
266
+ assert_equal({}, parser.options)
267
+ assert_equal([ 'foo', 'bar', '-x', '--yyy', '-abc' ], parser.arguments)
268
+ assert_equal([ 'foo', 'bar', '--', '-x', '--yyy', '-abc' ], parser.arguments.raw)
268
269
  end
269
270
 
270
271
  def test_parse_with_end_marker_between_option_key_and_value
metadata CHANGED
@@ -1,71 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cri
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.1
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Defreyne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-11-29 00:00:00.000000000 Z
11
+ date: 2014-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colored
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - ~>
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - ~>
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - ~>
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '10.1'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - ~>
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '10.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - ~>
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '5.3'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - ~>
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '5.3'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: yard
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ! '>='
59
+ - - ~>
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '0.8'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ! '>='
66
+ - - ~>
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: '0.8'
69
69
  description: Cri allows building easy-to-use commandline interfaces with support for
70
70
  subcommands.
71
71
  email: denis.defreyne@stoneship.org
@@ -73,26 +73,30 @@ executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files:
75
75
  - LICENSE
76
- - README.md
76
+ - README.adoc
77
77
  - NEWS.md
78
78
  files:
79
79
  - Gemfile
80
80
  - Gemfile.lock
81
81
  - LICENSE
82
82
  - NEWS.md
83
+ - README.adoc
83
84
  - Rakefile
84
- - README.md
85
+ - cri.gemspec
86
+ - lib/cri.rb
87
+ - lib/cri/argument_array.rb
85
88
  - lib/cri/command.rb
86
89
  - lib/cri/command_dsl.rb
87
90
  - lib/cri/command_runner.rb
88
91
  - lib/cri/commands/basic_help.rb
89
92
  - lib/cri/commands/basic_root.rb
90
- - lib/cri/core_ext/string.rb
91
93
  - lib/cri/core_ext.rb
94
+ - lib/cri/core_ext/string.rb
95
+ - lib/cri/help_renderer.rb
92
96
  - lib/cri/option_parser.rb
93
97
  - lib/cri/version.rb
94
- - lib/cri.rb
95
98
  - test/helper.rb
99
+ - test/test_argument_array.rb
96
100
  - test/test_base.rb
97
101
  - test/test_basic_help.rb
98
102
  - test/test_basic_root.rb
@@ -100,7 +104,6 @@ files:
100
104
  - test/test_command_dsl.rb
101
105
  - test/test_core_ext.rb
102
106
  - test/test_option_parser.rb
103
- - cri.gemspec
104
107
  homepage: http://stoneship.org/software/cri/
105
108
  licenses:
106
109
  - MIT
@@ -108,7 +111,7 @@ metadata: {}
108
111
  post_install_message:
109
112
  rdoc_options:
110
113
  - --main
111
- - README.md
114
+ - README.adoc
112
115
  require_paths:
113
116
  - lib
114
117
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -123,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
126
  version: '0'
124
127
  requirements: []
125
128
  rubyforge_project:
126
- rubygems_version: 2.1.11
129
+ rubygems_version: 2.2.2
127
130
  signing_key:
128
131
  specification_version: 4
129
132
  summary: a library for building easy-to-use commandline tools
data/README.md DELETED
@@ -1,138 +0,0 @@
1
- Cri
2
- ===
3
-
4
- Cri is a library for building easy-to-use commandline tools with support for
5
- nested commands.
6
-
7
- Usage
8
- -----
9
-
10
- The central concept in Cri is the _command_, which has option definitions as
11
- well as code for actually executing itself. In Cri, the commandline tool
12
- itself is a command as well.
13
-
14
- Here’s a sample command definition:
15
-
16
- command = Cri::Command.define do
17
- name 'dostuff'
18
- usage 'dostuff [options]'
19
- aliases :ds, :stuff
20
- summary 'does stuff'
21
- description 'This command does a lot of stuff. I really mean a lot.'
22
-
23
- flag :h, :help, 'show help for this command' do |value, cmd|
24
- puts cmd.help
25
- exit 0
26
- end
27
- flag nil, :more, 'do even more stuff'
28
- option :s, :stuff, 'specify stuff to do', :argument => :required
29
-
30
- run do |opts, args, cmd|
31
- stuff = opts[:stuff] || 'generic stuff'
32
- puts "Doing #{stuff}!"
33
-
34
- if opts[:more]
35
- puts 'Doing it even more!'
36
- end
37
- end
38
- end
39
-
40
- To run this command, invoke the `#run` method with the raw arguments. For
41
- example, for a root command (the commandline tool itself), the command could
42
- be called like this:
43
-
44
- command.run(ARGS)
45
-
46
- Each command has automatically generated help. This help can be printed using
47
- {Cri::Command#help}; something like this will be shown:
48
-
49
- usage: dostuff [options]
50
-
51
- does stuff
52
-
53
- This command does a lot of stuff. I really mean a lot.
54
-
55
- options:
56
-
57
- -h --help show help for this command
58
- --more do even more stuff
59
- -s --stuff specify stuff to do
60
-
61
- Let’s disect the command definition and start with the first five lines:
62
-
63
- name 'dostuff'
64
- usage 'dostuff [options]'
65
- aliases :ds, :stuff
66
- summary 'does stuff'
67
- description 'This command does a lot of stuff. I really mean a lot.'
68
-
69
- These lines of the command definition specify the name of the command (or the
70
- commandline tool, if the command is the root command), the usage, a list of
71
- aliases that can be used to call this command, a one-line summary and a (long)
72
- description. The usage should not include a “usage:” prefix nor the name of
73
- the supercommand, because the latter will be automatically prepended.
74
-
75
- Aliases don’t make sense for root commands, but for subcommands they do.
76
-
77
- The next few lines contain the command’s option definitions:
78
-
79
- flag :h, :help, 'show help for this command' do |value, cmd|
80
- puts cmd.help
81
- exit 0
82
- end
83
- flag nil, :more, 'do even more stuff'
84
- option :s, :stuff, 'specify stuff to do', :argument => :required
85
-
86
- Options can be defined using the following methods:
87
-
88
- * {Cri::CommandDSL#option} or {Cri::CommandDSL#opt}
89
- * {Cri::CommandDSL#flag} (implies forbidden argument)
90
- * {Cri::CommandDSL#required} (implies required argument)
91
- * {Cri::CommandDSL#optional} (implies optional argument)
92
-
93
- All these methods take the short option form as their first argument, and a
94
- long option form as their second argument. Either the short or the long form
95
- can be nil, but not both (because that would not make any sense). In the
96
- example above, the `--more` option has no short form.
97
-
98
- Each of the above methods also take a block, which will be executed when the
99
- option is found. The argument to the block are the option value (`true` in
100
- case the option does not have an argument) and the command.
101
-
102
- The last part of the command defines the execution itself:
103
-
104
- run do |opts, args, cmd|
105
- stuff = opts[:stuff] || 'generic stuff'
106
- puts "Doing #{stuff}!"
107
-
108
- if opts[:more]
109
- puts 'Doing it even more!'
110
- end
111
- end
112
-
113
- The {Cri::CommandDSL#run} method takes a block with the actual code to
114
- execute. This block takes three arguments: the options, any arguments passed
115
- to the command, and the command itself.
116
-
117
- Instead of defining a run block, it is possible to declare a class, the
118
- _command runner_ class ({Cri::CommandRunner}) that will perform the actual
119
- execution of the command. This makes it easier to break up large run blocks
120
- into manageable pieces.
121
-
122
- Commands can have subcommands. For example, the `git` commandline tool would be represented by a command that has subcommands named `commit`, `add`, and so on. Commands with subcommands do not use a run block; execution will always be dispatched to a subcommand (or none, if no subcommand is found).
123
-
124
- To add a command as a subcommand to another command, use the {Cri::Command#add_command} method, like this:
125
-
126
- root_cmd.add_command cmd_add
127
- root_cmd.add_command cmd_commit
128
- root.cmd.add_command cmd_init
129
-
130
- Contributors
131
- ------------
132
-
133
- * Toon Willems
134
- * Ken Coar
135
-
136
- Thanks for Lee “injekt” Jarvis for [Slop][1], which has inspired the design of Cri 2.0.
137
-
138
- [1]: https://github.com/injekt/slop