s0nspark-choice 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/choice.rb ADDED
@@ -0,0 +1,155 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ require 'choice/option'
3
+ require 'choice/parser'
4
+ require 'choice/writer'
5
+ require 'choice/lazyhash'
6
+
7
+ #
8
+ # Usage of this module is lovingly detailed in the README file.
9
+ #
10
+ module Choice
11
+ extend self
12
+
13
+ # The main method, which defines the options
14
+ def options(hash = {}, &block)
15
+ # if we are passing in a hash to define our options, use that straight
16
+ options_from_hash(hash) unless hash.empty?
17
+
18
+ # Setup all instance variables
19
+ reset! if hash.empty?
20
+ @@args ||= ARGV
21
+
22
+ # Eval the passed block to define the options.
23
+ instance_eval(&block) if block_given?
24
+
25
+ # Parse what we've got.
26
+ parse unless parsed?
27
+ end
28
+
29
+ # Set options from a hash, shorthand style
30
+ def options_from_hash(options_hash)
31
+ options_hash.each do |name, definition|
32
+ option = Option.new
33
+ definition.each do |key, value|
34
+ Array(value).each { |hit| option.send(key, hit) }
35
+ end
36
+ @@options << [name.to_s, option]
37
+ end
38
+ end
39
+
40
+ # Returns a hash representing options passed in via the command line.
41
+ def choices
42
+ @@choices
43
+ end
44
+
45
+ # Defines an option.
46
+ def option(opt, options = {}, &block)
47
+ # Notice: options is maintained as an array of arrays, the first element
48
+ # the option name and the second the option object.
49
+ @@options << [opt.to_s, Option.new(options, &block)]
50
+ end
51
+
52
+ # Separators are text displayed by --help within the options block.
53
+ def separator(str)
54
+ # We store separators as simple strings in the options array to maintain
55
+ # order. They are ignored by the parser.
56
+ @@options << str
57
+ end
58
+
59
+ # Define the banner, header, footer methods. All are just getters/setters
60
+ # of class variables.
61
+ %w[banner header footer].each do |method|
62
+ define_method(method) do |string|
63
+ variable = "@@#{method}"
64
+ return class_variable_get(variable) if string.nil?
65
+ val = class_variable_get(variable) || ''
66
+ class_variable_set(variable, val << string)
67
+ end
68
+ end
69
+
70
+
71
+ # Parse the provided args against the defined options.
72
+ def parse #:nodoc:
73
+ # Do nothing if options are not defined.
74
+ return unless @@options.size > 0
75
+
76
+ # Show help if it's anywhere in the argument list.
77
+ if @@args.include?('--help') or @@args.include?('-h')
78
+ help
79
+ else
80
+ begin
81
+ # Delegate parsing to our parser class, passing it our defined
82
+ # options and the passed arguments.
83
+ @@choices = LazyHash.new(Parser.parse(@@options, @@args))
84
+ rescue Choice::Parser::ParseError
85
+ # If we get an expected exception, show the help file.
86
+ help
87
+ end
88
+ end
89
+ end
90
+
91
+ # Did we already parse the arguments?
92
+ def parsed? #:nodoc:
93
+ @@choices ||= false
94
+ end
95
+
96
+ # Print the help screen by calling our Writer object
97
+ def help #:nodoc:
98
+ Writer.help( { :banner => @@banner, :header => @@header,
99
+ :options => @@options, :footer => @@footer },
100
+ output_to, exit_on_help? )
101
+ end
102
+
103
+ # Set the args, potentially to something other than ARGV.
104
+ def args=(args) #:nodoc:
105
+ @@args = args.dup.map { |a| a + '' }
106
+ parse if parsed?
107
+ end
108
+
109
+ # Return the args.
110
+ def args #:nodoc:
111
+ @@args
112
+ end
113
+
114
+ # Returns the arguments that follow an argument
115
+ def args_of(opt)
116
+ args_of_opt = []
117
+
118
+ # Return an array of the arguments between opt and the next option,
119
+ # which all start with "-"
120
+ @@args.slice(@@args.index(opt)+1, @@args.length).select do |arg|
121
+ if arg[0].chr != "-"
122
+ args_of_opt << arg
123
+ else
124
+ break
125
+ end
126
+ end
127
+ args_of_opt
128
+ end
129
+
130
+ # You can choose to not kill the script after the help screen is printed.
131
+ def dont_exit_on_help=(val) #:nodoc:
132
+ @@exit = true
133
+ end
134
+
135
+ # Do we want to exit on help?
136
+ def exit_on_help? #:nodoc:
137
+ @@exit rescue false
138
+ end
139
+
140
+ # If we want to write to somewhere other than STDOUT.
141
+ def output_to(target = nil) #:nodoc:
142
+ @@output_to ||= STDOUT
143
+ return @@output_to if target.nil?
144
+ @@output_to = target
145
+ end
146
+
147
+ # Reset all the class variables.
148
+ def reset! #:nodoc:
149
+ @@args = false
150
+ @@banner = false
151
+ @@header = Array.new
152
+ @@options = Array.new
153
+ @@footer = Array.new
154
+ end
155
+ end
@@ -0,0 +1,67 @@
1
+ module Choice
2
+
3
+ # This class lets us get away with really bad, horrible, lazy hash accessing.
4
+ # Like so:
5
+ # hash = LazyHash.new
6
+ # hash[:someplace] = "somewhere"
7
+ # puts hash[:someplace]
8
+ # puts hash['someplace']
9
+ # puts hash.someplace
10
+ #
11
+ # If you'd like, you can pass in a current hash when initializing to convert
12
+ # it into a lazyhash. Or you can use the .to_lazyhash method attached to the
13
+ # Hash object (evil!).
14
+ class LazyHash < Hash
15
+
16
+ # Keep the old methods around.
17
+ alias_method :old_store, :store
18
+ alias_method :old_fetch, :fetch
19
+
20
+ # You can pass in a normal hash to convert it to a LazyHash.
21
+ def initialize(hash = nil)
22
+ hash.each { |key, value| self[key] = value } if !hash.nil? && hash.is_a?(Hash)
23
+ end
24
+
25
+ # Wrapper for []
26
+ def store(key, value)
27
+ self[key] = value
28
+ end
29
+
30
+ # Wrapper for []=
31
+ def fetch(key)
32
+ self[key]
33
+ end
34
+
35
+ # Store every key as a string.
36
+ def []=(key, value)
37
+ key = key.to_s if key.is_a? Symbol
38
+ self.old_store(key, value)
39
+ end
40
+
41
+ # Every key is stored as a string. Like a normal hash, nil is returned if
42
+ # the key does not exist.
43
+ def [](key)
44
+ key = key.to_s if key.is_a? Symbol
45
+ self.old_fetch(key) rescue return nil
46
+ end
47
+
48
+ # You can use hash.something or hash.something = 'thing' since this is
49
+ # truly a lazy hash.
50
+ def method_missing(meth, *args)
51
+ meth = meth.to_s
52
+ if meth =~ /=/
53
+ self[meth.sub('=','')] = args.first
54
+ else
55
+ self[meth]
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+
62
+ # Really ugly, horrible, extremely fun hack.
63
+ class Hash #:nodoc:
64
+ def to_lazyhash
65
+ return Choice::LazyHash.new(self)
66
+ end
67
+ end
@@ -0,0 +1,90 @@
1
+ module Choice
2
+
3
+ # The Option class parses and stores all the information about a specific
4
+ # option.
5
+ class Option #:nodoc: all
6
+
7
+ # Since we define getters/setters on the fly, we need a white list of
8
+ # which to accept. Here's the list.
9
+ CHOICES = %w[short long desc default filter action cast validate valid]
10
+
11
+ # You can instantiate an option on its own or by passing it a name and
12
+ # a block. If you give it a block, it will eval() the block and set itself
13
+ # up nicely.
14
+ def initialize(options = {}, &block)
15
+ # Here we store the definitions this option contains, to make to_a and
16
+ # to_h easier.
17
+ @choices = []
18
+
19
+ # If we got a block, eval it and set everything up.
20
+ instance_eval(&block) if block_given?
21
+
22
+ # Is this option required?
23
+ @required = options[:required] || false
24
+ @choices << 'required'
25
+ end
26
+
27
+ # This is the catch all for the getter/setter choices defined in CHOICES.
28
+ # It also gives us choice? methods.
29
+ def method_missing(method, *args, &block)
30
+ # Get the name of the choice we want, as a class variable string.
31
+ var = "@#{method.to_s.sub('?','')}"
32
+
33
+ # To string, for regex purposes.
34
+ method = method.to_s
35
+
36
+ # Don't let in any choices not defined in our white list array.
37
+ raise ParseError, "I don't know `#{method}'" unless CHOICES.include? method.sub('?','')
38
+
39
+ # If we're asking a question, give an answer. Like 'short?'.
40
+ return !!instance_variable_get(var) if method =~ /\?/
41
+
42
+ # If we were called with no arguments, we want a get.
43
+ return instance_variable_get(var) unless args[0] || block_given?
44
+
45
+ # If we were given a block or an argument, save it.
46
+ instance_variable_set(var, args[0]) if args[0]
47
+ instance_variable_set(var, block) if block_given?
48
+
49
+ # Add the choice to the @choices array if we're setting it for the first
50
+ # time.
51
+ @choices << method if args[0] || block_given? unless @choices.index(method)
52
+ end
53
+
54
+ # The desc method is slightly special: it stores itself as an array and
55
+ # each subsequent call adds to that array, rather than overwriting it.
56
+ # This is so we can do multi-line descriptions easily.
57
+ def desc(string = nil)
58
+ return @desc if string.nil?
59
+
60
+ @desc ||= []
61
+ @desc.push(string)
62
+
63
+ # Only add to @choices array if it's not already present.
64
+ @choices << 'desc' unless @choices.index('desc')
65
+ end
66
+
67
+ # Simple, desc question method.
68
+ def desc?() !!@desc end
69
+
70
+ # Returns Option converted to an array.
71
+ def to_a
72
+ @choices.inject([]) do |array, choice|
73
+ return array unless @choices.include? choice
74
+ array + [instance_variable_get("@#{choice}")]
75
+ end
76
+ end
77
+
78
+ # Returns Option converted to a hash.
79
+ def to_h
80
+ @choices.inject({}) do |hash, choice|
81
+ return hash unless @choices.include? choice
82
+ hash.merge choice => instance_variable_get("@#{choice}")
83
+ end
84
+ end
85
+
86
+ # In case someone tries to use a method we don't know about in their
87
+ # option block.
88
+ class ParseError < Exception; end
89
+ end
90
+ end
@@ -0,0 +1,227 @@
1
+ module Choice
2
+
3
+ # The parser takes our option definitions and our arguments and produces
4
+ # a hash of values.
5
+ module Parser #:nodoc: all
6
+ extend self
7
+
8
+ # What method to call on an object for each given 'cast' value.
9
+ CAST_METHODS = { Integer => :to_i, String => :to_s, Float => :to_f,
10
+ Symbol => :to_sym }
11
+
12
+ # Perhaps this method does too much. It is, however, a parser.
13
+ # You pass it an array of arrays, the first element of each element being
14
+ # the option's name and the second element being a hash of the option's
15
+ # info. You also pass in your current arguments, so it knows what to
16
+ # check against.
17
+ def parse(options, args)
18
+ # Return empty hash if the parsing adventure would be fruitless.
19
+ return {} if options.nil? || !options || args.nil? || !args.is_a?(Array)
20
+
21
+ # If we are passed an array, make the best of it by converting it
22
+ # to a hash.
23
+ options = options.inject({}) do |hash, value|
24
+ value.is_a?(Array) ? hash.merge(value.first => value[1]) : hash
25
+ end if options.is_a? Array
26
+
27
+ # Define local hashes we're going to use. choices is where we store
28
+ # the actual values we've pulled from the argument list.
29
+ hashes, longs, required, validators, choices, arrayed = {}, {}, {}, {}, {}, {}
30
+ hard_required = {}
31
+
32
+ # We can define these on the fly because they are all so similar.
33
+ params = %w[short cast filter action default valid]
34
+ params.each { |param| hashes["#{param}s"] = {} }
35
+
36
+ # Inspect each option and move its info into our local hashes.
37
+ options.each do |name, obj|
38
+ name = name.to_s
39
+
40
+ # Only take hashes or hash-like duck objects.
41
+ raise HashExpectedForOption unless obj.respond_to? :to_h
42
+ obj = obj.to_h
43
+
44
+ # Is this option required?
45
+ hard_required[name] = true if obj['required']
46
+
47
+ # Set the local hashes if the value exists on this option object.
48
+ params.each { |param| hashes["#{param}s"][name] = obj[param] if obj[param] }
49
+
50
+ # If there is a validate statement, make it a regex or proc.
51
+ validators[name] = make_validation(obj['validate']) if obj['validate']
52
+
53
+ # Parse the long option. If it contains a =, figure out if the
54
+ # argument is required or optional. Optional arguments are formed
55
+ # like [=ARG], whereas required are just ARG (in --long=ARG style).
56
+ if obj['long'] && obj['long'] =~ /(=|\[| )/
57
+ # Save the separator we used, as we're gonna need it, then split
58
+ sep = $1
59
+ option, *argument = obj['long'].split(sep)
60
+
61
+ # The actual name of the long switch
62
+ longs[name] = option
63
+
64
+ # Preserve the original argument, as it may contain [ or =,
65
+ # by joining with the character we split on. Add a [ in front if
66
+ # we split on that.
67
+ argument = (sep == '[' ? '[' : '') << Array(argument).join(sep)
68
+
69
+ # Do we expect multiple arguments which get turned into an array?
70
+ arrayed[name] = true if argument =~ /^\[?=?\*(.+)\]?$/
71
+
72
+ # Is this long required or optional?
73
+ required[name] = true unless argument =~ /^\[=?\*?(.+)\]$/
74
+ elsif obj['long']
75
+ # We can't have a long as a switch when valid is set -- die.
76
+ raise ArgumentRequiredWithValid if obj['valid']
77
+
78
+ # Set without any checking if it's just --long
79
+ longs[name] = obj['long']
80
+ end
81
+
82
+ # If we were given a list of valid arguments with 'valid,' this option
83
+ # is definitely required.
84
+ required[name] = true if obj['valid']
85
+ end
86
+
87
+ # Go through the arguments and try to figure out whom they belong to
88
+ # at this point.
89
+ args.each_with_index do |arg, i|
90
+ if hashes['shorts'].value?(arg)
91
+ # Set the value to the next element in the args array since
92
+ # this is a short.
93
+ value = args[i+1]
94
+
95
+ # If the next element doesn't exist or starts with a -, make this
96
+ # value true.
97
+ value = true if !value || value =~ /^-/
98
+
99
+ # Add this value to the choices hash with the key of the option's
100
+ # name. If we expect an array, tack this argument on.
101
+ name = hashes['shorts'].index(arg)
102
+ if arrayed[name]
103
+ choices[name] ||= []
104
+ choices[name] += arrayize_arguments(name, args[i+1..-1])
105
+ else
106
+ choices[name] = value
107
+ end
108
+
109
+ elsif /^(--[^=]+)=?/ =~ arg && longs.value?($1)
110
+ # The joke here is we always accept both --long=VALUE and --long VALUE.
111
+
112
+ # Grab values from --long=VALUE format
113
+ if arg =~ /=/ && longs.value?((longed = arg.split('=')).first)
114
+ name = longs.index(longed.shift)
115
+ value = longed * '='
116
+ # For the arrayed options.
117
+ potential_args = args[i+1..-1]
118
+ else
119
+ # Grab value otherwise if not in --long=VALUE format. Assume --long VALUE.
120
+ name = longs.index(arg)
121
+ # Value is nil if we don't have a = and the next argument is no good
122
+ value = args[i+1] =~ /^-/ ? nil : args[i+1]
123
+ # For the arrayed options.
124
+ potential_args = args[i+2..-1]
125
+ end
126
+
127
+ # If we expect an array, tack this argument on.
128
+ if arrayed[name] && !value.nil?
129
+ # If this is arrayed and the value isn't nil, set it.
130
+ choices[name] ||= []
131
+ choices[name] << value
132
+ choices[name] += arrayize_arguments(name, potential_args)
133
+ else
134
+ # If we set the value to nil, that means nothing was set and we
135
+ # need to set the value to true. We'll find out later if that's
136
+ # acceptable or not.
137
+ choices[name] = value.nil? ? true : value
138
+ end
139
+
140
+ else
141
+ # If we're here, we have no idea what the passed argument is. Die.
142
+ raise UnknownOption if arg =~ /^-/
143
+ end
144
+ end
145
+
146
+ # Okay, we got all the choices. Now go through and run any filters or
147
+ # whatever on them.
148
+ choices.each do |name, value|
149
+ # Check to make sure we have all the required arguments.
150
+ raise ArgumentRequired if required[name] && value === true
151
+
152
+ # Validate the argument if we need to, against a regexp or a block.
153
+ if validators[name]
154
+ if validators[name].is_a?(Regexp) && validators[name] =~ value
155
+ elsif validators[name].is_a?(Proc) && validators[name].call(value)
156
+ else raise ArgumentValidationFails
157
+ end
158
+ end
159
+
160
+ # Make sure the argument is valid
161
+ raise InvalidArgument unless value.to_a.all? { |v| hashes['valids'][name].include?(v) } if hashes['valids'][name]
162
+
163
+ # Cast the argument using the method defined in the constant hash.
164
+ value = value.send(CAST_METHODS[hashes['casts'][name]]) if hashes['casts'].include?(name)
165
+
166
+ # Run the value through a filter and re-set it with the return.
167
+ value = hashes['filters'][name].call(value) if hashes['filters'].include?(name)
168
+
169
+ # Run an action block if there is one associated.
170
+ hashes['actions'][name].call(value) if hashes['actions'].include?(name)
171
+
172
+ # Now that we've done all that, re-set the element of the choice hash
173
+ # with the (potentially) new value.
174
+ choices[name] = value
175
+ end
176
+
177
+ # Die if we're missing any required arguments
178
+ hard_required.each do |name, value|
179
+ raise ArgumentRequired unless choices[name]
180
+ end
181
+
182
+ # Home stretch. Go through all the defaults defined and if a choice
183
+ # does not exist in our choices hash, set its value to the requested
184
+ # default.
185
+ hashes['defaults'].each do |name, value|
186
+ choices[name] = value unless choices[name]
187
+ end
188
+
189
+ # Return the choices hash.
190
+ choices
191
+ end
192
+
193
+ private
194
+ # Turns trailing command line arguments into an array for an arrayed value
195
+ def arrayize_arguments(name, args)
196
+ # Go through trailing arguments and suck them in if they don't seem
197
+ # to have an owner.
198
+ array = []
199
+ potential_args = args.dup
200
+ until (arg = potential_args.shift) =~ /^-/ || arg.nil?
201
+ array << arg
202
+ end
203
+ array
204
+ end
205
+
206
+ def make_validation(validation)
207
+ case validation
208
+ when Proc then
209
+ validation
210
+ when Regexp, String then
211
+ Regexp.new(validation.to_s)
212
+ else
213
+ raise ValidateExpectsRegexpOrBlock
214
+ end
215
+ end
216
+
217
+ # All the possible exceptions this module can raise.
218
+ class ParseError < Exception; end
219
+ class HashExpectedForOption < Exception; end
220
+ class UnknownOption < ParseError; end
221
+ class ArgumentRequired < ParseError; end
222
+ class ValidateExpectsRegexpOrBlock < ParseError; end
223
+ class ArgumentValidationFails < ParseError; end
224
+ class InvalidArgument < ParseError; end
225
+ class ArgumentRequiredWithValid < ParseError; end
226
+ end
227
+ end