flags 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +41 -0
  3. data/lib/flags.rb +545 -0
  4. data/test/flags_test.rb +277 -0
  5. metadata +70 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright © 2011 Ooyala, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ Flags - A Ruby Library for Simple Command-Line Flags
2
+ ====================================================
3
+ Flags is a framework for Ruby which allows the definition of command-line flags, which are parsed in and can be accessed smartly from within your Ruby code. This framework allows for numerous flag types and takes care of the process of type conversion and flag validation (type and value checking).
4
+
5
+ This flags framework is modeled after, and loosely resembles Google's, python-gflags library. The advantage of these kinds of flags over other well-known libraries is the ability to define flags in the place at which they are used (libraries, utility files, setup files), rather than entirely in the main execution function of the calling program.
6
+
7
+ Usage
8
+ -----
9
+ Require the "flags" gem at the top of your file. Then, define flags after require statements but before any other code. A flag with a given name may not be defined more than once, and doing so will raise an error. Examples:
10
+
11
+ Flags.define_string(:my_string_flag, "default_value", "This is my comment for a string flag")
12
+ Flags.define_symbol(:my_symbol_flag, :default_val, "This is my comment for a symbol flag")
13
+ Flags.define_int(:my_int_flag, 1000, "This is my comment for an int flag")
14
+ Flags.define_float(:my_float_flag, 2.0, "This is my comment for a float flag")
15
+ Flags.define_bool(:my_bool_flag, true, "This is my comment for a bool flag")
16
+
17
+ In your main:
18
+
19
+ if __FILE__ == $0
20
+ Flags.init # This will parse and consume all the flags from your command line that match a defined flag
21
+ end
22
+
23
+ Run your file with some command-line flags:
24
+ ./myfile -my_string_flag "foo" -my_int_flag 2
25
+
26
+ Note that specifying a boolean flag requires the word "true" or "false" following the flag name.
27
+
28
+ Then your code can access the flags any time after Flags.init with:
29
+ Flags.my_string_flag and Flags.my_int_flag
30
+
31
+ Contributing
32
+ ------------
33
+ Feel free to create tickets for enhancement ideas, or just fork and submit a pull request on our [GitHub page](https://github.com/ooyala/flags). Note that this first distribution is pre-release code, and while it is stable, there is potential for significant changes in future releases.
34
+
35
+ License
36
+ -------
37
+ Licensed under the [MIT license](http://opensource.org/licenses/mit-license.php).
38
+
39
+ Credits
40
+ -------
41
+ Copyright © 2011 Ooyala, Inc.
@@ -0,0 +1,545 @@
1
+ # Copyright © 2011 Ooyala, Inc.
2
+ # A simple command-line flag framework.
3
+ #
4
+ # Usage:
5
+ #
6
+ # Define flags at the top of your ruby file, after require statements but before any other code. A flag with
7
+ # a given name may not be defined more than once, and doing so will raise an error. Examples:
8
+ #
9
+ # Flags.define_string(:my_string_flag, "default_value", "This is my comment for a string flag")
10
+ # Flags.define_symbol(:my_symbol_flag, :default_val, "This is my comment for a symbol flag")
11
+ # Flags.define_int(:my_int_flag, 1000, "This is my comment for an int flag")
12
+ # Flags.define_float(:my_float_flag, 2.0, "This is my comment for a float flag")
13
+ # Flags.define_bool(:my_bool_flag, true, "This is my comment for a bool flag")
14
+ #
15
+ # In your main:
16
+ # if __FILE__ == $0
17
+ # Flags.init # This will parse and consume all the flags from your command line that match a defined flag
18
+ # end
19
+ #
20
+ # Run your file with some command-line flags:
21
+ # ./myfile -my_string_flag "foo" -my_int_flag 2
22
+ #
23
+ # Note that specifying a boolean flag requires the word "true" or "false" following the flag name.
24
+ #
25
+ # Then your code can access the flags any time after Flags.init with
26
+ # Flags.my_string_flag and Flags.my_int_flag
27
+
28
+ require 'yaml'
29
+
30
+ # Note that the Flags class is a singleton, and all of its methods and data storage are class-level rather
31
+ # than instance-level. It is currently not possible to have multiple Flags instances within a single process.
32
+ class Flags
33
+ # Parses the command line args and extracts flag value pairs. Should be called at least once before getting
34
+ # or setting any flag value. In the future, Flags will start throwing errors if any flag is accessed
35
+ # prior to Flags.init being called.
36
+ #
37
+ # NOTE: this method HAS SIDE EFFECTS - it removes the flag name/value pairs from the supplied array of
38
+ # argument strings! For example, if -meaning_of_life is a flag then after this call to Flags.init:
39
+ # args = [ "-meaning_of_life", "42", "path/to/output/file" ]
40
+ # Flags.init(args)
41
+ # the args array will be [ "path/to/output/file" ].
42
+ def self.init(args=$*)
43
+ @@init_called = true
44
+
45
+ if args.index("--help") || args.index("-help")
46
+ puts self.help_message
47
+ exit(0)
48
+ end
49
+ @@flags.each do |flag_name, flag|
50
+ flag_name = "-#{flag.name}"
51
+ setter = "#{flag.name}="
52
+ # Use a loop in order to consume all settings for a flag and to keep the last one.
53
+ while true
54
+ value = self.extract_value_for_flag(args, flag_name)
55
+ break if value.nil? # Check for nil because false values are ok for boolean flags.
56
+ self.send(setter, value)
57
+ end
58
+ end
59
+ end
60
+
61
+ # Defines a new flag of string type, such as:
62
+ # Flags.define_string(:my_flag_name, "hello world", "An example of a string flag")
63
+ def self.define_string(name, default_value, description)
64
+ self.define_flag(name, default_value, description, StringFlag)
65
+ end
66
+
67
+ # Defines a new flag of symbol type, such as:
68
+ # Flags.define_symbol(:my_flag_name, :a_symbol, "An example of a symbol flag")
69
+ def self.define_symbol(name, default_value, description)
70
+ self.define_flag(name, default_value, description, SymbolFlag)
71
+ end
72
+
73
+ # Defines a new flag of integer type, such as:
74
+ # Flags.define_int(:my_flag_name, 42, "An example of an integer flag")
75
+ def self.define_int(name, default_value, description)
76
+ self.define_flag(name, default_value, description, IntFlag)
77
+ end
78
+
79
+ # Defines a new flag of boolean type, such as:
80
+ # Flags.define_bool(:my_flag_name, true, "An example of a boolean flag")
81
+ def self.define_bool(name, default_value, description)
82
+ self.define_flag(name, default_value, description, BoolFlag)
83
+ end
84
+
85
+ # Defines a new flag of float type, such as:
86
+ # Flags.define_float(:my_flag_name, 3.14, "An example of a float flag")
87
+ def self.define_float(name, default_value, description)
88
+ self.define_flag(name, default_value, description, FloatFlag)
89
+ end
90
+
91
+ # TODO: Get rid of the longer define_*_flag() methods and just keep the define_*() ones.
92
+ # Alias the older, longer methods to the new, shorter ones.
93
+ class << self
94
+ alias :define_string_flag :define_string
95
+ alias :define_symbol_flag :define_symbol
96
+ alias :define_int_flag :define_int
97
+ alias :define_bool_flag :define_bool
98
+ alias :define_float_flag :define_float
99
+ end
100
+
101
+ # Registers a flag validator that checks the range of a flag.
102
+ def self.register_range_validator(name, range)
103
+ raise_unless_symbol!(name)
104
+ raise_unless_flag_defined!(name)
105
+ flag = @@flags[name]
106
+ flag.add_validator(RangeValidator.new(range))
107
+ end
108
+
109
+ # Registers a flag validator that ensures that the flag value is one of the allowed values.
110
+ def self.register_allowed_values_validator(name, *allowed_values)
111
+ raise_unless_symbol!(name)
112
+ raise_unless_flag_defined!(name)
113
+ flag = @@flags[name]
114
+ flag.add_validator(AllowedValuesValidator.new(allowed_values))
115
+ end
116
+
117
+ # Registers a flag validator that ensures that the flag value is not one of the disallowed values.
118
+ def self.register_disallowed_values_validator(name, *disallowed_values)
119
+ raise_unless_symbol!(name)
120
+ raise_unless_flag_defined!(name)
121
+ flag = @@flags[name]
122
+ flag.add_validator(DisallowedValuesValidator.new(disallowed_values))
123
+ end
124
+
125
+ # Registers a custom flag validator that will raise an error with the specified message if proc.call(value)
126
+ # returns false whenever a value is assigned to the flag.
127
+ def self.register_custom_validator(name, proc, error_message)
128
+ raise_unless_symbol!(name)
129
+ raise_unless_flag_defined!(name)
130
+ flag = @@flags[name]
131
+ validator = FlagValidator.new(proc, error_message)
132
+ flag.add_validator(validator)
133
+ end
134
+
135
+ # Returns the description of a flag, or nil if the given flag is not defined.
136
+ # TODO: Deprecate and remove?
137
+ def self.comment_for_flag(name)
138
+ return nil unless @@flags.key? name.to_sym
139
+ return @@flags[name.to_sym].description
140
+ end
141
+
142
+ # TODO: how to do aliases for class methods?
143
+ def self.flags_get_comment(name)
144
+ self.comment_for_flag(name)
145
+ end
146
+
147
+ def self.flags_get_default_value(name)
148
+ raise_unless_flag_defined!(name)
149
+ return @@flags[name.to_sym].default_value
150
+ end
151
+
152
+ def self.flags_is_default(name)
153
+ raise_unless_flag_defined!(name)
154
+ return @@flags[name.to_sym].default?
155
+ end
156
+
157
+ # Sets the value of the named flag if the default value has not been overridden. A default value can be
158
+ # overridden in one of these ways:
159
+ # 1) By specifying a value for the flag on the command line (i.e. "-x y")
160
+ # 2) By explicitly changing the value of the flag in the code (i.e. Flags.x = y)
161
+ # 3) By loading a value for the flag from a hash or yaml file.
162
+ #
163
+ # Returns true if the flag value was changed and false if it was not. Raises an error if the named flag does
164
+ # not exist.
165
+ def self.set_if_default(name, value)
166
+ raise_unless_symbol!(name)
167
+ raise_unless_flag_defined!(name)
168
+
169
+ flag = @@flags[name]
170
+ return false unless flag.default?
171
+ flag.value = value # NOTE: calling the setter method changes the internal is_default field to false
172
+ return true
173
+ end
174
+
175
+ # Takes a hash of { flag name => flag value } pairs and calls self.set_if_default() on each pair.
176
+ def self.set_multiple_if_default(hash)
177
+ hash.each_pair do |flag_name, flag_value|
178
+ self.set_if_default(flag_name, flag_value)
179
+ end
180
+ end
181
+
182
+ # Restores the value of the named flag to its default setting, and returns nil. Raises an error if the named
183
+ # flag does not exist.
184
+ def self.restore_default(name)
185
+ raise_unless_symbol!(name)
186
+ raise_unless_flag_defined!(name)
187
+
188
+ @@flags[name].restore_default
189
+ nil
190
+ end
191
+
192
+ # Takes an Array or Set of flag names, and calls self.restore_default() on each element.
193
+ def self.restore_multiple_defaults(names)
194
+ names.each do |flag_name|
195
+ self.restore_default(flag_name)
196
+ end
197
+ end
198
+
199
+ # Restores the values of all flags to their default settings, and returns nil.
200
+ def self.restore_all_defaults()
201
+ @@flags.each_pair do |name, flag|
202
+ flag.restore_default
203
+ end
204
+ nil
205
+ end
206
+
207
+ # Dumps the flags, serialized to yaml to the specified io object. Returns a string if io is nil.
208
+ def self.to_yaml(io=nil)
209
+ flags = []
210
+ @@flags.keys.sort { |a, b| a.to_s <=> b.to_s }.each do |name|
211
+ flag = @@flags[name]
212
+ flags += [ "-#{name}", flag.value ]
213
+ end
214
+ YAML.dump(flags, io)
215
+ end
216
+
217
+ # Serializes the arguments from yaml and returns an array of arguments that can be passed to
218
+ # init.
219
+ def self.args_from_yaml(yaml)
220
+ YAML.load(yaml)
221
+ end
222
+
223
+ # This function takes the @@flags object, and returns a hash of just the key, value pairs
224
+ def self.to_hash
225
+ return Hash[@@flags.map { |name, flag| [name, flag.value] }]
226
+ end
227
+
228
+ # Converts the flags to a printable string.
229
+ def self.to_s
230
+ flags = []
231
+ @@flags.keys.sort { |a, b| a.to_s <=> b.to_s }.each do |name|
232
+ flag = @@flags[name]
233
+ flag_name = "-#{name}"
234
+ value = flag.value
235
+ if value.is_a?(String) # we call inspect() on String flags to take care of embedded quotes, spaces, etc.
236
+ flags << "-#{name} #{value.inspect}"
237
+ else
238
+ flags << "-#{name} #{value.to_s}"
239
+ end
240
+ end
241
+ flags.join(" ")
242
+ end
243
+
244
+ # This override makes it so that the mocha rubygem prints nicer debug messages. The downside
245
+ # is that now you need to call Flags.to_s to print human-readable strings.
246
+ def self.inspect
247
+ "Flags"
248
+ end
249
+
250
+ private
251
+
252
+ # Has Flags.init been called already? Flag values should not be read or written prior to calling Flags.init.
253
+ @@init_called = false
254
+
255
+ # Hash of flag name symbols => Flag objects.
256
+ @@flags = {}
257
+
258
+ # Internal method that defines a new flag with the given name, default value, description, and type. Used
259
+ # by the public define_* and define_*_flag methods.
260
+ def self.define_flag(name, default_value, description, flag_class)
261
+ raise_unless_symbol!(name)
262
+ raise_if_flag_defined!(name)
263
+
264
+ # For each flag, we store the file name where it was defined, and print these file names if called with
265
+ # the --help argument. To get the file names we have to examine the call stack, which is accessible
266
+ # through the Kernel.caller() method. The caller method returns an array of strings that look like this:
267
+ # ["./bar.rb:1", "foo.rb:2:in `require'", "foo.rb:2"]
268
+ #
269
+ # The caller frame immediately above this one (index 0) is one of the public define_*_flag methods. The
270
+ # frame above that (index 1) is the location where the Flags.define_*_flag method was called, and is the
271
+ # one we want. We also strip the leading "./" from the file name if it's present, for improved readability.
272
+ definition_file = caller[1].split(":")[0].gsub("./", "")
273
+ flag = flag_class.new(name, default_value, description, definition_file)
274
+ @@flags[name] = flag
275
+
276
+ # TODO: The flag getter and setter methods should call raise_unless_initialized! However, for now
277
+ # this breaks unit tests so not viable until we add some kind of hook with test/unit that calls Flags.init
278
+ # with empty args before each test case runs.
279
+
280
+ # TODO: Would look cleaner if we used the eigenclass approach and class_eval + code block instead
281
+ # of instance_eval + string.
282
+
283
+ instance_eval "def self.#{name}; @@flags[:#{name.to_s}].value; end"
284
+ instance_eval "def self.#{name}=(value); @@flags[:#{name.to_s}].value = value; end"
285
+ end
286
+
287
+ # Internal method that undefines a flag. Used by the unit test to clean up state between test cases.
288
+ def self.undefine_flag(name)
289
+ raise_unless_symbol!(name)
290
+ raise_unless_flag_defined!(name)
291
+
292
+ @@flags.delete(name)
293
+
294
+ eigenclass = class << Flags; self; end
295
+ eigenclass.class_eval "remove_method :#{name}"
296
+ eigenclass.class_eval "remove_method :#{name}="
297
+ end
298
+
299
+ # Raises an error if a flag with the given name is already defined, or if the flag name would conflict
300
+ # with an existing Flags method.
301
+ def self.raise_if_flag_defined!(name)
302
+ raise "Flag #{name} already defined" if @@flags.key?(name.to_sym)
303
+ raise "Flag #{name} conflicts with an internal Flags method" if self.respond_to?(name.to_sym)
304
+ nil
305
+ end
306
+
307
+ # Raises an error unless a flag with the given name is defined.
308
+ def self.raise_unless_flag_defined!(name)
309
+ raise "Flag #{name} not defined" unless @@flags.key?(name.to_sym)
310
+ nil
311
+ end
312
+
313
+ # Raises an error unless Flags.init has been called at least once. Useful for catching subtle "I forgot to
314
+ # call Flags.init at the start of my program" bugs.
315
+ def self.raise_unless_initialized!()
316
+ raise "Flags.init has not been called" unless @@init_called
317
+ end
318
+
319
+ def self.raise_unless_symbol!(name)
320
+ raise ArgumentError, "Flag name must be a symbol but is a #{name.class}" unless name.is_a?(Symbol)
321
+ end
322
+
323
+ def self.extract_value_for_flag(args, flag)
324
+ index = args.index(flag)
325
+ if index
326
+ value = args[index+1]
327
+ args[index..(index+1)] = [] # delete from args
328
+ return value
329
+ end
330
+ end
331
+
332
+ def self.help_message
333
+ help = "Known command line flags:\n\n"
334
+ max_name_length = @@flags.values.map { |flag| flag.name.to_s.length }.max
335
+ # Sort the flags by the location in which they are defined (primary) and flag name (secondary).
336
+ definition_file_to_flags = Hash.new { |h, k| h[k] = [] }
337
+ @@flags.each_pair do |name, flag|
338
+ definition_file_to_flags[flag.definition_file].push name
339
+ end
340
+
341
+ definition_file_to_flags.keys.sort.each do |file|
342
+ flags = definition_file_to_flags[file]
343
+ help << "Defined in #{file}:\n"
344
+ flags.sort { |a, b| a.to_s <=> b.to_s }.each do |flag_name|
345
+ flag = @@flags[flag_name]
346
+ help << " -#{flag_name.to_s.ljust(max_name_length+1)} (#{flag.type}) #{flag.description} "\
347
+ "(Default: #{flag.default_value.inspect})\n"
348
+ end
349
+ help << "\n"
350
+ end
351
+ help
352
+ end
353
+
354
+ # A subclass of ArgumentError that's raised when an invalid value is assigned to a flag.
355
+ class InvalidFlagValueError < ArgumentError
356
+ def initialize(flag_name, flag_value, error_message)
357
+ super("Flag value #{flag_value.inspect} for flag -#{flag_name.to_s} is invalid: #{error_message}")
358
+ end
359
+ end
360
+
361
+ # A FlagValidator can check whether a flag's value is valid (with validity implicitly defined via a provided
362
+ # callback), and raise an error if the validation check fails.
363
+ class FlagValidator
364
+ def initialize(proc, error_message)
365
+ @proc = proc
366
+ @error_message = error_message
367
+ end
368
+
369
+ def validate!(flag_name, flag_value)
370
+ raise InvalidFlagValueError.new(flag_name, flag_value, @error_message) unless @proc.call(flag_value)
371
+ end
372
+ end
373
+
374
+ # A validator that raises an error if the class of the flag's value is not one of the expected classes.
375
+ class ClassValidator < FlagValidator
376
+ def initialize(*expected_value_classes)
377
+ expected_value_classes = expected_value_classes.to_a.flatten
378
+ proc = Proc.new { |flag_value| expected_value_classes.include?(flag_value.class) }
379
+ error_message = "unexpected value class, expecting one of [#{expected_value_classes.join(',')}]"
380
+ super(proc, error_message)
381
+ end
382
+ end
383
+
384
+ # A validator that raises an error if the flag's value is outside the given Range.
385
+ # No type checking is performed (that's the job of the ClassValidator).
386
+ class RangeValidator < FlagValidator
387
+ def initialize(range)
388
+ proc = Proc.new { |flag_value| range.include?(flag_value) }
389
+ error_message = "value out of range! Valid range is #{range.inspect}"
390
+ super(proc, error_message)
391
+ end
392
+ end
393
+
394
+ # A validator that raises an error if the flag's value is not in the provided set of allowed values.
395
+ class AllowedValuesValidator < FlagValidator
396
+ def initialize(*allowed_values)
397
+ allowed_values = allowed_values.to_a.flatten
398
+ proc = Proc.new { |flag_value| allowed_values.include?(flag_value) }
399
+ error_message = "illegal value, expecting one of [#{allowed_values.join(',')}]"
400
+ super(proc, error_message)
401
+ end
402
+ end
403
+
404
+ # A validator that raises an error if the flag's value is in the provided set of disallowed values.
405
+ class DisallowedValuesValidator < FlagValidator
406
+ def initialize(*disallowed_values)
407
+ disallowed_values = disallowed_values.to_a.flatten
408
+ proc = Proc.new { |flag_value| !disallowed_values.include?(flag_value) }
409
+ error_message = "illegal value, may not be one of [#{disallowed_values.join(',')}]"
410
+ super(proc, error_message)
411
+ end
412
+ end
413
+
414
+ # A Flag represents everything we know about a flag - its name, value, default value, description, where
415
+ # it was defined, whether the default value has been explicitly modified, and an optional callback to
416
+ # validate the flag.
417
+ class Flag
418
+ attr_reader :type # the type of flag, as a symbol
419
+ attr_reader :name # the name of the flag
420
+ attr_reader :value # the current value of the flag
421
+ attr_reader :default_value # the default value of the flag
422
+ attr_reader :is_explicit # false unless the flag's value has been explicitly changed from default.
423
+ # NOTE: if a flag is explicitly set to the default value, this will be true!
424
+ attr_reader :description # a string description of the flag
425
+ attr_reader :definition_file # The file name where the flag was defined
426
+
427
+ # Initializes the Flag. Arguments:
428
+ # type - the type of this flag, as one of the symbols [ :string, :symbol, :int, :float, :bool ]
429
+ # name - the name of this flag, as a symbol
430
+ # default_value - the default value of the flag. The current value always equals the default when the
431
+ # Flag object is initialized.
432
+ # description - a String describing the flag.
433
+ # definition_file - a String describing the path to the file where this flag was defined.
434
+ # validators - a list of FlagValidator objects. Additional validators can be added after the flag is
435
+ # constructed, and all of the registered validators are checked whenever a flag assignment is
436
+ # performed.
437
+ def initialize(type, name, default_value, description, definition_file, validators)
438
+ @type = type
439
+ @name = name
440
+ @default_value = default_value
441
+ @description = description
442
+ @definition_file = definition_file
443
+ @validators = validators
444
+ self.value = default_value # use the public setter method which performs type checking
445
+ @is_explicit = false # @is_explicit must be set to false AFTER calling the value= method
446
+ end
447
+
448
+ # Sets the value of the flag. The new_value argument can be a String or the appropriate type for the flag
449
+ # (i.e. a Float for FloatFlag). If it's a String, an attempt will be made to convert to the correct type.
450
+ def value=(new_value)
451
+ new_value = value_from_string(new_value) if new_value.is_a?(String)
452
+ validate!(new_value)
453
+ @is_explicit = true
454
+ @value = new_value
455
+ end
456
+
457
+ # Restores the default value of the flag.
458
+ def restore_default()
459
+ @is_explicit = false
460
+ @value = @default_value
461
+ end
462
+
463
+ # Returns true if the flag has been explicitly set.
464
+ alias :explicit? :is_explicit
465
+
466
+ # Returns true if the flag has not been explicitly set.
467
+ def default?; return !explicit?; end
468
+
469
+ # Adds a new validator object to this flag and immediately validates the current value against it.
470
+ def add_validator(validator)
471
+ @validators.push validator
472
+ validate!(@value)
473
+ end
474
+
475
+ private
476
+
477
+ # Method for subclasses to implement that converts from a String argument to the flag's internal data
478
+ # type. Subclasses may throw an ArgumentError if the input is malformed. Subclasses return the string
479
+ # itself if they cannot convert, which will be caught by the Class Validator
480
+ def value_from_string(string)
481
+ raise NotImplementedError, "Subclass must implement value_from_string()"
482
+ end
483
+
484
+ def validate!(value)
485
+ @validators.each { |validator| validator.validate!(@name, value) }
486
+ end
487
+ end
488
+
489
+ # A flag with a string value.
490
+ class StringFlag < Flag
491
+ def initialize(name, default_value, description, definition_file)
492
+ super(:string, name, default_value, description, definition_file, [ClassValidator.new(String)])
493
+ end
494
+
495
+ private
496
+ def value_from_string(string); return string.to_s; end
497
+ end
498
+
499
+ # A flag with a symbol value.
500
+ class SymbolFlag < Flag
501
+ def initialize(name, default_value, description, definition_file)
502
+ super(:symbol, name, default_value, description, definition_file, [ClassValidator.new(Symbol)])
503
+ end
504
+
505
+ private
506
+ def value_from_string(string); return string.to_sym; end
507
+ end
508
+
509
+ # A flag with an integer value.
510
+ class IntFlag < Flag
511
+ def initialize(name, default_value, description, definition_file)
512
+ super(:int, name, default_value, description, definition_file,
513
+ [ClassValidator.new(Integer, Bignum, Fixnum)])
514
+ end
515
+
516
+ private
517
+ def value_from_string(string); Integer(string) rescue string; end
518
+ end
519
+
520
+ # A flag with a boolean value.
521
+ class BoolFlag < Flag
522
+ def initialize(name, default_value, description, definition_file)
523
+ super(:bool, name, default_value, description, definition_file,
524
+ [ClassValidator.new(TrueClass, FalseClass)])
525
+ end
526
+
527
+ private
528
+ STRING_TO_BOOL_VALUE = { 'true' => true, 'false' => false }
529
+
530
+ def value_from_string(string)
531
+ result = STRING_TO_BOOL_VALUE[string.downcase]
532
+ return result.nil? ? string : result
533
+ end
534
+ end
535
+
536
+ # A flag with a floating-point numeric value.
537
+ class FloatFlag < Flag
538
+ def initialize(name, default_value, description, definition_file)
539
+ super(:float, name, default_value, description, definition_file, [ClassValidator.new(Float)])
540
+ end
541
+
542
+ private
543
+ def value_from_string(string); return Float(string) rescue string; end
544
+ end
545
+ end
@@ -0,0 +1,277 @@
1
+ # Copyright © 2011 Ooyala, Inc.
2
+ # Tests for flags.rb - To run, execute ruby test/flags_test.rb from the root gem directory
3
+ require "rubygems"
4
+ require "shoulda"
5
+ require "mocha"
6
+
7
+ require "flags"
8
+
9
+ class FlagsTest < Test::Unit::TestCase
10
+ # Tests for undefine functionality used in the teardown block of all other tests. We want to make sure
11
+ # those work before running any other tests, so these tests are purposefully placed outside a context.
12
+ # Notice that these tests manually do their own teardown.
13
+ def test_undefine_flag
14
+ assert_raise(NoMethodError) { Flags.my_flag_name }
15
+ Flags.define_int_flag(:my_flag_name, 42, "Description")
16
+ assert_equal 42, Flags.my_flag_name
17
+ Flags.send(:undefine_flag, :my_flag_name)
18
+ assert_raise(NoMethodError) { Flags.my_flag_name }
19
+ end
20
+
21
+ def test_access_flag_names
22
+ flag_names = Flags.send(:class_variable_get, "@@flags").map { |name, flag| name }
23
+ assert_equal 0, flag_names.size
24
+ Flags.define_int_flag(:my_flag_name, 42, "Description")
25
+ flag_names = Flags.send(:class_variable_get, "@@flags").map { |name, flag| name }
26
+ assert_equal [ :my_flag_name ], flag_names
27
+ Flags.send(:undefine_flag, :my_flag_name)
28
+ end
29
+
30
+ context "Flags" do
31
+ teardown do # Undefine all flags after running a test
32
+ flag_names = Flags.send(:class_variable_get, "@@flags").map { |name, flag| name }
33
+ flag_names.each do |flag_name|
34
+ Flags.send(:undefine_flag, flag_name)
35
+ end
36
+ end
37
+
38
+ should "define_string_flag" do
39
+ Flags.define_string_flag(:foo, "bar", "Test string flag")
40
+ assert_equal "bar", Flags.foo
41
+ end
42
+
43
+ should "define_symbol_flag" do
44
+ Flags.define_symbol_flag(:foo, :bar, "Test symbol flag")
45
+ Flags.define_symbol_flag(:foo2, "baz", "Test symbol flag from string conversion")
46
+ assert_equal :bar, Flags.foo
47
+ assert_equal :baz, Flags.foo2
48
+ end
49
+
50
+ should "define_int_flag" do
51
+ Flags.define_int_flag(:bar, 1, "Test int flag")
52
+ Flags.define_int_flag(:baz, "2", "Test int flag from string conversion")
53
+ assert_equal 1, Flags.bar
54
+ assert_equal 2, Flags.baz
55
+ end
56
+
57
+ should "define_bool_flag" do
58
+ Flags.define_bool_flag(:true_bool_flag, true, "Test true bool flag")
59
+ Flags.define_bool_flag(:false_bool_flag, false, "Test false bool flag")
60
+ Flags.define_bool_flag(:true_bool_flag2, "true", "Test true bool flag from string conversion")
61
+ Flags.define_bool_flag(:false_bool_flag2, "false", "Test false bool flag from string conversion")
62
+ assert Flags.true_bool_flag
63
+ assert Flags.true_bool_flag2
64
+ assert !Flags.false_bool_flag
65
+ assert !Flags.false_bool_flag2
66
+ end
67
+
68
+ should "define_float_flag" do
69
+ Flags.define_float_flag(:float_flag, 2.0, "Test float flag")
70
+ Flags.define_float_flag(:float_flag2, "3.0", "Test float flag from string conversion")
71
+ assert_equal 2.0, Flags.float_flag
72
+ assert_equal 3.0, Flags.float_flag2
73
+ end
74
+
75
+ should "return flag comment" do
76
+ Flags.define_string_flag(:foo, "foobar", "Test string flag")
77
+ assert_equal "Test string flag", Flags.flags_get_comment(:foo)
78
+ end
79
+
80
+ should "return default value" do
81
+ Flags.define_string_flag(:foo, "foobar", "Test string flag")
82
+ Flags.foo = "baz"
83
+ assert_equal "baz", Flags.foo
84
+ assert_equal "foobar", Flags.flags_get_default_value(:foo)
85
+ end
86
+
87
+ should "initialize flags with init" do
88
+ Flags.define_float_flag(:bar, 2.0, "")
89
+ Flags.define_string_flag(:foo, "foobar", "")
90
+ Flags.define_int_flag(:baz, 3, "")
91
+ Flags.init(["-bar", 3.0, "-foo", "you rock"])
92
+ assert_equal 3.0, Flags.bar
93
+ assert_equal "you rock", Flags.foo
94
+ assert_equal 3, Flags.baz
95
+ end
96
+
97
+ should "remove known flags from args array but leave non-flags alone" do
98
+ Flags.define_int_flag(:foo, 41, "")
99
+ args = [ "-foo", "42", "hello world" ]
100
+ Flags.init(args)
101
+ assert_equal [ "hello world" ], args
102
+ end
103
+
104
+ should "init bool flag with true default" do
105
+ Flags.define_bool_flag(:bar, true, "")
106
+ Flags.init(["-bar", false])
107
+ assert !Flags.bar
108
+ end
109
+
110
+ should "init bool flag with true default and string initial value" do
111
+ Flags.define_bool_flag(:bar, true, "")
112
+ Flags.init(["-bar", "false"])
113
+ assert !Flags.bar
114
+ end
115
+
116
+ should "pick last flag value when multiple args are present" do
117
+ Flags.define_bool_flag(:bar, true, "")
118
+ Flags.define_int_flag(:baz, 3, "")
119
+ Flags.init(["-bar", "false", "-baz", 4, "-bar", "true", "-baz" , 5])
120
+ assert Flags.bar
121
+ assert_equal 5, Flags.baz
122
+ end
123
+
124
+ should "serialize to string" do
125
+ Flags.define_float_flag(:bar, 2.0, "bla")
126
+ Flags.define_string_flag(:baz, "you rock", "comment \"bla\" 'bla'")
127
+ Flags.define_int_flag(:foo, 3, "")
128
+ Flags.define_bool_flag(:bool, true, "")
129
+ assert_equal "-bar 2.0 -baz \"you rock\" -bool true -foo 3", Flags.to_s
130
+ end
131
+
132
+ should "override the default state flags with init" do
133
+ Flags.define_float_flag(:bar, 2.0, "")
134
+ Flags.define_string_flag(:foo, "foobar", "")
135
+ Flags.define_int_flag(:baz, 3, "")
136
+ Flags.init(["-bar", 3.0, "-foo", "you rock"])
137
+ assert_equal 3.0, Flags.bar
138
+ assert_equal "you rock", Flags.foo
139
+ assert_equal 3, Flags.baz
140
+ end
141
+
142
+ should "serialize to/from yaml" do
143
+ Flags.define_float_flag(:bar, 2.0, "")
144
+ Flags.define_string_flag(:foo, "foobar", "")
145
+ args = Flags.args_from_yaml Flags.to_yaml
146
+ assert_equal ["-bar", 2.0, "-foo", "foobar"], args
147
+ end
148
+
149
+ context "default values" do
150
+ should "flags_is_default should return true initially" do
151
+ Flags.define_int_flag(:foo, 42, "")
152
+ assert Flags.flags_is_default(:foo)
153
+ end
154
+
155
+ should "flags_is_default should return false after flag assignment" do
156
+ Flags.define_int_flag(:foo, 42, "")
157
+ Flags.foo = 43
158
+ assert !Flags.flags_is_default(:foo)
159
+ end
160
+
161
+ should "flags_is_default should return false after loading flags from command line" do
162
+ Flags.define_int_flag(:foo, 42, "")
163
+ Flags.define_int_flag(:bar, 43, "")
164
+ Flags.init(["-foo", "41"])
165
+ assert !Flags.flags_is_default(:foo)
166
+ assert Flags.flags_is_default(:bar)
167
+ end
168
+
169
+ should "set_if_default should change default value" do
170
+ Flags.define_int_flag(:foo, 42, "")
171
+ Flags.set_if_default(:foo, 43)
172
+ assert_equal 43, Flags.foo
173
+ assert !Flags.flags_is_default(:foo)
174
+ end
175
+
176
+ should "set_if_default should not change non-default value" do
177
+ Flags.define_int_flag(:foo, 42, "")
178
+ Flags.foo = 41
179
+ Flags.set_if_default(:foo, 43)
180
+ assert_equal 41, Flags.foo
181
+ end
182
+
183
+ should "restore default value" do
184
+ Flags.define_int_flag(:foo, 42, "")
185
+ Flags.foo = 41
186
+ Flags.restore_default(:foo)
187
+ assert_equal 42, Flags.foo
188
+ assert Flags.flags_is_default(:foo)
189
+ end
190
+
191
+ should "restore all default values" do
192
+ Flags.define_int_flag(:foo, 42, "")
193
+ Flags.define_int_flag(:bar, 43, "")
194
+ Flags.foo, Flags.bar = 41, 40
195
+ Flags.restore_all_defaults()
196
+ assert_equal 42, Flags.foo
197
+ assert_equal 43, Flags.bar
198
+ assert Flags.flags_is_default(:foo) && Flags.flags_is_default(:bar)
199
+ end
200
+ end
201
+
202
+ context "flag validators" do
203
+ should "fail expected class validation" do
204
+ Flags.define_int_flag(:foo, 1, "...")
205
+ assert_raise(Flags::InvalidFlagValueError) { Flags.foo = "not an int" }
206
+ Flags.define_float_flag(:bar, 1.0, "...")
207
+ assert_raise(Flags::InvalidFlagValueError) { Flags.bar = 1 }
208
+ Flags.define_bool_flag(:baz, true, "...")
209
+ assert_raise(Flags::InvalidFlagValueError) { Flags.baz = :hi_there }
210
+ # Converting an empty string to a symbol is an error in Ruby 1.8 but not 1.9
211
+ if RUBY_VERSION[0..2] == '1.8'
212
+ Flags.define_symbol_flag(:boo, :a, "...")
213
+ assert_raise(ArgumentError) { Flags.boo = "" }
214
+ end
215
+ Flags.define_string_flag(:faz, "hello world", "...")
216
+ assert_raise(Flags::InvalidFlagValueError) { Flags.faz = 123 }
217
+ end
218
+
219
+ should "register numeric range validators" do
220
+ # A range open on the 'max' end
221
+ Flags.define_int_flag(:foo, 1, "Must be greater than or equal to 0")
222
+ positive_infinity = 1.0 / 0 # TODO: is there a better way to get a reference to the Infinity constant?
223
+ Flags.register_range_validator(:foo, 0..positive_infinity)
224
+ Flags.foo = 2 ** 128
225
+ Flags.foo = 0
226
+ assert_raise(Flags::InvalidFlagValueError) { Flags.foo = -1 }
227
+ assert_equal 0, Flags.foo
228
+
229
+ # A range open on the 'min' end
230
+ Flags.define_int_flag(:bar, -1, "Must be less than or equal to 0")
231
+ negative_infinity = -1.0 / 0
232
+ Flags.register_range_validator(:bar, negative_infinity..0)
233
+ Flags.bar = -2 ** 127
234
+ Flags.bar = 0
235
+ assert_raise(Flags::InvalidFlagValueError) { Flags.bar = 1 }
236
+ assert_equal 0, Flags.bar
237
+
238
+ # A range closed on both ends
239
+ Flags.define_int_flag(:boo, 0, "Must be between -2 and 2")
240
+ Flags.register_range_validator(:boo, -2..2)
241
+ Flags.boo = -2
242
+ Flags.boo = 2
243
+ assert_raise(Flags::InvalidFlagValueError) { Flags.boo = -3 }
244
+ assert_raise(Flags::InvalidFlagValueError) { Flags.boo = 3 }
245
+ assert_equal 2, Flags.boo
246
+ end
247
+
248
+ should "register allowed values validator" do
249
+ Flags.define_symbol_flag(:foo, :north, "Can be one of [:north, :south, :east, :west]")
250
+ Flags.register_allowed_values_validator(:foo, [:north, :south, :east, :west])
251
+ assert_raise(Flags::InvalidFlagValueError) { Flags.foo = :up }
252
+ # One more time and use the variable args version this time
253
+ Flags.define_string_flag(:bar, "left", "Can be one of (left, right)")
254
+ Flags.register_allowed_values_validator(:bar, "left", "right")
255
+ assert_raise(Flags::InvalidFlagValueError) { Flags.bar = "down" }
256
+ end
257
+
258
+ should "register disallowed values validator" do
259
+ Flags.define_symbol_flag(:foo, :company, "Cannot be any of [:competitor1, :competitor2]")
260
+ Flags.register_disallowed_values_validator(:foo, [:competitor1, :competitor2])
261
+ Flags.foo = :client
262
+ assert_raise(Flags::InvalidFlagValueError) { Flags.foo = :competitor1 }
263
+ assert_equal :client, Flags.foo
264
+ end
265
+
266
+ should "register custom validator" do
267
+ Flags.define_int_flag(:even_number, 2, "Even numbers only!")
268
+ Flags.register_custom_validator(:even_number,
269
+ Proc.new { |flag_value| flag_value % 2 == 0 },
270
+ "Flag value must be an even integer")
271
+ Flags.even_number = 10
272
+ assert_raise(Flags::InvalidFlagValueError) { Flags.even_number = 11 }
273
+ assert_equal 10, Flags.even_number
274
+ end
275
+ end
276
+ end
277
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flags
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
11
+ platform: ruby
12
+ authors:
13
+ - Ooyala, Inc.
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-16 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Flags is a framework for Ruby which allows the definition of command-line flags, which are parsed in and can be accessed smartly from within your Ruby code. This framework allows for numerous different flag types, and takes care of the process of type conversion and flag validation (type and value checking).
23
+ email: rubygems@ooyala.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - README.markdown
32
+ - LICENSE
33
+ - lib/flags.rb
34
+ - test/flags_test.rb
35
+ has_rdoc: true
36
+ homepage: https://github.com/ooyala/flags
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ hash: 3
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.5.2
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: A simple command-line flag framework.
69
+ test_files: []
70
+