cri 1.0.1 → 2.0a1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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