cri 1.0.1 → 2.0a1

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.
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ module Cri
4
+
5
+ # @todo Document
6
+ class CommandDSL
7
+
8
+ def initialize(command=nil)
9
+ @command = command || Cri::Command.new
10
+ end
11
+
12
+ # @todo Document
13
+ def command
14
+ @command
15
+ end
16
+
17
+ # @todo Document
18
+ def subcommand(cmd=nil, &block)
19
+ if cmd.nil?
20
+ cmd = Cri::Command.define(&block)
21
+ end
22
+
23
+ @command.add_command(cmd)
24
+ end
25
+
26
+ # @todo Document
27
+ def name(arg)
28
+ @command.name = arg
29
+ end
30
+
31
+ # @todo Document
32
+ def aliases(*args)
33
+ @command.aliases = args.flatten
34
+ end
35
+
36
+ # @todo Document
37
+ def summary(arg)
38
+ @command.short_desc = arg
39
+ end
40
+
41
+ # @todo Document
42
+ def description(arg)
43
+ @command.long_desc = arg
44
+ end
45
+
46
+ # @todo Document
47
+ def usage(arg)
48
+ @command.usage = arg
49
+ end
50
+
51
+ # @todo Document
52
+ def option(short, long, desc, params={}, &block)
53
+ requiredness = params[:argument] || :forbidden
54
+ self.add_option(short, long, desc, requiredness, block)
55
+ end
56
+ alias_method :opt, :option
57
+
58
+ # @todo Document
59
+ def required(short, long, desc, &block)
60
+ self.add_option(short, long, desc, :required, block)
61
+ end
62
+
63
+ # @todo Document
64
+ def flag(short, long, desc, &block)
65
+ self.add_option(short, long, desc, :forbidden, block)
66
+ end
67
+ alias_method :forbidden, :flag
68
+
69
+ # @todo Document
70
+ def optional(short, long, desc, &block)
71
+ self.add_option(short, long, desc, :optional, block)
72
+ end
73
+
74
+ # @todo Document
75
+ def run(&block)
76
+ if block.arity != 2
77
+ raise ArgumentError,
78
+ "The block given to Cri::Command#run expects exactly two args"
79
+ end
80
+
81
+ @command.block = block
82
+ end
83
+
84
+ protected
85
+
86
+ # @todo Document
87
+ def add_option(short, long, desc, argument, block)
88
+ @command.option_definitions << {
89
+ :short => short.to_s,
90
+ :long => long.to_s,
91
+ :desc => desc,
92
+ :argument => argument,
93
+ :block => block }
94
+ end
95
+
96
+ end
97
+
98
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ name 'help'
4
+ usage 'help [command_name]'
5
+ summary 'show help'
6
+ description <<-EOS
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
+ commandline options. When a command is given, a command description as well as
10
+ command-specific commandline options are shown.
11
+ EOS
12
+
13
+ run do |opts, args|
14
+ if args.empty?
15
+ puts self.supercommand.help
16
+ elsif args.size == 1
17
+ puts self.supercommand.command_named(args[0]).help
18
+ else
19
+ $stderr.puts self.usage
20
+ exit 1
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ option :h, :help, 'show help for this command' do |value|
4
+ puts self.help
5
+ exit 0
6
+ end
7
+
8
+ subcommand Cri::Command.new_basic_help
data/lib/cri/core_ext.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Cri::CoreExtensions
2
4
  end
3
5
 
@@ -1,7 +1,25 @@
1
+ # encoding: utf-8
2
+
1
3
  module Cri::CoreExtensions
2
4
 
3
5
  module String
4
6
 
7
+ # @todo Document
8
+ def to_paragraphs
9
+ lines = self.scan(/([^\n]+\n|[^\n]*$)/).map { |s| s[0].strip }
10
+
11
+ paragraphs = [ [] ]
12
+ lines.each do |line|
13
+ if line.empty?
14
+ paragraphs << []
15
+ else
16
+ paragraphs.last << line
17
+ end
18
+ end
19
+
20
+ paragraphs.reject { |p| p.empty? }.map { |p| p.join(' ') }
21
+ end
22
+
5
23
  # Word-wraps and indents the string.
6
24
  #
7
25
  # +width+:: The maximal width of each line. This also includes indentation,
@@ -10,7 +28,7 @@ module Cri::CoreExtensions
10
28
  # +indentation+:: The number of spaces to indent each wrapped line.
11
29
  def wrap_and_indent(width, indentation)
12
30
  # Split into paragraphs
13
- paragraphs = self.split("\n").map { |p| p.strip }.reject { |p| p == '' }
31
+ paragraphs = self.to_paragraphs
14
32
 
15
33
  # Wrap and indent each paragraph
16
34
  paragraphs.map do |paragraph|
@@ -1,17 +1,93 @@
1
+ # encoding: utf-8
2
+
1
3
  module Cri
2
4
 
3
5
  # Cri::OptionParser is used for parsing commandline options.
4
6
  class OptionParser
5
7
 
8
+ # Superclass for generic option parser errors.
9
+ class GenericError < StandardError
10
+ end
11
+
6
12
  # Error that will be raised when an unknown option is encountered.
7
- class IllegalOptionError < RuntimeError ; end
13
+ class IllegalOptionError < Cri::OptionParser::GenericError
14
+ end
8
15
 
9
16
  # Error that will be raised when an option without argument is
10
17
  # encountered.
11
- class OptionRequiresAnArgumentError < RuntimeError ; end
18
+ class OptionRequiresAnArgumentError < Cri::OptionParser::GenericError
19
+ end
20
+
21
+ # The delegate to which events will be sent. The following methods will
22
+ # be send to the delegate:
23
+ #
24
+ # * `option_added(key, value, cmd)`
25
+ # * `argument_added(argument, cmd)`
26
+ #
27
+ # @return [#option_added, #argument_added] The delegate
28
+ attr_accessor :delegate
29
+
30
+ # The options that have already been parsed.
31
+ #
32
+ # If the parser was stopped before it finished, this will not contain all
33
+ # options and `unprocessed_arguments_and_options` will contain what is
34
+ # left to be processed.
35
+ #
36
+ # @return [Hash] The already parsed options.
37
+ attr_reader :options
38
+
39
+ # The arguments that have already been parsed.
40
+ #
41
+ # If the parser was stopped before it finished, this will not contain all
42
+ # options and `unprocessed_arguments_and_options` will contain what is
43
+ # left to be processed.
44
+ #
45
+ # @return [Array] The already parsed arguments.
46
+ attr_reader :arguments
47
+
48
+ # The options and arguments that have not yet been processed. If the
49
+ # parser wasn’t stopped (using {#stop}), this list will be empty.
50
+ #
51
+ # @return [Array] The not yet parsed options and arguments.
52
+ attr_reader :unprocessed_arguments_and_options
53
+
54
+ # Parses the commandline arguments. See the instance `parse` method for
55
+ # details.
56
+ def self.parse(arguments_and_options, definitions)
57
+ self.new(arguments_and_options, definitions).run
58
+ end
12
59
 
13
- # Parses the commandline arguments in +arguments_and_options+, using the
14
- # commandline option definitions in +definitions+.
60
+ # Creates a new parser with the given options/arguments and definitions.
61
+ #
62
+ # @param [Array<String>] arguments_and_options An array containing the
63
+ # commandline arguments
64
+ #
65
+ # @param [Array<Hash>] definitions An array of option definitions
66
+ def initialize(arguments_and_options, definitions)
67
+ @unprocessed_arguments_and_options = arguments_and_options.dup
68
+ @definitions = definitions
69
+
70
+ @options = {}
71
+ @arguments = []
72
+
73
+ @running = false
74
+ @no_more_options = false
75
+ end
76
+
77
+ # @return [Boolean] true if the parser is running, false otherwise.
78
+ def running?
79
+ @running
80
+ end
81
+
82
+ # Stops the parser. The parser will finish its current parse cycle but
83
+ # will not start parsing new options and/or arguments.
84
+ #
85
+ # @return [void]
86
+ def stop
87
+ @running = false
88
+ end
89
+
90
+ # Parses the commandline arguments into options and arguments
15
91
  #
16
92
  # +arguments_and_options+ is an array of commandline arguments and
17
93
  # options. This will usually be +ARGV+.
@@ -85,27 +161,19 @@ module Cri
85
161
  # :name => 'luke'
86
162
  # }
87
163
  # }
88
- def self.parse(arguments_and_options, definitions)
89
- # Don't touch original argument
90
- unprocessed_arguments_and_options = arguments_and_options.dup
91
-
92
- # Initialize
93
- arguments = []
94
- options = {}
164
+ def run
165
+ @running = true
95
166
 
96
- # Determines whether we've passed the '--' marker or not
97
- no_more_options = false
98
-
99
- loop do
167
+ while running?
100
168
  # Get next item
101
- e = unprocessed_arguments_and_options.shift
169
+ e = @unprocessed_arguments_and_options.shift
102
170
  break if e.nil?
103
171
 
104
172
  # Handle end-of-options marker
105
173
  if e == '--'
106
- no_more_options = true
174
+ @no_more_options = true
107
175
  # Handle incomplete options
108
- elsif e =~ /^--./ and !no_more_options
176
+ elsif e =~ /^--./ and !@no_more_options
109
177
  # Get option key, and option value if included
110
178
  if e =~ /^--([^=]+)=(.+)$/
111
179
  option_key = $1
@@ -116,38 +184,38 @@ module Cri
116
184
  end
117
185
 
118
186
  # Find definition
119
- definition = definitions.find { |d| d[:long] == option_key }
187
+ definition = @definitions.find { |d| d[:long] == option_key }
120
188
  raise IllegalOptionError.new(option_key) if definition.nil?
121
189
 
122
190
  if [ :required, :optional ].include?(definition[:argument])
123
191
  # Get option value if necessary
124
192
  if option_value.nil?
125
- option_value = unprocessed_arguments_and_options.shift
193
+ option_value = @unprocessed_arguments_and_options.shift
126
194
  if option_value.nil? || option_value =~ /^-/
127
195
  if definition[:argument] == :required
128
196
  raise OptionRequiresAnArgumentError.new(option_key)
129
197
  else
130
- unprocessed_arguments_and_options.unshift(option_value)
198
+ @unprocessed_arguments_and_options.unshift(option_value)
131
199
  option_value = true
132
200
  end
133
201
  end
134
202
  end
135
203
 
136
204
  # Store option
137
- options[definition[:long].to_sym] = option_value
205
+ add_option(definition[:long].to_sym, option_value)
138
206
  else
139
207
  # Store option
140
- options[definition[:long].to_sym] = true
208
+ add_option(definition[:long].to_sym, true)
141
209
  end
142
210
  # Handle -xyz options
143
- elsif e =~ /^-./ and !no_more_options
211
+ elsif e =~ /^-./ and !@no_more_options
144
212
  # Get option keys
145
213
  option_keys = e[1..-1].scan(/./)
146
214
 
147
215
  # For each key
148
216
  option_keys.each do |option_key|
149
217
  # Find definition
150
- definition = definitions.find { |d| d[:short] == option_key }
218
+ definition = @definitions.find { |d| d[:short] == option_key }
151
219
  raise IllegalOptionError.new(option_key) if definition.nil?
152
220
 
153
221
  if option_keys.length > 1 and definition[:argument] == :required
@@ -155,30 +223,44 @@ module Cri
155
223
  raise OptionRequiresAnArgumentError.new(option_key)
156
224
  elsif [ :required, :optional ].include?(definition[:argument])
157
225
  # Get option value
158
- option_value = unprocessed_arguments_and_options.shift
226
+ option_value = @unprocessed_arguments_and_options.shift
159
227
  if option_value.nil? || option_value =~ /^-/
160
228
  if definition[:argument] == :required
161
229
  raise OptionRequiresAnArgumentError.new(option_key)
162
230
  else
163
- unprocessed_arguments_and_options.unshift(option_value)
231
+ @unprocessed_arguments_and_options.unshift(option_value)
164
232
  option_value = true
165
233
  end
166
234
  end
167
235
 
168
236
  # Store option
169
- options[definition[:long].to_sym] = option_value
237
+ add_option(definition[:long].to_sym, option_value)
170
238
  else
171
239
  # Store option
172
- options[definition[:long].to_sym] = true
240
+ add_option(definition[:long].to_sym, true)
173
241
  end
174
242
  end
175
243
  # Handle normal arguments
176
244
  else
177
- arguments << e
245
+ add_argument(e)
178
246
  end
179
247
  end
180
248
 
181
249
  { :options => options, :arguments => arguments }
250
+ ensure
251
+ @running = false
252
+ end
253
+
254
+ private
255
+
256
+ def add_option(key, value)
257
+ options[key] = value
258
+ delegate.option_added(key, value, self) unless delegate.nil?
259
+ end
260
+
261
+ def add_argument(value)
262
+ arguments << value
263
+ delegate.argument_added(value, self) unless delegate.nil?
182
264
  end
183
265
 
184
266
  end
data/test/helper.rb ADDED
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ require 'stringio'
4
+
5
+ class Cri::TestCase < MiniTest::Unit::TestCase
6
+
7
+ def capture_io_while(&block)
8
+ $orig_stdout = $stdout
9
+ $orig_stderr = $stderr
10
+
11
+ $stdout = StringIO.new
12
+ $stderr = StringIO.new
13
+
14
+ block.call
15
+
16
+ [ $stdout.string, $stderr.string ]
17
+ ensure
18
+ $stdout = $orig_stdout
19
+ $stderr = $orig_stderr
20
+ end
21
+
22
+ def lines(string)
23
+ string.scan(/^.*\n/).map { |s| s.chomp }
24
+ end
25
+
26
+ end
data/test/test_base.rb ADDED
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ class Cri::BaseTestCase < Cri::TestCase
4
+
5
+ def test_stub
6
+ end
7
+
8
+ end
@@ -0,0 +1,232 @@
1
+ # encoding: utf-8
2
+
3
+ class Cri::CommandTestCase < Cri::TestCase
4
+
5
+ def simple_cmd
6
+ Cri::Command.define do
7
+ name 'moo'
8
+ usage 'dunno whatever'
9
+ summary 'does stuff'
10
+ description 'This command does a lot of stuff.'
11
+
12
+ option :a, :aaa, 'opt a', :argument => :optional do |value|
13
+ $stdout.puts "#{name}:#{value}"
14
+ end
15
+ required :b, :bbb, 'opt b'
16
+ optional :c, :ccc, 'opt c'
17
+ flag :d, :ddd, 'opt d'
18
+ forbidden :e, :eee, 'opt e'
19
+
20
+ run do |opts, args|
21
+ $stdout.puts "Awesome #{name}!"
22
+
23
+ $stdout.puts args.join(',')
24
+
25
+ opts_strings = []
26
+ opts.each_pair { |k,v| opts_strings << "#{k}=#{v}" }
27
+ $stdout.puts opts_strings.join(',')
28
+ end
29
+ end
30
+ end
31
+
32
+ def nested_cmd
33
+ super_cmd = Cri::Command.define do
34
+ name 'super'
35
+ usage 'does something super'
36
+ summary 'does super stuff'
37
+ description 'This command does super stuff.'
38
+
39
+ option :a, :aaa, 'opt a', :argument => :optional do |value|
40
+ $stdout.puts "#{name}:#{value}"
41
+ end
42
+ required :b, :bbb, 'opt b'
43
+ optional :c, :ccc, 'opt c'
44
+ flag :d, :ddd, 'opt d'
45
+ forbidden :e, :eee, 'opt e'
46
+ end
47
+
48
+ super_cmd.define_command do
49
+ name 'sub'
50
+ aliases 'sup'
51
+ usage 'does something subby'
52
+ summary 'does subby stuff'
53
+ description 'This command does subby stuff.'
54
+
55
+ option :m, :mmm, 'opt m', :argument => :optional
56
+ required :n, :nnn, 'opt n'
57
+ optional :o, :ooo, 'opt o'
58
+ flag :p, :ppp, 'opt p'
59
+ forbidden :q, :qqq, 'opt q'
60
+
61
+ run do |opts, args|
62
+ $stdout.puts "Sub-awesome!"
63
+
64
+ $stdout.puts args.join(',')
65
+
66
+ opts_strings = []
67
+ opts.each_pair { |k,v| opts_strings << "#{k}=#{v}" }
68
+ $stdout.puts opts_strings.join(',')
69
+ end
70
+ end
71
+
72
+ super_cmd.define_command do
73
+ name 'sink'
74
+ usage 'sink thing_to_sink'
75
+ summary 'sinks stuff'
76
+ description 'Sinks stuff (like ships and the like).'
77
+
78
+ run do |opts, args|
79
+ $stdout.puts "Sinking!"
80
+ end
81
+ end
82
+
83
+ super_cmd
84
+ end
85
+
86
+ def test_invoke_simple_without_opts_or_args
87
+ out, err = capture_io_while do
88
+ simple_cmd.run(%w())
89
+ end
90
+
91
+ assert_equal [ 'Awesome moo!', '', '' ], lines(out)
92
+ assert_equal [], lines(err)
93
+ end
94
+
95
+ def test_invoke_simple_with_args
96
+ out, err = capture_io_while do
97
+ simple_cmd.run(%w(abc xyz))
98
+ end
99
+
100
+ assert_equal [ 'Awesome moo!', 'abc,xyz', '' ], lines(out)
101
+ assert_equal [], lines(err)
102
+ end
103
+
104
+ def test_invoke_simple_with_opts
105
+ out, err = capture_io_while do
106
+ simple_cmd.run(%w(-c -b x))
107
+ end
108
+
109
+ assert_equal [ 'Awesome moo!', '', 'ccc=true,bbb=x' ], lines(out)
110
+ assert_equal [], lines(err)
111
+ end
112
+
113
+ def test_invoke_simple_with_missing_opt_arg
114
+ out, err = capture_io_while do
115
+ assert_raises SystemExit do
116
+ simple_cmd.run(%w( -b ))
117
+ end
118
+ end
119
+
120
+ assert_equal [], lines(out)
121
+ assert_equal [ "moo: option requires an argument -- b" ], lines(err)
122
+ end
123
+
124
+ def test_invoke_simple_with_illegal_opt
125
+ out, err = capture_io_while do
126
+ assert_raises SystemExit do
127
+ simple_cmd.run(%w( -z ))
128
+ end
129
+ end
130
+
131
+ assert_equal [], lines(out)
132
+ assert_equal [ "moo: illegal option -- z" ], lines(err)
133
+ end
134
+
135
+ def test_invoke_simple_with_opt_with_block
136
+ out, err = capture_io_while do
137
+ simple_cmd.run(%w( -a 123 ))
138
+ end
139
+
140
+ assert_equal [ 'moo:123', 'Awesome moo!', '', 'aaa=123' ], lines(out)
141
+ assert_equal [], lines(err)
142
+ end
143
+
144
+ def test_invoke_nested_without_opts_or_args
145
+ out, err = capture_io_while do
146
+ assert_raises SystemExit do
147
+ nested_cmd.run(%w())
148
+ end
149
+ end
150
+
151
+ assert_equal [ ], lines(out)
152
+ assert_equal [ 'super: no command given' ], lines(err)
153
+ end
154
+
155
+ def test_invoke_nested_with_correct_command_name
156
+ out, err = capture_io_while do
157
+ nested_cmd.run(%w( sub ))
158
+ end
159
+
160
+ assert_equal [ 'Sub-awesome!', '', '' ], lines(out)
161
+ assert_equal [ ], lines(err)
162
+ end
163
+
164
+ def test_invoke_nested_with_incorrect_command_name
165
+ out, err = capture_io_while do
166
+ assert_raises SystemExit do
167
+ nested_cmd.run(%w( oogabooga ))
168
+ end
169
+ end
170
+
171
+ assert_equal [ ], lines(out)
172
+ assert_equal [ "super: unknown command 'oogabooga'" ], lines(err)
173
+ end
174
+
175
+ def test_invoke_nested_with_ambiguous_command_name
176
+ out, err = capture_io_while do
177
+ assert_raises SystemExit do
178
+ nested_cmd.run(%w( s ))
179
+ end
180
+ end
181
+
182
+ assert_equal [ ], lines(out)
183
+ assert_equal [ "super: 's' is ambiguous:", " sub sink" ], lines(err)
184
+ end
185
+
186
+ def test_invoke_nested_with_alias
187
+ out, err = capture_io_while do
188
+ nested_cmd.run(%w( sup ))
189
+ end
190
+
191
+ assert_equal [ 'Sub-awesome!', '', '' ], lines(out)
192
+ assert_equal [ ], lines(err)
193
+ end
194
+
195
+ def test_invoke_nested_with_options_before_command
196
+ out, err = capture_io_while do
197
+ nested_cmd.run(%w( -a 666 sub ))
198
+ end
199
+
200
+ assert_equal [ 'super:666', 'Sub-awesome!', '', 'aaa=666' ], lines(out)
201
+ assert_equal [ ], lines(err)
202
+ end
203
+
204
+ def test_modify
205
+ cmd = Cri::Command.define do
206
+ name 'build'
207
+ end
208
+ assert_equal 'build', cmd.name
209
+
210
+ cmd.modify do
211
+ name 'compile'
212
+ end
213
+
214
+ assert_equal 'compile', cmd.name
215
+ end
216
+
217
+ def test_new_basic_root
218
+ cmd = Cri::Command.new_basic_root.modify do
219
+ name 'mytool'
220
+ end
221
+
222
+ # Check option definitions
223
+ assert_equal 1, cmd.option_definitions.size
224
+ opt_def = cmd.option_definitions.to_a[0]
225
+ assert_equal 'help', opt_def[:long]
226
+
227
+ # Check subcommand
228
+ assert_equal 1, cmd.subcommands.size
229
+ assert_equal 'help', cmd.subcommands.to_a[0].name
230
+ end
231
+
232
+ end