thor 0.14.6 → 0.15.0

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