qoobaa-user-choices 1.1.7
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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +39 -0
- data/README.rdoc +7 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/examples/older/README.txt +133 -0
- data/examples/older/command-line.rb +46 -0
- data/examples/older/default-values.rb +41 -0
- data/examples/older/multiple-sources.rb +58 -0
- data/examples/older/postprocess.rb +39 -0
- data/examples/older/switches.rb +44 -0
- data/examples/older/two-args.rb +31 -0
- data/examples/older/types.rb +61 -0
- data/examples/tutorial/css/LICENSE.txt +1 -0
- data/examples/tutorial/css/bg2.gif +0 -0
- data/examples/tutorial/css/left.gif +0 -0
- data/examples/tutorial/css/left_on.gif +0 -0
- data/examples/tutorial/css/main.css +242 -0
- data/examples/tutorial/css/right.gif +0 -0
- data/examples/tutorial/css/right_on.gif +0 -0
- data/examples/tutorial/css/tvline.gif +0 -0
- data/examples/tutorial/css/von-foerster.jpg +0 -0
- data/examples/tutorial/css/von-foerster2.jpg +0 -0
- data/examples/tutorial/index.html +703 -0
- data/examples/tutorial/tutorial1.rb +41 -0
- data/examples/tutorial/tutorial2.rb +44 -0
- data/examples/tutorial/tutorial3.rb +47 -0
- data/examples/tutorial/tutorial4.rb +47 -0
- data/examples/tutorial/tutorial5.rb +35 -0
- data/examples/tutorial/tutorial6.rb +35 -0
- data/examples/tutorial/tutorial7.rb +41 -0
- data/lib/user-choices.rb +131 -0
- data/lib/user-choices/arglist-strategies.rb +179 -0
- data/lib/user-choices/builder.rb +118 -0
- data/lib/user-choices/command-line-source.rb +224 -0
- data/lib/user-choices/command.rb +42 -0
- data/lib/user-choices/conversions.rb +169 -0
- data/lib/user-choices/ruby-extensions.rb +20 -0
- data/lib/user-choices/sources.rb +278 -0
- data/lib/user-choices/version.rb +3 -0
- data/test/arglist_strategy_test.rb +42 -0
- data/test/builder_test.rb +631 -0
- data/test/command_line_source_test.rb +443 -0
- data/test/conversion_test.rb +172 -0
- data/test/source_test.rb +451 -0
- data/test/test_helper.rb +9 -0
- data/test/user_choices_slow_test.rb +276 -0
- data/user-choices.gemspec +104 -0
- metadata +122 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-08-10.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
|
7
|
+
class Range # :nodoc:
|
8
|
+
def in_words
|
9
|
+
last_element = self.last
|
10
|
+
last_element -= 1 if exclude_end?
|
11
|
+
"#{self.first} to #{last_element}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class String # :nodoc:
|
16
|
+
def to_inputable_sym
|
17
|
+
gsub(/-/, '_').to_sym
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,278 @@
|
|
1
|
+
require 'xmlsimple' unless self.class.const_defined?("XmlSimple") # ActiveSupport includes it in /vendor. Grr.
|
2
|
+
require 'yaml'
|
3
|
+
require 's4t-utils'
|
4
|
+
include S4tUtils
|
5
|
+
|
6
|
+
require 'user-choices/ruby-extensions'
|
7
|
+
require 'user-choices/conversions'
|
8
|
+
|
9
|
+
|
10
|
+
module UserChoices # :nodoc
|
11
|
+
|
12
|
+
|
13
|
+
# TODO: Right now, elements that are named in a source, but not in an
|
14
|
+
# add_choice() call, nevertheless appear in the final array. Is that good?
|
15
|
+
# Bad? Irrelevant?
|
16
|
+
class AbstractSource < Hash # :nodoc:
|
17
|
+
|
18
|
+
attr_reader :external_names
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
super()
|
22
|
+
@external_names = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def source; subclass_responsibility; end
|
26
|
+
|
27
|
+
|
28
|
+
def fill; subclass_responsibility; end
|
29
|
+
|
30
|
+
def apply(choice_conversions)
|
31
|
+
each_conversion(choice_conversions) do | choice, conversion |
|
32
|
+
next unless self.has_key?(choice)
|
33
|
+
|
34
|
+
user_claims(conversion.suitable?(self[choice])) {
|
35
|
+
error_prefix + bad_look(choice, conversion)
|
36
|
+
}
|
37
|
+
|
38
|
+
self[choice] = conversion.convert(self[choice])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def adjust(final_results)
|
43
|
+
# Do nothing
|
44
|
+
end
|
45
|
+
|
46
|
+
def each_conversion(choice_conversion_hash)
|
47
|
+
choice_conversion_hash.each do | choice, conversion_list |
|
48
|
+
conversion_list.each do | conversion |
|
49
|
+
yield(choice, conversion)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def error_prefix
|
58
|
+
"Error in #{source}: "
|
59
|
+
end
|
60
|
+
|
61
|
+
def pretty_value(value)
|
62
|
+
case value
|
63
|
+
when Array
|
64
|
+
value.inspect
|
65
|
+
else
|
66
|
+
"'#{value}'"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def bad_look(key, conversion)
|
71
|
+
like_what = conversion.description
|
72
|
+
"#{@external_names[key]}'s value must be #{like_what}, and #{pretty_value(self[key])} doesn't look right."
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
class DefaultSource < AbstractSource # :nodoc:
|
78
|
+
|
79
|
+
def use_hash(defaults)
|
80
|
+
@defaults = defaults
|
81
|
+
count_symbols_as_external_names(@defaults.keys)
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def fill
|
86
|
+
merge!(@defaults)
|
87
|
+
end
|
88
|
+
|
89
|
+
def source
|
90
|
+
"the default values"
|
91
|
+
end
|
92
|
+
|
93
|
+
def count_symbols_as_external_names(symbols)
|
94
|
+
symbols.each { | symbol |
|
95
|
+
# Use inspect so that symbol prints with leading colon
|
96
|
+
@external_names[symbol] = symbol.inspect
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
DefaultChoices = DefaultSource # Backward compatibility
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
# Describe the environment as a source of choices.
|
105
|
+
class EnvironmentSource < AbstractSource
|
106
|
+
def fill # :nodoc:
|
107
|
+
@external_names.each { | key, env_var |
|
108
|
+
self[key] = ENV[env_var] if ENV.has_key?(env_var)
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
# Environment variables beginning with _prefix_ (a string)
|
113
|
+
# are considered to be user choices relevant to this script.
|
114
|
+
# Everything after the prefix names a choice (that is, a symbol).
|
115
|
+
# Dashes are converted to underscores. Examples:
|
116
|
+
# * Environment variable <tt>prefix-my-choice</tt> with prefix <tt>"prefix-" is choice <tt>:my_choice</tt>.
|
117
|
+
# * Environment variable <tt>PREFIX_FOO</tt> with prefix <tt>"PREFIX_" is choice <tt>:FOO</tt>
|
118
|
+
#
|
119
|
+
# If you want an array of strings, separate the values by commas:
|
120
|
+
# ENV_VAR=a,b,c
|
121
|
+
# There's currently no way to escape a comma and no cleverness about
|
122
|
+
# quotes or whitespace.
|
123
|
+
|
124
|
+
def with_prefix(prefix)
|
125
|
+
matches = ENV.collect do | env_var, ignored_value |
|
126
|
+
if /^#{prefix}(.+)/ =~ env_var
|
127
|
+
[$1.to_inputable_sym, env_var]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
@external_names.merge!(Hash[*matches.compact.flatten])
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
# Some environment variables have names you don't like. For example, $HOME
|
135
|
+
# might be annoying because of the uppercase. Also, if most of your program's
|
136
|
+
# environment variables have some prefix (see with_prefix) but you also want to use
|
137
|
+
# $HOME, you need a way to do that. You can satisfy both desires with
|
138
|
+
#
|
139
|
+
# EnvironmentSource.new.with_prefix("my_").mapping(:home => "HOME")
|
140
|
+
|
141
|
+
def mapping(map)
|
142
|
+
@external_names.merge!(map)
|
143
|
+
self
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
def source # :nodoc:
|
148
|
+
"the environment"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
EnvironmentChoices = EnvironmentSource # Backward compatibility
|
152
|
+
|
153
|
+
|
154
|
+
class FileSource < AbstractSource # :nodoc:
|
155
|
+
|
156
|
+
def from_file(filename)
|
157
|
+
from_complete_path(File.join(S4tUtils.find_home, filename))
|
158
|
+
end
|
159
|
+
|
160
|
+
def from_complete_path(path)
|
161
|
+
@path = path
|
162
|
+
@contents_as_hash = self.read_into_hash
|
163
|
+
@contents_as_hash.each do | external_name, value |
|
164
|
+
sym = external_name.to_inputable_sym
|
165
|
+
@external_names[sym] = external_name
|
166
|
+
end
|
167
|
+
self
|
168
|
+
end
|
169
|
+
|
170
|
+
def fill # :nodoc:
|
171
|
+
@external_names.each do | symbol, external_name |
|
172
|
+
self[symbol] = @contents_as_hash[external_name]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def source # :nodoc:
|
177
|
+
"configuration file #{@path}"
|
178
|
+
end
|
179
|
+
|
180
|
+
def read_into_hash # :nodoc:
|
181
|
+
return {} unless File.exist?(@path)
|
182
|
+
begin
|
183
|
+
format_specific_reading
|
184
|
+
rescue Exception => ex
|
185
|
+
if format_specific_exception?(ex)
|
186
|
+
msg = "Badly formatted #{source}: " + format_specific_message(ex)
|
187
|
+
ex = ex.class.new(msg)
|
188
|
+
end
|
189
|
+
raise ex
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
protected
|
194
|
+
|
195
|
+
def format_specific_message(ex)
|
196
|
+
ex.message
|
197
|
+
end
|
198
|
+
|
199
|
+
def format_specific_exception_handling(ex); subclass_responsibility; end
|
200
|
+
def format_specific_reading; subclass_responsibility; end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Use an XML file as a source of choices. The XML file is parsed
|
204
|
+
# with <tt>XmlSimple('ForceArray' => false)</tt>. That means that
|
205
|
+
# single elements like <home>Mars</home> are read as the value
|
206
|
+
# <tt>"Mars"</tt>, whereas <home>Mars</home><home>Venus</home> is
|
207
|
+
# read as <tt>["Mars", "Venus"]</tt>.
|
208
|
+
class XmlConfigFileSource < FileSource
|
209
|
+
|
210
|
+
# Treat _filename_ as the configuration file. _filename_ is expected
|
211
|
+
# to be in the home directory. The home directory is found in the
|
212
|
+
# same way Rubygems finds it. (First look in environment variables
|
213
|
+
# <tt>$HOME</tt>, <tt>$USERPROFILE</tt>, <tt>$HOMEDRIVE:$HOMEPATH</tt>,
|
214
|
+
# file expansion of <tt>"~"</tt> and finally the root.)
|
215
|
+
def format_specific_reading
|
216
|
+
XmlSimple.xml_in(@path, 'ForceArray' => false)
|
217
|
+
end
|
218
|
+
|
219
|
+
def format_specific_exception?(ex)
|
220
|
+
ex.is_a?(REXML::ParseException)
|
221
|
+
end
|
222
|
+
|
223
|
+
def format_specific_message(ex)
|
224
|
+
ex.continued_exception
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
XmlConfigFileChoices = XmlConfigFileSource # Backward compatibility
|
229
|
+
|
230
|
+
|
231
|
+
|
232
|
+
|
233
|
+
# Use an YAML file as a source of choices. Note: because the YAML parser
|
234
|
+
# can produce something out of many typo-filled YAML files, it's a
|
235
|
+
# good idea to check that your file looks like you'd expect before
|
236
|
+
# trusting in it. Do that with:
|
237
|
+
#
|
238
|
+
# irb> require 'yaml'
|
239
|
+
# irb> YAML.load_file('config.yaml')
|
240
|
+
#
|
241
|
+
class YamlConfigFileSource < FileSource
|
242
|
+
# Treat _filename_ as the configuration file. _filename_ is expected
|
243
|
+
# to be in the home directory. The home directory is found in the
|
244
|
+
# same way Rubygems finds it.
|
245
|
+
def format_specific_reading
|
246
|
+
result = YAML.load_file(@path)
|
247
|
+
ensure_hash_values_are_strings(result)
|
248
|
+
result
|
249
|
+
end
|
250
|
+
|
251
|
+
def format_specific_exception?(ex)
|
252
|
+
ex.is_a?(ArgumentError)
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
|
257
|
+
def ensure_hash_values_are_strings(h)
|
258
|
+
h.each { |k, v| ensure_element_is_string(h, k) }
|
259
|
+
end
|
260
|
+
|
261
|
+
def ensure_array_values_are_strings(a)
|
262
|
+
a.each_with_index { |elt, index| ensure_element_is_string(a, index) }
|
263
|
+
end
|
264
|
+
|
265
|
+
def ensure_element_is_string(collection, key)
|
266
|
+
case collection[key]
|
267
|
+
when Hash
|
268
|
+
ensure_hash_values_are_strings(collection[key])
|
269
|
+
when Array
|
270
|
+
ensure_array_values_are_strings(collection[key])
|
271
|
+
else
|
272
|
+
collection[key] = collection[key].to_s
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
end
|
278
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-08-10.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 's4t-utils'
|
8
|
+
require 'builder'
|
9
|
+
require 'user-choices/arglist-strategies'
|
10
|
+
include S4tUtils
|
11
|
+
set_test_paths(__FILE__)
|
12
|
+
|
13
|
+
# Since ArglistStrategies were extracted from CommandLineSource, most
|
14
|
+
# of the testing is implicit.
|
15
|
+
|
16
|
+
class AbstractArglistStrategyTests < Test::Unit::TestCase
|
17
|
+
include UserChoices
|
18
|
+
|
19
|
+
def test_range_violation_descriptions
|
20
|
+
@arglist_handler = AbstractArglistStrategy.new('unimportant')
|
21
|
+
# Good about plurals.
|
22
|
+
assert_match(/2 arguments given, 3 expected/,
|
23
|
+
@arglist_handler.arglist_arity_error(2, 3))
|
24
|
+
|
25
|
+
assert_match(/1 argument given, 3 expected/,
|
26
|
+
@arglist_handler.arglist_arity_error(1, 3))
|
27
|
+
|
28
|
+
assert_match(/0 arguments given, 1 expected/,
|
29
|
+
@arglist_handler.arglist_arity_error(0, 1))
|
30
|
+
|
31
|
+
# Handle both types of ranges.
|
32
|
+
assert_match(/2 arguments given, 3 to 5 expected/,
|
33
|
+
@arglist_handler.arglist_arity_error(2, 3..5))
|
34
|
+
assert_match(/1 argument given, 3 to 5 expected/,
|
35
|
+
@arglist_handler.arglist_arity_error(1, 3...6))
|
36
|
+
|
37
|
+
# Use 'or' if there are only two alternatives.
|
38
|
+
assert_match(/2 arguments given, 3 or 4 expected/,
|
39
|
+
@arglist_handler.arglist_arity_error(2, 3..4))
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,631 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 's4t-utils'
|
3
|
+
require 'user-choices'
|
4
|
+
include S4tUtils
|
5
|
+
set_test_paths(__FILE__)
|
6
|
+
|
7
|
+
|
8
|
+
class TestDefaultsAndTypes < Test::Unit::TestCase
|
9
|
+
include UserChoices
|
10
|
+
|
11
|
+
def test_builder_can_add_defaults
|
12
|
+
b = ChoicesBuilder.new
|
13
|
+
b.add_choice(:trip_steps, :default => '5')
|
14
|
+
choices = b.build
|
15
|
+
assert_equal('5', choices[:trip_steps])
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_defaults_need_not_be_strings
|
19
|
+
b = ChoicesBuilder.new
|
20
|
+
b.add_choice(:trip_steps, :default => 5)
|
21
|
+
choices = b.build
|
22
|
+
assert_equal(5, choices[:trip_steps])
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_builder_can_declare_types_and_do_error_checking
|
26
|
+
b = ChoicesBuilder.new
|
27
|
+
b.add_choice(:trip_steps, :default => 'a', :type => :integer)
|
28
|
+
assert_raises_with_matching_message(StandardError,
|
29
|
+
/:trip_steps's value must be an integer, and 'a'/) {
|
30
|
+
b.build
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_builder_can_declare_types_and_do_conversions
|
35
|
+
b = ChoicesBuilder.new
|
36
|
+
b.add_choice(:csv, :default => 'true', :type => :boolean)
|
37
|
+
choices = b.build
|
38
|
+
assert_equal(true, choices[:csv])
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_some_types_cause_no_conversion
|
42
|
+
# Checking is done
|
43
|
+
b = ChoicesBuilder.new
|
44
|
+
b.add_choice(:trip_steps, :default => 'a', :type => ['b', 'c'])
|
45
|
+
assert_raises_with_matching_message(StandardError,
|
46
|
+
/'a' doesn't look right/) {
|
47
|
+
b.build
|
48
|
+
}
|
49
|
+
|
50
|
+
# ... but, if checking passes, no changes are made
|
51
|
+
b = ChoicesBuilder.new
|
52
|
+
b.add_choice(:trip_steps, :default => 'b', :type => ['b', 'c'])
|
53
|
+
assert_equal('b', b.build[:trip_steps])
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_arrays_can_be_built_from_comma_separated_list
|
57
|
+
b = ChoicesBuilder.new
|
58
|
+
b.add_choice(:targets, :default => 'a,b,cd',
|
59
|
+
:type => [:string])
|
60
|
+
assert_equal(['a', 'b', 'cd'],
|
61
|
+
b.build[:targets])
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_arrays_can_be_accepted_as_is
|
65
|
+
b = ChoicesBuilder.new
|
66
|
+
b.add_choice(:targets, :default => ['a', 'b', 'c'],
|
67
|
+
:type => [:string])
|
68
|
+
assert_equal(['a', 'b', 'c'], b.build[:targets])
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_arrays_are_constructed_from_single_elements
|
72
|
+
b = ChoicesBuilder.new
|
73
|
+
b.add_choice(:targets, :default => 'a',
|
74
|
+
:type => [:string])
|
75
|
+
assert_equal(['a'], b.build[:targets])
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_array_lengths_can_be_specified_exactly
|
79
|
+
b = ChoicesBuilder.new
|
80
|
+
b.add_choice(:targets, :length => 2, :default => ['wrong'])
|
81
|
+
assert_raises_with_matching_message(StandardError, /must be of length 2/) {
|
82
|
+
b.build
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_array_lengths_can_be_specified_by_range
|
87
|
+
b = ChoicesBuilder.new
|
88
|
+
b.add_choice(:targets, :length => 2..3, :default => ['wrong'])
|
89
|
+
assert_raises_with_matching_message(StandardError, /this range: 2..3/) {
|
90
|
+
b.build
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_array_lengths_apply_to_command_line_args # didn't used to, not in the same way.
|
95
|
+
with_command_args("a b c d") {
|
96
|
+
b = ChoicesBuilder.new
|
97
|
+
b.add_source(CommandLineSource, :usage, 'foo')
|
98
|
+
b.add_choice(:targets, :length => 2..3) { |command_line |
|
99
|
+
command_line.uses_arglist
|
100
|
+
}
|
101
|
+
|
102
|
+
output = capturing_stderr do
|
103
|
+
assert_wants_to_exit do
|
104
|
+
b.build
|
105
|
+
end
|
106
|
+
end
|
107
|
+
assert_match(/^Error in the command line: 4 arguments given, 2 or 3 expected/, output)
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_missing_required_arg_is_caught_by_command_line
|
112
|
+
with_command_args("") {
|
113
|
+
b = ChoicesBuilder.new
|
114
|
+
b.add_source(PosixCommandLineSource, :usage, 'foo')
|
115
|
+
b.add_choice(:targets) { |command_line |
|
116
|
+
command_line.uses_arg
|
117
|
+
}
|
118
|
+
|
119
|
+
output = capturing_stderr do
|
120
|
+
assert_wants_to_exit do
|
121
|
+
b.build
|
122
|
+
end
|
123
|
+
end
|
124
|
+
assert_match(/^Error in the command line: 0 arguments given, 1 expected/, output)
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_extra_required_arg_is_caught_by_command_line
|
129
|
+
with_command_args("one extra") {
|
130
|
+
b = ChoicesBuilder.new
|
131
|
+
b.add_source(CommandLineSource, :usage, 'foo')
|
132
|
+
b.add_choice(:targets) { |command_line |
|
133
|
+
command_line.uses_arg
|
134
|
+
}
|
135
|
+
|
136
|
+
output = capturing_stderr do
|
137
|
+
assert_wants_to_exit do
|
138
|
+
b.build
|
139
|
+
end
|
140
|
+
end
|
141
|
+
assert_match(/^Error in the command line: 2 arguments given, 1 expected/, output)
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_extra_optional_arg_is_caught_by_command_line
|
146
|
+
with_command_args("one extra") {
|
147
|
+
b = ChoicesBuilder.new
|
148
|
+
b.add_source(PosixCommandLineSource, :usage, 'foo')
|
149
|
+
b.add_choice(:targets) { |command_line |
|
150
|
+
command_line.uses_optional_arg
|
151
|
+
}
|
152
|
+
|
153
|
+
output = capturing_stderr do
|
154
|
+
assert_wants_to_exit do
|
155
|
+
b.build
|
156
|
+
end
|
157
|
+
end
|
158
|
+
assert_match(/^Error in the command line: 2 arguments given, 0 or 1 expected/, output)
|
159
|
+
}
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class TestChainingOfSources < Test::Unit::TestCase
|
164
|
+
include UserChoices
|
165
|
+
|
166
|
+
def test_sources_are_chained_correctly
|
167
|
+
with_environment_vars("prefix_in_ecd" => "e") {
|
168
|
+
with_local_config_file(".builder_rc",
|
169
|
+
"<config>
|
170
|
+
<in_ecd>c</in_ecd>
|
171
|
+
<in_cd>c</in_cd>
|
172
|
+
</config>") {
|
173
|
+
b = ChoicesBuilder.new
|
174
|
+
b.add_source(EnvironmentSource, :with_prefix, "prefix_")
|
175
|
+
b.add_source(XmlConfigFileSource, :from_file, ".builder_rc")
|
176
|
+
|
177
|
+
b.add_choice(:in_ecd, :default => "d")
|
178
|
+
b.add_choice(:in_cd, :default => "d")
|
179
|
+
b.add_choice(:in_d, :default => "d")
|
180
|
+
|
181
|
+
choices = b.build
|
182
|
+
|
183
|
+
assert_equal('e', choices[:in_ecd])
|
184
|
+
assert_equal('c', choices[:in_cd])
|
185
|
+
assert_equal('d', choices[:in_d])
|
186
|
+
}
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
def test_priority_over_default
|
192
|
+
with_command_args("--option perfectly-fine --only-cmd=oc") {
|
193
|
+
b = ChoicesBuilder.new
|
194
|
+
b.add_source(CommandLineSource, :usage, "blah, blah")
|
195
|
+
b.add_choice(:option, :default => '0.3') { | command_line |
|
196
|
+
command_line.uses_option('--option VALUE')
|
197
|
+
}
|
198
|
+
b.add_choice(:only_cmd) { |command_line|
|
199
|
+
command_line.uses_option('--only-cmd VALUE')
|
200
|
+
}
|
201
|
+
b.add_choice(:only_default, :default => 'od')
|
202
|
+
choices = b.build
|
203
|
+
assert_equal("perfectly-fine", choices[:option])
|
204
|
+
assert_equal("oc", choices[:only_cmd])
|
205
|
+
assert_equal("od", choices[:only_default])
|
206
|
+
}
|
207
|
+
end
|
208
|
+
|
209
|
+
def test_checking_is_done_for_all_sources
|
210
|
+
with_command_args("--command-option perfectly-fine") {
|
211
|
+
assert_raises_with_matching_message(StandardError,
|
212
|
+
/^Error in the default values/) {
|
213
|
+
b = ChoicesBuilder.new
|
214
|
+
b.add_source(CommandLineSource, :usage, "blah")
|
215
|
+
b.add_choice(:command_option) { | command_line |
|
216
|
+
command_line.uses_option('--command-option VALUE')
|
217
|
+
}
|
218
|
+
b.add_choice(:broken, :default => '0.3', :type => :integer)
|
219
|
+
b.build
|
220
|
+
}
|
221
|
+
}
|
222
|
+
end
|
223
|
+
|
224
|
+
def test_conversions_are_done_for_all_sources
|
225
|
+
with_environment_vars("amazon_rc" => "1") {
|
226
|
+
b = ChoicesBuilder.new
|
227
|
+
b.add_source(EnvironmentSource, :with_prefix, 'amazon')
|
228
|
+
b.add_choice(:_rc, :type => :integer, :default => '3')
|
229
|
+
assert_equal(1, b.build[:_rc])
|
230
|
+
}
|
231
|
+
end
|
232
|
+
|
233
|
+
def test_unmentioned_choices_are_nil
|
234
|
+
with_environment_vars("amazon_rc" => "1") {
|
235
|
+
b = ChoicesBuilder.new
|
236
|
+
b.add_source(EnvironmentSource, :with_prefix, 'amazon_')
|
237
|
+
b.add_choice(:rc, :default => 5, :type => :integer)
|
238
|
+
choices = b.build
|
239
|
+
assert_equal(nil, choices[:unmentioned])
|
240
|
+
assert_equal(1, choices[:rc]) # for fun
|
241
|
+
}
|
242
|
+
end
|
243
|
+
|
244
|
+
def test_given_optional_args_override_lower_precedence_sources
|
245
|
+
with_command_args("override") {
|
246
|
+
b = ChoicesBuilder.new
|
247
|
+
b.add_source(CommandLineSource, :usage, "blah")
|
248
|
+
b.add_choice(:name, :default => 'default') { | command_line |
|
249
|
+
command_line.uses_optional_arg
|
250
|
+
}
|
251
|
+
choices = b.build
|
252
|
+
assert_equal('override', choices[:name])
|
253
|
+
}
|
254
|
+
end
|
255
|
+
|
256
|
+
def test_non_empty_arglists_override_lower_precedence_sources
|
257
|
+
with_command_args("1") {
|
258
|
+
b = ChoicesBuilder.new
|
259
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
260
|
+
b.add_choice(:name, :default => ['default', '1']) { |command_line|
|
261
|
+
command_line.uses_arglist
|
262
|
+
}
|
263
|
+
assert_equal(['1'], b.build[:name])
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
class TestInteractionOfArglistsWithOtherSources < Test::Unit::TestCase
|
271
|
+
include UserChoices
|
272
|
+
|
273
|
+
def test_missing_optional_args_do_not_override_lower_precedence_sources
|
274
|
+
with_command_args("") {
|
275
|
+
b = ChoicesBuilder.new
|
276
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
277
|
+
b.add_choice(:name, :default => 'default') { |command_line|
|
278
|
+
command_line.uses_optional_arg
|
279
|
+
}
|
280
|
+
assert_equal('default', b.build[:name])
|
281
|
+
}
|
282
|
+
end
|
283
|
+
|
284
|
+
def test_missing_optional_args_are_ok_if_no_lower_precedence_sources
|
285
|
+
with_command_args("") {
|
286
|
+
b = ChoicesBuilder.new
|
287
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
288
|
+
b.add_choice(:name) { |command_line|
|
289
|
+
command_line.uses_optional_arg
|
290
|
+
}
|
291
|
+
assert_false(b.build.has_key?(:name))
|
292
|
+
}
|
293
|
+
end
|
294
|
+
|
295
|
+
def test_present_optional_args_do_override_lower_precedence_sources
|
296
|
+
with_command_args("1") {
|
297
|
+
b = ChoicesBuilder.new
|
298
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
299
|
+
b.add_choice(:name, :default => 'default') { |command_line|
|
300
|
+
command_line.uses_optional_arg
|
301
|
+
}
|
302
|
+
assert_equal('1', b.build[:name])
|
303
|
+
}
|
304
|
+
end
|
305
|
+
|
306
|
+
|
307
|
+
|
308
|
+
def test_empty_arglists_do_not_override_lower_precedence_sources
|
309
|
+
# nothing / stuff
|
310
|
+
xml = "<config><names>1</names><names>2</names></config>"
|
311
|
+
with_local_config_file("test-config", xml) {
|
312
|
+
with_command_args("") {
|
313
|
+
b = ChoicesBuilder.new
|
314
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
315
|
+
b.add_source(XmlConfigFileSource, :from_file, 'test-config')
|
316
|
+
b.add_choice(:names) { |command_line|
|
317
|
+
command_line.uses_arglist
|
318
|
+
}
|
319
|
+
assert_equal(['1', '2'], b.build[:names])
|
320
|
+
}
|
321
|
+
}
|
322
|
+
end
|
323
|
+
|
324
|
+
def test_default_empty_arglist_is_empty_array
|
325
|
+
# nothing / nothing
|
326
|
+
with_command_args("") {
|
327
|
+
b = ChoicesBuilder.new
|
328
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
329
|
+
b.add_choice(:names) { |command_line|
|
330
|
+
command_line.uses_arglist
|
331
|
+
}
|
332
|
+
assert_equal([], b.build[:names])
|
333
|
+
}
|
334
|
+
end
|
335
|
+
|
336
|
+
def test_default_empty_arglist_can_be_set_explicitly
|
337
|
+
# nothing / nothing
|
338
|
+
with_command_args("") {
|
339
|
+
b = ChoicesBuilder.new
|
340
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
341
|
+
b.add_choice(:names, :default => ['fred']) { |command_line|
|
342
|
+
command_line.uses_arglist
|
343
|
+
}
|
344
|
+
assert_equal(['fred'], b.build[:names])
|
345
|
+
}
|
346
|
+
end
|
347
|
+
|
348
|
+
def test_present_arglist_does_override_lower_precedence_sources
|
349
|
+
# stuff / stuff
|
350
|
+
with_command_args("1") {
|
351
|
+
b = ChoicesBuilder.new
|
352
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
353
|
+
b.add_choice(:names, :default => ['default']) { |command_line|
|
354
|
+
command_line.uses_arglist
|
355
|
+
}
|
356
|
+
assert_equal(['1'], b.build[:names])
|
357
|
+
}
|
358
|
+
end
|
359
|
+
|
360
|
+
def test_setup_for_overridable_empty_argument_list_is_compatible_with_length
|
361
|
+
# This is an implementation-dependent test. Command-line arguments can
|
362
|
+
# be empty, yet overridden by less-important sources. This is done by
|
363
|
+
# initializing the default value to the empty list. But that must not
|
364
|
+
# trigger the length check.
|
365
|
+
with_command_args("1 2") {
|
366
|
+
b = ChoicesBuilder.new
|
367
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
368
|
+
b.add_choice(:names, :length => 2) { |command_line|
|
369
|
+
command_line.uses_arglist
|
370
|
+
}
|
371
|
+
assert_equal(['1', '2'], b.build[:names])
|
372
|
+
}
|
373
|
+
end
|
374
|
+
|
375
|
+
def test_an_empty_arglist_is_caught_by_the_length_check
|
376
|
+
with_command_args("") {
|
377
|
+
b = ChoicesBuilder.new
|
378
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
379
|
+
b.add_choice(:names, :length => 2) { |command_line|
|
380
|
+
command_line.uses_arglist
|
381
|
+
}
|
382
|
+
output = capturing_stderr do
|
383
|
+
assert_wants_to_exit do
|
384
|
+
b.build
|
385
|
+
end
|
386
|
+
end
|
387
|
+
assert_match(/command line: 0 arguments given, 2 expected/, output)
|
388
|
+
}
|
389
|
+
end
|
390
|
+
|
391
|
+
|
392
|
+
def test_choices_from_earlier_defaults_prevent_failing_arglist_arity_check
|
393
|
+
with_command_args("") {
|
394
|
+
b = ChoicesBuilder.new
|
395
|
+
b.add_source(CommandLineSource, :usage, 'blah')
|
396
|
+
b.add_choice(:names, :length => 1..2, :default => ['1']) { |command_line|
|
397
|
+
command_line.uses_arglist
|
398
|
+
}
|
399
|
+
assert_equal(['1'], b.build[:names])
|
400
|
+
}
|
401
|
+
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
class TestCommandLineConstruction < Test::Unit::TestCase
|
406
|
+
include UserChoices
|
407
|
+
|
408
|
+
def test_command_line_choices_requires_blocks_for_initialization
|
409
|
+
with_command_args("--switch -c 5 arg") {
|
410
|
+
b = ChoicesBuilder.new
|
411
|
+
b.add_source(CommandLineSource, :usage, "hi")
|
412
|
+
|
413
|
+
b.add_choice(:unused) { | command_line |
|
414
|
+
command_line.uses_switch("-u", "--unused")
|
415
|
+
}
|
416
|
+
|
417
|
+
b.add_choice(:switch, :type=>:boolean) { | command_line |
|
418
|
+
command_line.uses_switch("--switch")
|
419
|
+
}
|
420
|
+
|
421
|
+
b.add_choice(:clear, :type => :integer) { | command_line |
|
422
|
+
command_line.uses_option("-c", "--clear N",
|
423
|
+
"Clear the frobbistat N times.")
|
424
|
+
}
|
425
|
+
|
426
|
+
b.add_choice(:args) { | command_line |
|
427
|
+
command_line.uses_arglist
|
428
|
+
}
|
429
|
+
|
430
|
+
choices = b.build
|
431
|
+
|
432
|
+
assert_equal(true, choices[:switch])
|
433
|
+
assert_false(choices.has_key?(:unused))
|
434
|
+
assert_equal(5, choices[:clear])
|
435
|
+
assert_equal(['arg'], choices[:args])
|
436
|
+
}
|
437
|
+
end
|
438
|
+
|
439
|
+
def test_command_line_source_initializes_help_text
|
440
|
+
with_command_args('--help') {
|
441
|
+
output = capturing_stderr do
|
442
|
+
assert_wants_to_exit do
|
443
|
+
b = ChoicesBuilder.new
|
444
|
+
b.add_source(CommandLineSource, :usage,
|
445
|
+
"Usage: prog [options]",
|
446
|
+
"This is supplemental.")
|
447
|
+
|
448
|
+
b.add_choice(:test, :type => :boolean) { | command_line |
|
449
|
+
command_line.uses_switch("--test",
|
450
|
+
"Here's text for a switch")
|
451
|
+
}
|
452
|
+
b.add_choice(:renew) { | command_line |
|
453
|
+
command_line.uses_option("-r", "--renew VALUE",
|
454
|
+
"Here's text for an option")
|
455
|
+
}
|
456
|
+
b.build
|
457
|
+
end
|
458
|
+
end
|
459
|
+
assert(l1 = output.index("Usage: prog [options]"))
|
460
|
+
assert(l2 = output.index("This is supplemental"))
|
461
|
+
assert(l3 = output.index(/--\[no-\]test.*Here's text for a switch/))
|
462
|
+
assert(l4 = output.index(/-r.*--renew.*VALUE.*Here's text for an option/))
|
463
|
+
assert(l5 = output.index("--help"))
|
464
|
+
|
465
|
+
assert(l1 < l2)
|
466
|
+
assert(l2 < l3)
|
467
|
+
assert(l3 < l4)
|
468
|
+
assert(l4 < l5)
|
469
|
+
}
|
470
|
+
end
|
471
|
+
|
472
|
+
def test_builder_can_add_separators_to_help_text
|
473
|
+
with_command_args('--help') {
|
474
|
+
output = capturing_stderr do
|
475
|
+
assert_wants_to_exit do
|
476
|
+
b = ChoicesBuilder.new
|
477
|
+
b.add_source(CommandLineSource, :usage,
|
478
|
+
"Usage: prog [options]",
|
479
|
+
"This is supplemental.")
|
480
|
+
|
481
|
+
b.add_help_line("==============")
|
482
|
+
b.add_choice(:test) { | command_line |
|
483
|
+
command_line.uses_switch("--test",
|
484
|
+
"Here's text for a switch")
|
485
|
+
}
|
486
|
+
b.build
|
487
|
+
end
|
488
|
+
end
|
489
|
+
assert(l1 = output.index("This is supplemental"))
|
490
|
+
assert(l2 = output.index(/==============/))
|
491
|
+
assert(l3 = output.index(/--\[no-\]test.*Here's text for a switch/))
|
492
|
+
|
493
|
+
assert(l1 < l2)
|
494
|
+
assert(l2 < l3)
|
495
|
+
}
|
496
|
+
end
|
497
|
+
|
498
|
+
def test_builder_can_group_help_text_in_sections
|
499
|
+
with_command_args('--help') {
|
500
|
+
output = capturing_stderr do
|
501
|
+
assert_wants_to_exit do
|
502
|
+
b = ChoicesBuilder.new
|
503
|
+
b.add_source(CommandLineSource, :usage,
|
504
|
+
"Usage: prog [options]",
|
505
|
+
"This is supplemental.")
|
506
|
+
|
507
|
+
b.section("section head") do
|
508
|
+
b.add_choice(:test) { | command_line |
|
509
|
+
command_line.uses_switch("--test",
|
510
|
+
"Here's text for a switch")
|
511
|
+
}
|
512
|
+
end
|
513
|
+
b.add_choice(:renew) { | command_line |
|
514
|
+
command_line.uses_option("-r", "--renew VALUE",
|
515
|
+
"Here's text for an option")
|
516
|
+
}
|
517
|
+
b.build
|
518
|
+
end
|
519
|
+
end
|
520
|
+
assert(l1 = output.index("This is supplemental"))
|
521
|
+
assert(l2 = output.index(/section head/))
|
522
|
+
assert(l3 = output.index(/--\[no-\]test.*Here's text for a switch/))
|
523
|
+
assert(l4 = output.index(/---------/))
|
524
|
+
assert(l5 = output.index(/Here's text for an option/))
|
525
|
+
|
526
|
+
assert(l1 < l2)
|
527
|
+
assert(l2 < l3)
|
528
|
+
assert(l3 < l4)
|
529
|
+
assert(l4 < l5)
|
530
|
+
}
|
531
|
+
end
|
532
|
+
|
533
|
+
end
|
534
|
+
|
535
|
+
class TestSpecialCases < Test::Unit::TestCase
|
536
|
+
include UserChoices
|
537
|
+
|
538
|
+
def test_environment_choices_can_be_given_prefix_and_mapping
|
539
|
+
with_environment_vars("prefix_e" => "e", "HOME" => '/Users/marick') {
|
540
|
+
b = ChoicesBuilder.new
|
541
|
+
b.add_source(EnvironmentSource, :with_prefix, "prefix_", :mapping, {:home => "HOME" })
|
542
|
+
b.add_choice(:e)
|
543
|
+
b.add_choice(:home)
|
544
|
+
choices = b.build
|
545
|
+
assert_equal("e", choices[:e])
|
546
|
+
assert_equal("/Users/marick", choices[:home])
|
547
|
+
}
|
548
|
+
|
549
|
+
end
|
550
|
+
|
551
|
+
|
552
|
+
def test_required_arg_with_type_conversion
|
553
|
+
with_command_args("2") {
|
554
|
+
b = ChoicesBuilder.new
|
555
|
+
b.add_source(CommandLineSource, :usage, "blah")
|
556
|
+
b.add_choice(:e, :type => :integer) { |command_line |
|
557
|
+
command_line.uses_arg
|
558
|
+
}
|
559
|
+
choices = b.build
|
560
|
+
assert_equal(2, choices[:e])
|
561
|
+
}
|
562
|
+
end
|
563
|
+
|
564
|
+
def test_required_arg_conversion_prints_right_message
|
565
|
+
with_command_args("b") {
|
566
|
+
b = ChoicesBuilder.new
|
567
|
+
b.add_source(CommandLineSource, :usage, "blah")
|
568
|
+
b.add_choice(:e, :type => :integer) { |command_line |
|
569
|
+
command_line.uses_arg
|
570
|
+
}
|
571
|
+
output = capturing_stderr do
|
572
|
+
assert_wants_to_exit do
|
573
|
+
b.build
|
574
|
+
end
|
575
|
+
end
|
576
|
+
assert_no_match(/^Error in the command line: Error in the command line: /, output)
|
577
|
+
}
|
578
|
+
end
|
579
|
+
|
580
|
+
def test_optional_arg_with_type_conversion
|
581
|
+
with_command_args('2') {
|
582
|
+
b = ChoicesBuilder.new
|
583
|
+
b.add_source(CommandLineSource, :usage, "blah")
|
584
|
+
b.add_choice(:e, :type => :integer) { |command_line |
|
585
|
+
command_line.uses_optional_arg
|
586
|
+
}
|
587
|
+
choices = b.build
|
588
|
+
assert_equal(2, choices[:e])
|
589
|
+
}
|
590
|
+
end
|
591
|
+
|
592
|
+
def test_missing_optional_arg_with_type_conversion_is_OK
|
593
|
+
# The type check applies only if the value is given.
|
594
|
+
with_command_args('') {
|
595
|
+
b = ChoicesBuilder.new
|
596
|
+
b.add_source(CommandLineSource, :usage, "blah")
|
597
|
+
b.add_choice(:e, :type => :integer) { |command_line |
|
598
|
+
command_line.uses_optional_arg
|
599
|
+
}
|
600
|
+
assert_equal(nil, b.build[:e])
|
601
|
+
}
|
602
|
+
end
|
603
|
+
|
604
|
+
end
|
605
|
+
|
606
|
+
|
607
|
+
class TestUtilities < Test::Unit::TestCase
|
608
|
+
include UserChoices
|
609
|
+
|
610
|
+
def setup
|
611
|
+
@builder = ChoicesBuilder.new
|
612
|
+
end
|
613
|
+
|
614
|
+
def test_message_send_splitter
|
615
|
+
assert_equal([[:usage, 'arg']],
|
616
|
+
@builder.message_sends([:usage, 'arg']))
|
617
|
+
assert_equal([[:usage, 'arg1', 2]],
|
618
|
+
@builder.message_sends([:usage, 'arg1', 2]))
|
619
|
+
assert_equal([[:msg, 'arg1', 2], [:next, 1]],
|
620
|
+
@builder.message_sends([:msg, 'arg1', 2, :next, 1]))
|
621
|
+
# a common case
|
622
|
+
assert_equal([[:with_prefix, 'p_'], [:mapping, {:home => 'home'}]],
|
623
|
+
@builder.message_sends([:with_prefix, 'p_', :mapping, {:home => 'home'}]))
|
624
|
+
end
|
625
|
+
|
626
|
+
def test_symbol_indices
|
627
|
+
assert_equal([0, 3], @builder.symbol_indices([:foo, 1, 2, :bar, "quux"]))
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
|