user-choices 1.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/History.txt +17 -0
- data/LICENSE.txt +34 -0
- data/Manifest.txt +40 -0
- data/README.txt +1 -0
- data/Rakefile +19 -0
- data/Rakefile.hoe +24 -0
- data/examples/older/README.txt +133 -0
- data/examples/older/command-line.rb +51 -0
- data/examples/older/default-values.rb +47 -0
- data/examples/older/multiple-sources.rb +63 -0
- data/examples/older/postprocess.rb +45 -0
- data/examples/older/switches.rb +50 -0
- data/examples/older/two-args.rb +37 -0
- data/examples/older/types.rb +67 -0
- data/examples/tutorial/index.html +648 -0
- data/examples/tutorial/tutorial1.rb +48 -0
- data/examples/tutorial/tutorial2.rb +52 -0
- data/examples/tutorial/tutorial3.rb +55 -0
- data/examples/tutorial/tutorial4.rb +55 -0
- data/examples/tutorial/tutorial5.rb +42 -0
- data/examples/tutorial/tutorial6.rb +42 -0
- data/examples/tutorial/tutorial7.rb +48 -0
- data/lib/user-choices/arglist-strategies.rb +178 -0
- data/lib/user-choices/builder.rb +89 -0
- data/lib/user-choices/command-line-source.rb +220 -0
- data/lib/user-choices/command.rb +42 -0
- data/lib/user-choices/conversions.rb +154 -0
- data/lib/user-choices/ruby-extensions.rb +20 -0
- data/lib/user-choices/sources.rb +269 -0
- data/lib/user-choices/version.rb +3 -0
- data/lib/user-choices.rb +131 -0
- data/setup.rb +1585 -0
- data/test/arglist-strategy-tests.rb +42 -0
- data/test/builder-tests.rb +569 -0
- data/test/command-line-source-tests.rb +443 -0
- data/test/conversion-tests.rb +157 -0
- data/test/set-standalone-test-paths.rb +5 -0
- data/test/source-tests.rb +442 -0
- data/test/user-choices-slowtests.rb +274 -0
- data/user-choices.tmproj +575 -0
- metadata +138 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 's4t-utils'
|
3
|
+
require 'user-choices/sources.rb'
|
4
|
+
require 'user-choices/arglist-strategies'
|
5
|
+
include S4tUtils
|
6
|
+
require 'extensions/string'
|
7
|
+
|
8
|
+
module UserChoices # :nodoc
|
9
|
+
|
10
|
+
# Treat the command line (including the arguments) as a source
|
11
|
+
# of choices.
|
12
|
+
class CommandLineSource < AbstractSource
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
super
|
16
|
+
@parser = OptionParser.new
|
17
|
+
@arglist_handler = NoArguments.new(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def source # :nodoc:
|
21
|
+
"the command line"
|
22
|
+
end
|
23
|
+
|
24
|
+
# The _usage_lines_ will be used to produce the output from
|
25
|
+
# --help (or on error).
|
26
|
+
def usage(*usage_lines)
|
27
|
+
help_banner(*usage_lines)
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Called in the case of command-line error or explicit request (--help)
|
32
|
+
# to print usage information.
|
33
|
+
def help
|
34
|
+
$stderr.puts @parser
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
# What we can parse out of the command line
|
39
|
+
|
40
|
+
# Describes how a particular _choice_ is represented on the
|
41
|
+
# command line. The _args_ are passed to OptionParser. Each arg
|
42
|
+
# will either describe one variant of option (such as <tt>"-s"</tt>
|
43
|
+
# or <tt>"--show VALUE"</tt>) or is a line of help text about
|
44
|
+
# the option (multiple lines are allowed).
|
45
|
+
#
|
46
|
+
# If the option takes an array of values, separate the values by commas:
|
47
|
+
# --files a,b,c
|
48
|
+
# There's currently no way to escape a comma and no cleverness about
|
49
|
+
# quotes.
|
50
|
+
def uses_option(choice, *args)
|
51
|
+
external_names[choice] = '--' + extract_switch_raw_name(args)
|
52
|
+
@parser.on(*args) do | value |
|
53
|
+
self[choice] = value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# A switch is an option that doesn't take a value. A switch
|
58
|
+
# described as <tt>"--switch"</tt> has these effects:
|
59
|
+
# * If it is not given, the _choice_ is the default value
|
60
|
+
# or is not present in the hash that holds all the choices.
|
61
|
+
# * If it is given as <tt>--switch</tt>, the _choice_ has the
|
62
|
+
# value <tt>"true"</tt>. (If the _choice_ was described in
|
63
|
+
# ChoicesBuilder#add_choice as having a <tt>:type => :boolean</tt>,
|
64
|
+
# that value is converted from a string to +true+.)
|
65
|
+
# * If it is given as <tt>--no-switch</tt>, the _choice_ has the
|
66
|
+
# value <tt>"false"</tt>.
|
67
|
+
def uses_switch(choice, *args)
|
68
|
+
external_name = extract_switch_raw_name(args)
|
69
|
+
external_names[choice] = '--' + external_name
|
70
|
+
args = change_name_to_switch(external_name, args)
|
71
|
+
@parser.on(*args) do | value |
|
72
|
+
self[choice] = value.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Bundle up all non-option and non-switch arguments into an
|
77
|
+
# array of strings indexed by _choice_.
|
78
|
+
def uses_arglist(choice)
|
79
|
+
use_strategy(choice, ArbitraryArglist)
|
80
|
+
end
|
81
|
+
|
82
|
+
# The single argument required argument is turned into
|
83
|
+
# a string indexed by _choice_. Any other case is an error.
|
84
|
+
def uses_arg(choice)
|
85
|
+
use_strategy(choice, OneRequiredArg)
|
86
|
+
end
|
87
|
+
|
88
|
+
# If a single argument is present, it (as a string) is the value of
|
89
|
+
# _choice_. If no argument is present, _choice_ has no value.
|
90
|
+
# Any other case is an error.
|
91
|
+
def uses_optional_arg(choice)
|
92
|
+
use_strategy(choice, OneOptionalArg)
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
# Public for testing.
|
99
|
+
|
100
|
+
def fill # :nodoc:
|
101
|
+
exit_upon_error do
|
102
|
+
remainder = @parser.parse(ARGV)
|
103
|
+
@arglist_handler.fill(remainder)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def apply(all_choice_conversions) # :nodoc:
|
108
|
+
safely_modifiable_conversions = deep_copy(all_choice_conversions)
|
109
|
+
@arglist_handler.claim_conversions(safely_modifiable_conversions)
|
110
|
+
|
111
|
+
exit_upon_error do
|
112
|
+
@arglist_handler.apply_claimed_conversions
|
113
|
+
super(safely_modifiable_conversions)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def adjust(all_choices) # :nodoc:
|
118
|
+
exit_upon_error do
|
119
|
+
@arglist_handler.adjust(all_choices)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def help_banner(banner, *more) # :nodoc:
|
124
|
+
@parser.banner = banner
|
125
|
+
more.each do | line |
|
126
|
+
@parser.separator(line)
|
127
|
+
end
|
128
|
+
@parser.separator ''
|
129
|
+
@parser.separator 'Options:'
|
130
|
+
|
131
|
+
@parser.on_tail("-?", "-h", "--help", "Show this message.") do
|
132
|
+
help
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def deep_copy(conversions) # :nodoc:
|
137
|
+
copy = conversions.dup
|
138
|
+
copy.each do |k, v|
|
139
|
+
copy[k] = v.collect { |conversion| conversion.dup }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def use_strategy(choice, strategy) # :nodoc:
|
144
|
+
# The argument list choice probably does not need a name.
|
145
|
+
# (Currently, the name is unused.) But I'll give it one, just
|
146
|
+
# in case, and for debugging.
|
147
|
+
external_names[choice] = "the argument list"
|
148
|
+
@arglist_handler = strategy.new(self, choice)
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
def exit_upon_error # :nodoc:
|
153
|
+
begin
|
154
|
+
yield
|
155
|
+
rescue SystemExit
|
156
|
+
raise
|
157
|
+
rescue Exception => ex
|
158
|
+
message = if ex.message.starts_with?(error_prefix)
|
159
|
+
ex.message
|
160
|
+
else
|
161
|
+
error_prefix + ex.message
|
162
|
+
end
|
163
|
+
$stderr.puts(message)
|
164
|
+
help
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def extract_switch_raw_name(option_descriptions)
|
173
|
+
option_descriptions.each do | desc |
|
174
|
+
break $1 if /^--([\w-]+)/ =~ desc
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def change_name_to_switch(name, option_descriptions)
|
179
|
+
option_descriptions.collect do | desc |
|
180
|
+
/^--/ =~ desc ? "--[no-]#{name}" : desc
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
# Process command-line choices according to POSIX rules. Consider
|
187
|
+
#
|
188
|
+
# ruby copy.rb file1 --odd-file-name
|
189
|
+
#
|
190
|
+
# Ordinarily, that's permuted so that --odd-file-name is expected to
|
191
|
+
# be an option or switch, not an argument. One way to make
|
192
|
+
# CommandLineSource parsing treat it as an argument is to use a -- to
|
193
|
+
# signal the end of option parsing:
|
194
|
+
#
|
195
|
+
# ruby copy.rb -- file1 --odd-file-name
|
196
|
+
#
|
197
|
+
# Another is to rely on the user to set environment variable
|
198
|
+
# POSIXLY_CORRECT.
|
199
|
+
#
|
200
|
+
# Since both of those require the user to do something, they're error-prone.
|
201
|
+
#
|
202
|
+
# Another way is to use this class, which obeys POSIX-standard rules. Under
|
203
|
+
# those rules, the first word on the command line that does not begin with
|
204
|
+
# a dash marks the end of all options. In that case, the first command line
|
205
|
+
# above would parse into two arguments and no options.
|
206
|
+
class PosixCommandLineSource < CommandLineSource
|
207
|
+
def fill
|
208
|
+
begin
|
209
|
+
already_set = ENV.include?('POSIXLY_CORRECT')
|
210
|
+
ENV['POSIXLY_CORRECT'] = 'true' unless already_set
|
211
|
+
super
|
212
|
+
ensure
|
213
|
+
ENV.delete('POSIXLY_CORRECT') unless already_set
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module UserChoices
|
2
|
+
|
3
|
+
# A template class. Subclasses describe all the choices a user may
|
4
|
+
# make to affect the execution of the command, and the sources for
|
5
|
+
# those choices (such as the command line).
|
6
|
+
class Command
|
7
|
+
|
8
|
+
attr_reader :user_choices
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
builder = ChoicesBuilder.new
|
12
|
+
add_sources(builder)
|
13
|
+
add_choices(builder)
|
14
|
+
@user_choices = builder.build
|
15
|
+
postprocess_user_choices
|
16
|
+
end
|
17
|
+
|
18
|
+
# Add sources such as EnvironmentSource, XmlConfigFileSource,
|
19
|
+
# and CommandLineSource to the ChoicesBuilder.
|
20
|
+
#
|
21
|
+
# Must be defined in a subclass.
|
22
|
+
def add_sources(builder); subclass_responsibility; end
|
23
|
+
|
24
|
+
# Add choices to the ChoicesBuilder. A choice is a symbol that ends up in the
|
25
|
+
# @user_choices hash. It may have a :default value and a :type, as well
|
26
|
+
# as an OptionParser-style description of command-line arguments.
|
27
|
+
#
|
28
|
+
# Must be defined in a subclass.
|
29
|
+
def add_choices(builder); subclass_responsibility; end
|
30
|
+
|
31
|
+
# After choices from all sources are collected into @user_choices, this
|
32
|
+
# method is called to do any postprocessing. Does nothing unless
|
33
|
+
# overridden.
|
34
|
+
def postprocess_user_choices
|
35
|
+
end
|
36
|
+
|
37
|
+
# Execute the command, using @user_choices for parameterization.
|
38
|
+
#
|
39
|
+
# Must be defined in a subclass.
|
40
|
+
def execute; subclass_responsibility; end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-08-06.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
module UserChoices
|
7
|
+
class Conversion # :nodoc:
|
8
|
+
@@subclasses = []
|
9
|
+
def self.inherited(subclass)
|
10
|
+
@@subclasses << subclass
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.is_abstract
|
14
|
+
@@subclasses.delete(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.record_for(conversion_tag, recorder)
|
18
|
+
return if conversion_tag.nil? # This simplifies caller.
|
19
|
+
recorder << self.for(conversion_tag)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.for(conversion_tag)
|
23
|
+
subclass = @@subclasses.find { |sc| sc.described_by?(conversion_tag) }
|
24
|
+
user_claims(subclass) { "#{conversion_tag} doesn't describe any Conversion object." }
|
25
|
+
subclass.new(conversion_tag)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(conversion_tag)
|
29
|
+
@conversion_tag = conversion_tag
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.described_by?(conversion_tag); subclass_responsibility; end
|
33
|
+
def suitable?(actual); subclass_responsibility; end
|
34
|
+
def description; subclass_responsibility; end
|
35
|
+
|
36
|
+
def convert(value); value; end # Some conversions are just for error-checking
|
37
|
+
def does_length_check?; false; end
|
38
|
+
end
|
39
|
+
|
40
|
+
class ConversionToInteger < Conversion # :nodoc:
|
41
|
+
def self.described_by?(conversion_tag)
|
42
|
+
conversion_tag == :integer
|
43
|
+
end
|
44
|
+
|
45
|
+
def description; "an integer"; end
|
46
|
+
|
47
|
+
def suitable?(actual)
|
48
|
+
return true if actual.is_a?(Integer)
|
49
|
+
actual.is_a?(String) and /^\d+$/ =~ actual # String check for better error message.
|
50
|
+
end
|
51
|
+
|
52
|
+
def convert(value); value.to_i; end
|
53
|
+
end
|
54
|
+
|
55
|
+
class ConversionToBoolean < Conversion # :nodoc:
|
56
|
+
def self.described_by?(conversion_tag)
|
57
|
+
conversion_tag == :boolean
|
58
|
+
end
|
59
|
+
|
60
|
+
def description; "a boolean"; end
|
61
|
+
|
62
|
+
def suitable?(actual)
|
63
|
+
return true if [true, false].include?(actual)
|
64
|
+
return false unless actual.is_a?(String)
|
65
|
+
['true', 'false'].include?(actual.downcase)
|
66
|
+
end
|
67
|
+
|
68
|
+
def convert(value)
|
69
|
+
case value
|
70
|
+
when String: eval(value.downcase)
|
71
|
+
else value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class SplittingConversion < Conversion # :nodoc:
|
77
|
+
def self.described_by?(conversion_tag)
|
78
|
+
conversion_tag == [:string]
|
79
|
+
end
|
80
|
+
|
81
|
+
def description; "a comma-separated list"; end
|
82
|
+
|
83
|
+
def suitable?(actual)
|
84
|
+
actual.is_a?(String) || actual.is_a?(Array)
|
85
|
+
end
|
86
|
+
|
87
|
+
def convert(value)
|
88
|
+
case value
|
89
|
+
when String: value.split(',')
|
90
|
+
when Array: value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
class LengthConversion < Conversion # :nodoc:
|
97
|
+
is_abstract
|
98
|
+
|
99
|
+
attr_reader :required_length
|
100
|
+
def initialize(conversion_tag)
|
101
|
+
super
|
102
|
+
@required_length = conversion_tag[:length]
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.described_by?(conversion_tag, value_class)
|
106
|
+
conversion_tag.is_a?(Hash) && conversion_tag[:length].is_a?(value_class)
|
107
|
+
end
|
108
|
+
|
109
|
+
def suitable?(actual)
|
110
|
+
actual.respond_to?(:length) && yield
|
111
|
+
end
|
112
|
+
|
113
|
+
def does_length_check?; true; end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
class ExactLengthConversion < LengthConversion # :nodoc:
|
118
|
+
def self.described_by?(conversion_tag)
|
119
|
+
super(conversion_tag, Integer)
|
120
|
+
end
|
121
|
+
|
122
|
+
def description; "of length #{@required_length}"; end
|
123
|
+
|
124
|
+
def suitable?(actual)
|
125
|
+
super(actual) { actual.length == @required_length }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class RangeLengthConversion < LengthConversion # :nodoc:
|
130
|
+
def self.described_by?(conversion_tag)
|
131
|
+
super(conversion_tag, Range)
|
132
|
+
end
|
133
|
+
|
134
|
+
def description; "a list whose length is in this range: #{@required_length}"; end
|
135
|
+
|
136
|
+
def suitable?(actual)
|
137
|
+
super(actual) { @required_length.include?(actual.length) }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Note: since some of the above classes are described_by? methods that
|
142
|
+
# respond_to :include, this class should be last, so that it's checked
|
143
|
+
# last.
|
144
|
+
class ChoiceCheckingConversion < Conversion # :nodoc:
|
145
|
+
def self.described_by?(conversion_tag)
|
146
|
+
conversion_tag.respond_to?(:include?)
|
147
|
+
end
|
148
|
+
|
149
|
+
def suitable?(actual); @conversion_tag.include?(actual); end
|
150
|
+
def description; "one of #{friendly_list('or', @conversion_tag)}"; end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
|
@@ -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,269 @@
|
|
1
|
+
require 'xmlsimple'
|
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: value.inspect
|
64
|
+
else "'#{value}'"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def bad_look(key, conversion)
|
69
|
+
like_what = conversion.description
|
70
|
+
"#{@external_names[key]}'s value must be #{like_what}, and #{pretty_value(self[key])} doesn't look right."
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class DefaultSource < AbstractSource # :nodoc:
|
76
|
+
|
77
|
+
def use_hash(defaults)
|
78
|
+
@defaults = defaults
|
79
|
+
count_symbols_as_external_names(@defaults.keys)
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def fill
|
84
|
+
merge!(@defaults)
|
85
|
+
end
|
86
|
+
|
87
|
+
def source
|
88
|
+
"the default values"
|
89
|
+
end
|
90
|
+
|
91
|
+
def count_symbols_as_external_names(symbols)
|
92
|
+
symbols.each { | symbol |
|
93
|
+
# Use inspect so that symbol prints with leading colon
|
94
|
+
@external_names[symbol] = symbol.inspect
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
DefaultChoices = DefaultSource # Backward compatibility
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
# Describe the environment as a source of choices.
|
103
|
+
class EnvironmentSource < AbstractSource
|
104
|
+
def fill # :nodoc:
|
105
|
+
@external_names.each { | key, env_var |
|
106
|
+
self[key] = ENV[env_var] if ENV.has_key?(env_var)
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Environment variables beginning with _prefix_ (a string)
|
111
|
+
# are considered to be user choices relevant to this script.
|
112
|
+
# Everything after the prefix names a choice (that is, a symbol).
|
113
|
+
# Dashes are converted to underscores. Examples:
|
114
|
+
# * Environment variable <tt>prefix-my-choice</tt> with prefix <tt>"prefix-" is choice <tt>:my_choice</tt>.
|
115
|
+
# * Environment variable <tt>PREFIX_FOO</tt> with prefix <tt>"PREFIX_" is choice <tt>:FOO</tt>
|
116
|
+
#
|
117
|
+
# If you want an array of strings, separate the values by commas:
|
118
|
+
# ENV_VAR=a,b,c
|
119
|
+
# There's currently no way to escape a comma and no cleverness about
|
120
|
+
# quotes or whitespace.
|
121
|
+
|
122
|
+
def with_prefix(prefix)
|
123
|
+
matches = ENV.collect do | env_var, ignored_value |
|
124
|
+
if /^#{prefix}(.+)/ =~ env_var
|
125
|
+
[$1.to_inputable_sym, env_var]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
@external_names.merge!(Hash[*matches.compact.flatten])
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
# Some environment variables have names you don't like. For example, $HOME
|
133
|
+
# might be annoying because of the uppercase. Also, if most of your program's
|
134
|
+
# environment variables have some prefix (see with_prefix) but you also want to use
|
135
|
+
# $HOME, you need a way to do that. You can satisfy both desires with
|
136
|
+
#
|
137
|
+
# EnvironmentSource.new.with_prefix("my_").mapping(:home => "HOME")
|
138
|
+
|
139
|
+
def mapping(map)
|
140
|
+
@external_names.merge!(map)
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
def source # :nodoc:
|
146
|
+
"the environment"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
EnvironmentChoices = EnvironmentSource # Backward compatibility
|
150
|
+
|
151
|
+
|
152
|
+
class FileSource < AbstractSource # :nodoc:
|
153
|
+
|
154
|
+
def from_file(filename)
|
155
|
+
@path = File.join(S4tUtils.find_home, filename)
|
156
|
+
@contents_as_hash = self.read_into_hash
|
157
|
+
@contents_as_hash.each do | external_name, value |
|
158
|
+
sym = external_name.to_inputable_sym
|
159
|
+
@external_names[sym] = external_name
|
160
|
+
end
|
161
|
+
self
|
162
|
+
end
|
163
|
+
|
164
|
+
def fill # :nodoc:
|
165
|
+
@external_names.each do | symbol, external_name |
|
166
|
+
self[symbol] = @contents_as_hash[external_name]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def source # :nodoc:
|
171
|
+
"configuration file #{@path}"
|
172
|
+
end
|
173
|
+
|
174
|
+
def read_into_hash # :nodoc:
|
175
|
+
return {} unless File.exist?(@path)
|
176
|
+
begin
|
177
|
+
format_specific_reading
|
178
|
+
rescue Exception => ex
|
179
|
+
if format_specific_exception?(ex)
|
180
|
+
msg = "Badly formatted #{source}: " + format_specific_message(ex)
|
181
|
+
ex = ex.class.new(msg)
|
182
|
+
end
|
183
|
+
raise ex
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
protected
|
188
|
+
|
189
|
+
def format_specific_message(ex)
|
190
|
+
ex.message
|
191
|
+
end
|
192
|
+
|
193
|
+
def format_specific_exception_handling(ex); subclass_responsibility; end
|
194
|
+
def format_specific_reading; subclass_responsibility; end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Use an XML file as a source of choices. The XML file is parsed
|
198
|
+
# with <tt>XmlSimple('ForceArray' => false)</tt>. That means that
|
199
|
+
# single elements like <home>Mars</home> are read as the value
|
200
|
+
# <tt>"Mars"</tt>, whereas <home>Mars</home><home>Venus</home> is
|
201
|
+
# read as <tt>["Mars", "Venus"]</tt>.
|
202
|
+
class XmlConfigFileSource < FileSource
|
203
|
+
|
204
|
+
# Treat _filename_ as the configuration file. _filename_ is expected
|
205
|
+
# to be in the home directory. The home directory is found in the
|
206
|
+
# same way Rubygems finds it. (First look in environment variables
|
207
|
+
# <tt>$HOME</tt>, <tt>$USERPROFILE</tt>, <tt>$HOMEDRIVE:$HOMEPATH</tt>,
|
208
|
+
# file expansion of <tt>"~"</tt> and finally the root.)
|
209
|
+
def format_specific_reading
|
210
|
+
XmlSimple.xml_in(@path, 'ForceArray' => false)
|
211
|
+
end
|
212
|
+
|
213
|
+
def format_specific_exception?(ex)
|
214
|
+
ex.is_a?(REXML::ParseException)
|
215
|
+
end
|
216
|
+
|
217
|
+
def format_specific_message(ex)
|
218
|
+
ex.continued_exception
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
XmlConfigFileChoices = XmlConfigFileSource # Backward compatibility
|
223
|
+
|
224
|
+
|
225
|
+
|
226
|
+
|
227
|
+
# Use an YAML file as a source of choices. Note: because the YAML parser
|
228
|
+
# can produce something out of many typo-filled YAML files, it's a
|
229
|
+
# good idea to check that your file looks like you'd expect before
|
230
|
+
# trusting in it. Do that with:
|
231
|
+
#
|
232
|
+
# irb> require 'yaml'
|
233
|
+
# irb> YAML.load_file('config.yaml')
|
234
|
+
#
|
235
|
+
class YamlConfigFileSource < FileSource
|
236
|
+
# Treat _filename_ as the configuration file. _filename_ is expected
|
237
|
+
# to be in the home directory. The home directory is found in the
|
238
|
+
# same way Rubygems finds it.
|
239
|
+
def format_specific_reading
|
240
|
+
result = YAML.load_file(@path)
|
241
|
+
ensure_hash_values_are_strings(result)
|
242
|
+
result
|
243
|
+
end
|
244
|
+
|
245
|
+
def format_specific_exception?(ex)
|
246
|
+
ex.is_a?(ArgumentError)
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
|
251
|
+
def ensure_hash_values_are_strings(h)
|
252
|
+
h.each { |k, v| ensure_element_is_string(h, k) }
|
253
|
+
end
|
254
|
+
|
255
|
+
def ensure_array_values_are_strings(a)
|
256
|
+
a.each_with_index { |elt, index| ensure_element_is_string(a, index) }
|
257
|
+
end
|
258
|
+
|
259
|
+
def ensure_element_is_string(collection, key)
|
260
|
+
case collection[key]
|
261
|
+
when Hash: ensure_hash_values_are_strings(collection[key])
|
262
|
+
when Array: ensure_array_values_are_strings(collection[key])
|
263
|
+
else collection[key] = collection[key].to_s
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
end
|
269
|
+
end
|