clip 0.0.6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +7 -0
- data/{README.txt → README.rdoc} +10 -3
- data/Rakefile +48 -0
- data/clip.gemspec +32 -0
- data/lib/clip.rb +136 -50
- data/spec/clip_spec.rb +174 -32
- metadata +7 -4
data/History.txt
CHANGED
data/Manifest.txt
ADDED
data/{README.txt → README.rdoc}
RENAMED
@@ -31,8 +31,13 @@ And it goes a little something like this...
|
|
31
31
|
p.optional 'p', 'port', :desc => 'The port', :default => 8080 do |v|
|
32
32
|
v.to_i # always deal with integers
|
33
33
|
end
|
34
|
-
p.required 'f', 'files', :multi => true, :desc => 'Files to send'
|
35
|
-
|
34
|
+
p.required 'f', 'files', :multi => true, :desc => 'Files to send' do |files|
|
35
|
+
files.each do |f|
|
36
|
+
raise("unable to read file #{f}") unless File::readable? f
|
37
|
+
f
|
38
|
+
end
|
39
|
+
end
|
40
|
+
p.flag 'v', 'verbose', :desc => 'Make it chatty'
|
36
41
|
end
|
37
42
|
|
38
43
|
if options.valid?
|
@@ -56,7 +61,9 @@ deal with when you're parsing command-line parameters.
|
|
56
61
|
You can optionally process parsed arguments by passing a block to the
|
57
62
|
<tt>required</tt> or <tt>optional</tt> methods which will set the value of the
|
58
63
|
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.
|
64
|
+
should return whatever transformed value that is appropriate to your use case. If
|
65
|
+
the passed value fails validation you can raise an error which will be reported
|
66
|
+
correctly.
|
60
67
|
|
61
68
|
Simply invoking the <tt>to_s</tt> method on a parser instance will dump both the
|
62
69
|
correct usage and any errors encountered during parsing. No need for you to manage
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/clip.rb'
|
6
|
+
|
7
|
+
Hoe.new('clip', Clip::VERSION) do |p|
|
8
|
+
p.name = 'clip'
|
9
|
+
p.developer('Alex Vollmer', 'alex.vollmer@gmail.com')
|
10
|
+
p.description = p.paragraphs_of('README.rdoc', 5..5).join("\n\n")
|
11
|
+
p.summary = 'Command-line parsing made short and sweet'
|
12
|
+
p.url = 'http://clip.rubyforge.org'
|
13
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
14
|
+
p.remote_rdoc_dir = ''
|
15
|
+
end
|
16
|
+
|
17
|
+
require "spec/rake/spectask"
|
18
|
+
Spec::Rake::SpecTask.new do |t|
|
19
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
20
|
+
end
|
21
|
+
|
22
|
+
HERE = "web"
|
23
|
+
THERE = "avollmer@clip.rubyforge.org:/var/www/gforge-projects/clip"
|
24
|
+
|
25
|
+
desc "Sync web files here"
|
26
|
+
task :sync_here do
|
27
|
+
puts %x(rsync \
|
28
|
+
--verbose \
|
29
|
+
--recursive \
|
30
|
+
#{THERE}/ #{HERE})
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "Sync web files there"
|
34
|
+
task :sync_there do
|
35
|
+
puts %x(rsync \
|
36
|
+
--verbose \
|
37
|
+
--recursive \
|
38
|
+
--delete \
|
39
|
+
#{HERE}/ #{THERE}})
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "Code statistics"
|
43
|
+
task :stats do
|
44
|
+
require 'code_statistics'
|
45
|
+
CodeStatistics.new(['lib'], ['spec']).to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
task :default => :spec
|
data/clip.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{clip}
|
3
|
+
s.version = "1.0.0"
|
4
|
+
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
s.authors = ["Alex Vollmer"]
|
7
|
+
s.date = %q{2008-09-19}
|
8
|
+
s.description = %q{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?}
|
9
|
+
s.email = ["alex.vollmer@gmail.com"]
|
10
|
+
s.extra_rdoc_files = ["History.txt", "Manifest.txt"]
|
11
|
+
s.files = ["History.txt", "Manifest.txt", "README.rdoc", "Rakefile", "clip.gemspec", "lib/clip.rb", "spec/clip_spec.rb"]
|
12
|
+
s.has_rdoc = true
|
13
|
+
s.homepage = %q{http://clip.rubyforge.org}
|
14
|
+
s.rdoc_options = ["--main", "README.rdoc"]
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.rubyforge_project = %q{clip}
|
17
|
+
s.rubygems_version = %q{1.2.0}
|
18
|
+
s.summary = %q{Command-line parsing made short and sweet}
|
19
|
+
|
20
|
+
if s.respond_to? :specification_version then
|
21
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
22
|
+
s.specification_version = 2
|
23
|
+
|
24
|
+
if current_version >= 3 then
|
25
|
+
s.add_development_dependency(%q<hoe>, [">= 1.7.0"])
|
26
|
+
else
|
27
|
+
s.add_dependency(%q<hoe>, [">= 1.7.0"])
|
28
|
+
end
|
29
|
+
else
|
30
|
+
s.add_dependency(%q<hoe>, [">= 1.7.0"])
|
31
|
+
end
|
32
|
+
end
|
data/lib/clip.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require 'shellwords'
|
4
|
+
|
3
5
|
##
|
4
6
|
# Parse arguments (defaults to <tt>ARGV</tt>) with the Clip::Parser
|
5
7
|
# configured in the given block. This is the main method you
|
@@ -13,7 +15,7 @@ def Clip(args=ARGV)
|
|
13
15
|
end
|
14
16
|
|
15
17
|
module Clip
|
16
|
-
VERSION = "0.0
|
18
|
+
VERSION = "1.0.0"
|
17
19
|
|
18
20
|
##
|
19
21
|
# Indicates that the parser was incorrectly configured in the
|
@@ -33,6 +35,16 @@ module Clip
|
|
33
35
|
# If the value is set this completely replaces the default
|
34
36
|
attr_accessor :banner
|
35
37
|
|
38
|
+
##
|
39
|
+
# Override the flag to trigger help usage. By default the short
|
40
|
+
# flag '-h' and long flag '--help' will trigger displaying usage.
|
41
|
+
# If you need to override this, particularly in the case of '-h',
|
42
|
+
# call this method
|
43
|
+
def help_with(short, long="--help")
|
44
|
+
@help_short = short
|
45
|
+
@help_long = long
|
46
|
+
end
|
47
|
+
|
36
48
|
##
|
37
49
|
# Declare an optional parameter for your parser. This creates an accessor
|
38
50
|
# method matching the <tt>long</tt> parameter. The <tt>short</tt> parameter
|
@@ -55,28 +67,33 @@ module Clip
|
|
55
67
|
# comma-separated value can be specified which will then be broken up into
|
56
68
|
# separate tokens.
|
57
69
|
def optional(short, long, options={}, &block)
|
58
|
-
short = short.to_sym
|
59
|
-
long = long.to_sym
|
60
70
|
check_args(short, long)
|
61
71
|
|
72
|
+
short = short.to_sym
|
73
|
+
long = long.gsub('-', '_').to_sym
|
74
|
+
|
62
75
|
var_name = "@#{long}".to_sym
|
63
|
-
|
64
|
-
|
65
|
-
|
76
|
+
self.class.class_eval do
|
77
|
+
define_method("#{long}=".to_sym) do |v|
|
78
|
+
begin
|
79
|
+
v = yield(v) if block_given?
|
80
|
+
instance_variable_set(var_name, v)
|
81
|
+
rescue StandardError => e
|
82
|
+
@valid = false
|
83
|
+
@errors[long] = e.message
|
84
|
+
end
|
66
85
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
86
|
+
|
87
|
+
define_method(long.to_sym) do
|
88
|
+
instance_variable_get(var_name)
|
70
89
|
end
|
71
90
|
end
|
72
91
|
|
73
|
-
self.
|
74
|
-
|
75
|
-
end
|
92
|
+
self.options[short] = self.options[long] =
|
93
|
+
Option.new(short, long, options)
|
76
94
|
|
77
|
-
self.options[long] = Option.new(short, long, options)
|
78
|
-
self.options[short] = self.options[long]
|
79
95
|
self.order << self.options[long]
|
96
|
+
check_longest(long)
|
80
97
|
end
|
81
98
|
|
82
99
|
alias_method :opt, :optional
|
@@ -102,29 +119,32 @@ module Clip
|
|
102
119
|
# Valid options are:
|
103
120
|
# * <tt>desc</tt>: Descriptive text for the flag
|
104
121
|
def flag(short, long, options={})
|
105
|
-
short = short.to_sym
|
106
|
-
long = long.to_sym
|
107
|
-
|
108
122
|
check_args(short, long)
|
109
123
|
|
110
|
-
|
111
|
-
|
112
|
-
|
124
|
+
short = short.to_sym
|
125
|
+
long = long.gsub('-', '_').to_sym
|
126
|
+
self.class.class_eval do
|
127
|
+
define_method("flag_#{long}") do
|
128
|
+
instance_variable_set("@#{long}", true)
|
113
129
|
end
|
114
130
|
|
115
|
-
|
116
|
-
|
131
|
+
define_method("#{long}?") do
|
132
|
+
instance_variable_get("@#{long}")
|
117
133
|
end
|
118
|
-
|
134
|
+
end
|
119
135
|
|
120
136
|
self.options[long] = Flag.new(short, long, options)
|
121
137
|
self.options[short] = self.options[long]
|
122
138
|
self.order << self.options[long]
|
139
|
+
check_longest(long)
|
123
140
|
end
|
124
141
|
|
125
142
|
def initialize # :nodoc:
|
126
143
|
@errors = {}
|
127
144
|
@valid = true
|
145
|
+
@longest = 10
|
146
|
+
@help_long = "--help"
|
147
|
+
@help_short = "-h"
|
128
148
|
end
|
129
149
|
|
130
150
|
##
|
@@ -133,21 +153,25 @@ module Clip
|
|
133
153
|
# you can get them from the <tt>Hash</tt> returned by the +errors+ method.
|
134
154
|
def parse(args)
|
135
155
|
@valid = true
|
136
|
-
args = args
|
156
|
+
args = Shellwords::shellwords(args) unless args.kind_of?(Array)
|
137
157
|
consumed = []
|
138
|
-
if args.member?("--help")
|
139
|
-
puts help
|
140
|
-
exit 0
|
141
|
-
end
|
142
158
|
option = nil
|
143
|
-
|
159
|
+
|
144
160
|
args.each do |token|
|
145
161
|
case token
|
162
|
+
when @help_long, @help_short
|
163
|
+
puts help
|
164
|
+
exit 0
|
165
|
+
|
166
|
+
when /\A--\z/
|
167
|
+
consumed << token
|
168
|
+
break
|
169
|
+
|
146
170
|
when /^-(-)?\w/
|
147
171
|
consumed << token
|
148
|
-
param = token.sub(/^-(-)?/, '').
|
172
|
+
param = token.sub(/^-(-)?/, '').gsub('-', '_').to_sym
|
149
173
|
option = options[param]
|
150
|
-
|
174
|
+
if option.nil?
|
151
175
|
@errors[param] = "Unrecognized parameter"
|
152
176
|
@valid = false
|
153
177
|
next
|
@@ -199,7 +223,8 @@ module Clip
|
|
199
223
|
end
|
200
224
|
|
201
225
|
##
|
202
|
-
# Returns a formatted <tt>String</tt> indicating the usage of the parser
|
226
|
+
# Returns a formatted <tt>String</tt> indicating the usage of the parser,
|
227
|
+
# formatted to fit within 80 display columns.
|
203
228
|
def help
|
204
229
|
out = ""
|
205
230
|
if banner
|
@@ -209,7 +234,38 @@ module Clip
|
|
209
234
|
end
|
210
235
|
|
211
236
|
order.each do |option|
|
212
|
-
|
237
|
+
line = sprintf("-%-2s --%-#{@longest}s ",
|
238
|
+
option.short,
|
239
|
+
option.long.to_s.gsub('_', '-'))
|
240
|
+
|
241
|
+
out << line
|
242
|
+
if line.length + option.description.length <= 80
|
243
|
+
out << option.description
|
244
|
+
else
|
245
|
+
rem = 80 - line.length
|
246
|
+
desc = option.description
|
247
|
+
i = 0
|
248
|
+
while i < desc.length
|
249
|
+
out << "\n" if i > 0
|
250
|
+
j = [i + rem, desc.length - 1].min
|
251
|
+
while desc[j..j] =~ /[\w\d]/
|
252
|
+
j -= 1
|
253
|
+
end
|
254
|
+
chunk = desc[i..j].strip
|
255
|
+
out << " " * line.length if i > 0
|
256
|
+
out << chunk
|
257
|
+
i = j + 1
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
if option.has_default?
|
262
|
+
out << " (default: #{option.default})"
|
263
|
+
end
|
264
|
+
|
265
|
+
if option.required?
|
266
|
+
out << " REQUIRED"
|
267
|
+
end
|
268
|
+
out << "\n"
|
213
269
|
end
|
214
270
|
out
|
215
271
|
end
|
@@ -237,8 +293,20 @@ module Clip
|
|
237
293
|
(@order ||= [])
|
238
294
|
end
|
239
295
|
|
240
|
-
private
|
296
|
+
private
|
241
297
|
def check_args(short, long)
|
298
|
+
if short.size != 1
|
299
|
+
raise IllegalConfiguration.new("Short options must be a single character.")
|
300
|
+
end
|
301
|
+
|
302
|
+
if short !~ /[\w]+/
|
303
|
+
raise IllegalConfiguration.new("Illegal option: #{short}. Option names can only use [a-zA-Z_-]")
|
304
|
+
end
|
305
|
+
|
306
|
+
if long !~ /\A\w[\w-]*\z/
|
307
|
+
raise IllegalConfiguration.new("Illegal option: #{long}'. Parameter names can only use [a-zA-Z_-]")
|
308
|
+
end
|
309
|
+
|
242
310
|
short = short.to_sym
|
243
311
|
long = long.to_sym
|
244
312
|
|
@@ -246,8 +314,8 @@ module Clip
|
|
246
314
|
raise IllegalConfiguration.new("You cannot override the built-in 'help' parameter")
|
247
315
|
end
|
248
316
|
|
249
|
-
if short ==
|
250
|
-
raise IllegalConfiguration.new("You cannot override the built-in '
|
317
|
+
if short == '?'.to_sym
|
318
|
+
raise IllegalConfiguration.new("You cannot override the built-in '?' parameter")
|
251
319
|
end
|
252
320
|
|
253
321
|
if self.options.has_key?(long)
|
@@ -258,6 +326,11 @@ module Clip
|
|
258
326
|
raise IllegalConfiguration.new("You already have a defined parameter/flag for the short key '#{short}")
|
259
327
|
end
|
260
328
|
end
|
329
|
+
|
330
|
+
def check_longest(name)
|
331
|
+
l = name.to_s.length
|
332
|
+
@longest = l if l > @longest
|
333
|
+
end
|
261
334
|
end
|
262
335
|
|
263
336
|
class Option # :nodoc:
|
@@ -293,7 +366,7 @@ module Clip
|
|
293
366
|
def multi?
|
294
367
|
@multi == true
|
295
368
|
end
|
296
|
-
|
369
|
+
|
297
370
|
def usage
|
298
371
|
out = sprintf('-%-2s --%-10s %s',
|
299
372
|
@short,
|
@@ -306,7 +379,7 @@ module Clip
|
|
306
379
|
end
|
307
380
|
|
308
381
|
class Flag # :nodoc:
|
309
|
-
|
382
|
+
|
310
383
|
attr_accessor :long, :short, :description
|
311
384
|
|
312
385
|
##
|
@@ -328,13 +401,9 @@ module Clip
|
|
328
401
|
def has_default?
|
329
402
|
false
|
330
403
|
end
|
331
|
-
|
332
|
-
def usage
|
333
|
-
sprintf('-%-2s --%-10s %s', @short, @long, @description)
|
334
|
-
end
|
335
404
|
end
|
336
405
|
|
337
|
-
HASHER_REGEX =
|
406
|
+
HASHER_REGEX = /^--?(\w+)/
|
338
407
|
##
|
339
408
|
# Turns ARGV into a hash.
|
340
409
|
#
|
@@ -343,17 +412,34 @@ module Clip
|
|
343
412
|
# my_clip_script com -c config.yml -d # Clip.hash == { 'c' => 'config.yml' }
|
344
413
|
# my_clip_script -c config.yml --mode optimistic
|
345
414
|
# # Clip.hash == { 'c' => 'config.yml', 'mode' => 'optimistic' }
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
415
|
+
#
|
416
|
+
# The returned hash also has a +remainder+ method that contains
|
417
|
+
# unparsed values.
|
418
|
+
#
|
419
|
+
def self.hash(argv = ARGV.dup, keys = [])
|
420
|
+
return @hash if @hash # used the cached value if available
|
421
|
+
|
422
|
+
opts = Clip(argv) do |clip|
|
423
|
+
keys = argv.select{ |a| a =~ HASHER_REGEX }.map do |a|
|
424
|
+
a = a.sub(HASHER_REGEX, '\\1')
|
425
|
+
clip.optional(a[0,1], a); a
|
351
426
|
end
|
352
|
-
Hash[*values]
|
353
427
|
end
|
428
|
+
|
429
|
+
# The "|| true" on the end is for when no value is found for a
|
430
|
+
# key; it's assumed that a flag was meant instead of an optional
|
431
|
+
# argument, so it's set to true. A bit weird-looking, but more useful.
|
432
|
+
@hash = keys.inject({}) { |h, key| h.merge(key => opts.send(key) || true) }
|
433
|
+
|
434
|
+
# module_eval is necessary to define a singleton method using a closure =\
|
435
|
+
(class << @hash; self; end).module_eval do
|
436
|
+
define_method(:remainder) { opts.remainder }
|
437
|
+
end
|
438
|
+
|
439
|
+
return @hash
|
354
440
|
end
|
355
441
|
|
356
442
|
##
|
357
443
|
# Clear the cached hash value. Probably only useful for tests, but whatever.
|
358
|
-
def
|
444
|
+
def self.reset_hash!; @hash = nil end
|
359
445
|
end
|
data/spec/clip_spec.rb
CHANGED
@@ -54,10 +54,13 @@ describe Clip do
|
|
54
54
|
p.optional 'p', 'port', :desc => 'The port number', :default => 8080
|
55
55
|
p.required 'f', 'files', :desc => 'Files to upload', :multi => true
|
56
56
|
p.optional 'e', 'exclude_from', :desc => 'Directories to exclude'
|
57
|
+
p.optional 'x', 'exclude_from_all', :desc => 'Directories to exclude'
|
58
|
+
p.flag 'd', 'allow-dashes', :desc => 'Dashes allowed in definition'
|
59
|
+
p.flag 'z', 'allow-dashes-all', :desc => 'Dashes allowed in definition'
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
60
|
-
describe "When long command-line parameters are parsed" do
|
63
|
+
describe "When long command-line parameters are parsed" do
|
61
64
|
|
62
65
|
it "should create accessor methods for declarations" do
|
63
66
|
parser = parse('')
|
@@ -69,6 +72,8 @@ describe Clip do
|
|
69
72
|
parser.should respond_to(:files=)
|
70
73
|
parser.should respond_to(:verbose?)
|
71
74
|
parser.should respond_to(:flag_verbose)
|
75
|
+
parser.should respond_to(:allow_dashes?)
|
76
|
+
parser.should respond_to(:allow_dashes_all?)
|
72
77
|
end
|
73
78
|
|
74
79
|
it "should set fields for flags to 'true'" do
|
@@ -77,8 +82,8 @@ describe Clip do
|
|
77
82
|
parser.should be_valid
|
78
83
|
parser.should_not have_errors
|
79
84
|
end
|
80
|
-
|
81
|
-
it "should set fields for
|
85
|
+
|
86
|
+
it "should set fields for options with the given values" do
|
82
87
|
parser = parse('--server localhost --port 8080 --files foo')
|
83
88
|
parser.server.should eql("localhost")
|
84
89
|
parser.port.should eql("8080")
|
@@ -86,14 +91,27 @@ describe Clip do
|
|
86
91
|
parser.should_not have_errors
|
87
92
|
end
|
88
93
|
|
89
|
-
it "should map
|
94
|
+
it "should map options with '-' to methods with '_'" do
|
90
95
|
parser = parse('--exclude-from /Users --files foo')
|
91
96
|
parser.exclude_from.should eql("/Users")
|
92
97
|
parser.should be_valid
|
93
98
|
parser.should_not have_errors
|
94
99
|
end
|
100
|
+
|
101
|
+
it "should map flags with '-' to methods with '_'" do
|
102
|
+
parser = parse('--allow-dashes')
|
103
|
+
parser.should be_allow_dashes
|
104
|
+
parser.should_not be_allow_dashes_all
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should map options with multiple '-' to methods with '_'" do
|
108
|
+
parser = parse('--exclude-from-all /Users --files foo')
|
109
|
+
parser.exclude_from_all.should eql("/Users")
|
110
|
+
parser.should be_valid
|
111
|
+
parser.should_not have_errors
|
112
|
+
end
|
95
113
|
|
96
|
-
it "should be invalid for unknown
|
114
|
+
it "should be invalid for unknown options" do
|
97
115
|
parser = parse('--non-existent')
|
98
116
|
parser.should_not be_valid
|
99
117
|
parser.should have_errors_on(:non_existent)
|
@@ -101,14 +119,14 @@ describe Clip do
|
|
101
119
|
end
|
102
120
|
|
103
121
|
describe "When short (single-letter) command-line parse are parsed" do
|
104
|
-
|
122
|
+
|
105
123
|
it "should set flags to true" do
|
106
124
|
parser = parse("-v --files foo")
|
107
125
|
parser.should be_verbose
|
108
126
|
parser.should_not have_errors
|
109
127
|
parser.should be_valid
|
110
128
|
end
|
111
|
-
|
129
|
+
|
112
130
|
it "should set fields for short options" do
|
113
131
|
parser = parse("-s localhost -p 8080 --files foo")
|
114
132
|
parser.should_not have_errors
|
@@ -119,8 +137,8 @@ describe Clip do
|
|
119
137
|
end
|
120
138
|
end
|
121
139
|
|
122
|
-
describe "When
|
123
|
-
|
140
|
+
describe "When options are marked as required" do
|
141
|
+
|
124
142
|
it "should be invalid when there are missing arguments" do
|
125
143
|
parser = parse('--server localhost')
|
126
144
|
parser.should_not be_valid
|
@@ -128,9 +146,9 @@ describe Clip do
|
|
128
146
|
end
|
129
147
|
end
|
130
148
|
|
131
|
-
describe "When
|
132
|
-
|
133
|
-
it "should provide default
|
149
|
+
describe "When options are marked with defaults" do
|
150
|
+
|
151
|
+
it "should provide default option values when none are parsed" do
|
134
152
|
parser = parse('--files foo')
|
135
153
|
parser.should be_valid
|
136
154
|
parser.should_not have_errors
|
@@ -139,9 +157,9 @@ describe Clip do
|
|
139
157
|
end
|
140
158
|
end
|
141
159
|
|
142
|
-
describe "Multi-valued
|
160
|
+
describe "Multi-valued options" do
|
143
161
|
|
144
|
-
it "should handle multiple value for the same
|
162
|
+
it "should handle multiple value for the same option" do
|
145
163
|
parser = parse("--files foo --files bar --files baz")
|
146
164
|
parser.should be_valid
|
147
165
|
parser.should_not have_errors
|
@@ -157,14 +175,43 @@ describe Clip do
|
|
157
175
|
end
|
158
176
|
|
159
177
|
describe "Help output" do
|
178
|
+
def opts(args=nil)
|
179
|
+
Clip(args) do |o|
|
180
|
+
o.req 's', 'server', :desc => 'The server name'
|
181
|
+
o.opt 'p', 'port', :desc => 'The port number', :default => 80
|
182
|
+
o.flag 'v', 'verbose', :desc => 'Enables verbose output'
|
183
|
+
yield o if block_given?
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
160
187
|
it "should print out some sensible usage info for to_s" do
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
188
|
+
help = opts("-s localhost").to_s.split("\n")
|
189
|
+
help[0].should match(/Usage/)
|
190
|
+
help[1].should == "-s --server The server name REQUIRED"
|
191
|
+
help[2].should == "-p --port The port number (default: 80)"
|
192
|
+
help[3].should == "-v --verbose Enables verbose output"
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should expand columns to largest option name" do
|
196
|
+
help = opts("-s localhost") do |o|
|
197
|
+
o.opt 'b', 'big-honking-file-list', :desc => 'The file list'
|
198
|
+
end.to_s.split("\n")
|
199
|
+
help[0].should match(/Usage/)
|
200
|
+
help[1].should == "-s --server The server name REQUIRED"
|
201
|
+
help[2].should == "-p --port The port number (default: 80)"
|
202
|
+
help[3].should == "-v --verbose Enables verbose output"
|
203
|
+
help[4].should == "-b --big-honking-file-list The file list"
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should wrap column descriptions to fit 80 columns" do
|
207
|
+
opts = Clip do |o|
|
208
|
+
o.opt 'd', 'description',
|
209
|
+
:desc => 'An unending stream of words that goes on endlessly for an eternity until, at last, they stop.',
|
210
|
+
:default => 'wordy'
|
211
|
+
end
|
212
|
+
help = opts.to_s.split("\n")
|
213
|
+
help[1].should == "-d --description An unending stream of words that goes on endlessly for an"
|
214
|
+
help[2].should == " eternity until, at last, they stop. (default: wordy)"
|
168
215
|
end
|
169
216
|
|
170
217
|
it "should include error messages in to_s" do
|
@@ -184,15 +231,31 @@ describe Clip do
|
|
184
231
|
out = opts.to_s.split("\n")
|
185
232
|
out[0].should == 'USAGE foo bar baz'
|
186
233
|
end
|
234
|
+
|
235
|
+
it "should support overriding help flags" do
|
236
|
+
opts = Clip('-?') do |p|
|
237
|
+
p.opt 'h', 'host', :desc => 'The hostname'
|
238
|
+
p.help_with '?'
|
239
|
+
end
|
240
|
+
help = opts.to_s.split("\n")
|
241
|
+
help[0].should match(/Usage/)
|
242
|
+
help[1].should match(/-h\s+--host\s+The hostname/)
|
243
|
+
end
|
187
244
|
end
|
188
245
|
|
189
246
|
describe "Remaining arguments" do
|
190
|
-
it "should be
|
247
|
+
it "should be taken following parsed arguments" do
|
191
248
|
parser = parse('--files foo alpha bravo')
|
192
249
|
parser.files.should == %w[foo]
|
193
250
|
parser.remainder.should == %w[alpha bravo]
|
194
251
|
end
|
195
252
|
|
253
|
+
it "should be taken preceeding parsed arguments" do
|
254
|
+
parser = parse('alpha bravo --files foo')
|
255
|
+
parser.files.should == %w[foo]
|
256
|
+
parser.remainder.should == %w[alpha bravo]
|
257
|
+
end
|
258
|
+
|
196
259
|
it "should be available when only flags are declared" do
|
197
260
|
opts = Clip('foobar') do |p|
|
198
261
|
p.flag 'v', 'verbose'
|
@@ -212,6 +275,32 @@ describe Clip do
|
|
212
275
|
opts.should be_verbose
|
213
276
|
opts.should_not be_debug
|
214
277
|
end
|
278
|
+
|
279
|
+
it "Should handle quoted strings correctly" do
|
280
|
+
opts = Clip(%q(-- "param 1" 'param 2' param\ 3)) {|p|}
|
281
|
+
opts.remainder.should include('param 1', 'param 2', 'param 3')
|
282
|
+
end
|
283
|
+
end
|
284
|
+
describe "Remaining arguments for Clip.hash" do
|
285
|
+
setup { Clip.reset_hash! }
|
286
|
+
|
287
|
+
it "should be populated" do
|
288
|
+
Clip.hash(['captain', 'lieutenant', '-c', 'jorge']).remainder.
|
289
|
+
should == ['captain', 'lieutenant']
|
290
|
+
end
|
291
|
+
|
292
|
+
it "should be empty for an empty arg list" do
|
293
|
+
Clip.hash([]).remainder.should be_empty
|
294
|
+
end
|
295
|
+
|
296
|
+
it "should be empty for a completely-parsed arg list" do
|
297
|
+
Clip.hash(['-c', '/etc/clip.yml']).remainder.should be_empty
|
298
|
+
end
|
299
|
+
|
300
|
+
it "should be the arg list for an unparsed arg list" do
|
301
|
+
Clip.hash(['git', 'bzr', 'hg', 'darcs', 'arch']).remainder.
|
302
|
+
should == ['git', 'bzr', 'hg', 'darcs', 'arch']
|
303
|
+
end
|
215
304
|
end
|
216
305
|
|
217
306
|
describe "Declaring bad options and flags" do
|
@@ -224,6 +313,26 @@ describe Clip do
|
|
224
313
|
end.should raise_error(Clip::IllegalConfiguration)
|
225
314
|
end
|
226
315
|
|
316
|
+
it "should reject '-' as a short option" do
|
317
|
+
misconfig_parser { |c| c.flag '-', 'foo' }
|
318
|
+
end
|
319
|
+
|
320
|
+
it "should reject long parameters that do not start with a word character" do
|
321
|
+
misconfig_parser { |c| c.optional 'o', '-foo' }
|
322
|
+
misconfig_parser { |c| c.optional 'o', '-' }
|
323
|
+
end
|
324
|
+
|
325
|
+
it "should reject options with non-word characters '-' ok" do
|
326
|
+
misconfig_parser { |c| c.flag '#', 'count' }
|
327
|
+
misconfig_parser { |c| c.flag 'c', 'bang!' }
|
328
|
+
misconfig_parser { |c| c.optional '#', 'count' }
|
329
|
+
misconfig_parser { |c| c.optional 'c', 'bang!' }
|
330
|
+
end
|
331
|
+
|
332
|
+
it "should reject short params that aren't single characters" do
|
333
|
+
misconfig_parser { |c| c.optional 'foo', 'foo' }
|
334
|
+
end
|
335
|
+
|
227
336
|
it "should reject :help as a flag name" do
|
228
337
|
misconfig_parser { |c| c.flag 'x', 'help' }
|
229
338
|
end
|
@@ -232,12 +341,12 @@ describe Clip do
|
|
232
341
|
misconfig_parser { |c| c.optional 'x', 'help' }
|
233
342
|
end
|
234
343
|
|
235
|
-
it "should reject '
|
236
|
-
misconfig_parser { |c| c.flag '
|
344
|
+
it "should reject '?' as a short flag name" do
|
345
|
+
misconfig_parser { |c| c.flag '?', 'foo' }
|
237
346
|
end
|
238
347
|
|
239
|
-
it "should reject '
|
240
|
-
misconfig_parser { |c| c.optional '
|
348
|
+
it "should reject '?' as a short parameter name" do
|
349
|
+
misconfig_parser { |c| c.optional '?', 'foo' }
|
241
350
|
end
|
242
351
|
|
243
352
|
it "should reject redefining an existing long name for two options" do
|
@@ -304,21 +413,31 @@ describe Clip do
|
|
304
413
|
v.to_i
|
305
414
|
end
|
306
415
|
end
|
307
|
-
|
416
|
+
|
308
417
|
opts.value.should == 123
|
309
418
|
end
|
419
|
+
|
420
|
+
it "should trap exceptions from block and report error" do
|
421
|
+
opts = Clip("-v 123") do |c|
|
422
|
+
c.req('v', 'value') {|v| raise("a good error message") }
|
423
|
+
end
|
424
|
+
|
425
|
+
opts.should_not be_valid
|
426
|
+
opts.should have_errors
|
427
|
+
opts.should have_errors_on(:value)
|
428
|
+
end
|
310
429
|
end
|
311
430
|
|
312
431
|
describe "when parsing ARGV as a hash" do
|
313
432
|
setup { Clip.reset_hash! }
|
314
|
-
|
433
|
+
|
315
434
|
it "should make sense of '-c my_config.yml'" do
|
316
435
|
Clip.hash(['-c', 'config.yml']).should == { 'c' => 'config.yml' }
|
317
436
|
end
|
318
437
|
|
319
|
-
it "should
|
320
|
-
Clip.hash(['-c', 'config.yml',
|
321
|
-
|
438
|
+
it "should treat flag-style arguments as booleans" do
|
439
|
+
Clip.hash(['-f', '-c', 'config.yml', '-d']).
|
440
|
+
should == { 'c' => 'config.yml', 'd' => true, 'f' => true }
|
322
441
|
end
|
323
442
|
|
324
443
|
it "should ignore leading/trailing non-dashed arguments" do
|
@@ -330,9 +449,32 @@ describe Clip do
|
|
330
449
|
Clip.hash(['-c', 'config.yml', '--mode', 'optimistic']).
|
331
450
|
should == { 'c' => 'config.yml', 'mode' => 'optimistic' }
|
332
451
|
end
|
333
|
-
|
452
|
+
|
334
453
|
it "should return an empty hash for empty ARGV" do
|
335
454
|
Clip.hash([]).should == {}
|
336
455
|
end
|
337
456
|
end
|
457
|
+
|
458
|
+
describe "stopping parsing after finding --" do
|
459
|
+
it "should not blow up" do
|
460
|
+
opts = Clip('--') {|p|}
|
461
|
+
opts.should be_valid
|
462
|
+
opts.remainder.should be_empty
|
463
|
+
end
|
464
|
+
|
465
|
+
it "should not parse after --" do
|
466
|
+
opts = Clip('-- --help') {|p|}
|
467
|
+
opts.should be_valid
|
468
|
+
opts.remainder.should include('--help')
|
469
|
+
end
|
470
|
+
|
471
|
+
it "should parse args before --" do
|
472
|
+
opts = Clip('-v -- other stuff') do |p|
|
473
|
+
p.flag 'v', 'verbose'
|
474
|
+
end
|
475
|
+
opts.should be_valid
|
476
|
+
opts.should be_verbose
|
477
|
+
opts.remainder.should include('other', 'stuff')
|
478
|
+
end
|
479
|
+
end
|
338
480
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clip
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Vollmer
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-09-19 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -31,10 +31,13 @@ extensions: []
|
|
31
31
|
|
32
32
|
extra_rdoc_files:
|
33
33
|
- History.txt
|
34
|
-
-
|
34
|
+
- Manifest.txt
|
35
35
|
files:
|
36
36
|
- History.txt
|
37
|
-
-
|
37
|
+
- Manifest.txt
|
38
|
+
- README.rdoc
|
39
|
+
- Rakefile
|
40
|
+
- clip.gemspec
|
38
41
|
- lib/clip.rb
|
39
42
|
- spec/clip_spec.rb
|
40
43
|
has_rdoc: true
|