alexvollmer-clip 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/History.txt +25 -0
  2. data/README.txt +107 -0
  3. data/lib/clip.rb +359 -0
  4. data/spec/clip_spec.rb +338 -0
  5. metadata +67 -0
data/History.txt ADDED
@@ -0,0 +1,25 @@
1
+ === 0.0.6 / 2008-07-10
2
+
3
+ * Fixed a bug with getting the 'remainder' when only flags are declared.
4
+
5
+ === 0.0.5 / 2008-06-12
6
+
7
+ * Removed sample_parser from bin (technomancy)
8
+ * fix a stupid bug causing an infinite loop for empty ARGV (technomancy)
9
+
10
+ === 0.0.4 / 2008-06-06
11
+
12
+ * Fixed typo in error message (thanks francois!)
13
+
14
+ === 0.0.3 / 2008-06-05
15
+
16
+ * Merged technomancy's patches for simple 1 LOC parsing -> hash
17
+
18
+ === 0.0.2 / 2008-05-20
19
+
20
+ * Cleaned up README
21
+ * Added support for late-binding option processing with blocks
22
+
23
+ === 0.0.1 / 2008-04-10
24
+
25
+ * Initial release for y'all to throw rotten veggies at.
data/README.txt ADDED
@@ -0,0 +1,107 @@
1
+ = clip
2
+
3
+ == DESCRIPTION:
4
+
5
+ Yeah yeah yeah. Why in heaven's name do we need yet another
6
+ command-line parser? Well, OptionParser is all well and good[1], but
7
+ doesn't grease the skids as much as I'd like. Simple things should be
8
+ dead simple (1 LOC), and more flexibility is there if you need it.
9
+
10
+ Cheers!
11
+
12
+ == FEATURES
13
+
14
+ You like command-line parsing, but you hate all of the bloat. Why
15
+ should you have to create a Hash, then create a parser, fill the Hash
16
+ out then throw the parser away (unless you want to print out a usage
17
+ message) and deal with a Hash? Why, for Pete's sake, should the parser
18
+ and the parsed values be handled by two different objects?
19
+
20
+ Introducing Clip...
21
+
22
+ == SYNOPSIS:
23
+
24
+ And it goes a little something like this...
25
+
26
+ require "rubygems"
27
+ require "clip"
28
+
29
+ options = Clip do |p|
30
+ p.optional 's', 'server', :desc => 'The server name', :default => 'localhost'
31
+ p.optional 'p', 'port', :desc => 'The port', :default => 8080 do |v|
32
+ v.to_i # always deal with integers
33
+ end
34
+ p.required 'f', 'files', :multi => true, :desc => 'Files to send'
35
+ p.flag 'v', 'verbose', :desc => 'Make it chatty'
36
+ end
37
+
38
+ if options.valid?
39
+ if options.verbose?
40
+ puts options.host
41
+ puts options.port
42
+ puts 'files:'
43
+ options.files.each do |f|
44
+ puts "\t#{f}"
45
+ end
46
+ end
47
+ else
48
+ # print error message(s) and usage
49
+ $stderr.puts options.to_s
50
+ end
51
+
52
+ The names of the options and flags that you declare in the block are accessible
53
+ as methods on the returned object, reducing the amount of objects you have to
54
+ deal with when you're parsing command-line parameters.
55
+
56
+ You can optionally process parsed arguments by passing a block to the
57
+ <tt>required</tt> or <tt>optional</tt> methods which will set the value of the
58
+ option to the result of the block. The block will receive the parsed value and
59
+ should return whatever transformed value that is appropriate to your use case.
60
+
61
+ Simply invoking the <tt>to_s</tt> method on a parser instance will dump both the
62
+ correct usage and any errors encountered during parsing. No need for you to manage
63
+ the state of what's required and what isn't by yourself. Also, '--help' and '-h'
64
+ will automatically trigger Clip to dump out usage and exit.
65
+
66
+ Sometimes you have additional arguments you need to process that don't require
67
+ a named option or flag. Whatever remains on the command line that doesn't fit
68
+ either a flag or an option/value pair will be made available via the
69
+ <tt>remainder</tt> method of the returned object.
70
+
71
+ Sometimes even passing a block is overkill. Say you want to grab just
72
+ a hash from a set of name/value argument pairs provided:
73
+
74
+ $ my_clip_script subcommand -c config.yml # Allows:
75
+ Clip.hash == { 'c' => 'config.yml' }
76
+
77
+ $ my_clip_script -c config.yml --mode optimistic # Allows:
78
+ Clip.hash == { 'c' => 'config.yml', 'mode' => 'optimistic' }
79
+
80
+ ----------------------------------------
81
+
82
+ [1] - Not really.
83
+
84
+ == LICENSE:
85
+
86
+ (The MIT License)
87
+
88
+ Copyright (c) 2008 Alex Vollmer
89
+
90
+ Permission is hereby granted, free of charge, to any person obtaining
91
+ a copy of this software and associated documentation files (the
92
+ 'Software'), to deal in the Software without restriction, including
93
+ without limitation the rights to use, copy, modify, merge, publish,
94
+ distribute, sublicense, and/or sell copies of the Software, and to
95
+ permit persons to whom the Software is furnished to do so, subject to
96
+ the following conditions:
97
+
98
+ The above copyright notice and this permission notice shall be
99
+ included in all copies or substantial portions of the Software.
100
+
101
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
102
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
103
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
104
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
105
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
106
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
107
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/lib/clip.rb ADDED
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ##
4
+ # Parse arguments (defaults to <tt>ARGV</tt>) with the Clip::Parser
5
+ # configured in the given block. This is the main method you
6
+ # call to get the ball rolling.
7
+ def Clip(args=ARGV)
8
+ parser = Clip::Parser.new
9
+ raise "Dontcha wanna configure your parser?" unless block_given?
10
+ yield parser
11
+ parser.parse(args)
12
+ parser
13
+ end
14
+
15
+ module Clip
16
+ VERSION = "0.0.6"
17
+
18
+ ##
19
+ # Indicates that the parser was incorrectly configured in the
20
+ # block yielded by the +parse+ method.
21
+ class IllegalConfiguration < Exception
22
+ end
23
+
24
+ class Parser
25
+ ##
26
+ # Returns any remaining command line arguments that were not parsed
27
+ # because they were neither flags or option/value pairs
28
+ attr_reader :remainder
29
+
30
+ ##
31
+ # Set the usage 'banner' displayed when calling <tt>to_s</tt> to
32
+ # display the usage message. If not set, the default will be used.
33
+ # If the value is set this completely replaces the default
34
+ attr_accessor :banner
35
+
36
+ ##
37
+ # Declare an optional parameter for your parser. This creates an accessor
38
+ # method matching the <tt>long</tt> parameter. The <tt>short</tt> parameter
39
+ # indicates the single-letter equivalent. Options that use the '-'
40
+ # character as a word separator are converted to method names using
41
+ # '_'. For example the name 'exclude-files' would create a method named
42
+ # <tt>exclude_files</tt>.
43
+ #
44
+ # When the <tt>:multi</tt> option is enabled, the associated accessor
45
+ # method will return an <tt>Array</tt> instead of a single scalar value.
46
+ # === options
47
+ # Valid options include:
48
+ # * <tt>desc</tt>: a helpful description (used for printing usage)
49
+ # * <tt>default</tt>: a default value to provide if one is not given
50
+ # * <tt>multi</tt>: indicates that mulitple values are okay for this param.
51
+ # * <tt>block</tt>: an optional block to process the parsed value
52
+ #
53
+ # Note that specifying the <tt>:multi</tt> option means that the parameter
54
+ # can be specified several times with different values, or that a single
55
+ # comma-separated value can be specified which will then be broken up into
56
+ # separate tokens.
57
+ def optional(short, long, options={}, &block)
58
+ short = short.to_sym
59
+ long = long.to_sym
60
+ check_args(short, long)
61
+
62
+ var_name = "@#{long}".to_sym
63
+ if block
64
+ self.class.send(:define_method, "#{long}=".to_sym) do |v|
65
+ instance_variable_set(var_name, block.call(v))
66
+ end
67
+ else
68
+ self.class.send(:define_method, "#{long}=".to_sym) do |v|
69
+ instance_variable_set(var_name, v)
70
+ end
71
+ end
72
+
73
+ self.class.send(:define_method, long.to_sym) do
74
+ instance_variable_get(var_name)
75
+ end
76
+
77
+ self.options[long] = Option.new(short, long, options)
78
+ self.options[short] = self.options[long]
79
+ self.order << self.options[long]
80
+ end
81
+
82
+ alias_method :opt, :optional
83
+
84
+ ##
85
+ # Declare a required parameter for your parser. If this parameter
86
+ # is not provided in the parsed content, the parser instance
87
+ # will be invalid (i.e. where valid? returns <tt>false</tt>).
88
+ #
89
+ # This method takes the same options as the optional method.
90
+ def required(short, long, options={}, &block)
91
+ optional(short, long, options.merge({ :required => true }), &block)
92
+ end
93
+
94
+ alias_method :req, :required
95
+
96
+ ##
97
+ # Declare a parameter as a simple boolean flag. This declaration
98
+ # will create a "question" method matching the given <tt>long</tt>.
99
+ # For example, declaring with the name of 'verbose' will create a
100
+ # method on your parser called <tt>verbose?</tt>.
101
+ # === options
102
+ # Valid options are:
103
+ # * <tt>desc</tt>: Descriptive text for the flag
104
+ def flag(short, long, options={})
105
+ short = short.to_sym
106
+ long = long.to_sym
107
+
108
+ check_args(short, long)
109
+
110
+ eval <<-EOF
111
+ def flag_#{long}
112
+ @#{long} = true
113
+ end
114
+
115
+ def #{long}?
116
+ return @#{long} || false
117
+ end
118
+ EOF
119
+
120
+ self.options[long] = Flag.new(short, long, options)
121
+ self.options[short] = self.options[long]
122
+ self.order << self.options[long]
123
+ end
124
+
125
+ def initialize # :nodoc:
126
+ @errors = {}
127
+ @valid = true
128
+ end
129
+
130
+ ##
131
+ # Parse the given <tt>args</tt> and set the corresponding instance
132
+ # fields to the given values. If any errors occurred during parsing
133
+ # you can get them from the <tt>Hash</tt> returned by the +errors+ method.
134
+ def parse(args)
135
+ @valid = true
136
+ args = args.split(/\s+/) unless args.kind_of?(Array)
137
+ consumed = []
138
+ if args.member?("--help")
139
+ puts help
140
+ exit 0
141
+ end
142
+ option = nil
143
+
144
+ args.each do |token|
145
+ case token
146
+ when /^-(-)?\w/
147
+ consumed << token
148
+ param = token.sub(/^-(-)?/, '').sub('-', '_').to_sym
149
+ option = options[param]
150
+ unless option
151
+ @errors[param] = "Unrecognized parameter"
152
+ @valid = false
153
+ next
154
+ end
155
+
156
+ if option.kind_of?(Flag)
157
+ option.process(self, nil)
158
+ option = nil
159
+ end
160
+ else
161
+ if option
162
+ consumed << token
163
+ option.process(self, token)
164
+ option = nil
165
+ end
166
+ end
167
+ end
168
+
169
+ @remainder = args - consumed
170
+
171
+ # Find required options that are missing arguments
172
+ options.each do |param, opt|
173
+ if opt.kind_of?(Option) and self.send(opt.long).nil?
174
+ if opt.required?
175
+ @valid = false
176
+ @errors[opt.long.to_sym] = "Missing required parameter: #{opt.long}"
177
+ elsif opt.has_default?
178
+ opt.process(self, opt.default)
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ ##
185
+ # Indicates whether or not the parsing process succeeded. If this
186
+ # returns <tt>false</tt> you probably just want to print out a call
187
+ # to the to_s method.
188
+ def valid?
189
+ @valid
190
+ end
191
+
192
+ ##
193
+ # Returns a <tt>Hash</tt> of errors (by the long name) of any errors
194
+ # encountered during parsing. If you simply want to display error
195
+ # messages to the user, you can just print out a call to the
196
+ # to_s method.
197
+ def errors
198
+ @errors
199
+ end
200
+
201
+ ##
202
+ # Returns a formatted <tt>String</tt> indicating the usage of the parser
203
+ def help
204
+ out = ""
205
+ if banner
206
+ out << "#{banner}\n"
207
+ else
208
+ out << "Usage:\n"
209
+ end
210
+
211
+ order.each do |option|
212
+ out << "#{option.usage}\n"
213
+ end
214
+ out
215
+ end
216
+
217
+ ##
218
+ # Returns a formatted <tt>String</tt> of the +help+ method prefixed by
219
+ # any parsing errors. Either way you have _one_ method to call to
220
+ # let your users know what to do.
221
+ def to_s
222
+ out = ""
223
+ unless valid?
224
+ out << "Errors:\n"
225
+ errors.each do |field, msg|
226
+ out << "#{field}: #{msg}\n"
227
+ end
228
+ end
229
+ out << help
230
+ end
231
+
232
+ def options # :nodoc:
233
+ (@options ||= {})
234
+ end
235
+
236
+ def order # :nodoc:
237
+ (@order ||= [])
238
+ end
239
+
240
+ private
241
+ def check_args(short, long)
242
+ short = short.to_sym
243
+ long = long.to_sym
244
+
245
+ if long == :help
246
+ raise IllegalConfiguration.new("You cannot override the built-in 'help' parameter")
247
+ end
248
+
249
+ if short == :h
250
+ raise IllegalConfiguration.new("You cannot override the built-in 'h' parameter")
251
+ end
252
+
253
+ if self.options.has_key?(long)
254
+ raise IllegalConfiguration.new("You have already defined a parameter/flag for #{long}")
255
+ end
256
+
257
+ if self.options.has_key?(short)
258
+ raise IllegalConfiguration.new("You already have a defined parameter/flag for the short key '#{short}")
259
+ end
260
+ end
261
+ end
262
+
263
+ class Option # :nodoc:
264
+ attr_accessor :long, :short, :description, :default, :required, :multi
265
+
266
+ def initialize(short, long, options)
267
+ @short = short
268
+ @long = long
269
+ @description = options[:desc]
270
+ @default = options[:default]
271
+ @required = options[:required]
272
+ @multi = options[:multi]
273
+ end
274
+
275
+ def process(parser, value)
276
+ if @multi
277
+ current = parser.send(@long) || []
278
+ current.concat(value.split(','))
279
+ parser.send("#{@long}=".to_sym, current)
280
+ else
281
+ parser.send("#{@long}=".to_sym, value)
282
+ end
283
+ end
284
+
285
+ def required?
286
+ @required == true
287
+ end
288
+
289
+ def has_default?
290
+ not @default.nil?
291
+ end
292
+
293
+ def multi?
294
+ @multi == true
295
+ end
296
+
297
+ def usage
298
+ out = sprintf('-%-2s --%-10s %s',
299
+ @short,
300
+ @long.to_s.gsub('_', '-').to_sym,
301
+ @description)
302
+ out << " (defaults to '#{@default}')" if @default
303
+ out << " REQUIRED" if @required
304
+ out
305
+ end
306
+ end
307
+
308
+ class Flag # :nodoc:
309
+
310
+ attr_accessor :long, :short, :description
311
+
312
+ ##
313
+ # nodoc
314
+ def initialize(short, long, options)
315
+ @short = short
316
+ @long = long
317
+ @description = options[:desc]
318
+ end
319
+
320
+ def process(parser, value)
321
+ parser.send("flag_#{@long}".to_sym)
322
+ end
323
+
324
+ def required?
325
+ false
326
+ end
327
+
328
+ def has_default?
329
+ false
330
+ end
331
+
332
+ def usage
333
+ sprintf('-%-2s --%-10s %s', @short, @long, @description)
334
+ end
335
+ end
336
+
337
+ HASHER_REGEX = /^--?\w+/
338
+ ##
339
+ # Turns ARGV into a hash.
340
+ #
341
+ # my_clip_script -c config.yml # Clip.hash == { 'c' => 'config.yml' }
342
+ # my_clip_script command -c config.yml # Clip.hash == { 'c' => 'config.yml' }
343
+ # my_clip_script com -c config.yml -d # Clip.hash == { 'c' => 'config.yml' }
344
+ # my_clip_script -c config.yml --mode optimistic
345
+ # # Clip.hash == { 'c' => 'config.yml', 'mode' => 'optimistic' }
346
+ def self.hash(argv = ARGV.dup, values = [])
347
+ @hash ||= begin
348
+ argv.shift until argv.first =~ HASHER_REGEX or argv.empty?
349
+ while argv.first =~ HASHER_REGEX and argv.size >= 2 do
350
+ values += [argv.shift.sub(/^--?/, ''), argv.shift]
351
+ end
352
+ Hash[*values]
353
+ end
354
+ end
355
+
356
+ ##
357
+ # Clear the cached hash value. Probably only useful for tests, but whatever.
358
+ def Clip.reset_hash!; @hash = nil end
359
+ end
data/spec/clip_spec.rb ADDED
@@ -0,0 +1,338 @@
1
+ require "#{File.dirname(__FILE__)}/../lib/clip"
2
+ require "rubygems"
3
+ require "spec"
4
+
5
+ class HaveErrors
6
+
7
+ def matches?(target)
8
+ @target = target
9
+ not @target.errors.empty?
10
+ end
11
+
12
+ def failure_message
13
+ "expected #{@target} to have errors"
14
+ end
15
+
16
+ def negative_failure_message
17
+ "expected #{@target} to have no errors, but... #{@target.errors.inspect}"
18
+ end
19
+ end
20
+
21
+ def have_errors
22
+ HaveErrors.new
23
+ end
24
+
25
+ class HaveErrorsOn
26
+ def initialize(expected)
27
+ @expected = expected
28
+ end
29
+
30
+ def matches?(target)
31
+ @target = target
32
+ not @target.errors[@expected.to_sym].nil?
33
+ end
34
+
35
+ def failure_message
36
+ "expected error message for #{@expected} on #{@target}"
37
+ end
38
+
39
+ def negative_failure_message
40
+ "unexpected error message for #{@expected} on #{@target}"
41
+ end
42
+ end
43
+
44
+ def have_errors_on(expected)
45
+ HaveErrorsOn.new(expected)
46
+ end
47
+
48
+ describe Clip do
49
+
50
+ def parse(line)
51
+ Clip(line) do |p|
52
+ p.flag 'v', 'verbose', :desc => 'Provide verbose output'
53
+ p.optional 's', 'server', :desc => 'The hostname', :default => 'localhost'
54
+ p.optional 'p', 'port', :desc => 'The port number', :default => 8080
55
+ p.required 'f', 'files', :desc => 'Files to upload', :multi => true
56
+ p.optional 'e', 'exclude_from', :desc => 'Directories to exclude'
57
+ end
58
+ end
59
+
60
+ describe "When long command-line parameters are parsed" do
61
+
62
+ it "should create accessor methods for declarations" do
63
+ parser = parse('')
64
+ parser.should respond_to(:server)
65
+ parser.should respond_to(:server=)
66
+ parser.should respond_to(:port)
67
+ parser.should respond_to(:port)
68
+ parser.should respond_to(:files)
69
+ parser.should respond_to(:files=)
70
+ parser.should respond_to(:verbose?)
71
+ parser.should respond_to(:flag_verbose)
72
+ end
73
+
74
+ it "should set fields for flags to 'true'" do
75
+ parser = parse('--verbose --files foo')
76
+ parser.should be_verbose
77
+ parser.should be_valid
78
+ parser.should_not have_errors
79
+ end
80
+
81
+ it "should set fields for flags with the given values" do
82
+ parser = parse('--server localhost --port 8080 --files foo')
83
+ parser.server.should eql("localhost")
84
+ parser.port.should eql("8080")
85
+ parser.should be_valid
86
+ parser.should_not have_errors
87
+ end
88
+
89
+ it "should map flags with '-' to methods with '_'" do
90
+ parser = parse('--exclude-from /Users --files foo')
91
+ parser.exclude_from.should eql("/Users")
92
+ parser.should be_valid
93
+ parser.should_not have_errors
94
+ end
95
+
96
+ it "should be invalid for unknown flags" do
97
+ parser = parse('--non-existent')
98
+ parser.should_not be_valid
99
+ parser.should have_errors_on(:non_existent)
100
+ end
101
+ end
102
+
103
+ describe "When short (single-letter) command-line parse are parsed" do
104
+
105
+ it "should set flags to true" do
106
+ parser = parse("-v --files foo")
107
+ parser.should be_verbose
108
+ parser.should_not have_errors
109
+ parser.should be_valid
110
+ end
111
+
112
+ it "should set fields for short options" do
113
+ parser = parse("-s localhost -p 8080 --files foo")
114
+ parser.should_not have_errors
115
+ parser.should be_valid
116
+ parser.server.should eql("localhost")
117
+ parser.port.should eql("8080")
118
+ parser.should_not be_verbose
119
+ end
120
+ end
121
+
122
+ describe "When parameters are marked as required" do
123
+
124
+ it "should be invalid when there are missing arguments" do
125
+ parser = parse('--server localhost')
126
+ parser.should_not be_valid
127
+ parser.should have_errors_on(:files)
128
+ end
129
+ end
130
+
131
+ describe "When parameters are marked with defaults" do
132
+
133
+ it "should provide default parameter values when none are parsed" do
134
+ parser = parse('--files foo')
135
+ parser.should be_valid
136
+ parser.should_not have_errors
137
+ parser.server.should eql("localhost")
138
+ parser.port.should eql(8080)
139
+ end
140
+ end
141
+
142
+ describe "Multi-valued parameters" do
143
+
144
+ it "should handle multiple value for the same parameter" do
145
+ parser = parse("--files foo --files bar --files baz")
146
+ parser.should be_valid
147
+ parser.should_not have_errors
148
+ parser.files.should == %w[foo bar baz]
149
+ end
150
+
151
+ it "should handle comma-separated values as multiples" do
152
+ parser = parse("--files foo,bar,baz")
153
+ parser.should be_valid
154
+ parser.should_not have_errors
155
+ parser.files.should == %w[foo bar baz]
156
+ end
157
+ end
158
+
159
+ describe "Help output" do
160
+ it "should print out some sensible usage info for to_s" do
161
+ out = parse('--files foo').to_s.split("\n")
162
+ out[0].should match(/Usage/)
163
+ out[1].should match(/-v\s+--verbose\s+Provide verbose output/)
164
+ out[2].should match(/-s\s+--server\s+The hostname.*default.*localhost/)
165
+ out[3].should match(/-p\s+--port\s+The port number/)
166
+ out[4].should match(/-f\s+--files\s+Files to upload.*REQUIRED/)
167
+ out[5].should match(/-e\s+--exclude-from\s+Directories to exclude/)
168
+ end
169
+
170
+ it "should include error messages in to_s" do
171
+ parser = parse('')
172
+ out = parser.to_s.split("\n")
173
+ out[0].should match(/Error/)
174
+ out[1].should match(/missing required.*files/i)
175
+ out[2..-1].join("\n").strip.should == parser.help.strip
176
+ end
177
+
178
+ it "should support declaring a banner" do
179
+ opts = Clip('-v') do |p|
180
+ p.banner = "USAGE foo bar baz"
181
+ p.flag 'v', 'verbose', :desc => 'Provide verbose output'
182
+ end
183
+
184
+ out = opts.to_s.split("\n")
185
+ out[0].should == 'USAGE foo bar baz'
186
+ end
187
+ end
188
+
189
+ describe "Remaining arguments" do
190
+ it "should be made available" do
191
+ parser = parse('--files foo alpha bravo')
192
+ parser.files.should == %w[foo]
193
+ parser.remainder.should == %w[alpha bravo]
194
+ end
195
+
196
+ it "should be available when only flags are declared" do
197
+ opts = Clip('foobar') do |p|
198
+ p.flag 'v', 'verbose'
199
+ p.flag 'd', 'debug'
200
+ end
201
+ opts.remainder.should == ['foobar']
202
+ opts.should_not be_verbose
203
+ opts.should_not be_debug
204
+ end
205
+
206
+ it "should be available when flags are declared and parsed" do
207
+ opts = Clip('-v foobar') do |p|
208
+ p.flag 'v', 'verbose'
209
+ p.flag 'd', 'debug'
210
+ end
211
+ opts.remainder.should == ['foobar']
212
+ opts.should be_verbose
213
+ opts.should_not be_debug
214
+ end
215
+ end
216
+
217
+ describe "Declaring bad options and flags" do
218
+
219
+ def misconfig_parser
220
+ lambda do
221
+ Clip("foo") do |c|
222
+ yield c
223
+ end
224
+ end.should raise_error(Clip::IllegalConfiguration)
225
+ end
226
+
227
+ it "should reject :help as a flag name" do
228
+ misconfig_parser { |c| c.flag 'x', 'help' }
229
+ end
230
+
231
+ it "should reject :help as an optional name" do
232
+ misconfig_parser { |c| c.optional 'x', 'help' }
233
+ end
234
+
235
+ it "should reject 'h' as a short flag name" do
236
+ misconfig_parser { |c| c.flag 'h', 'foo' }
237
+ end
238
+
239
+ it "should reject 'h' as a short parameter name" do
240
+ misconfig_parser { |c| c.optional 'h', 'foo' }
241
+ end
242
+
243
+ it "should reject redefining an existing long name for two options" do
244
+ misconfig_parser do |c|
245
+ c.optional 'f', 'foo'
246
+ c.optional 'x', 'foo'
247
+ end
248
+ end
249
+
250
+ it "should reject redefining an existing long name for an option & flag" do
251
+ misconfig_parser do |c|
252
+ c.optional 'f', 'foo'
253
+ c.flag 'x', 'foo'
254
+ end
255
+ end
256
+
257
+ it "should reject redefining the same flag" do
258
+ misconfig_parser do |c|
259
+ c.flag 'f', 'foo'
260
+ c.flag 'x', 'foo'
261
+ end
262
+ end
263
+
264
+ it "should reject defining a flag with an option" do
265
+ misconfig_parser do |c|
266
+ c.flag 'f', 'foo'
267
+ c.optional 'x', 'foo'
268
+ end
269
+ end
270
+
271
+ it "should reject redefining an existing short name for options" do
272
+ misconfig_parser do |c|
273
+ c.optional 'f', 'foo'
274
+ c.optional 'f', 'files'
275
+ end
276
+ end
277
+
278
+ it "should reject redefining a short option with a flag" do
279
+ misconfig_parser do |c|
280
+ c.optional 'f', 'foo'
281
+ c.flag 'f', 'fail'
282
+ end
283
+ end
284
+
285
+ it "should reject redefining a short flag with a flag" do
286
+ misconfig_parser do |c|
287
+ c.flag 'f', 'fail'
288
+ c.flag 'f', 'foo'
289
+ end
290
+ end
291
+
292
+ it "should reject redefining a flag with an optional" do
293
+ misconfig_parser do |c|
294
+ c.flag 'f', 'fail'
295
+ c.optional 'f', 'foo'
296
+ end
297
+ end
298
+ end
299
+
300
+ describe "when specifying a block for a parameter" do
301
+ it "should run the block" do
302
+ opts = Clip("-v 123") do |c|
303
+ c.req 'v', 'value', :desc => 'The value' do |v|
304
+ v.to_i
305
+ end
306
+ end
307
+
308
+ opts.value.should == 123
309
+ end
310
+ end
311
+
312
+ describe "when parsing ARGV as a hash" do
313
+ setup { Clip.reset_hash! }
314
+
315
+ it "should make sense of '-c my_config.yml'" do
316
+ Clip.hash(['-c', 'config.yml']).should == { 'c' => 'config.yml' }
317
+ end
318
+
319
+ it "should only use pairs of dash + value args" do
320
+ Clip.hash(['-c', 'config.yml',
321
+ '-d']).should == { 'c' => 'config.yml' }
322
+ end
323
+
324
+ it "should ignore leading/trailing non-dashed arguments" do
325
+ Clip.hash(['subcommand', '-c', 'config.yml',
326
+ 'do']).should == { 'c' => 'config.yml' }
327
+ end
328
+
329
+ it "should allow -s (short) or --long arguments" do
330
+ Clip.hash(['-c', 'config.yml', '--mode', 'optimistic']).
331
+ should == { 'c' => 'config.yml', 'mode' => 'optimistic' }
332
+ end
333
+
334
+ it "should return an empty hash for empty ARGV" do
335
+ Clip.hash([]).should == {}
336
+ end
337
+ end
338
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alexvollmer-clip
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ platform: ruby
6
+ authors:
7
+ - Alex Vollmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-07-14 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.6.0
23
+ version:
24
+ description: You like command-line parsing, but you hate all of the bloat. Why should you have to create a Hash, then create a parser, fill the Hash out then throw the parser away (unless you want to print out a usage message) and deal with a Hash? Why, for Pete's sake, should the parser and the parsed values be handled by two different objects?
25
+ email:
26
+ - alex.vollmer@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - History.txt
33
+ - README.txt
34
+ files:
35
+ - History.txt
36
+ - README.txt
37
+ - lib/clip.rb
38
+ - spec/clip_spec.rb
39
+ has_rdoc: true
40
+ homepage: http://clip.rubyforge.org
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --main
44
+ - README.txt
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project: clip
62
+ rubygems_version: 1.2.0
63
+ signing_key:
64
+ specification_version: 2
65
+ summary: Command-line parsing made short and sweet
66
+ test_files: []
67
+