ivanvc-choice 0.1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,233 @@
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
+ # Operate on a copy of the inputs
22
+ args = args.dup
23
+
24
+ # If we are passed an array, make the best of it by converting it
25
+ # to a hash.
26
+ options = options.inject({}) do |hash, value|
27
+ value.is_a?(Array) ? hash.merge(value.first => value[1]) : hash
28
+ end if options.is_a? Array
29
+
30
+ # Define local hashes we're going to use. choices is where we store
31
+ # the actual values we've pulled from the argument list.
32
+ hashes, longs, required, validators, choices, arrayed = {}, {}, {}, {}, {}, {}
33
+ hard_required = {}
34
+
35
+ # We can define these on the fly because they are all so similar.
36
+ params = %w[short cast filter action default valid]
37
+ params.each { |param| hashes["#{param}s"] = {} }
38
+
39
+ # Inspect each option and move its info into our local hashes.
40
+ options.each do |name, obj|
41
+ name = name.to_s
42
+
43
+ # Only take hashes or hash-like duck objects.
44
+ raise HashExpectedForOption unless obj.respond_to? :to_h
45
+ obj = obj.to_h
46
+
47
+ # Is this option required?
48
+ hard_required[name] = true if obj['required']
49
+
50
+ # Set the local hashes if the value exists on this option object.
51
+ params.each { |param| hashes["#{param}s"][name] = obj[param] if obj[param] }
52
+
53
+ # If there is a validate statement, make it a regex or proc.
54
+ validators[name] = make_validation(obj['validate']) if obj['validate']
55
+
56
+ # Parse the long option. If it contains a =, figure out if the
57
+ # argument is required or optional. Optional arguments are formed
58
+ # like [=ARG], whereas required are just ARG (in --long=ARG style).
59
+ if obj['long'] && obj['long'] =~ /(=|\[| )/
60
+ # Save the separator we used, as we're gonna need it, then split
61
+ sep = $1
62
+ option, *argument = obj['long'].split(sep)
63
+
64
+ # The actual name of the long switch
65
+ longs[name] = option
66
+
67
+ # Preserve the original argument, as it may contain [ or =,
68
+ # by joining with the character we split on. Add a [ in front if
69
+ # we split on that.
70
+ argument = (sep == '[' ? '[' : '') << Array(argument).join(sep)
71
+
72
+ # Do we expect multiple arguments which get turned into an array?
73
+ arrayed[name] = true if argument =~ /^\[?=?\*(.+)\]?$/
74
+
75
+ # Is this long required or optional?
76
+ required[name] = true unless argument =~ /^\[=?\*?(.+)\]$/
77
+ elsif obj['long']
78
+ # We can't have a long as a switch when valid is set -- die.
79
+ raise ArgumentRequiredWithValid if obj['valid']
80
+
81
+ # Set without any checking if it's just --long
82
+ longs[name] = obj['long']
83
+ end
84
+
85
+ # If we were given a list of valid arguments with 'valid,' this option
86
+ # is definitely required.
87
+ required[name] = true if obj['valid']
88
+ end
89
+
90
+ rest = []
91
+
92
+ # Go through the arguments and try to figure out whom they belong to
93
+ # at this point.
94
+ while arg = args.shift
95
+ if hashes['shorts'].value?(arg)
96
+ # Set the value to the next element in the args array since
97
+ # this is a short.
98
+
99
+ # If the next argument isn't a value, set this value to true
100
+ if args.empty? || args.first.match(/^-/)
101
+ value = true
102
+ else
103
+ value = args.shift
104
+ end
105
+
106
+ # Add this value to the choices hash with the key of the option's
107
+ # name. If we expect an array, tack this argument on.
108
+ name = hashes['shorts'].index(arg)
109
+ if arrayed[name]
110
+ choices[name] ||= []
111
+ choices[name] << value unless value.nil?
112
+ choices[name] += arrayize_arguments(args)
113
+ else
114
+ choices[name] = value
115
+ end
116
+
117
+ elsif (m = arg.match(/^(--[^=]+)=?/)) && longs.value?(m[1])
118
+ # The joke here is we always accept both --long=VALUE and --long VALUE.
119
+
120
+ # Grab values from --long=VALUE format
121
+ name, value = arg.split('=', 2)
122
+ name = longs.index(name)
123
+
124
+ if value.nil? && args.first !~ /^-/
125
+ # Grab value otherwise if not in --long=VALUE format. Assume --long VALUE.
126
+ # Value is nil if we don't have a = and the next argument is no good
127
+ value = args.shift
128
+ end
129
+
130
+ # If we expect an array, tack this argument on.
131
+ if arrayed[name]
132
+ # If this is arrayed and the value isn't nil, set it.
133
+ choices[name] ||= []
134
+ choices[name] << value unless value.nil?
135
+ choices[name] += arrayize_arguments(args)
136
+ else
137
+ # If we set the value to nil, that means nothing was set and we
138
+ # need to set the value to true. We'll find out later if that's
139
+ # acceptable or not.
140
+ choices[name] = value.nil? ? true : value
141
+ end
142
+
143
+ else
144
+ # If we're here, we have no idea what the passed argument is. Die.
145
+ if arg =~ /^-/
146
+ raise UnknownOption
147
+ else
148
+ rest << arg
149
+ end
150
+ end
151
+ end
152
+
153
+ # Okay, we got all the choices. Now go through and run any filters or
154
+ # whatever on them.
155
+ choices.each do |name, value|
156
+ # Check to make sure we have all the required arguments.
157
+ raise ArgumentRequired if required[name] && value === true
158
+
159
+ # Validate the argument if we need to, against a regexp or a block.
160
+ if validators[name]
161
+ if validators[name].is_a?(Regexp) && validators[name] =~ value
162
+ elsif validators[name].is_a?(Proc) && validators[name].call(value)
163
+ else raise ArgumentValidationFails
164
+ end
165
+ end
166
+
167
+ # Make sure the argument is valid
168
+ raise InvalidArgument unless value.to_a.all? { |v| hashes['valids'][name].include?(v) } if hashes['valids'][name]
169
+
170
+ # Cast the argument using the method defined in the constant hash.
171
+ value = value.send(CAST_METHODS[hashes['casts'][name]]) if hashes['casts'].include?(name)
172
+
173
+ # Run the value through a filter and re-set it with the return.
174
+ value = hashes['filters'][name].call(value) if hashes['filters'].include?(name)
175
+
176
+ # Run an action block if there is one associated.
177
+ hashes['actions'][name].call(value) if hashes['actions'].include?(name)
178
+
179
+ # Now that we've done all that, re-set the element of the choice hash
180
+ # with the (potentially) new value.
181
+ choices[name] = value
182
+ end
183
+
184
+ # Die if we're missing any required arguments
185
+ hard_required.each do |name, value|
186
+ raise ArgumentRequired unless choices[name]
187
+ end
188
+
189
+ # Home stretch. Go through all the defaults defined and if a choice
190
+ # does not exist in our choices hash, set its value to the requested
191
+ # default.
192
+ hashes['defaults'].each do |name, value|
193
+ choices[name] = value unless choices[name]
194
+ end
195
+
196
+ # Return the choices hash and the rest of the args
197
+ [ choices, rest ]
198
+ end
199
+
200
+ private
201
+ # Turns trailing command line arguments into an array for an arrayed value
202
+ def arrayize_arguments(args)
203
+ # Go through trailing arguments and suck them in if they don't seem
204
+ # to have an owner.
205
+ array = []
206
+ until args.empty? || args.first.match(/^-/)
207
+ array << args.shift
208
+ end
209
+ array
210
+ end
211
+
212
+ def make_validation(validation)
213
+ case validation
214
+ when Proc then
215
+ validation
216
+ when Regexp, String then
217
+ Regexp.new(validation.to_s)
218
+ else
219
+ raise ValidateExpectsRegexpOrBlock
220
+ end
221
+ end
222
+
223
+ # All the possible exceptions this module can raise.
224
+ class ParseError < Exception; end
225
+ class HashExpectedForOption < Exception; end
226
+ class UnknownOption < ParseError; end
227
+ class ArgumentRequired < ParseError; end
228
+ class ValidateExpectsRegexpOrBlock < ParseError; end
229
+ class ArgumentValidationFails < ParseError; end
230
+ class InvalidArgument < ParseError; end
231
+ class ArgumentRequiredWithValid < ParseError; end
232
+ end
233
+ end
@@ -0,0 +1,9 @@
1
+ module Choice
2
+ module Version #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 3
6
+ PATCH = 1
7
+ STRING = [MAJOR, MINOR, TINY, PATCH] * '.'
8
+ end
9
+ end
@@ -0,0 +1,187 @@
1
+ module Choice
2
+ # This module writes to the screen. As of now, its only real use is writing
3
+ # the help screen.
4
+ module Writer #:nodoc: all
5
+
6
+ # Some constants used for printing and line widths
7
+ SHORT_LENGTH = 6
8
+ SHORT_BREAK_LENGTH = 2
9
+ LONG_LENGTH = 29
10
+ PRE_DESC_LENGTH = SHORT_LENGTH + SHORT_BREAK_LENGTH + LONG_LENGTH
11
+
12
+
13
+ # The main method. Takes a hash of arguments with the following possible
14
+ # keys, running them through the appropriate method:
15
+ # banner, header, options, footer
16
+ #
17
+ # Can also be told where to print (default STDOUT) and not to exit after
18
+ # printing the help screen, which it does by default.
19
+ def self.help(args, target = STDOUT, dont_exit = false)
20
+ # Set our printing target.
21
+ self.target = target
22
+
23
+ # The banner method needs to know about the passed options if it's going
24
+ # to do its magic. Only really needs :options if :banner is nil.
25
+ banner(args[:banner], args[:options])
26
+
27
+ # Run these three methods, passing in the appropriate hash element.
28
+ %w[header options footer].each do |meth|
29
+ send(meth, args[meth.to_sym])
30
+ end
31
+
32
+ # Exit. Unless you don't want to.
33
+ exit unless dont_exit
34
+ end
35
+
36
+ class <<self
37
+ private
38
+
39
+ # Print a passed banner or assemble the default banner, which is usage.
40
+ def banner(banner, options)
41
+ if banner
42
+ puts banner
43
+ else
44
+ # Usage needs to know about the defined options.
45
+ usage(options)
46
+ end
47
+ end
48
+
49
+ # Print our header, which is just lines after the banner and before the
50
+ # options block. Needs an array, prints each element as a line.
51
+ def header(header)
52
+ if header.is_a?(Array) and header.size > 0
53
+ header.each { |line| puts line }
54
+ end
55
+ end
56
+
57
+ # Print out the options block by going through each option and printing
58
+ # it as a line (or more). Expects an array.
59
+ def options(options)
60
+ # Do nothing if there's nothing to do.
61
+ return if options.nil? || !options.size
62
+
63
+ # If the option is a hash, run it through option_line. Otherwise
64
+ # just print it out as is.
65
+ options.each do |name, option|
66
+ if option.respond_to?(:to_h)
67
+ option_line(option.to_h)
68
+ else
69
+ puts name
70
+ end
71
+ end
72
+ end
73
+
74
+ # The heavy lifting: print a line for an option. Has intimate knowledge
75
+ # of what keys are expected.
76
+ def option_line(option)
77
+ # Expect a hash
78
+ return unless option.is_a?(Hash)
79
+
80
+ # Make this easier on us
81
+ short = option['short']
82
+ long = option['long']
83
+ line = ''
84
+
85
+ # Get the short part.
86
+ line << sprintf("%#{SHORT_LENGTH}s", short)
87
+ line << sprintf("%-#{SHORT_BREAK_LENGTH}s", (',' if short && long))
88
+
89
+ # Get the long part.
90
+ line << sprintf("%-#{LONG_LENGTH}s", long)
91
+
92
+ # Print what we have so far
93
+ print line
94
+
95
+ # If there's a desc, print it.
96
+ if option['desc']
97
+ # If the line is too long, spill over to the next line
98
+ if line.length > PRE_DESC_LENGTH
99
+ puts
100
+ print " " * PRE_DESC_LENGTH
101
+ end
102
+
103
+ puts option['desc'].shift
104
+
105
+ # If there is more than one desc line, print each one in succession
106
+ # as separate lines.
107
+ option['desc'].each do |desc|
108
+ puts ' '*37 + desc
109
+ end
110
+
111
+ else
112
+ # No desc, just print a newline.
113
+ puts
114
+
115
+ end
116
+ end
117
+
118
+ # Expects an array, prints each element as a line.
119
+ def footer(footer)
120
+ footer.each { |line| puts line } unless footer.nil?
121
+ end
122
+
123
+ # Prints the usage statement, e.g. Usage prog.rb [-abc]
124
+ # Expects an array.
125
+ def usage(options)
126
+ # Really we just need an enumerable.
127
+ return unless options.respond_to?(:each)
128
+
129
+ # Start off the options with a dash.
130
+ opts = '-'
131
+
132
+ # Figure out the option shorts.
133
+ options.dup.each do |option|
134
+ # We really need an array here.
135
+ next unless option.is_a?(Array)
136
+
137
+ # Grab the hash of the last element, which should be the second
138
+ # element.
139
+ option = option.last.to_h
140
+
141
+ # Add the short to the options string.
142
+ opts << option['short'].sub('-','') if option['short']
143
+ end
144
+
145
+ # Figure out if we actually got any options.
146
+ opts = if opts =~ /^-(.+)/
147
+ " [#{opts}]"
148
+ end.to_s
149
+
150
+ # Print it out, with our newly aquired options string.
151
+ puts "Usage: #{program}" << opts
152
+ end
153
+
154
+ # Figure out the name of this program based on what was run.
155
+ def program
156
+ (/(\/|\\)/ =~ $0) ? File.basename($0) : $0
157
+ end
158
+
159
+ # Set where we print.
160
+ def target=(target)
161
+ @@target = target
162
+ end
163
+
164
+ # Where do we print?
165
+ def target
166
+ @@target
167
+ end
168
+
169
+ public
170
+ # Fake puts
171
+ def puts(str = nil)
172
+ str = '' if str.nil?
173
+ print(str + "\n")
174
+ end
175
+
176
+ # Fake printf
177
+ def printf(format, *args)
178
+ print(sprintf(format, *args))
179
+ end
180
+
181
+ # Fake print -- just add to target, which may not be STDOUT.
182
+ def print(str)
183
+ target << str
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,234 @@
1
+ $:.unshift "../lib:lib"
2
+ require 'test/unit'
3
+ require 'choice'
4
+
5
+ $VERBOSE = nil
6
+
7
+ class TestChoice < Test::Unit::TestCase
8
+
9
+ def setup
10
+ Choice.reset!
11
+ Choice.dont_exit_on_help = true
12
+ Choice.send(:class_variable_set, '@@choices', true)
13
+ end
14
+
15
+ def test_choices
16
+ Choice.options do
17
+ header "Tell me about yourself?"
18
+ header ""
19
+ option :band do
20
+ short "-b"
21
+ long "--band=BAND"
22
+ cast String
23
+ desc "Your favorite band."
24
+ validate /\w+/
25
+ end
26
+ option :animal do
27
+ short "-a"
28
+ long "--animal=ANIMAL"
29
+ cast String
30
+ desc "Your favorite animal."
31
+ end
32
+ footer ""
33
+ footer "--help This message"
34
+ end
35
+
36
+ band = 'LedZeppelin'
37
+ animal = 'Reindeer'
38
+
39
+ args = ['-b', band, "--animal=#{animal}"]
40
+ Choice.args = args
41
+
42
+ assert_equal band, Choice.choices['band']
43
+ assert_equal animal, Choice.choices[:animal]
44
+ assert_equal ["Tell me about yourself?", ""], Choice.header
45
+ assert_equal ["", "--help This message"], Choice.footer
46
+
47
+ assert_equal Choice.choices['band'], Choice['band']
48
+ assert_equal Choice.choices[:animal], Choice[:animal]
49
+ end
50
+
51
+ def test_failed_parse
52
+ assert Hash.new, Choice.parse
53
+ end
54
+
55
+ HELP_STRING = ''
56
+ def test_help
57
+ Choice.output_to(HELP_STRING)
58
+
59
+ Choice.options do
60
+ banner "Usage: choice [-mu]"
61
+ header ""
62
+ option :meal do
63
+ short '-m'
64
+ desc 'Your favorite meal.'
65
+ end
66
+
67
+ separator ""
68
+ separator "And you eat it with..."
69
+
70
+ option :utencil do
71
+ short "-u"
72
+ long "--utencil[=UTENCIL]"
73
+ desc "Your favorite eating utencil."
74
+ end
75
+ end
76
+
77
+ Choice.args = ['-m', 'lunch', '--help']
78
+
79
+ help_string = <<-HELP
80
+ Usage: choice [-mu]
81
+
82
+ -m Your favorite meal.
83
+
84
+ And you eat it with...
85
+ -u, --utencil[=UTENCIL] Your favorite eating utencil.
86
+ HELP
87
+
88
+ assert_equal help_string, HELP_STRING
89
+ end
90
+
91
+ UNKNOWN_STRING = ''
92
+ def test_unknown_argument
93
+ Choice.output_to(UNKNOWN_STRING)
94
+
95
+ Choice.options do
96
+ banner "Usage: choice [-mu]"
97
+ header ""
98
+ option :meal do
99
+ short '-m'
100
+ desc 'Your favorite meal.'
101
+ end
102
+
103
+ separator ""
104
+ separator "And you eat it with..."
105
+
106
+ option :utencil do
107
+ short "-u"
108
+ long "--utencil[=UTENCIL]"
109
+ desc "Your favorite eating utencil."
110
+ end
111
+ end
112
+
113
+ Choice.args = ['-m', 'lunch', '--motorcycles']
114
+
115
+ help_string = <<-HELP
116
+ Usage: choice [-mu]
117
+
118
+ -m Your favorite meal.
119
+
120
+ And you eat it with...
121
+ -u, --utencil[=UTENCIL] Your favorite eating utencil.
122
+ HELP
123
+
124
+ assert_equal help_string, UNKNOWN_STRING
125
+ end
126
+
127
+ REQUIRED_STRING = ''
128
+ def test_required_argument
129
+ Choice.output_to(REQUIRED_STRING)
130
+
131
+ Choice.options do
132
+ banner "Usage: choice [-mu]"
133
+ header ""
134
+ option :meal, :required => true do
135
+ short '-m'
136
+ desc 'Your favorite meal.'
137
+ end
138
+
139
+ separator ""
140
+ separator "And you eat it with..."
141
+
142
+ option :utencil do
143
+ short "-u"
144
+ long "--utencil[=UTENCIL]"
145
+ desc "Your favorite eating utencil."
146
+ end
147
+ end
148
+
149
+ Choice.args = ['-u', 'spork']
150
+
151
+ help_string = <<-HELP
152
+ Usage: choice [-mu]
153
+
154
+ -m Your favorite meal.
155
+
156
+ And you eat it with...
157
+ -u, --utencil[=UTENCIL] Your favorite eating utencil.
158
+ HELP
159
+
160
+ assert_equal help_string, REQUIRED_STRING
161
+ end
162
+
163
+ def test_shorthand_choices
164
+ Choice.options do
165
+ header "Tell me about yourself?"
166
+ header ""
167
+ options :band => { :short => "-b", :long => "--band=BAND", :cast => String, :desc => ["Your favorite band.", "Something cool."],
168
+ :validate => /\w+/ },
169
+ :animal => { :short => "-a", :long => "--animal=ANIMAL", :cast => String, :desc => "Your favorite animal." }
170
+
171
+ footer ""
172
+ footer "--help This message"
173
+ end
174
+
175
+ band = 'LedZeppelin'
176
+ animal = 'Reindeer'
177
+
178
+ args = ['-b', band, "--animal=#{animal}"]
179
+ Choice.args = args
180
+
181
+ assert_equal band, Choice.choices['band']
182
+ assert_equal animal, Choice.choices[:animal]
183
+ assert_equal ["Tell me about yourself?", ""], Choice.header
184
+ assert_equal ["", "--help This message"], Choice.footer
185
+ end
186
+
187
+ def test_args_of
188
+ suits = %w[clubs diamonds spades hearts]
189
+ stringed_numerics = (1..13).to_a.map { |a| a.to_s }
190
+ valid_cards = stringed_numerics + %w[jack queen king ace]
191
+ cards = {}
192
+ stringed_numerics.each { |n| cards[n] = n }
193
+ cards.merge!('1' => 'ace', '11' => 'jack', '12' => 'queen', '13' => 'king')
194
+
195
+ Choice.options do
196
+ header "Gambling is fun again! Pick a card and a suit (or two), then see if you win!"
197
+ header ""
198
+ header "Options:"
199
+
200
+ option :suit, :required => true do
201
+ short '-s'
202
+ long '--suit *SUITS'
203
+ desc "The suit you wish to choose. Required. You can pass in more than one, even."
204
+ desc " Valid suits: #{suits * ' '}"
205
+ valid suits
206
+ end
207
+
208
+ separator ''
209
+
210
+ option :card, :required => true do
211
+ short '-c'
212
+ long '--card CARD'
213
+ desc "The card you wish to gamble on. Required. Only one, please."
214
+ desc " Valid cards: 1 - 13, jack, queen, king, ace"
215
+ valid valid_cards
216
+ cast String
217
+ end
218
+
219
+ #cheat! to test --option=
220
+ option :autowin do
221
+ short '-a'
222
+ long '--autowin=PLAYER'
223
+ desc 'The person who should automatically win every time'
224
+ desc 'Beware: raises the suspitions of other players'
225
+ end
226
+ end
227
+
228
+ args = ["-c", "king", "--suit", "clubs", "diamonds", "spades", "hearts", "--autowin", "Grant"]
229
+ Choice.args = args
230
+ assert_equal ["king"], Choice.args_of("-c")
231
+ assert_equal ["clubs", "diamonds", "spades", "hearts"], Choice.args_of("--suit")
232
+ assert_equal ["Grant"], Choice.args_of("--autowin")
233
+ end
234
+ end