choice 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/LICENSE +18 -0
- data/README +347 -0
- data/examples/ftpd.rb +78 -0
- data/lib/choice.rb +132 -0
- data/lib/choice/lazyhash.rb +67 -0
- data/lib/choice/option.rb +104 -0
- data/lib/choice/parser.rb +145 -0
- data/lib/choice/version.rb +8 -0
- data/lib/choice/writer.rb +186 -0
- data/test/test_choice.rb +119 -0
- data/test/test_lazyhash.rb +76 -0
- data/test/test_option.rb +144 -0
- data/test/test_parser.rb +182 -0
- data/test/test_writer.rb +103 -0
- metadata +60 -0
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
|