qoobaa-user-choices 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
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
+