choice 0.1.0

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,132 @@
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
+ class <<self
12
+ # The main method, which defines the options
13
+ def options(&block)
14
+ # Setup all instance variables
15
+ @@args ||= false
16
+ @@banner ||= false
17
+ @@header ||= Array.new
18
+ @@options ||= Array.new
19
+ @@footer ||= Array.new
20
+
21
+ # Args can be overriden, but shouldn't be
22
+ self.args = @@args || ARGV
23
+
24
+ # Eval the passed block to define the options.
25
+ instance_eval(&block)
26
+
27
+ # Parse what we've got.
28
+ parse
29
+ end
30
+
31
+ # Returns a hash representing options passed in via the command line.
32
+ def choices
33
+ @@choices
34
+ end
35
+
36
+ # Defines an option.
37
+ def option(opt, &block)
38
+ # Notice: options is maintained as an array of arrays, the first element
39
+ # the option name and the second the option object.
40
+ @@options << [opt.to_s, Option.new(&block)]
41
+ end
42
+
43
+ # Separators are text displayed by --help within the options block.
44
+ def separator(str)
45
+ # We store separators as simple strings in the options array to maintain
46
+ # order. They are ignored by the parser.
47
+ @@options << str
48
+ end
49
+
50
+ # Define the banner, header, footer methods. All are just getters/setters
51
+ # of class variables.
52
+ %w[banner header footer].each do |method|
53
+ define_method(method) do |string|
54
+ variable = "@@#{method}"
55
+ return class_variable_get(variable) if string.nil?
56
+ val = class_variable_get(variable) || ''
57
+ class_variable_set(variable, val << string)
58
+ end
59
+ end
60
+
61
+
62
+ # Parse the provided args against the defined options.
63
+ def parse #:nodoc:
64
+ # Do nothing if options are not defined.
65
+ return unless @@options.size > 0
66
+
67
+ # Show help if it's anywhere in the argument list.
68
+ if @@args.include?('--help')
69
+ self.help
70
+ else
71
+ begin
72
+ # Delegate parsing to our parser class, passing it our defined
73
+ # options and the passed arguments.
74
+ @@choices = LazyHash.new(Parser.parse(@@options, @@args))
75
+ rescue Choice::Parser::ParseError
76
+ # If we get an expected exception, show the help file.
77
+ self.help
78
+ end
79
+ end
80
+ end
81
+
82
+ # Did we already parse the arguments?
83
+ def parsed? #:nodoc:
84
+ @@choices ||= false
85
+ end
86
+
87
+ # Print the help screen by calling our Writer object
88
+ def help #:nodoc:
89
+ Writer.help( { :banner => @@banner, :header => @@header,
90
+ :options => @@options, :footer => @@footer },
91
+ output_to, exit_on_help? )
92
+ end
93
+
94
+ # Set the args, potentially to something other than ARGV.
95
+ def args=(args) #:nodoc:
96
+ @@args = args.dup.map { |a| a + '' }
97
+ parse if parsed?
98
+ end
99
+
100
+ # Return the args.
101
+ def args #:nodoc:
102
+ @@args
103
+ end
104
+
105
+ # You can choose to not kill the script after the help screen is prtined.
106
+ def dont_exit_on_help=(val) #:nodoc:
107
+ @@exit = true
108
+ end
109
+
110
+ # Do we want to exit on help?
111
+ def exit_on_help? #:nodoc:
112
+ @@exit rescue false
113
+ end
114
+
115
+ # If we want to write to somewhere other than STDOUT.
116
+ def output_to(target = nil) #:nodoc:
117
+ @@output_to ||= STDOUT
118
+ return @@output_to if target.nil?
119
+ @@output_to = target
120
+ end
121
+
122
+ # Reset all the class variables.
123
+ def reset #:nodoc:
124
+ @@args = false
125
+ @@banner = false
126
+ @@header = Array.new
127
+ @@options = Array.new
128
+ @@footer = Array.new
129
+ end
130
+ end
131
+
132
+ 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,104 @@
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]
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(option = nil, &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
+ self.instance_eval(&block) if block_given?
21
+
22
+ # This might be going away in the future. If you pass nothing but a
23
+ # name, Option will try and guess what you want.
24
+ defaultize(option) unless option.nil?
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 true if method =~ /\?/ && instance_variable_get(var)
41
+ return false if method =~ /\?/
42
+
43
+ # If we were called with no arguments, we want a get.
44
+ return instance_variable_get(var) unless args[0] || block_given?
45
+
46
+ # If we were given a block or an argument, save it.
47
+ instance_variable_set(var, args[0]) if args[0]
48
+ instance_variable_set(var, block) if block_given?
49
+
50
+ # Add the choice to the @choices array if we're setting it for the first
51
+ # time.
52
+ @choices << method if args[0] || block_given? unless @choices.index(method)
53
+ end
54
+
55
+ # Might be going away soon. Tries to make some guesses about what you
56
+ # want if you instantiated Option with a name and no block.
57
+ def defaultize(option)
58
+ option = option.to_s
59
+ short "-#{option[0..0].downcase}"
60
+ long "--#{option.downcase}=#{option.upcase}"
61
+ end
62
+
63
+ # The desc method is slightly special: it stores itself as an array and
64
+ # each subsequent call adds to that array, rather than overwriting it.
65
+ # This is so we can do multi-line descriptions easily.
66
+ def desc(string = nil)
67
+ return @desc if string.nil?
68
+
69
+ @desc ||= []
70
+ @desc.push(string)
71
+
72
+ # Only add to @choices array if it's not already present.
73
+ @choices << 'desc' unless @choices.index('desc')
74
+ end
75
+
76
+ # Simple, desc question method.
77
+ def desc?
78
+ return false if @desc.nil?
79
+ true
80
+ end
81
+
82
+ # Returns Option converted to an array.
83
+ def to_a
84
+ array = []
85
+ @choices.each do |choice|
86
+ array << instance_variable_get("@#{choice}") if @choices.include? choice
87
+ end
88
+ array
89
+ end
90
+
91
+ # Returns Option converted to a hash.
92
+ def to_h
93
+ hash = {}
94
+ @choices.each do |choice|
95
+ hash[choice] = instance_variable_get("@#{choice}") if @choices.include? choice
96
+ end
97
+ hash
98
+ end
99
+
100
+ # In case someone tries to use a method we don't know about in their
101
+ # option block.
102
+ class ParseError < Exception; end
103
+ end
104
+ end
@@ -0,0 +1,145 @@
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
+
7
+ # What method to call on an object for each given 'cast' value.
8
+ CAST_METHODS = { Integer => :to_i, String => :to_s, Float => :to_f,
9
+ Symbol => :to_sym }
10
+
11
+ # Perhaps this method does too much. It is, however, a parser.
12
+ # You pass it an array of arrays, the first element of each element being
13
+ # the option's name and the second element being a hash of the option's
14
+ # info. You also pass in your current arguments, so it knows what to
15
+ # check against.
16
+ def self.parse(options, args)
17
+ # Return empty hash if the parsing adventure would be fruitless.
18
+ return {} if options.nil? || !options || args.nil? || !args.is_a?(Array)
19
+
20
+ # If we are passed an array, make the best of it by converting it
21
+ # to a hash.
22
+ if options.is_a?(Array)
23
+ new_options = {}
24
+ options.each { |o| new_options[o[0]] = o[1] if o.is_a?(Array) }
25
+ options = new_options
26
+ end
27
+
28
+ # Define local hashes we're going to use. choices is where we store
29
+ # the actual values we've pulled from the argument list.
30
+ hashes, longs, required, validators, choices = {}, {}, {}, {}, {}
31
+
32
+ # We can define these on the fly because they are all so similar.
33
+ params = %w[short cast filter action default]
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
+ if obj.respond_to?(:to_h)
42
+ obj = obj.to_h
43
+ else
44
+ raise HashExpectedForOption
45
+ end
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, save it as a regex.
51
+ # If it's present but can't pull off a to_s (wtf?), raise an error.
52
+ if obj['validate'] && obj['validate'].respond_to?(:to_s)
53
+ validators[name] = Regexp.new(obj['validate'].to_s)
54
+ elsif obj['validate']
55
+ raise ValidateExpectsRegexp
56
+ end
57
+
58
+ # Parse the long option. If it contains a =, figure out if the
59
+ # argument is required or optional. Optional arguments are formed
60
+ # like [ARG], whereas required are just ARG (in --long=ARG style).
61
+ if obj['long'] && obj['long'] =~ /=/
62
+ option, argument = obj['long'].split('=')
63
+ longs[name] = option
64
+ required[name] = true unless argument =~ /^\[(.+)\]$/
65
+ elsif obj['long']
66
+ # Set without any checking if it's just --long
67
+ longs[name] = obj['long']
68
+ end
69
+ end
70
+
71
+ # Go through the arguments and try to figure out whom they belong to
72
+ # at this point.
73
+ args.each_with_index do |arg, i|
74
+ if hashes['shorts'].value?(arg)
75
+ # Set the value to the next element in the args array since
76
+ # this is a short.
77
+ value = args[i+1]
78
+
79
+ # If the next element doesn't exist or starts with a -, make this
80
+ # value true.
81
+ value = true if !value || value =~ /^-/
82
+
83
+ # Add this value to the choices hash with the key of the option's
84
+ # name.
85
+ choices[hashes['shorts'].index(arg)] = value
86
+
87
+ elsif arg =~ /=/ && longs.value?(arg.split('=')[0])
88
+ # If we get a long with a = in it, grab it and the argument
89
+ # passed to it.
90
+ choices[longs.index(arg.split('=')[0])] = arg.split('=')[1]
91
+
92
+ elsif longs.value?(arg)
93
+ # If we get a long with no =, just set it to true.
94
+ choices[longs.index(arg)] = true
95
+
96
+ else
97
+ # If we're here, we have no idea what the passed argument is. Die.
98
+ raise UnknownArgument if arg =~ /^-/
99
+
100
+ end
101
+ end
102
+
103
+ # Okay, we got all the choices. Now go through and run any filters or
104
+ # whatever on them.
105
+ choices.each do |name, value|
106
+ # Check to make sure we have all the required arguments.
107
+ raise ArgumentRequired if required[name] && value === true
108
+
109
+ # Validate the argument if we need to.
110
+ raise ArgumentValidationFails if validators[name] && validators[name] !~ value
111
+
112
+ # Cast the argument using the method defined in the constant hash.
113
+ value = value.send(CAST_METHODS[hashes['casts'][name]]) if hashes['casts'].include?(name)
114
+
115
+ # Run the value through a filter and re-set it with the return.
116
+ value = hashes['filters'][name].call(value) if hashes['filters'].include?(name)
117
+
118
+ # Run an action block if there is one associated.
119
+ hashes['actions'][name].call(value) if hashes['actions'].include?(name)
120
+
121
+ # Now that we've done all that, re-set the element of the choice hash
122
+ # with the (potentially) new value.
123
+ choices[name] = value
124
+ end
125
+
126
+ # Home stretch. Go through all the defaults defined and if a choice
127
+ # does not exist in our choices hash, set its value to the requested
128
+ # default.
129
+ hashes['defaults'].each do |name, value|
130
+ choices[name] = value unless choices[name]
131
+ end
132
+
133
+ # Return the choices hash.
134
+ choices
135
+ end
136
+
137
+ # All the possible exceptions this module can raise.
138
+ class ParseError < Exception; end
139
+ class HashExpectedForOption < Exception; end
140
+ class UnknownArgument < ParseError; end
141
+ class ArgumentRequired < ParseError; end
142
+ class ValidateExpectsRegexp < ParseError; end
143
+ class ArgumentValidationFails < ParseError; end
144
+ end
145
+ end