alexvollmer-clip 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +25 -0
- data/README.txt +107 -0
- data/lib/clip.rb +359 -0
- data/spec/clip_spec.rb +338 -0
- 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
|
+
|