thor 0.14.6 → 0.15.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.
Files changed (54) hide show
  1. data/.autotest +8 -0
  2. data/.document +5 -0
  3. data/.gemtest +0 -0
  4. data/.gitignore +44 -0
  5. data/.rspec +2 -0
  6. data/.travis.yml +9 -0
  7. data/CHANGELOG.rdoc +4 -4
  8. data/Gemfile +19 -0
  9. data/{LICENSE → LICENSE.md} +2 -2
  10. data/README.md +21 -300
  11. data/Thorfile +21 -15
  12. data/lib/thor.rb +56 -11
  13. data/lib/thor/actions.rb +7 -3
  14. data/lib/thor/actions/create_link.rb +1 -1
  15. data/lib/thor/actions/directory.rb +7 -3
  16. data/lib/thor/actions/empty_directory.rb +24 -5
  17. data/lib/thor/actions/file_manipulation.rb +40 -2
  18. data/lib/thor/base.rb +66 -28
  19. data/lib/thor/error.rb +6 -1
  20. data/lib/thor/group.rb +20 -8
  21. data/lib/thor/invocation.rb +4 -2
  22. data/lib/thor/parser/arguments.rb +6 -2
  23. data/lib/thor/parser/option.rb +3 -2
  24. data/lib/thor/parser/options.rb +13 -8
  25. data/lib/thor/rake_compat.rb +13 -8
  26. data/lib/thor/runner.rb +16 -4
  27. data/lib/thor/shell.rb +2 -2
  28. data/lib/thor/shell/basic.rb +86 -29
  29. data/lib/thor/shell/color.rb +40 -4
  30. data/lib/thor/shell/html.rb +28 -26
  31. data/lib/thor/task.rb +26 -8
  32. data/lib/thor/util.rb +26 -7
  33. data/lib/thor/version.rb +1 -1
  34. data/spec/actions/create_link_spec.rb +81 -0
  35. data/spec/actions/empty_directory_spec.rb +32 -0
  36. data/spec/actions/file_manipulation_spec.rb +61 -1
  37. data/spec/actions_spec.rb +4 -0
  38. data/spec/base_spec.rb +10 -5
  39. data/spec/exit_condition_spec.rb +19 -0
  40. data/spec/fixtures/script.thor +8 -2
  41. data/spec/group_spec.rb +39 -1
  42. data/spec/parser/arguments_spec.rb +1 -0
  43. data/spec/parser/options_spec.rb +12 -2
  44. data/spec/rake_compat_spec.rb +11 -7
  45. data/spec/register_spec.rb +43 -0
  46. data/spec/runner_spec.rb +34 -3
  47. data/spec/shell/basic_spec.rb +50 -3
  48. data/spec/shell/color_spec.rb +46 -6
  49. data/spec/shell/html_spec.rb +10 -5
  50. data/spec/spec_helper.rb +4 -0
  51. data/spec/task_spec.rb +26 -16
  52. data/spec/thor_spec.rb +56 -3
  53. data/thor.gemspec +26 -0
  54. metadata +174 -117
@@ -1,6 +1,6 @@
1
1
  class Thor
2
2
  # Thor::Error is raised when it's caused by wrong usage of thor classes. Those
3
- # errors have their backtrace supressed and are nicely shown to the user.
3
+ # errors have their backtrace suppressed and are nicely shown to the user.
4
4
  #
5
5
  # Errors that are caused by the developer, like declaring a method which
6
6
  # overwrites a thor keyword, it SHOULD NOT raise a Thor::Error. This way, we
@@ -27,4 +27,9 @@ class Thor
27
27
 
28
28
  class MalformattedArgumentError < InvocationError
29
29
  end
30
+
31
+ # Raised when a user tries to call a private method encoded in templated filename.
32
+ #
33
+ class PrivateMethodEncodedError < Error
34
+ end
30
35
  end
@@ -104,7 +104,7 @@ class Thor::Group
104
104
  #
105
105
  # ==== Custom invocations
106
106
  #
107
- # You can also supply a block to customize how the option is giong to be
107
+ # You can also supply a block to customize how the option is going to be
108
108
  # invoked. The block receives two parameters, an instance of the current
109
109
  # class and the klass to be invoked.
110
110
  #
@@ -187,9 +187,9 @@ class Thor::Group
187
187
  human_name = value.respond_to?(:classify) ? value.classify : value
188
188
 
189
189
  group_options[human_name] ||= []
190
- group_options[human_name] += klass.class_options.values.select do |option|
191
- base_options[option.name.to_sym].nil? && option.group.nil? &&
192
- !group_options.values.flatten.any? { |i| i.name == option.name }
190
+ group_options[human_name] += klass.class_options.values.select do |class_option|
191
+ base_options[class_option.name.to_sym].nil? && class_option.group.nil? &&
192
+ !group_options.values.flatten.any? { |i| i.name == class_option.name }
193
193
  end
194
194
 
195
195
  yield klass if block_given?
@@ -204,8 +204,16 @@ class Thor::Group
204
204
  [item]
205
205
  end
206
206
 
207
- def handle_argument_error(task, error) #:nodoc:
208
- raise error, "#{task.name.inspect} was called incorrectly. Are you sure it has arity equals to 0?"
207
+ def handle_argument_error(task, error, arity=nil) #:nodoc:
208
+ if arity > 0
209
+ msg = "#{basename} #{task.name} takes #{arity} argument"
210
+ msg << "s" if arity > 1
211
+ msg << ", but it should not."
212
+ else
213
+ msg = "You should not pass arguments to #{basename} #{task.name}."
214
+ end
215
+
216
+ raise error, msg
209
217
  end
210
218
 
211
219
  protected
@@ -220,10 +228,14 @@ class Thor::Group
220
228
  args, opts = Thor::Options.split(given_args)
221
229
  opts = given_opts || opts
222
230
 
231
+ instance = new(args, opts, config)
232
+ yield instance if block_given?
233
+ args = instance.args
234
+
223
235
  if task
224
- new(args, opts, config).invoke_task(all_tasks[task])
236
+ instance.invoke_task(all_tasks[task])
225
237
  else
226
- new(args, opts, config).invoke_all
238
+ instance.invoke_all
227
239
  end
228
240
  end
229
241
 
@@ -85,7 +85,7 @@ class Thor
85
85
  # that it's going to use.
86
86
  #
87
87
  # If you want Rspec::RR to be initialized with its own set of options, you
88
- # have to do that explicitely:
88
+ # have to do that explicitly:
89
89
  #
90
90
  # invoke "rspec:rr", [], :style => :foo
91
91
  #
@@ -106,7 +106,9 @@ class Thor
106
106
  raise "Expected Thor class, got #{klass}" unless klass <= Thor::Base
107
107
 
108
108
  args, opts, config = _parse_initialization_options(args, opts, config)
109
- klass.send(:dispatch, task, args, opts, config)
109
+ klass.send(:dispatch, task, args, opts, config) do |instance|
110
+ instance.parent_options = options
111
+ end
110
112
  end
111
113
 
112
114
  # Invoke the given task if the given args.
@@ -28,7 +28,7 @@ class Thor
28
28
  @switches = arguments
29
29
 
30
30
  arguments.each do |argument|
31
- if argument.default
31
+ if argument.default != nil
32
32
  @assigns[argument.human_name] = argument.default
33
33
  elsif argument.required?
34
34
  @non_assigned_required << argument
@@ -49,6 +49,10 @@ class Thor
49
49
  @assigns
50
50
  end
51
51
 
52
+ def remaining
53
+ @pile
54
+ end
55
+
52
56
  private
53
57
 
54
58
  def no_or_skip?(arg)
@@ -94,7 +98,7 @@ class Thor
94
98
  hash = {}
95
99
 
96
100
  while current_is_value? && peek.include?(?:)
97
- key, value = shift.split(':')
101
+ key, value = shift.split(':',2)
98
102
  hash[key] = value
99
103
  end
100
104
  hash
@@ -1,14 +1,15 @@
1
1
  class Thor
2
2
  class Option < Argument #:nodoc:
3
- attr_reader :aliases, :group, :lazy_default
3
+ attr_reader :aliases, :group, :lazy_default, :hide
4
4
 
5
5
  VALID_TYPES = [:boolean, :numeric, :hash, :array, :string]
6
6
 
7
- def initialize(name, description=nil, required=nil, type=nil, default=nil, banner=nil, lazy_default=nil, group=nil, aliases=nil)
7
+ def initialize(name, description=nil, required=nil, type=nil, default=nil, banner=nil, lazy_default=nil, group=nil, aliases=nil, hide=nil)
8
8
  super(name, description, required, type, default, banner)
9
9
  @lazy_default = lazy_default
10
10
  @group = group.to_s.capitalize if group
11
11
  @aliases = [*aliases].compact
12
+ @hide = hide
12
13
  end
13
14
 
14
15
  # This parse quick options given as method_options. It makes several
@@ -1,7 +1,4 @@
1
1
  class Thor
2
- # This is a modified version of Daniel Berger's Getopt::Long class, licensed
3
- # under Ruby's license.
4
- #
5
2
  class Options < Arguments #:nodoc:
6
3
  LONG_RE = /^(--\w+(?:-\w+)*)$/
7
4
  SHORT_RE = /^(-[a-z])$/i
@@ -38,7 +35,7 @@ class Thor
38
35
  @non_assigned_required.delete(hash_options[key])
39
36
  end
40
37
 
41
- @shorts, @switches, @unknown = {}, {}, []
38
+ @shorts, @switches, @extra = {}, {}, []
42
39
 
43
40
  options.each do |option|
44
41
  @switches[option.switch_name] = option
@@ -49,14 +46,19 @@ class Thor
49
46
  end
50
47
  end
51
48
 
49
+ def remaining
50
+ @extra
51
+ end
52
+
52
53
  def parse(args)
53
54
  @pile = args.dup
54
55
 
55
56
  while peek
56
57
  match, is_switch = current_is_switch?
58
+ shifted = shift
57
59
 
58
60
  if is_switch
59
- case shift
61
+ case shifted
60
62
  when SHORT_SQ_RE
61
63
  unshift($1.split('').map { |f| "-#{f}" })
62
64
  next
@@ -71,9 +73,10 @@ class Thor
71
73
  option = switch_option(switch)
72
74
  @assigns[option.human_name] = parse_peek(switch, option)
73
75
  elsif match
74
- @unknown << shift
76
+ @extra << shifted
77
+ @extra << shift while peek && peek !~ /^-/
75
78
  else
76
- shift
79
+ @extra << shifted
77
80
  end
78
81
  end
79
82
 
@@ -85,7 +88,9 @@ class Thor
85
88
  end
86
89
 
87
90
  def check_unknown!
88
- raise UnknownArgumentError, "Unknown switches '#{@unknown.join(', ')}'" unless @unknown.empty?
91
+ # an unknown option starts with - or -- and has no more --'s afterward.
92
+ unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ }
93
+ raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty?
89
94
  end
90
95
 
91
96
  protected
@@ -1,4 +1,5 @@
1
1
  require 'rake'
2
+ require 'rake/dsl_definition'
2
3
 
3
4
  class Thor
4
5
  # Adds a compatibility layer to your Thor classes which allows you to use
@@ -16,6 +17,8 @@ class Thor
16
17
  # end
17
18
  #
18
19
  module RakeCompat
20
+ include Rake::DSL if defined?(Rake::DSL)
21
+
19
22
  def self.rake_classes
20
23
  @rake_classes ||= []
21
24
  end
@@ -29,12 +32,12 @@ class Thor
29
32
  end
30
33
  end
31
34
 
32
- class Object #:nodoc:
33
- alias :rake_task :task
34
- alias :rake_namespace :namespace
35
+ # override task on (main), for compatibility with Rake 0.9
36
+ self.instance_eval do
37
+ alias rake_namespace namespace
35
38
 
36
- def task(*args, &block)
37
- task = rake_task(*args, &block)
39
+ def task(*)
40
+ task = super
38
41
 
39
42
  if klass = Thor::RakeCompat.rake_classes.last
40
43
  non_namespaced_name = task.name.split(':').last
@@ -43,7 +46,8 @@ class Object #:nodoc:
43
46
  description << task.arg_names.map{ |n| n.to_s.upcase }.join(' ')
44
47
  description.strip!
45
48
 
46
- klass.desc description, task.comment || non_namespaced_name
49
+ klass.desc description, Rake.application.last_description || non_namespaced_name
50
+ Rake.application.last_description = nil
47
51
  klass.send :define_method, non_namespaced_name do |*args|
48
52
  Rake::Task[task.name.to_sym].invoke(*args)
49
53
  end
@@ -52,7 +56,7 @@ class Object #:nodoc:
52
56
  task
53
57
  end
54
58
 
55
- def namespace(name, &block)
59
+ def namespace(name)
56
60
  if klass = Thor::RakeCompat.rake_classes.last
57
61
  const_name = Thor::Util.camel_case(name.to_s).to_sym
58
62
  klass.const_set(const_name, Class.new(Thor))
@@ -60,7 +64,8 @@ class Object #:nodoc:
60
64
  Thor::RakeCompat.rake_classes << new_klass
61
65
  end
62
66
 
63
- rake_namespace(name, &block)
67
+ super
64
68
  Thor::RakeCompat.rake_classes.pop
65
69
  end
66
70
  end
71
+
@@ -17,6 +17,7 @@ class Thor::Runner < Thor #:nodoc:
17
17
  if meth && !self.respond_to?(meth)
18
18
  initialize_thorfiles(meth)
19
19
  klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
20
+ self.class.handle_no_task_error(task, false) if klass.nil?
20
21
  klass.start(["-h", task].compact, :shell => self.shell)
21
22
  else
22
23
  super
@@ -30,6 +31,7 @@ class Thor::Runner < Thor #:nodoc:
30
31
  meth = meth.to_s
31
32
  initialize_thorfiles(meth)
32
33
  klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
34
+ self.class.handle_no_task_error(task, false) if klass.nil?
33
35
  args.unshift(task) if task
34
36
  klass.start(args, :shell => self.shell)
35
37
  end
@@ -73,7 +75,7 @@ class Thor::Runner < Thor #:nodoc:
73
75
  as = basename if as.empty?
74
76
  end
75
77
 
76
- location = if options[:relative] || name =~ /^http:\/\//
78
+ location = if options[:relative] || name =~ /^https?:\/\//
77
79
  name
78
80
  else
79
81
  File.expand_path(name)
@@ -124,7 +126,17 @@ class Thor::Runner < Thor #:nodoc:
124
126
 
125
127
  old_filename = thor_yaml[name][:filename]
126
128
  self.options = self.options.merge("as" => name)
127
- filename = install(thor_yaml[name][:location])
129
+
130
+ if File.directory? File.expand_path(name)
131
+ FileUtils.rm_rf(File.join(thor_root, old_filename))
132
+
133
+ thor_yaml.delete(old_filename)
134
+ save_yaml(thor_yaml)
135
+
136
+ filename = install(name)
137
+ else
138
+ filename = install(thor_yaml[name][:location])
139
+ end
128
140
 
129
141
  unless filename == old_filename
130
142
  File.delete(File.join(thor_root, old_filename))
@@ -190,7 +202,7 @@ class Thor::Runner < Thor #:nodoc:
190
202
  true
191
203
  end
192
204
 
193
- # Load the thorfiles. If relevant_to is supplied, looks for specific files
205
+ # Load the Thorfiles. If relevant_to is supplied, looks for specific files
194
206
  # in the thor_root instead of loading them all.
195
207
  #
196
208
  # By default, it also traverses the current path until find Thor files, as
@@ -244,7 +256,7 @@ class Thor::Runner < Thor #:nodoc:
244
256
  end
245
257
  end
246
258
 
247
- # Load thorfiles relevant to the given method. If you provide "foo:bar" it
259
+ # Load Thorfiles relevant to the given method. If you provide "foo:bar" it
248
260
  # will load all thor files in the thor.yaml that has "foo" e "foo:bar"
249
261
  # namespaces registered.
250
262
  #
@@ -8,7 +8,7 @@ class Thor
8
8
  def self.shell
9
9
  @shell ||= if ENV['THOR_SHELL'] && ENV['THOR_SHELL'].size > 0
10
10
  Thor::Shell.const_get(ENV['THOR_SHELL'])
11
- elsif RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
11
+ elsif ((RbConfig::CONFIG['host_os'] =~ /mswin|mingw/) && !(ENV['ANSICON']))
12
12
  Thor::Shell::Basic
13
13
  else
14
14
  Thor::Shell::Color
@@ -23,7 +23,7 @@ class Thor
23
23
  end
24
24
 
25
25
  module Shell
26
- SHELL_DELEGATED_METHODS = [:ask, :yes?, :no?, :say, :say_status, :print_table]
26
+ SHELL_DELEGATED_METHODS = [:ask, :error, :set_color, :yes?, :no?, :say, :say_status, :print_table, :print_wrapped, :file_collision]
27
27
 
28
28
  autoload :Basic, 'thor/shell/basic'
29
29
  autoload :Color, 'thor/shell/color'
@@ -5,10 +5,10 @@ class Thor
5
5
  class Basic
6
6
  attr_accessor :base, :padding
7
7
 
8
- # Initialize base and padding to nil.
8
+ # Initialize base, mute and padding to nil.
9
9
  #
10
10
  def initialize #:nodoc:
11
- @base, @padding = nil, 0
11
+ @base, @mute, @padding = nil, false, 0
12
12
  end
13
13
 
14
14
  # Mute everything that's inside given block
@@ -23,7 +23,7 @@ class Thor
23
23
  # Check if base is muted
24
24
  #
25
25
  def mute?
26
- @mute
26
+ @mute ||= false
27
27
  end
28
28
 
29
29
  # Sets the output padding, not allowing less than zero values.
@@ -32,14 +32,22 @@ class Thor
32
32
  @padding = [0, value].max
33
33
  end
34
34
 
35
- # Ask something to the user and receives a response.
35
+ # Asks something to the user and receives a response.
36
+ #
37
+ # If asked to limit the correct responses, you can pass in an
38
+ # array of acceptable answers. If one of those is not supplied,
39
+ # they will be shown a message stating that one of those answers
40
+ # must be given and re-asked the question.
36
41
  #
37
42
  # ==== Example
38
43
  # ask("What is your name?")
39
44
  #
40
- def ask(statement, color=nil)
41
- say("#{statement} ", color)
42
- $stdin.gets.strip
45
+ # ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"])
46
+ #
47
+ def ask(statement, *args)
48
+ options = args.last.is_a?(Hash) ? args.pop : {}
49
+
50
+ options[:limited_to] ? ask_filtered(statement, options[:limited_to], *args) : ask_simply(statement, *args)
43
51
  end
44
52
 
45
53
  # Say (print) something to the user. If the sentence ends with a whitespace
@@ -51,16 +59,17 @@ class Thor
51
59
  #
52
60
  def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)$/))
53
61
  message = message.to_s
54
- message = set_color(message, color) if color
62
+
63
+ message = set_color(message, *color) if color
55
64
 
56
65
  spaces = " " * padding
57
66
 
58
67
  if force_new_line
59
- $stdout.puts(spaces + message)
68
+ stdout.puts(spaces + message)
60
69
  else
61
- $stdout.print(spaces + message)
70
+ stdout.print(spaces + message)
62
71
  end
63
- $stdout.flush
72
+ stdout.flush
64
73
  end
65
74
 
66
75
  # Say a status with the given color and appends the message. Since this
@@ -76,8 +85,8 @@ class Thor
76
85
  status = status.to_s.rjust(12)
77
86
  status = set_color status, color, true if color
78
87
 
79
- $stdout.puts "#{status}#{spaces}#{message}"
80
- $stdout.flush
88
+ stdout.puts "#{status}#{spaces}#{message}"
89
+ stdout.flush
81
90
  end
82
91
 
83
92
  # Make a question the to user and returns true if the user replies "y" or
@@ -100,35 +109,47 @@ class Thor
100
109
  # Array[Array[String, String, ...]]
101
110
  #
102
111
  # ==== Options
103
- # ident<Integer>:: Indent the first column by ident value.
112
+ # indent<Integer>:: Indent the first column by indent value.
104
113
  # colwidth<Integer>:: Force the first column to colwidth spaces wide.
105
114
  #
106
115
  def print_table(table, options={})
107
116
  return if table.empty?
108
117
 
109
- formats, ident, colwidth = [], options[:ident].to_i, options[:colwidth]
118
+ formats, indent, colwidth = [], options[:indent].to_i, options[:colwidth]
110
119
  options[:truncate] = terminal_width if options[:truncate] == true
111
120
 
112
121
  formats << "%-#{colwidth + 2}s" if colwidth
113
122
  start = colwidth ? 1 : 0
114
123
 
115
- start.upto(table.first.length - 2) do |i|
116
- maxima ||= table.max{|a,b| a[i].size <=> b[i].size }[i].size
124
+ colcount = table.max{|a,b| a.size <=> b.size }.size
125
+
126
+ maximas = []
127
+
128
+ start.upto(colcount - 2) do |i|
129
+ maxima = table.map {|row| row[i] ? row[i].to_s.size : 0 }.max
130
+ maximas << maxima
117
131
  formats << "%-#{maxima + 2}s"
118
132
  end
119
133
 
120
- formats[0] = formats[0].insert(0, " " * ident)
134
+ formats[0] = formats[0].insert(0, " " * indent)
121
135
  formats << "%s"
122
136
 
123
137
  table.each do |row|
124
138
  sentence = ""
125
139
 
126
140
  row.each_with_index do |column, i|
127
- sentence << formats[i] % column.to_s
141
+ maxima = maximas[i]
142
+
143
+ if column.is_a?(Numeric)
144
+ f = "%#{maxima}s "
145
+ else
146
+ f = formats[i]
147
+ end
148
+ sentence << f % column.to_s
128
149
  end
129
150
 
130
151
  sentence = truncate(sentence, options[:truncate]) if options[:truncate]
131
- $stdout.puts sentence
152
+ stdout.puts sentence
132
153
  end
133
154
  end
134
155
 
@@ -139,11 +160,11 @@ class Thor
139
160
  # String
140
161
  #
141
162
  # ==== Options
142
- # ident<Integer>:: Indent each line of the printed paragraph by ident value.
163
+ # indent<Integer>:: Indent each line of the printed paragraph by indent value.
143
164
  #
144
165
  def print_wrapped(message, options={})
145
- ident = options[:ident] || 0
146
- width = terminal_width - ident
166
+ indent = options[:indent] || 0
167
+ width = terminal_width - indent
147
168
  paras = message.split("\n\n")
148
169
 
149
170
  paras.map! do |unwrapped|
@@ -154,14 +175,14 @@ class Thor
154
175
 
155
176
  paras.each do |para|
156
177
  para.split("\n").each do |line|
157
- $stdout.puts line.insert(0, " " * ident)
178
+ stdout.puts line.insert(0, " " * indent)
158
179
  end
159
- $stdout.puts unless para == paras.last
180
+ stdout.puts unless para == paras.last
160
181
  end
161
182
  end
162
183
 
163
184
  # Deals with file collision and returns true if the file should be
164
- # overwriten and false otherwise. If a block is given, it uses the block
185
+ # overwritten and false otherwise. If a block is given, it uses the block
165
186
  # response as the content for the diff.
166
187
  #
167
188
  # ==== Parameters
@@ -195,23 +216,40 @@ class Thor
195
216
  end
196
217
 
197
218
  # Called if something goes wrong during the execution. This is used by Thor
198
- # internally and should not be used inside your scripts. If someone went
219
+ # internally and should not be used inside your scripts. If something went
199
220
  # wrong, you can always raise an exception. If you raise a Thor::Error, it
200
221
  # will be rescued and wrapped in the method below.
201
222
  #
202
223
  def error(statement)
203
- $stderr.puts statement
224
+ stderr.puts statement
204
225
  end
205
226
 
206
227
  # Apply color to the given string with optional bold. Disabled in the
207
228
  # Thor::Shell::Basic class.
208
229
  #
209
- def set_color(string, color, bold=false) #:nodoc:
230
+ def set_color(string, *args) #:nodoc:
210
231
  string
211
232
  end
212
233
 
213
234
  protected
214
235
 
236
+ def lookup_color(color)
237
+ return color unless color.is_a?(Symbol)
238
+ self.class.const_get(color.to_s.upcase)
239
+ end
240
+
241
+ def stdout
242
+ $stdout
243
+ end
244
+
245
+ def stdin
246
+ $stdin
247
+ end
248
+
249
+ def stderr
250
+ $stderr
251
+ end
252
+
215
253
  def is?(value) #:nodoc:
216
254
  value = value.to_s
217
255
 
@@ -285,6 +323,25 @@ HELP
285
323
  end
286
324
  end
287
325
 
326
+ def ask_simply(statement, color = nil)
327
+ say("#{statement} ", color)
328
+ stdin.gets.strip
329
+ end
330
+
331
+ def ask_filtered(statement, answer_set, *args)
332
+ correct_answer = nil
333
+
334
+ until correct_answer
335
+ answer = ask_simply("#{statement} #{answer_set.inspect}", *args)
336
+
337
+ correct_answer = answer_set.include?(answer) ? answer : nil
338
+
339
+ answers = answer_set.map(&:inspect).join(", ")
340
+ say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer
341
+ end
342
+
343
+ correct_answer
344
+ end
288
345
  end
289
346
  end
290
347
  end