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