optout 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.rdoc +123 -0
  2. data/lib/optout.rb +596 -0
  3. data/spec/optout_spec.rb +380 -0
  4. metadata +98 -0
data/README.rdoc ADDED
@@ -0,0 +1,123 @@
1
+ = Optout
2
+
3
+ Optout helps you write code that will call +exec+ and +system+ like functions. It allows you to map hash keys to command line
4
+ arguments and define validation rules that must be met before the command line arguments are created.
5
+
6
+ == Overview
7
+
8
+ require "optout"
9
+
10
+ # Create options for `gem`
11
+ optout = Optout.options do
12
+ on :gem, "install", :required => true
13
+ on :os, "--platform", %w(mswin cygwin mingw)
14
+ on :version, "-v", /\A\d+(\.\d+)*\z/
15
+ on :user, "--user-install"
16
+ on :location, "-i", Optout::Dir.exists.under(ENV["HOME"])
17
+ end
18
+
19
+ options = {
20
+ :gem => "rake",
21
+ :os => "mswin",
22
+ :version => "0.9.2",
23
+ :user => true
24
+ }
25
+
26
+ exec "gem", *optout.argv(options)
27
+ # Returns: ["install", "rake", "--platform", "mswin", "-v", "0.9.2", "--user-install"]
28
+
29
+ `gem #{optout.shell(options)}`
30
+ # Returns: "'install' 'rake' --platform 'mswin' -v '0.9.2' --user-install"
31
+
32
+ == Install
33
+
34
+ <code>gem install optout</code>
35
+
36
+ == Defining Options
37
+
38
+ Inorder to turn the incoming option hash into something useful you must tell +Optout+ a bit about your options. This is done by calling <code>Optout#on</code> and passing it the name of a key in the option hash. The simplest case is an option with no switch:
39
+
40
+ optout = Optout.options do
41
+ on :path
42
+ end
43
+
44
+ optout.shell(:path => "/home/sshaw")
45
+ # Returns: '/home/sshaw'
46
+
47
+ Key names can be a +Symbol+ or a +String+, +Optout+ will check for both in the option hash.
48
+
49
+ If the option has a switch it can be given after the option's key:
50
+
51
+ optout = Optout.options do
52
+ on :path, "-p"
53
+ end
54
+
55
+ optout.shell(:path => "/home/sshaw")
56
+ # Returns: -p '/home/sshaw'
57
+
58
+ Some programs can be finicky about the space between the switch and the value, or require options
59
+ in a different format. +Optout+ accepts various configuration options that can remdy this:
60
+
61
+ optout = Optout.options do
62
+ on :path, "-p", :arg_separator => ""
63
+ end
64
+
65
+ optout.shell(:path => "/home/sshaw")
66
+ # Returns: -p'/home/sshaw'
67
+
68
+ optout = Optout.options do
69
+ on :path, "--path", :arg_separator => "=", :required => true
70
+ end
71
+
72
+ optout.shell(:path => "/home/sshaw")
73
+ # Returns: --path='/home/sshaw'
74
+
75
+ optout.shell({})
76
+ # Raises: Optout::OptionRequired
77
+
78
+ == Validating Options
79
+
80
+ +Optout+ can validate your options too. Just specify the validation rule after the option's key or switch:
81
+
82
+ optout = Optout.options do
83
+ # Must match [a-z]
84
+ on :path, "-p", /[a-z]/
85
+ end
86
+
87
+ optout = Optout.options do
88
+ # Must be true, false, or nil
89
+ on :path, "-p", Optout::Boolean
90
+ end
91
+
92
+ optout = Optout.options do
93
+ # Must be in the given set
94
+ on :path, %w(/home/sshaw /Users/gatinha /Users/fofinha)
95
+ end
96
+
97
+ optout = Optout.options do
98
+ # Must be a diretory under "/sshaw" and have user write permission
99
+ on :path, Optout::Dir.under("/home").permissions("w")
100
+ end
101
+
102
+ optout.shell(:path => "/root")
103
+ # Raises: Optout::OptionInvalid
104
+
105
+ There are plenty of other features, see {the RDoc}[http://rubydoc.info/github/sshaw/optout/frames].
106
+
107
+ == More Info
108
+
109
+ === RDoc
110
+
111
+ http://rubydoc.info/github/sshaw/optout/frames
112
+
113
+ === Bugs
114
+
115
+ https://github.com/sshaw/optout/issues
116
+
117
+ == Author
118
+
119
+ Skye Shaw [sshaw AT lucas.cis.temple.edu]
120
+
121
+ == License
122
+
123
+ Released under the MIT License: http://www.opensource.org/licenses/MIT
data/lib/optout.rb ADDED
@@ -0,0 +1,596 @@
1
+ require "rbconfig"
2
+ require "pathname"
3
+
4
+ class Optout
5
+ VERSION = "0.0.1"
6
+
7
+ class OptionError < StandardError
8
+ attr :key
9
+ def initialize(key, message)
10
+ super(message)
11
+ @key = key
12
+ end
13
+ end
14
+
15
+ class OptionRequired < OptionError
16
+ def initialize(key)
17
+ super(key, "option required: '#{key}'")
18
+ end
19
+ end
20
+
21
+ class OptionUnknown < OptionError
22
+ def initialize(key)
23
+ super(key, "option unknown: '#{key}'")
24
+ end
25
+ end
26
+
27
+ class OptionInvalid < OptionError
28
+ def initialize(key, message)
29
+ super(key, "option invalid: #{key}; #{message}")
30
+ end
31
+ end
32
+
33
+ class << self
34
+ ##
35
+ # Define a set of options to validate and create.
36
+ #
37
+ # === Parameters
38
+ #
39
+ # [config (Hash)] Configuration options
40
+ # [definition (Proc)] Option definitions
41
+ #
42
+ # === Configuration Options
43
+ #
44
+ # [:arg_separator] Set the default for all subsequent options defined via +on+. See Optout#on@Options.
45
+ # [:check_keys] If +true+ an <code>Optout::OptionUnknown</code> error will be raised when the incoming option hash contains a key that has not been associated with an option.
46
+ # Defaults to +true+.
47
+ # [:multiple] Set the default for all subsequent options defined via +on+. See Optout#on@Options.
48
+ # [:required] Set the default for all subsequent options defined via +on+. See Optout#on@Options.
49
+ #
50
+ # === Errors
51
+ #
52
+ # [ArgumentError] Calls to +on+ from inside a block can raise an +ArgumentError+.
53
+ #
54
+ # === Examples
55
+ #
56
+ # optz = Optout.options do
57
+ # on :all, "-a"
58
+ # on :size, "-b", /\A\d+\z/, :required => true
59
+ # on :file, Optout::File.under("/home/sshaw"), :default => "/home/sshaw/tmp"
60
+ # end
61
+ #
62
+ # optz.shell(:all => true, :size => 1024, :file => "/home/sshaw/some file")
63
+ # # Creates: "-a -b '1024' '/home/sshaw/some file'
64
+ #
65
+ # optz = Optout.options :required => true, :check_keys => false do
66
+ # on :lib, :index => 2
67
+ # on :prefix, "--prefix" , %w{/sshaw/lib /sshaw/usr/lib}, :arg_separator => "="
68
+ # end
69
+ #
70
+ # optz.argv(:lib => "libssl2",
71
+ # :prefix => "/sshaw/usr/lib",
72
+ # :bad_key => "No error raised because of moi")
73
+ # # Creates: ["--prefix='/sshaw/usr/lib'", "libssl2"]
74
+ #
75
+
76
+ def options(config = {}, &block)
77
+ optout = new(config)
78
+ optout.instance_eval(&block) if block_given?
79
+ optout
80
+ end
81
+
82
+ alias :keys :options
83
+ end
84
+
85
+ def initialize(args = {})
86
+ @options = {}
87
+ @check_keys = args.include?(:check_keys) ? args[:check_keys] : true
88
+ #@opt_seperator = args[:opt_seperator]
89
+ @default_opt_options = {
90
+ :required => args[:required],
91
+ :multiple => args[:multiple],
92
+ :arg_separator => args[:arg_separator]
93
+ }
94
+ end
95
+
96
+ ##
97
+ # Define an option.
98
+ #
99
+ # === Parameters
100
+ #
101
+ # [key (Symbol)] The key of the option in the option hash that will passed to +shell+ or +argv+.
102
+ # [switch (String)] Optional. The option's command line switch. If no switch is given only the option's value is output.
103
+ # [rule (Object)] Optional. Validation rule, see {Validating}[rdoc-ref:#on@Validating].
104
+ # [options (Hash)] Additional option configuration, see {Options}[rdoc-ref:#on@Options].
105
+ #
106
+ # === Options
107
+ #
108
+ # [:arg_separator] The +String+ used to separate the option's switch from its value. Defaults to <code>" "</code> (space).
109
+ # [:default] The option's default value. This will be used if the option is +nil+ or +empty?+.
110
+ # [:index] The index of the option in the resulting +String+ or +Array+.
111
+ # [:multiple] If +true+ the option will accept multiple values. If +false+ an <code>Optout::OptionInvalid</code> error will be raised if the option
112
+ # contains multiple values. If +true+ multiple values are joined on a comma, you can set this to a +String+
113
+ # to join on that string instead. Defaults to +false+.
114
+ # [:required] If +true+ the option must contian a value i.e., it must not be +false+ or +nil+ else an <code>Optout::OptionRequired</code> error will be raised.
115
+ # Defaults to +false+.
116
+ # [:validator] An additional validation rule, see Validating.
117
+ #
118
+ # === Validating
119
+ #
120
+ # A Validator will only be applied if there's a value. If the option is required pass <code>:required => true</code>
121
+ # to +on+ when defining the option. Validation rules can be in one of the following forms:
122
+ #
123
+ # [Regular Expresion] A pattern to match the option's value against.
124
+ # [An Array] Restrict the option's value(s) to item(s) contained in the given array.
125
+ # [Class] Restrict the option's value to instances of the given class.
126
+ # [Optout::Boolean] Restrict the option's value to something boolean, i.e., +true+, +false+, or +nil+.
127
+ # [Optout::File] The option's value must be a file. Note that the file does not have to exist. <code>Optout::File</code> has several methods that can be used to tune validation, see Optout::File.
128
+ # [Optout::Dir] The option's value must be a directory. <code>Optout::Dir</code> has several methods that can be used to tune validation, see Optout::Dir.
129
+ #
130
+ # === Errors
131
+ #
132
+ # [ArgumentError] An +ArgumentError+ is raised if +key+ is +nil+ or +key+ has already been defined
133
+
134
+ def on(*args)
135
+ key = args.shift
136
+
137
+ # switch is optional, this could be a validation rule
138
+ switch = args.shift if String === args[0]
139
+ raise ArgumentError, "option key required" if key.nil?
140
+ raise ArgumentError, "option already defined: '#{key}'" if @options[key]
141
+
142
+ opt_options = Hash === args.last ? @default_opt_options.merge(args.pop) : @default_opt_options.dup
143
+ opt_options[:index] ||= @options.size
144
+ opt_options[:validator] = args.shift
145
+
146
+ @options[key] = Option.create(key, switch, opt_options)
147
+ end
148
+
149
+ ##
150
+ # Create an argument string that can be to passed to a +system+ like function.
151
+ #
152
+ # === Parameters
153
+ # [options (Hash)] The option hash used to construct the argument string.
154
+ #
155
+ # === Returns
156
+ # [String] The argument string.
157
+ #
158
+ # === Errors
159
+ # See Optout#argv@Errors
160
+ def shell(options = {})
161
+ create_options(options).map { |opt| opt.to_s }.join " "
162
+ end
163
+
164
+ ##
165
+ # Create an +argv+ array that can be to passed to an +exec+ like function.
166
+ #
167
+ # === Parameters
168
+ # [options (Hash)] The options hash used to construct the +argv+ array.
169
+ #
170
+ # === Returns
171
+ # [Array] The +argv+ array, each element is a +String+
172
+ #
173
+ # === Errors
174
+ # [Optout::OptionRequired] The option hash is missing a required value.
175
+ # [Optout::OptionUnknown] The option hash contains an unknown key.
176
+ # [Optout::OptionInvalid] The option hash contains a value the does not conform to the defined specification.
177
+
178
+ def argv(options = {})
179
+ create_options(options).map { |opt| opt.to_a }.flatten
180
+ end
181
+
182
+ private
183
+ def create_options(options = {})
184
+ argv = []
185
+ options = options.dup
186
+
187
+ @options.each do |key, klass|
188
+ value = options.delete(key) || options.delete(key.to_s)
189
+ opt = klass.new(value)
190
+ opt.validate!
191
+ argv << opt
192
+ end
193
+
194
+ if @check_keys && options.any?
195
+ raise OptionUnknown, options.keys[0]
196
+ end
197
+
198
+ argv.select { |opt| !opt.empty? }.
199
+ sort_by { |opt| opt.index }
200
+ end
201
+
202
+ class Option
203
+ attr :key
204
+ attr :value
205
+ attr :index
206
+
207
+ ##
208
+ # Creates a subclass of +Option+
209
+ #
210
+ # === Parameters
211
+ #
212
+ # [key (Symbol)] The hash key that will be used to lookup and create this option.
213
+ # [switch (String)] Optional.
214
+ # [config (Hash)] Describe how to validate and create the option.
215
+ #
216
+ # === Examples
217
+ #
218
+ # MyOption = Optout::Option.create(:quality, "-q", :arg_separator => "=", :validator => Fixnum)
219
+ # opt = MyOption.new(75)
220
+ # opt.empty? # false
221
+ # opt.validate!
222
+ # opt.to_s # "-q='75'"
223
+ #
224
+ def self.create(key, *args)
225
+ options = Hash === args.last ? args.pop : {}
226
+ switch = args.shift
227
+
228
+ Class.new(Option) do
229
+ define_method(:initialize) do |*v|
230
+ @key = key
231
+ @switch = switch
232
+ @value = v.shift || options[:default]
233
+ @joinon = String === options[:multiple] ? options[:multiple] : ","
234
+ @index = options[:index].to_i
235
+ @separator = options[:arg_separator] || " "
236
+
237
+ @validators = []
238
+ @validators << Validator::Required.new(options[:required])
239
+ @validators << Validator::Multiple.new(options[:multiple])
240
+
241
+ # Could be an Array..?
242
+ @validators << Validator.for(options[:validator]) if options[:validator]
243
+ end
244
+ end
245
+ end
246
+
247
+ ##
248
+ # Turn the option into a string that can be to passed to a +system+ like function.
249
+ # This _does not_ validate the option. You must call <code>validate!</code>.
250
+ #
251
+ # === Examples
252
+ #
253
+ # MyOption = Optout::Option.create(:level, "-L", %w(fatal info warn debug))
254
+ # MyOption.new("debug").to_s
255
+ # # Returns: "-L 'debug'"
256
+ #
257
+ def to_s
258
+ opt = create_opt_array
259
+ if opt.any?
260
+ if opt.size == 1
261
+ opt[0] = quote(opt[0]) unless @switch
262
+ else
263
+ opt[1] = quote(opt[1])
264
+ end
265
+ end
266
+ opt.join(@separator)
267
+ end
268
+
269
+ ##
270
+ # Turn the option into an array that can be passed to an +exec+ like function.
271
+ # This _does not_ validate the option. You must call <code>validate!</code>.
272
+ #
273
+ # === Examples
274
+ #
275
+ # MyOption = Optout::Option.create(:level, "-L", %w(fatal info warn debug))
276
+ # MyOption.new("debug").to_a
277
+ # # Returns: [ "-L", "debug" ]
278
+ #
279
+ def to_a
280
+ opt = create_opt_array
281
+ opt = [ opt.join(@separator) ] unless @separator =~ /\A\s+\z/
282
+ opt
283
+ end
284
+
285
+ ##
286
+ # Check if the option contains a value
287
+ #
288
+ # === Returns
289
+ #
290
+ # +false+ if the option's value is +false+, +nil+, or an empty +String+, +true+ otherwise.
291
+ #
292
+ def empty?
293
+ !@value || @value.to_s.empty?
294
+ end
295
+
296
+ ##
297
+ # Validate the option
298
+ #
299
+ # === Errors
300
+ #
301
+ # [OptionRequired] The option is missing a required value
302
+ # [OptionUnknown] The option contains an unknown key
303
+ # [OptionInvalid] The option contains a value the does not conform to the defined specification
304
+ #
305
+ def validate!
306
+ @validators.each { |v| v.validate!(self) }
307
+ end
308
+
309
+ private
310
+ def create_opt_array
311
+ opt = []
312
+ opt << @switch if @switch && @value
313
+ opt << normalize(@value) if !empty? && @value != true # Only include @value for non-boolean options
314
+ opt
315
+ end
316
+
317
+ def quote(value)
318
+ if unix?
319
+ sprintf "'%s'", value.gsub("'") { "'\\''" }
320
+ else
321
+ # TODO: Real cmd.exe quoting
322
+ %|"#{value}"|
323
+ end
324
+ end
325
+
326
+ def unix?
327
+ RbConfig::CONFIG["host_os"] !~ /mswin|mingw/i
328
+ end
329
+
330
+ def normalize(value)
331
+ value.respond_to?(:entries) ? value.entries.join(@joinon) : value.to_s.strip
332
+ end
333
+ end
334
+
335
+ module Validator #:nodoc: all
336
+ def self.for(setting)
337
+ if setting.respond_to?(:validate!)
338
+ setting
339
+ else
340
+ # Load validator based on the setting's name or the name of its class
341
+ validator = setting.class.name
342
+ if validator == "Class"
343
+ name = setting.name.split("::", 2)
344
+ validator = name[1] if name[1] && name[0] == "Optout"
345
+ end
346
+
347
+ # Support 1.8 and 1.9, avoid String/Symbol and const_defined? differences
348
+ if !constants.include?(validator) && !constants.include?(validator.to_sym)
349
+ raise ArgumentError, "don't know how to validate with #{setting}"
350
+ end
351
+
352
+ const_get(validator).new(setting)
353
+ end
354
+ end
355
+
356
+ Base = Struct.new :setting
357
+
358
+ # Check for multiple values
359
+ class Multiple < Base
360
+ def validate!(opt)
361
+ if !opt.empty? && opt.value.respond_to?(:entries) && opt.value.entries.size > 1 && !multiple_values_allowed?
362
+ raise OptionInvalid.new(opt.key, "multiple values are not allowed")
363
+ end
364
+ end
365
+
366
+ private
367
+ def multiple_values_allowed?
368
+ !!setting
369
+ end
370
+ end
371
+
372
+ class Required < Base
373
+ def validate!(opt)
374
+ if opt.empty? && option_required?
375
+ raise OptionRequired, opt.key
376
+ end
377
+ end
378
+
379
+ private
380
+ def option_required?
381
+ !!setting
382
+ end
383
+ end
384
+
385
+ class Array < Base
386
+ def validate!(opt)
387
+ values = [opt.value].flatten
388
+ values.each do |e|
389
+ if !setting.include?(e)
390
+ raise OptionInvalid.new(opt.key, "value '#{e}' must be one of (#{setting.join(", ")})")
391
+ end
392
+ end
393
+ end
394
+ end
395
+
396
+ class Regexp < Base
397
+ def validate!(opt)
398
+ if !opt.empty? && opt.value.to_s !~ setting
399
+ raise OptionInvalid.new(opt.key, "value '#{opt.value}' does not match pattern #{setting}")
400
+ end
401
+ end
402
+ end
403
+
404
+ class Class < Base
405
+ def validate!(opt)
406
+ if !(setting === opt.value)
407
+ raise OptionInvalid.new(opt.key, "value '#{opt.value}' must be type #{setting}")
408
+ end
409
+ end
410
+ end
411
+
412
+ class Boolean < Base
413
+ def validate!(opt)
414
+ if !(opt.value == true || opt.value == false || opt.value.nil?)
415
+ raise OptionInvalid.new(opt.key, "does not accept an argument")
416
+ end
417
+ end
418
+ end
419
+
420
+ class File < Base
421
+ RULES = %w|under named permissions|;
422
+ MODES = { "x" => :executable?, "r" => :readable?, "w" => :writable? }
423
+
424
+ RULES.each do |r|
425
+ define_method(r) do |arg|
426
+ instance_variable_set("@#{r}", arg)
427
+ self
428
+ end
429
+ end
430
+
431
+ def exists(wanted = true)
432
+ @exists = wanted
433
+ self
434
+ end
435
+
436
+ def validate!(opt)
437
+ return if opt.empty?
438
+
439
+ @file = Pathname.new(opt.value.to_s)
440
+ what = self.class.name.split("::")[-1].downcase
441
+ error = case
442
+ when !under?
443
+ "#{what} must be under '#{@under}'"
444
+ when !named?
445
+ "#{what} name must match '#{@named}'"
446
+ when !permissions?
447
+ "#{what} must have user permission of #{@permissions}"
448
+ when !exists?
449
+ "#{what} '#{@file}' does not exist"
450
+ when !creatable?
451
+ # TODO: Why can't it be created!?
452
+ "can't create a #{what} at '#{@file}'"
453
+ end
454
+ raise OptionInvalid.new(opt.key, error) if error
455
+ end
456
+
457
+ protected
458
+ def correct_type?
459
+ @file.file?
460
+ end
461
+
462
+ def permissions?
463
+ !@permissions ||
464
+ exists? &&
465
+ @permissions.split(//).inject(true) { |can, m| can && MODES[m] && @file.send(MODES[m]) }
466
+ end
467
+
468
+ def exists?
469
+ !@exists || @file.exist? && correct_type?
470
+ end
471
+
472
+ def named?
473
+ basename = @file.basename.to_s
474
+ !@named ||
475
+ (::Regexp === @named ?
476
+ basename =~ @named :
477
+ basename == @named)
478
+ end
479
+
480
+ def under?
481
+ !@under ||
482
+ exists? &&
483
+ (::Regexp === @under ?
484
+ @file.parent.to_s =~ @under :
485
+ @file.parent.expand_path.to_s == ::File.expand_path(@under))
486
+ end
487
+
488
+ def creatable?
489
+ @file.exist? && correct_type? ||
490
+ !@file.exist? && @file.parent.exist? && @file.parent.writable?
491
+ end
492
+ end
493
+
494
+ class Dir < File
495
+ protected
496
+ def correct_type?
497
+ @file.directory?
498
+ end
499
+ end
500
+ end
501
+
502
+
503
+ #
504
+ # These are shortcuts and/or marker classes used by the public interface so Validator.for()
505
+ # can load the equivalent validation class
506
+ #
507
+
508
+ ##
509
+ # <code>Optout::File</code> is a validaton rule that can be used to check that an option's value is a path to a file.
510
+ # By default <code>Optout::File</code> *does* *not* *check* that the file exists. Instead, it checks that the file's parent directory
511
+ # exists. This is done so that you can validate a path that _will_ be created by the program the options are for.
512
+ # If you _do_ want the file to exist just call the +exists+ method.
513
+ #
514
+ # Validation rules can be combined:
515
+ #
516
+ # Optout.options do
517
+ # on :path, "--path", Optout::File.exists.under("/home").named(/\.txt$/)
518
+ # end
519
+ #
520
+ class File
521
+ class << self
522
+ Validator::File::RULES.each do |r|
523
+ define_method(r) { |arg| proxy_for.new.send(r, arg) }
524
+ end
525
+
526
+ ##
527
+ # :singleton-method: under
528
+ # :call-seq:
529
+ # under(path)
530
+ # under(Regexp)
531
+ #
532
+ # The option must be under the given path.
533
+ #
534
+ # === Parameters
535
+ #
536
+ # This can be a +String+ denoting the parent directory or a +Regexp+ to match the parent directory against.
537
+
538
+
539
+ ##
540
+ # :singleton-method: named
541
+ # :call-seq:
542
+ # named(basename)
543
+ # named(Regexp)
544
+ #
545
+ # The option's basename must match the given basename.
546
+ #
547
+ # === Parameters
548
+ #
549
+ # A +String+ denoting the basename or a +Regexp+ to match the basename against.
550
+
551
+
552
+ ##
553
+ # :singleton-method: permissions
554
+ # :call-seq:
555
+ # permissions(symbolic_mode)
556
+ #
557
+ # The option's user permissions must match the given permission(s).
558
+ #
559
+ # === Parameters
560
+ #
561
+ # A +String+ denoting the desired permission. Any combination of <code>"r"</code>, <code>"w"</code> and <code>"x"</code> is supported.
562
+
563
+ ##
564
+ #
565
+ # If +wanted+ is true the file must exist.
566
+ #
567
+ def exists(wanted = true)
568
+ proxy_for.new.exists(wanted)
569
+ end
570
+
571
+ def proxy_for #:nodoc:
572
+ Validator::File
573
+ end
574
+ end
575
+ end
576
+
577
+ ##
578
+ # <code>Optout::Dir</code> is a validaton rule that can be used to check that an option's value is a path to a directory.
579
+ # Validation rules can be combined:
580
+ #
581
+ # Optout.options do
582
+ # on :path, "--path", Optout::Dir.exists.under("/tmp").named(/\d$/)
583
+ # end
584
+ #
585
+ # See Optout::File for a list of methods.
586
+ #
587
+ class Dir < File
588
+ def self.proxy_for #:nodoc:
589
+ Validator::Dir
590
+ end
591
+ end
592
+
593
+ class Boolean #:nodoc:
594
+ end
595
+ end
596
+
@@ -0,0 +1,380 @@
1
+ require "optout"
2
+ require "tempfile"
3
+ require "fileutils"
4
+ require "rbconfig"
5
+
6
+ def create_optout(options = {})
7
+ Optout.options(options) do
8
+ on :x, "-x"
9
+ on :y, "-y"
10
+ end
11
+ end
12
+
13
+ def optout_option(*options)
14
+ Optout.options { on :x, "-x", *options }
15
+ end
16
+
17
+ class Optout
18
+ class Option
19
+ def unix?
20
+ true
21
+ end
22
+ end
23
+ end
24
+
25
+ shared_examples_for "something that validates files" do
26
+ before(:all) { @tmpdir = Dir.mktmpdir }
27
+ after(:all) { FileUtils.rm_rf(@tmpdir) }
28
+
29
+ def options
30
+ { :x => @file }
31
+ end
32
+
33
+ it "should not raise an exception if a file does not exist but its directory does" do
34
+ file = File.join(@tmpdir, "__bad__")
35
+ optout = optout_option(@validator)
36
+ proc { optout.argv(:x => file) }.should_not raise_exception
37
+ end
38
+
39
+ describe "permissions" do
40
+ # Only some chmod() modes work on Win
41
+ if RbConfig::CONFIG["host_os"] !~ /mswin|mingw/i
42
+ it "should raise an exception when user permissions don't match" do
43
+ FileUtils.chmod(0100, @file)
44
+ optout = optout_option(@validator.permissions("r"))
45
+ proc { optout.argv(options) }.should raise_exception(Optout::OptionInvalid, /user permission/)
46
+ end
47
+
48
+ it "should not raise an exception when user permissions match" do
49
+ checker = proc do |validator|
50
+ proc { optout_option(validator).argv(options) }.should_not raise_exception
51
+ end
52
+
53
+ FileUtils.chmod(0100, @file)
54
+ checker.call(@validator.permissions("x"))
55
+
56
+ FileUtils.chmod(0200, @file)
57
+ checker.call(@validator.permissions("w"))
58
+
59
+ FileUtils.chmod(0400, @file)
60
+ checker.call(@validator.permissions("r"))
61
+
62
+ FileUtils.chmod(0700, @file)
63
+ checker.call(@validator.permissions("rwx"))
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "exists" do
69
+ it "should raise an exception if the file does not exist" do
70
+ optout = optout_option(@validator.exists)
71
+ proc { optout.argv(:x => @file + "no_file") }.should raise_exception(Optout::OptionInvalid, /does not exist/)
72
+ proc { optout.argv(options) }.should_not raise_exception
73
+ end
74
+ end
75
+
76
+ describe "under a directory" do
77
+ it "should raise an exception if not under the given directory" do
78
+ optout = optout_option(@validator.under(File.join("wrong", "path")))
79
+ proc { optout.argv(options) }.should raise_exception(Optout::OptionInvalid, /must be under/)
80
+
81
+ optout = optout_option(@validator.under(@tmpdir))
82
+ proc { optout.argv(options) }.should_not raise_exception
83
+ end
84
+
85
+ it "should raise an exception if the parent directory does not match the given pattern" do
86
+ # We need to respect the @file's type to ensure other validation rules implicitly applied by the @validator pass.
87
+ # First create parent dirs to validate against
88
+ tmp = File.join(@tmpdir, "a1", "b1")
89
+ FileUtils.mkdir_p(tmp)
90
+
91
+ # Then copy the target of the validation (file or directory) under the parent dir.
92
+ FileUtils.cp_r(@file, tmp)
93
+
94
+ # And create the option's value
95
+ tmp = File.join(tmp, File.basename(@file))
96
+ options = { :x => tmp }
97
+
98
+ optout = optout_option(@validator.under(/X$/))
99
+ proc { optout.argv(options) }.should raise_exception(Optout::OptionInvalid, /must be under/)
100
+
101
+ [ %r|(/[a-z]\d){2}|, %r|[a-z]\d$| ].each do |r|
102
+ optout = optout_option(@validator.under(r))
103
+ proc { optout.argv(options) }.should_not raise_exception
104
+ end
105
+ end
106
+ end
107
+
108
+ describe "basename" do
109
+ it "should raise an exception if it does not equal the given value" do
110
+ optout = optout_option(@validator.named("__bad__"))
111
+ proc { optout.argv(options) }.should raise_exception(Optout::OptionInvalid, /name must match/)
112
+
113
+ optout = optout_option(@validator.named(File.basename(@file)))
114
+ proc { optout.argv(options) }.should_not raise_exception
115
+ end
116
+
117
+ it "should raise an exception if it does not match the given pattern" do
118
+ optout = optout_option(@validator.named(/\A-_-_-_/))
119
+ proc { optout.argv(options) }.should raise_exception(Optout::OptionInvalid, /name must match/)
120
+
121
+ ends_with = File.basename(@file)[/.{2}\z/]
122
+ optout = optout_option(@validator.named(/#{Regexp.quote(ends_with)}\z/))
123
+ proc { optout.argv(options) }.should_not raise_exception
124
+ end
125
+ end
126
+ end
127
+
128
+ describe Optout do
129
+ describe "defining options" do
130
+ before(:each) { @optout = Optout.new }
131
+
132
+ it "should require the option's key" do
133
+ proc { @optout.on }.should raise_exception(ArgumentError, /option key required/)
134
+ proc { Optout.options { on } }.should raise_exception(ArgumentError, /option key required/)
135
+ end
136
+
137
+ it "should not allow an option to be defined twice" do
138
+ @optout.on :x
139
+ proc { @optout.on :x }.should raise_exception(ArgumentError, /already defined/)
140
+ proc do
141
+ Optout.options do
142
+ on :x
143
+ on :x
144
+ end
145
+ end.should raise_exception(ArgumentError, /already defined/)
146
+ end
147
+ end
148
+
149
+ describe "creating options" do
150
+ before(:each) { @optout = create_optout }
151
+
152
+ context "as a string" do
153
+ it "should only output the option's value if there's no switch" do
154
+ optout = Optout.options { on :x }
155
+ optout.shell(:x => "x").should eql("'x'")
156
+ end
157
+
158
+ it "should output an empty string if the option hash is empty" do
159
+ @optout.shell({}).should be_empty
160
+ end
161
+
162
+ it "should only output the option's switch if its value if true" do
163
+ @optout.shell(:x => true, :y => true).should eql("-x -y")
164
+ end
165
+
166
+ it "should not output the option if its value is false" do
167
+ @optout.shell(:x => false, :y => true).should eql("-y")
168
+ end
169
+
170
+ it "should only output the options that have a value" do
171
+ @optout.shell(:x => "x", :y => nil).should eql("-x 'x'")
172
+ end
173
+
174
+ it "should output all of the options" do
175
+ @optout.shell(:x => "x", :y => "y").should eql("-x 'x' -y 'y'")
176
+ end
177
+
178
+ it "should escape the single quote char" do
179
+ @optout.shell(:x => "' a'b'c '").should eql(%q|-x ''\'' a'\''b'\''c '\'''|)
180
+ end
181
+
182
+ it "should not separate switches from their value" do
183
+ optout = create_optout(:arg_separator => "")
184
+ optout.shell(:x => "x", :y => "y").should eql("-x'x' -y'y'")
185
+ end
186
+
187
+ it "should seperate all switches from their value with a '='" do
188
+ optout = create_optout(:arg_separator => "=")
189
+ optout.shell(:x => "x", :y => "y").should eql("-x='x' -y='y'")
190
+ end
191
+
192
+ it "should join all options with multiple values on a delimiter" do
193
+ optout = create_optout(:multiple => true)
194
+ optout.shell(:x => %w|a b c|, :y => "y").should eql("-x 'a,b,c' -y 'y'")
195
+ end
196
+
197
+ it "should join all options with multiple values on a ':'" do
198
+ optout = create_optout(:multiple => ":")
199
+ optout.shell(:x => %w|a b c|, :y => "y").should eql("-x 'a:b:c' -y 'y'")
200
+ end
201
+ end
202
+
203
+ context "as an array" do
204
+ it "should only output the option's value if there's no switch" do
205
+ optout = Optout.options { on :x }
206
+ optout.argv(:x => "x").should eql(["x"])
207
+ end
208
+
209
+ it "should output an empty array if the option hash is empty" do
210
+ @optout.argv({}).should be_empty
211
+ end
212
+
213
+ it "should only output the option's switch if its value if true" do
214
+ @optout.argv(:x => true, :y => true).should eql(["-x", "-y"])
215
+ end
216
+
217
+ it "should not output the option if its value is false" do
218
+ @optout.argv(:x => false, :y => true).should eql(["-y"])
219
+ end
220
+
221
+ it "should only output the options that have a value" do
222
+ @optout.argv(:x => "x", :y => nil).should eql(["-x", "x"])
223
+ end
224
+
225
+ it "should output all of the options" do
226
+ @optout.argv(:x => "x", :y => "y").should eql(["-x", "x", "-y", "y"])
227
+ end
228
+
229
+ it "should not escape the single quote char" do
230
+ @optout.argv(:x => "' a'b'c '").should eql(["-x", "' a'b'c '"])
231
+ end
232
+
233
+ it "should not separate switches from their value" do
234
+ optout = create_optout(:arg_separator => "")
235
+ optout.argv(:x => "x", :y => "y").should eql(["-xx", "-yy"])
236
+ end
237
+
238
+ it "should seperate all of switches from their value with a '='" do
239
+ optout = create_optout(:arg_separator => "=")
240
+ optout.argv(:x => "x", :y => "y").should eql(["-x=x", "-y=y"])
241
+ end
242
+
243
+ it "should join all options with multiple values on a delimiter" do
244
+ optout = create_optout(:multiple => true)
245
+ optout.argv(:x => %w|a b c|, :y => "y").should eql(["-x", "a,b,c", "-y", "y"])
246
+ end
247
+
248
+ it "should join all options with multiple values on a ':'" do
249
+ optout = create_optout(:multiple => ":")
250
+ optout.argv(:x => %w|a b c|, :y => "y").should eql(["-x", "a:b:c", "-y", "y"])
251
+ end
252
+ end
253
+ end
254
+
255
+ # TODO: Check exception.key
256
+ describe "validation rules" do
257
+ it "should raise an exception if the option hash contains an unknown key" do
258
+ optout = create_optout
259
+ proc { optout.argv(:bad => 123) }.should raise_exception(Optout::OptionUnknown)
260
+ end
261
+
262
+ it "should not raise an exception if the option hash contains an unknown key" do
263
+ optout = create_optout(:check_keys => false)
264
+ proc { optout.argv(:bad => 123) }.should_not raise_exception
265
+ end
266
+
267
+ it "should raise an exception if an option is missing" do
268
+ optout = create_optout(:required => true)
269
+ proc { optout.argv(:x => 123) }.should raise_exception(Optout::OptionRequired, /'y'/)
270
+ end
271
+
272
+ it "should raise an exception if a required option is missing" do
273
+ optout = Optout.options do
274
+ on :x
275
+ on :y, :required => true
276
+ end
277
+
278
+ [ { :x => 123 }, { :x => 123, :y => false } ].each do |options|
279
+ proc { optout.argv(options) }.should raise_exception(Optout::OptionRequired, /'y'/)
280
+ end
281
+ end
282
+
283
+ it "should raise an exception if any option contains multiple values" do
284
+ optout = create_optout(:multiple => false)
285
+
286
+ [ { :x => 123, :y => %w|a b c| },
287
+ { :x => 123, :y => { :a => "b", :b => "c" }} ].each do |options|
288
+ proc { optout.argv(options) }.should raise_exception(Optout::OptionInvalid)
289
+ end
290
+
291
+ # An Array with 1 value is OK
292
+ proc { optout.argv(:x => 123, :y => %w|a|) }.should_not raise_exception(Optout::OptionInvalid)
293
+ end
294
+
295
+ it "should raise an exception if a single value option contains multiple values" do
296
+ optout = Optout.options do
297
+ on :x
298
+ on :y, :multiple => false
299
+ end
300
+
301
+ proc { optout.argv(:x => "x", :y => %w|a b c|) }.should raise_exception(Optout::OptionInvalid, /\by\b/)
302
+ end
303
+
304
+ it "should check the option's type" do
305
+ optout = optout_option(Float)
306
+ proc { optout.argv(:x => 123) }.should raise_exception(Optout::OptionInvalid, /type Float/)
307
+ proc { optout.argv(:x => 123.0) }.should_not raise_exception(Optout::OptionInvalid)
308
+ end
309
+
310
+ it "should raise an exception if the option's value is not in the given set" do
311
+ optout = optout_option(%w|sshaw skye|, :multiple => true)
312
+
313
+ [ "bob", [ "jack", "jill" ] ].each do |v|
314
+ proc { optout.argv(:x => v) }.should raise_exception(Optout::OptionInvalid)
315
+ end
316
+
317
+ [ "sshaw", [ "sshaw", "skye" ] ].each do |v|
318
+ proc { optout.argv(:x => v) }.should_not raise_exception
319
+ end
320
+ end
321
+
322
+ it "should raise an exception if the option's value does not match the given pattern" do
323
+ optout = optout_option(/X\d{2}/)
324
+ proc { optout.argv(:x => "X7") }.should raise_exception(Optout::OptionInvalid, /match pattern/)
325
+ proc { optout.argv(:x => "X21") }.should_not raise_exception
326
+ end
327
+
328
+ it "should raise an exception if the option has a non-boolean value" do
329
+ optout = optout_option(Optout::Boolean)
330
+ proc { optout.argv(:x => "x") }.should raise_exception(Optout::OptionInvalid, /does not accept/)
331
+ [ false, true, nil ].each do |v|
332
+ proc { optout.argv(:x => v) }.should_not raise_exception
333
+ end
334
+ end
335
+
336
+ it "should call a custom validator" do
337
+ klass = Class.new do
338
+ def validate!(opt)
339
+ raise "raise up!"
340
+ end
341
+ end
342
+
343
+ optout = optout_option(klass.new)
344
+ proc { optout.argv(:x => "x") }.should raise_exception(RuntimeError, "raise up!")
345
+ end
346
+
347
+ it "should raise an exception if an unknown validation rule is used" do
348
+ optout = optout_option("whaaaaa")
349
+ proc { optout.argv(:x => "x") }.should raise_exception(ArgumentError, /don't know how to validate/)
350
+ end
351
+
352
+ context "when validating a file" do
353
+ it_should_behave_like "something that validates files"
354
+
355
+ before(:all) do
356
+ @file = Tempfile.new("", @tmpdir).path
357
+ @validator = Optout::File
358
+ end
359
+
360
+ it "should raise an exception if it's not a file" do
361
+ optout = optout_option(@validator)
362
+ proc { optout.argv(:x => @tmpdir) }.should raise_exception(Optout::OptionInvalid, /can't create a file/)
363
+ end
364
+ end
365
+
366
+ context "when validating a directory" do
367
+ it_should_behave_like "something that validates files"
368
+
369
+ before(:all) do
370
+ @file = Dir.mktmpdir(nil, @tmpdir)
371
+ @validator = Optout::Dir
372
+ end
373
+
374
+ it "should raise an exception if it's not a directory" do
375
+ optout = optout_option(@validator)
376
+ proc { optout.argv(:x => Tempfile.new("", @tmpdir).path) }.should raise_exception(Optout::OptionInvalid)
377
+ end
378
+ end
379
+ end
380
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: optout
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Skye Shaw
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-01-04 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rake
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 59
29
+ segments:
30
+ - 0
31
+ - 9
32
+ - 0
33
+ version: 0.9.0
34
+ type: :development
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 27
45
+ segments:
46
+ - 1
47
+ - 3
48
+ - 0
49
+ version: 1.3.0
50
+ type: :development
51
+ version_requirements: *id002
52
+ description: " Optout helps you write code that will call exec() and system() like functions. It allows you to map hash keys to command line \n arguments and define validation rules that must be me before the command line options are created. \n"
53
+ email: sshaw@lucas.cis.temple.edu
54
+ executables: []
55
+
56
+ extensions: []
57
+
58
+ extra_rdoc_files:
59
+ - README.rdoc
60
+ files:
61
+ - lib/optout.rb
62
+ - spec/optout_spec.rb
63
+ - README.rdoc
64
+ homepage: http://github.com/sshaw/optout
65
+ licenses:
66
+ - MIT
67
+ post_install_message:
68
+ rdoc_options: []
69
+
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ requirements: []
91
+
92
+ rubyforge_project:
93
+ rubygems_version: 1.8.10
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: "The opposite of getopt(): validate an option hash and turn it into something appropriate for exec() and system() like functions"
97
+ test_files:
98
+ - spec/optout_spec.rb