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.
Files changed (50) hide show
  1. data/.document +5 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE +39 -0
  4. data/README.rdoc +7 -0
  5. data/Rakefile +56 -0
  6. data/VERSION +1 -0
  7. data/examples/older/README.txt +133 -0
  8. data/examples/older/command-line.rb +46 -0
  9. data/examples/older/default-values.rb +41 -0
  10. data/examples/older/multiple-sources.rb +58 -0
  11. data/examples/older/postprocess.rb +39 -0
  12. data/examples/older/switches.rb +44 -0
  13. data/examples/older/two-args.rb +31 -0
  14. data/examples/older/types.rb +61 -0
  15. data/examples/tutorial/css/LICENSE.txt +1 -0
  16. data/examples/tutorial/css/bg2.gif +0 -0
  17. data/examples/tutorial/css/left.gif +0 -0
  18. data/examples/tutorial/css/left_on.gif +0 -0
  19. data/examples/tutorial/css/main.css +242 -0
  20. data/examples/tutorial/css/right.gif +0 -0
  21. data/examples/tutorial/css/right_on.gif +0 -0
  22. data/examples/tutorial/css/tvline.gif +0 -0
  23. data/examples/tutorial/css/von-foerster.jpg +0 -0
  24. data/examples/tutorial/css/von-foerster2.jpg +0 -0
  25. data/examples/tutorial/index.html +703 -0
  26. data/examples/tutorial/tutorial1.rb +41 -0
  27. data/examples/tutorial/tutorial2.rb +44 -0
  28. data/examples/tutorial/tutorial3.rb +47 -0
  29. data/examples/tutorial/tutorial4.rb +47 -0
  30. data/examples/tutorial/tutorial5.rb +35 -0
  31. data/examples/tutorial/tutorial6.rb +35 -0
  32. data/examples/tutorial/tutorial7.rb +41 -0
  33. data/lib/user-choices.rb +131 -0
  34. data/lib/user-choices/arglist-strategies.rb +179 -0
  35. data/lib/user-choices/builder.rb +118 -0
  36. data/lib/user-choices/command-line-source.rb +224 -0
  37. data/lib/user-choices/command.rb +42 -0
  38. data/lib/user-choices/conversions.rb +169 -0
  39. data/lib/user-choices/ruby-extensions.rb +20 -0
  40. data/lib/user-choices/sources.rb +278 -0
  41. data/lib/user-choices/version.rb +3 -0
  42. data/test/arglist_strategy_test.rb +42 -0
  43. data/test/builder_test.rb +631 -0
  44. data/test/command_line_source_test.rb +443 -0
  45. data/test/conversion_test.rb +172 -0
  46. data/test/source_test.rb +451 -0
  47. data/test/test_helper.rb +9 -0
  48. data/test/user_choices_slow_test.rb +276 -0
  49. data/user-choices.gemspec +104 -0
  50. metadata +122 -0
@@ -0,0 +1,118 @@
1
+ require 's4t-utils'
2
+ include S4tUtils
3
+ require 'enumerator'
4
+
5
+ require 'user-choices/conversions'
6
+ require 'user-choices/sources'
7
+
8
+ module UserChoices
9
+
10
+ # This class accepts a series of source and choice descriptions
11
+ # and then builds a hash-like object that describes all the choices
12
+ # a user has made before (or while) invoking a script.
13
+ class ChoicesBuilder
14
+
15
+ def initialize
16
+ @defaults = {}
17
+ @conversions = {}
18
+ @sources = []
19
+ end
20
+
21
+ # Add the choice named _choice_, a symbol. _args_ is a keyword
22
+ # argument:
23
+ # * <tt>:default</tt> takes a value that is the default value of the _choice_.
24
+ # * <tt>:type</tt> can be given an array of valid string values. These are
25
+ # checked.
26
+ # * <tt>:type</tt> can also be given <tt>:integer</tt>. The value is cast into
27
+ # an integer. If that's impossible, an exception is raised.
28
+ # * <tt>:type</tt> can also be given <tt>:boolean</tt>. The value is converted into
29
+ # +true+ or +false+ (or an exception is raised).
30
+ # * <tt>:type</tt> can also be given <tt>[:string]</tt>. The value
31
+ # will be an array of strings. For example, "--value a,b,c" will
32
+ # produce ['a', 'b', 'c'].
33
+ #
34
+ # You might also give <tt>:length => 5</tt> or <tt>:length => 3..4</tt>. (In
35
+ # this case, a <tt>:type</tt> of <tt>[:string]</tt> is assumed.)
36
+ #
37
+ # The _block_ is passed a CommandLineSource object. It's used
38
+ # to describe the command line.
39
+ def add_choice(choice, args={}, &block)
40
+ # TODO: does the has_key? actually make a difference?
41
+ @defaults[choice] = args[:default] if args.has_key?(:default)
42
+ @conversions[choice] = []
43
+ Conversion.record_for(args[:type], @conversions[choice])
44
+ if args.has_key?(:length)
45
+ Conversion.record_for({:length => args[:length]}, @conversions[choice])
46
+ end
47
+ block.call(ArgForwarder.new(@command_line_source, choice)) if block
48
+ end
49
+
50
+ # This adds a source of choices. The _source_ is a class like
51
+ # CommandLineSource. The <tt>messages_and_args</tt> are sent
52
+ # to a new object of that class.
53
+ def add_source(source_class, *messages_and_args)
54
+ source = source_class.new
55
+ message_sends(messages_and_args).each { | send_me | source.send(*send_me) }
56
+ @sources << source
57
+ @command_line_source = source if source_class <= CommandLineSource
58
+ end
59
+
60
+ # Add a single line composed of _string_ to the current position in the
61
+ # help output.
62
+ def add_help_line(string)
63
+ user_claims(@command_line_source) {
64
+ "Can't use 'add_help_string' when there's no command line source."
65
+ }
66
+ @command_line_source.add_help_line(string)
67
+ end
68
+
69
+ # Demarcate a section of help text. It begins with the _description_,
70
+ # ends with a dashed line.
71
+ def section(description)
72
+ add_help_line("... " + description + ":")
73
+ yield
74
+ add_help_line("---------------------------------")
75
+ add_help_line('')
76
+ end
77
+
78
+ # In groups of related commands, there are often choices that apply to
79
+ # all commands and choices that apply only to this particular command.
80
+ # Use this to define the latter.
81
+ def section_specific_to_script
82
+ section("specific to this script") do
83
+ yield
84
+ end
85
+ end
86
+
87
+
88
+
89
+ # Once sources and choices have been described, this builds and
90
+ # returns a hash-like object indexed by the choices.
91
+ def build
92
+ retval = {}
93
+ @sources << DefaultSource.new.use_hash(@defaults)
94
+ @sources.each { |s| s.fill }
95
+ @sources.each { |s| s.apply(@conversions) }
96
+ @sources.reverse.each { |s| retval.merge!(s) }
97
+ @sources.each { |s| s.adjust(retval) }
98
+ retval
99
+ end
100
+
101
+ # Public for testing.
102
+
103
+ def message_sends(messages_and_args) # :nodoc:
104
+ where_at = symbol_indices(messages_and_args)
105
+ where_end = where_at[1..-1] + [messages_and_args.length]
106
+ where_at.to_enum(:each_with_index).collect do |start, where_end_index |
107
+ messages_and_args[start...where_end[where_end_index]]
108
+ end
109
+ end
110
+
111
+ def symbol_indices(array) # :nodoc:
112
+ array.to_enum(:each_with_index).collect do |obj, index|
113
+ index if obj.is_a?(Symbol)
114
+ end.compact
115
+ end
116
+ end
117
+
118
+ end
@@ -0,0 +1,224 @@
1
+ require 'optparse'
2
+ require 's4t-utils'
3
+ require 'user-choices/sources.rb'
4
+ require 'user-choices/arglist-strategies'
5
+ include S4tUtils
6
+
7
+ module UserChoices # :nodoc
8
+
9
+ # Treat the command line (including the arguments) as a source
10
+ # of choices.
11
+ class CommandLineSource < AbstractSource
12
+
13
+ def initialize
14
+ super
15
+ @parser = OptionParser.new
16
+ @arglist_handler = NoArguments.new(self)
17
+ end
18
+
19
+ def source # :nodoc:
20
+ "the command line"
21
+ end
22
+
23
+ # The _usage_lines_ will be used to produce the output from
24
+ # --help (or on error).
25
+ def usage(*usage_lines)
26
+ help_banner(*usage_lines)
27
+ self
28
+ end
29
+
30
+ # Called in the case of command-line error or explicit request (--help)
31
+ # to print usage information.
32
+ def help
33
+ $stderr.puts @parser
34
+ exit
35
+ end
36
+
37
+ # What we can parse out of the command line
38
+
39
+ # Describes how a particular _choice_ is represented on the
40
+ # command line. The _args_ are passed to OptionParser. Each arg
41
+ # will either describe one variant of option (such as <tt>"-s"</tt>
42
+ # or <tt>"--show VALUE"</tt>) or is a line of help text about
43
+ # the option (multiple lines are allowed).
44
+ #
45
+ # If the option takes an array of values, separate the values by commas:
46
+ # --files a,b,c
47
+ # There's currently no way to escape a comma and no cleverness about
48
+ # quotes.
49
+ def uses_option(choice, *args)
50
+ external_names[choice] = '--' + extract_switch_raw_name(args)
51
+ @parser.on(*args) do | value |
52
+ self[choice] = value
53
+ end
54
+ end
55
+
56
+ # A switch is an option that doesn't take a value. A switch
57
+ # described as <tt>"--switch"</tt> has these effects:
58
+ # * If it is not given, the _choice_ is the default value
59
+ # or is not present in the hash that holds all the choices.
60
+ # * If it is given as <tt>--switch</tt>, the _choice_ has the
61
+ # value <tt>"true"</tt>. (If the _choice_ was described in
62
+ # ChoicesBuilder#add_choice as having a <tt>:type => :boolean</tt>,
63
+ # that value is converted from a string to +true+.)
64
+ # * If it is given as <tt>--no-switch</tt>, the _choice_ has the
65
+ # value <tt>"false"</tt>.
66
+ def uses_switch(choice, *args)
67
+ external_name = extract_switch_raw_name(args)
68
+ external_names[choice] = '--' + external_name
69
+ args = change_name_to_switch(external_name, args)
70
+ @parser.on(*args) do | value |
71
+ self[choice] = value.to_s
72
+ end
73
+ end
74
+
75
+ # Bundle up all non-option and non-switch arguments into an
76
+ # array of strings indexed by _choice_.
77
+ def uses_arglist(choice)
78
+ use_strategy(choice, ArbitraryArglist)
79
+ end
80
+
81
+ # The single argument required argument is turned into
82
+ # a string indexed by _choice_. Any other case is an error.
83
+ def uses_arg(choice)
84
+ use_strategy(choice, OneRequiredArg)
85
+ end
86
+
87
+ # If a single argument is present, it (as a string) is the value of
88
+ # _choice_. If no argument is present, _choice_ has no value.
89
+ # Any other case is an error.
90
+ def uses_optional_arg(choice)
91
+ use_strategy(choice, OneOptionalArg)
92
+ end
93
+
94
+ # Add a single line composed of _string_ to the current position in the
95
+ # help output.
96
+ def add_help_line(string)
97
+ @parser.separator(string)
98
+ end
99
+
100
+
101
+ # Public for testing.
102
+
103
+ def fill # :nodoc:
104
+ exit_upon_error do
105
+ remainder = @parser.parse(ARGV)
106
+ @arglist_handler.fill(remainder)
107
+ end
108
+ end
109
+
110
+ def apply(all_choice_conversions) # :nodoc:
111
+ safely_modifiable_conversions = deep_copy(all_choice_conversions)
112
+ @arglist_handler.claim_conversions(safely_modifiable_conversions)
113
+
114
+ exit_upon_error do
115
+ @arglist_handler.apply_claimed_conversions
116
+ super(safely_modifiable_conversions)
117
+ end
118
+ end
119
+
120
+ def adjust(all_choices) # :nodoc:
121
+ exit_upon_error do
122
+ @arglist_handler.adjust(all_choices)
123
+ end
124
+ end
125
+
126
+ def help_banner(banner, *more) # :nodoc:
127
+ @parser.banner = banner
128
+ more.each do | line |
129
+ add_help_line(line)
130
+ end
131
+
132
+ add_help_line ''
133
+ add_help_line 'Options:'
134
+
135
+ @parser.on_tail("-?", "-h", "--help", "Show this message.") do
136
+ help
137
+ end
138
+ end
139
+
140
+ def deep_copy(conversions) # :nodoc:
141
+ copy = conversions.dup
142
+ copy.each do |k, v|
143
+ copy[k] = v.collect { |conversion| conversion.dup }
144
+ end
145
+ end
146
+
147
+ def use_strategy(choice, strategy) # :nodoc:
148
+ # The argument list choice probably does not need a name.
149
+ # (Currently, the name is unused.) But I'll give it one, just
150
+ # in case, and for debugging.
151
+ external_names[choice] = "the argument list"
152
+ @arglist_handler = strategy.new(self, choice)
153
+ end
154
+
155
+
156
+ def exit_upon_error # :nodoc:
157
+ begin
158
+ yield
159
+ rescue SystemExit
160
+ raise
161
+ rescue Exception => ex
162
+ message = if ex.message.has_exact_prefix?(error_prefix)
163
+ ex.message
164
+ else
165
+ error_prefix + ex.message
166
+ end
167
+ $stderr.puts(message)
168
+ help
169
+ end
170
+ end
171
+
172
+
173
+
174
+ private
175
+
176
+ def extract_switch_raw_name(option_descriptions)
177
+ option_descriptions.each do | desc |
178
+ break $1 if /^--([\w-]+)/ =~ desc
179
+ end
180
+ end
181
+
182
+ def change_name_to_switch(name, option_descriptions)
183
+ option_descriptions.collect do | desc |
184
+ /^--/ =~ desc ? "--[no-]#{name}" : desc
185
+ end
186
+ end
187
+ end
188
+
189
+
190
+ # Process command-line choices according to POSIX rules. Consider
191
+ #
192
+ # ruby copy.rb file1 --odd-file-name
193
+ #
194
+ # Ordinarily, that's permuted so that --odd-file-name is expected to
195
+ # be an option or switch, not an argument. One way to make
196
+ # CommandLineSource parsing treat it as an argument is to use a -- to
197
+ # signal the end of option parsing:
198
+ #
199
+ # ruby copy.rb -- file1 --odd-file-name
200
+ #
201
+ # Another is to rely on the user to set environment variable
202
+ # POSIXLY_CORRECT.
203
+ #
204
+ # Since both of those require the user to do something, they're error-prone.
205
+ #
206
+ # Another way is to use this class, which obeys POSIX-standard rules. Under
207
+ # those rules, the first word on the command line that does not begin with
208
+ # a dash marks the end of all options. In that case, the first command line
209
+ # above would parse into two arguments and no options.
210
+ class PosixCommandLineSource < CommandLineSource
211
+ def fill
212
+ begin
213
+ already_set = ENV.include?('POSIXLY_CORRECT')
214
+ ENV['POSIXLY_CORRECT'] = 'true' unless already_set
215
+ super
216
+ ensure
217
+ ENV.delete('POSIXLY_CORRECT') unless already_set
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+
224
+
@@ -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,169 @@
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 NoOpConversion < Conversion # :nodoc:
41
+ def self.described_by?(conversion_tag)
42
+ conversion_tag == :string
43
+ end
44
+
45
+ def description; "a string"; end
46
+ def suitable?(actual); true; end
47
+ def convert(value); value; end
48
+ end
49
+
50
+
51
+ class ConversionToInteger < Conversion # :nodoc:
52
+ def self.described_by?(conversion_tag)
53
+ conversion_tag == :integer
54
+ end
55
+
56
+ def description; "an integer"; end
57
+
58
+ def suitable?(actual)
59
+ return true if actual.is_a?(Integer)
60
+ actual.is_a?(String) and /^\d+$/ =~ actual # String check for better error message.
61
+ end
62
+
63
+ def convert(value); value.to_i; end
64
+ end
65
+
66
+ class ConversionToBoolean < Conversion # :nodoc:
67
+ def self.described_by?(conversion_tag)
68
+ conversion_tag == :boolean
69
+ end
70
+
71
+ def description; "a boolean"; end
72
+
73
+ def suitable?(actual)
74
+ return true if [true, false].include?(actual)
75
+ return false unless actual.is_a?(String)
76
+ ['true', 'false'].include?(actual.downcase)
77
+ end
78
+
79
+ def convert(value)
80
+ case value
81
+ when String
82
+ eval(value.downcase)
83
+ else
84
+ value
85
+ end
86
+ end
87
+ end
88
+
89
+ class SplittingConversion < Conversion # :nodoc:
90
+ def self.described_by?(conversion_tag)
91
+ conversion_tag == [:string]
92
+ end
93
+
94
+ def description; "a comma-separated list"; end
95
+
96
+ def suitable?(actual)
97
+ actual.is_a?(String) || actual.is_a?(Array)
98
+ end
99
+
100
+ def convert(value)
101
+ case value
102
+ when String
103
+ value.split(',')
104
+ when Array
105
+ value
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+ class LengthConversion < Conversion # :nodoc:
112
+ is_abstract
113
+
114
+ attr_reader :required_length
115
+ def initialize(conversion_tag)
116
+ super
117
+ @required_length = conversion_tag[:length]
118
+ end
119
+
120
+ def self.described_by?(conversion_tag, value_class)
121
+ conversion_tag.is_a?(Hash) && conversion_tag[:length].is_a?(value_class)
122
+ end
123
+
124
+ def suitable?(actual)
125
+ actual.respond_to?(:length) && yield
126
+ end
127
+
128
+ def does_length_check?; true; end
129
+
130
+ end
131
+
132
+ class ExactLengthConversion < LengthConversion # :nodoc:
133
+ def self.described_by?(conversion_tag)
134
+ super(conversion_tag, Integer)
135
+ end
136
+
137
+ def description; "of length #{@required_length}"; end
138
+
139
+ def suitable?(actual)
140
+ super(actual) { actual.length == @required_length }
141
+ end
142
+ end
143
+
144
+ class RangeLengthConversion < LengthConversion # :nodoc:
145
+ def self.described_by?(conversion_tag)
146
+ super(conversion_tag, Range)
147
+ end
148
+
149
+ def description; "a list whose length is in this range: #{@required_length}"; end
150
+
151
+ def suitable?(actual)
152
+ super(actual) { @required_length.include?(actual.length) }
153
+ end
154
+ end
155
+
156
+ # Note: since some of the above classes are described_by? methods that
157
+ # respond_to :include, this class should be last, so that it's checked
158
+ # last.
159
+ class ChoiceCheckingConversion < Conversion # :nodoc:
160
+ def self.described_by?(conversion_tag)
161
+ conversion_tag.respond_to?(:include?)
162
+ end
163
+
164
+ def suitable?(actual); @conversion_tag.include?(actual); end
165
+ def description; "one of #{friendly_list('or', @conversion_tag)}"; end
166
+ end
167
+ end
168
+
169
+