s0nspark-choice 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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