choice 0.1.0

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,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