ivanvc-choice 0.1.3.1

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.
@@ -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