keybox 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,7 +12,7 @@ context "Keybox Password Generator Application" do
12
12
 
13
13
  specify "invalid options set the error message, exit 1 and have output on stderr" do
14
14
  kpg = Keybox::Application::PasswordGenerator.new(["--invalid-option"])
15
- kpg.stderr = StringIO.new
15
+ kpg.set_io(StringIO.new,StringIO.new,StringIO.new)
16
16
  begin
17
17
  kpg.run
18
18
  rescue SystemExit => se
@@ -24,14 +24,14 @@ context "Keybox Password Generator Application" do
24
24
 
25
25
  specify "can set the algorithm" do
26
26
  kpg = Keybox::Application::PasswordGenerator.new(["--alg", "pron"])
27
- kpg.stdout = StringIO.new
27
+ kpg.set_io(StringIO.new,StringIO.new)
28
28
  kpg.run
29
29
  kpg.options.algorithm.should == :pronounceable
30
30
  end
31
31
 
32
32
  specify "can specify the number of passwords created " do
33
33
  kpg = Keybox::Application::PasswordGenerator.new(["--num", "4"])
34
- kpg.stdout = StringIO.new
34
+ kpg.set_io(StringIO.new,StringIO.new)
35
35
  kpg.run
36
36
  kpg.options.number_to_generate.should_eql 4
37
37
  kpg.stdout.string.split(/\s+/).size.should == 4
@@ -39,7 +39,7 @@ context "Keybox Password Generator Application" do
39
39
 
40
40
  specify "help has output on stdout and exits 0" do
41
41
  kpg = Keybox::Application::PasswordGenerator.new(["--h"])
42
- kpg.stdout = StringIO.new
42
+ kpg.set_io(StringIO.new,StringIO.new)
43
43
  begin
44
44
  kpg.run
45
45
  rescue SystemExit => se
@@ -51,18 +51,18 @@ context "Keybox Password Generator Application" do
51
51
 
52
52
  specify "version has output on stdout and exits 0" do
53
53
  kpg = Keybox::Application::PasswordGenerator.new(["--version"])
54
- kpg.stdout = StringIO.new
54
+ kpg.set_io(StringIO.new,StringIO.new)
55
55
  begin
56
56
  kpg.run
57
57
  rescue SystemExit => se
58
58
  se.status.should_eql 0
59
59
  end
60
- kpg.stdout.string.should_satisfy { |msg| msg =~ /version 1.0.0/m }
60
+ kpg.stdout.string.should_satisfy { |msg| msg =~ /version 1/m }
61
61
  end
62
62
 
63
63
  specify "minimum length can be set and all generated passwords will have length >= minimum length" do
64
64
  kpg = Keybox::Application::PasswordGenerator.new(["--min", "4"])
65
- kpg.stdout = StringIO.new
65
+ kpg.set_io(StringIO.new,StringIO.new)
66
66
  kpg.run
67
67
 
68
68
  kpg.options.min_length.should_eql 4
@@ -73,7 +73,7 @@ context "Keybox Password Generator Application" do
73
73
 
74
74
  specify "maximum length can be set and all generated passwords will have length <= maximum length" do
75
75
  kpg = Keybox::Application::PasswordGenerator.new(["--max", "4", "--min", "3"])
76
- kpg.stdout = StringIO.new
76
+ kpg.set_io(StringIO.new,StringIO.new,StringIO.new)
77
77
  kpg.run
78
78
 
79
79
  kpg.options.max_length.should_eql 4
@@ -84,7 +84,7 @@ context "Keybox Password Generator Application" do
84
84
 
85
85
  specify "setting an invalid required symbol set exits 1 and outputs data on stderr" do
86
86
  kpg = Keybox::Application::PasswordGenerator.new(["--req","bunk"])
87
- kpg.stderr = StringIO.new
87
+ kpg.set_io(StringIO.new,StringIO.new,StringIO.new)
88
88
  begin
89
89
  kpg.run
90
90
  rescue SystemExit => se
@@ -97,7 +97,7 @@ context "Keybox Password Generator Application" do
97
97
 
98
98
  specify "setting an invalid use symbol set exits 1 and outputs data on stderr" do
99
99
  kpg = Keybox::Application::PasswordGenerator.new(["--use","bunk"])
100
- kpg.stderr = StringIO.new
100
+ kpg.set_io(StringIO.new,StringIO.new,StringIO.new)
101
101
  begin
102
102
  kpg.run
103
103
  rescue SystemExit => se
@@ -110,7 +110,7 @@ context "Keybox Password Generator Application" do
110
110
 
111
111
  specify "setting an valid use symbol works" do
112
112
  kpg = Keybox::Application::PasswordGenerator.new(["--use","l"])
113
- kpg.stdout = StringIO.new
113
+ kpg.set_io(StringIO.new,StringIO.new,StringIO.new)
114
114
  kpg.run
115
115
  kpg.options.use_symbols.should_include Keybox::SymbolSet::LOWER_ASCII
116
116
  kpg.stdout.string.split(/\s+/).size.should == 6
@@ -118,7 +118,7 @@ context "Keybox Password Generator Application" do
118
118
 
119
119
  specify "setting an valid required symbol works" do
120
120
  kpg = Keybox::Application::PasswordGenerator.new(["--req","l"])
121
- kpg.stdout = StringIO.new
121
+ kpg.set_io(StringIO.new,StringIO.new,StringIO.new)
122
122
  kpg.run
123
123
  kpg.options.require_symbols.should_include Keybox::SymbolSet::LOWER_ASCII
124
124
  kpg.stdout.string.split(/\s+/).size.should == 6
@@ -0,0 +1,704 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # highline.rb
4
+ #
5
+ # Created by James Edward Gray II on 2005-04-26.
6
+ # Copyright 2005 Gray Productions. All rights reserved.
7
+ #
8
+ # See HighLine for documentation.
9
+ #
10
+ # This is Free Software. See LICENSE and COPYING for details.
11
+
12
+ require "highline/system_extensions"
13
+ require "highline/question"
14
+ require "highline/menu"
15
+ require "highline/color_scheme"
16
+ require "erb"
17
+ require "optparse"
18
+ require "stringio"
19
+ require "abbrev"
20
+
21
+ #
22
+ # A HighLine object is a "high-level line oriented" shell over an input and an
23
+ # output stream. HighLine simplifies common console interaction, effectively
24
+ # replacing puts() and gets(). User code can simply specify the question to ask
25
+ # and any details about user interaction, then leave the rest of the work to
26
+ # HighLine. When HighLine.ask() returns, you'll have the answer you requested,
27
+ # even if HighLine had to ask many times, validate results, perform range
28
+ # checking, convert types, etc.
29
+ #
30
+ class HighLine
31
+ # The version of the installed library.
32
+ VERSION = "1.2.6".freeze
33
+
34
+ # An internal HighLine error. User code does not need to trap this.
35
+ class QuestionError < StandardError
36
+ # do nothing, just creating a unique error type
37
+ end
38
+
39
+ # The setting used to disable color output.
40
+ @@use_color = true
41
+
42
+ # Pass +false+ to _setting_ to turn off HighLine's color escapes.
43
+ def self.use_color=( setting )
44
+ @@use_color = setting
45
+ end
46
+
47
+ # Returns true if HighLine is currently using color escapes.
48
+ def self.use_color?
49
+ @@use_color
50
+ end
51
+
52
+ # The setting used to control color schemes.
53
+ @@color_scheme = nil
54
+
55
+ # Pass ColorScheme to _setting_ to turn set a HighLine color scheme.
56
+ def self.color_scheme=( setting )
57
+ @@color_scheme = setting
58
+ end
59
+
60
+ # Returns the current color scheme.
61
+ def self.color_scheme
62
+ @@color_scheme
63
+ end
64
+
65
+ # Returns +true+ if HighLine is currently using a color scheme.
66
+ def self.using_color_scheme?
67
+ not @@color_scheme.nil?
68
+ end
69
+
70
+ #
71
+ # Embed in a String to clear all previous ANSI sequences. This *MUST* be
72
+ # done before the program exits!
73
+ #
74
+ CLEAR = "\e[0m"
75
+ # An alias for CLEAR.
76
+ RESET = CLEAR
77
+ # Erase the current line of terminal output.
78
+ ERASE_LINE = "\e[K"
79
+ # The start of an ANSI bold sequence.
80
+ BOLD = "\e[1m"
81
+ # The start of an ANSI dark sequence. (Terminal support uncommon.)
82
+ DARK = "\e[2m"
83
+ # The start of an ANSI underline sequence.
84
+ UNDERLINE = "\e[4m"
85
+ # An alias for UNDERLINE.
86
+ UNDERSCORE = UNDERLINE
87
+ # The start of an ANSI blink sequence. (Terminal support uncommon.)
88
+ BLINK = "\e[5m"
89
+ # The start of an ANSI reverse sequence.
90
+ REVERSE = "\e[7m"
91
+ # The start of an ANSI concealed sequence. (Terminal support uncommon.)
92
+ CONCEALED = "\e[8m"
93
+
94
+ # Set the terminal's foreground ANSI color to black.
95
+ BLACK = "\e[30m"
96
+ # Set the terminal's foreground ANSI color to red.
97
+ RED = "\e[31m"
98
+ # Set the terminal's foreground ANSI color to green.
99
+ GREEN = "\e[32m"
100
+ # Set the terminal's foreground ANSI color to yellow.
101
+ YELLOW = "\e[33m"
102
+ # Set the terminal's foreground ANSI color to blue.
103
+ BLUE = "\e[34m"
104
+ # Set the terminal's foreground ANSI color to magenta.
105
+ MAGENTA = "\e[35m"
106
+ # Set the terminal's foreground ANSI color to cyan.
107
+ CYAN = "\e[36m"
108
+ # Set the terminal's foreground ANSI color to white.
109
+ WHITE = "\e[37m"
110
+
111
+ # Set the terminal's background ANSI color to black.
112
+ ON_BLACK = "\e[40m"
113
+ # Set the terminal's background ANSI color to red.
114
+ ON_RED = "\e[41m"
115
+ # Set the terminal's background ANSI color to green.
116
+ ON_GREEN = "\e[42m"
117
+ # Set the terminal's background ANSI color to yellow.
118
+ ON_YELLOW = "\e[43m"
119
+ # Set the terminal's background ANSI color to blue.
120
+ ON_BLUE = "\e[44m"
121
+ # Set the terminal's background ANSI color to magenta.
122
+ ON_MAGENTA = "\e[45m"
123
+ # Set the terminal's background ANSI color to cyan.
124
+ ON_CYAN = "\e[46m"
125
+ # Set the terminal's background ANSI color to white.
126
+ ON_WHITE = "\e[47m"
127
+
128
+ #
129
+ # Create an instance of HighLine, connected to the streams _input_
130
+ # and _output_.
131
+ #
132
+ def initialize( input = $stdin, output = $stdout,
133
+ wrap_at = nil, page_at = nil )
134
+ @input = input
135
+ @output = output
136
+
137
+ self.wrap_at = wrap_at
138
+ self.page_at = page_at
139
+
140
+ @question = nil
141
+ @answer = nil
142
+ @menu = nil
143
+ @header = nil
144
+ @prompt = nil
145
+ @gather = nil
146
+ @answers = nil
147
+ @key = nil
148
+ end
149
+
150
+ include HighLine::SystemExtensions
151
+
152
+ # The current column setting for wrapping output.
153
+ attr_reader :wrap_at
154
+ # The current row setting for paging output.
155
+ attr_reader :page_at
156
+
157
+ #
158
+ # A shortcut to HighLine.ask() a question that only accepts "yes" or "no"
159
+ # answers ("y" and "n" are allowed) and returns +true+ or +false+
160
+ # (+true+ for "yes"). If provided a +true+ value, _character_ will cause
161
+ # HighLine to fetch a single character response.
162
+ #
163
+ # Raises EOFError if input is exhausted.
164
+ #
165
+ def agree( yes_or_no_question, character = nil )
166
+ ask(yes_or_no_question, lambda { |yn| yn.downcase[0] == ?y}) do |q|
167
+ q.validate = /\Ay(?:es)?|no?\Z/i
168
+ q.responses[:not_valid] = 'Please enter "yes" or "no".'
169
+ q.responses[:ask_on_error] = :question
170
+ q.character = character
171
+ end
172
+ end
173
+
174
+ #
175
+ # This method is the primary interface for user input. Just provide a
176
+ # _question_ to ask the user, the _answer_type_ you want returned, and
177
+ # optionally a code block setting up details of how you want the question
178
+ # handled. See HighLine.say() for details on the format of _question_, and
179
+ # HighLine::Question for more information about _answer_type_ and what's
180
+ # valid in the code block.
181
+ #
182
+ # If <tt>@question</tt> is set before ask() is called, parameters are
183
+ # ignored and that object (must be a HighLine::Question) is used to drive
184
+ # the process instead.
185
+ #
186
+ # Raises EOFError if input is exhausted.
187
+ #
188
+ def ask( question, answer_type = String, &details ) # :yields: question
189
+ @question ||= Question.new(question, answer_type, &details)
190
+
191
+ return gather if @question.gather
192
+
193
+ # readline() needs to handle it's own output
194
+ say(@question) unless @question.readline
195
+ begin
196
+ @answer = @question.answer_or_default(get_response)
197
+ unless @question.valid_answer?(@answer)
198
+ explain_error(:not_valid)
199
+ raise QuestionError
200
+ end
201
+
202
+ @answer = @question.convert(@answer)
203
+
204
+ if @question.in_range?(@answer)
205
+ if @question.confirm
206
+ # need to add a layer of scope to ask a question inside a
207
+ # question, without destroying instance data
208
+ context_change = self.class.new(@input, @output, @wrap_at, @page_at)
209
+ if @question.confirm == true
210
+ confirm_question = "Are you sure? "
211
+ else
212
+ # evaluate ERb under initial scope, so it will have
213
+ # access to @question and @answer
214
+ template = ERB.new(@question.confirm, nil, "%")
215
+ confirm_question = template.result(binding)
216
+ end
217
+ unless context_change.agree(confirm_question)
218
+ explain_error(nil)
219
+ raise QuestionError
220
+ end
221
+ end
222
+
223
+ @answer
224
+ else
225
+ explain_error(:not_in_range)
226
+ raise QuestionError
227
+ end
228
+ rescue QuestionError
229
+ retry
230
+ rescue ArgumentError
231
+ explain_error(:invalid_type)
232
+ retry
233
+ rescue Question::NoAutoCompleteMatch
234
+ explain_error(:no_completion)
235
+ retry
236
+ rescue NameError
237
+ raise if $!.is_a?(NoMethodError)
238
+ explain_error(:ambiguous_completion)
239
+ retry
240
+ ensure
241
+ @question = nil # Reset Question object.
242
+ end
243
+ end
244
+
245
+ #
246
+ # This method is HighLine's menu handler. For simple usage, you can just
247
+ # pass all the menu items you wish to display. At that point, choose() will
248
+ # build and display a menu, walk the user through selection, and return
249
+ # their choice amoung the provided items. You might use this in a case
250
+ # statement for quick and dirty menus.
251
+ #
252
+ # However, choose() is capable of much more. If provided, a block will be
253
+ # passed a HighLine::Menu object to configure. Using this method, you can
254
+ # customize all the details of menu handling from index display, to building
255
+ # a complete shell-like menuing system. See HighLine::Menu for all the
256
+ # methods it responds to.
257
+ #
258
+ # Raises EOFError if input is exhausted.
259
+ #
260
+ def choose( *items, &details )
261
+ @menu = @question = Menu.new(&details)
262
+ @menu.choices(*items) unless items.empty?
263
+
264
+ # Set _answer_type_ so we can double as the Question for ask().
265
+ @menu.answer_type = if @menu.shell
266
+ lambda do |command| # shell-style selection
267
+ first_word = command.to_s.split.first || ""
268
+
269
+ options = @menu.options
270
+ options.extend(OptionParser::Completion)
271
+ answer = options.complete(first_word)
272
+
273
+ if answer.nil?
274
+ raise Question::NoAutoCompleteMatch
275
+ end
276
+
277
+ [answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
278
+ end
279
+ else
280
+ @menu.options # normal menu selection, by index or name
281
+ end
282
+
283
+ # Provide hooks for ERb layouts.
284
+ @header = @menu.header
285
+ @prompt = @menu.prompt
286
+
287
+ if @menu.shell
288
+ selected = ask("Ignored", @menu.answer_type)
289
+ @menu.select(self, *selected)
290
+ else
291
+ selected = ask("Ignored", @menu.answer_type)
292
+ @menu.select(self, selected)
293
+ end
294
+ end
295
+
296
+ #
297
+ # This method provides easy access to ANSI color sequences, without the user
298
+ # needing to remember to CLEAR at the end of each sequence. Just pass the
299
+ # _string_ to color, followed by a list of _colors_ you would like it to be
300
+ # affected by. The _colors_ can be HighLine class constants, or symbols
301
+ # (:blue for BLUE, for example). A CLEAR will automatically be embedded to
302
+ # the end of the returned String.
303
+ #
304
+ # This method returns the original _string_ unchanged if HighLine::use_color?
305
+ # is +false+.
306
+ #
307
+ def color( string, *colors )
308
+ return string unless self.class.use_color?
309
+
310
+ colors.map! do |c|
311
+ if self.class.using_color_scheme? and self.class.color_scheme.include? c
312
+ self.class.color_scheme[c]
313
+ elsif c.is_a? Symbol
314
+ self.class.const_get(c.to_s.upcase)
315
+ else
316
+ c
317
+ end
318
+ end
319
+ "#{colors.flatten.join}#{string}#{CLEAR}"
320
+ end
321
+
322
+ #
323
+ # This method is a utility for quickly and easily laying out lists. It can
324
+ # be accessed within ERb replacements of any text that will be sent to the
325
+ # user.
326
+ #
327
+ # The only required parameter is _items_, which should be the Array of items
328
+ # to list. A specified _mode_ controls how that list is formed and _option_
329
+ # has different effects, depending on the _mode_. Recognized modes are:
330
+ #
331
+ # <tt>:columns_across</tt>:: _items_ will be placed in columns, flowing
332
+ # from left to right. If given, _option_ is the
333
+ # number of columns to be used. When absent,
334
+ # columns will be determined based on _wrap_at_
335
+ # or a default of 80 characters.
336
+ # <tt>:columns_down</tt>:: Identical to <tt>:columns_across</tt>, save
337
+ # flow goes down.
338
+ # <tt>:inline</tt>:: All _items_ are placed on a single line. The
339
+ # last two _items_ are separated by _option_ or
340
+ # a default of " or ". All other _items_ are
341
+ # separated by ", ".
342
+ # <tt>:rows</tt>:: The default mode. Each of the _items_ is
343
+ # placed on it's own line. The _option_
344
+ # parameter is ignored in this mode.
345
+ #
346
+ # Each member of the _items_ Array is passed through ERb and thus can contain
347
+ # their own expansions. Color escape expansions do not contribute to the
348
+ # final field width.
349
+ #
350
+ def list( items, mode = :rows, option = nil )
351
+ items = items.to_ary.map do |item|
352
+ ERB.new(item, nil, "%").result(binding)
353
+ end
354
+
355
+ case mode
356
+ when :inline
357
+ option = " or " if option.nil?
358
+
359
+ case items.size
360
+ when 0
361
+ ""
362
+ when 1
363
+ items.first
364
+ when 2
365
+ "#{items.first}#{option}#{items.last}"
366
+ else
367
+ items[0..-2].join(", ") + "#{option}#{items.last}"
368
+ end
369
+ when :columns_across, :columns_down
370
+ max_length = actual_length(
371
+ items.max { |a, b| actual_length(a) <=> actual_length(b) }
372
+ )
373
+
374
+ if option.nil?
375
+ limit = @wrap_at || 80
376
+ option = (limit + 2) / (max_length + 2)
377
+ end
378
+
379
+ items = items.map do |item|
380
+ pad = max_length + (item.length - actual_length(item))
381
+ "%-#{pad}s" % item
382
+ end
383
+ row_count = (items.size / option.to_f).ceil
384
+
385
+ if mode == :columns_across
386
+ rows = Array.new(row_count) { Array.new }
387
+ items.each_with_index do |item, index|
388
+ rows[index / option] << item
389
+ end
390
+
391
+ rows.map { |row| row.join(" ") + "\n" }.join
392
+ else
393
+ columns = Array.new(option) { Array.new }
394
+ items.each_with_index do |item, index|
395
+ columns[index / row_count] << item
396
+ end
397
+
398
+ list = ""
399
+ columns.first.size.times do |index|
400
+ list << columns.map { |column| column[index] }.
401
+ compact.join(" ") + "\n"
402
+ end
403
+ list
404
+ end
405
+ else
406
+ items.map { |i| "#{i}\n" }.join
407
+ end
408
+ end
409
+
410
+ #
411
+ # The basic output method for HighLine objects. If the provided _statement_
412
+ # ends with a space or tab character, a newline will not be appended (output
413
+ # will be flush()ed). All other cases are passed straight to Kernel.puts().
414
+ #
415
+ # The _statement_ parameter is processed as an ERb template, supporting
416
+ # embedded Ruby code. The template is evaluated with a binding inside
417
+ # the HighLine instance, providing easy access to the ANSI color constants
418
+ # and the HighLine.color() method.
419
+ #
420
+ def say( statement )
421
+ statement = statement.to_str
422
+ return unless statement.length > 0
423
+
424
+ template = ERB.new(statement, nil, "%")
425
+ statement = template.result(binding)
426
+
427
+ statement = wrap(statement) unless @wrap_at.nil?
428
+ statement = page_print(statement) unless @page_at.nil?
429
+
430
+ if statement[-1, 1] == " " or statement[-1, 1] == "\t"
431
+ @output.print(statement)
432
+ @output.flush
433
+ else
434
+ @output.puts(statement)
435
+ end
436
+ end
437
+
438
+ #
439
+ # Set to an integer value to cause HighLine to wrap output lines at the
440
+ # indicated character limit. When +nil+, the default, no wrapping occurs. If
441
+ # set to <tt>:auto</tt>, HighLine will attempt to determing the columns
442
+ # available for the <tt>@output</tt> or use a sensible default.
443
+ #
444
+ def wrap_at=( setting )
445
+ @wrap_at = setting == :auto ? output_cols : setting
446
+ end
447
+
448
+ #
449
+ # Set to an integer value to cause HighLine to page output lines over the
450
+ # indicated line limit. When +nil+, the default, no paging occurs. If
451
+ # set to <tt>:auto</tt>, HighLine will attempt to determing the rows available
452
+ # for the <tt>@output</tt> or use a sensible default.
453
+ #
454
+ def page_at=( setting )
455
+ @page_at = setting == :auto ? output_rows : setting
456
+ end
457
+
458
+ #
459
+ # Returns the number of columns for the console, or a default it they cannot
460
+ # be determined.
461
+ #
462
+ def output_cols
463
+ return 80 unless @output.tty?
464
+ terminal_size.first
465
+ rescue
466
+ return 80
467
+ end
468
+
469
+ #
470
+ # Returns the number of rows for the console, or a default if they cannot be
471
+ # determined.
472
+ #
473
+ def output_rows
474
+ return 24 unless @output.tty?
475
+ terminal_size.last
476
+ rescue
477
+ return 24
478
+ end
479
+
480
+ private
481
+
482
+ #
483
+ # A helper method for sending the output stream and error and repeat
484
+ # of the question.
485
+ #
486
+ def explain_error( error )
487
+ say(@question.responses[error]) unless error.nil?
488
+ if @question.responses[:ask_on_error] == :question
489
+ say(@question)
490
+ elsif @question.responses[:ask_on_error]
491
+ say(@question.responses[:ask_on_error])
492
+ end
493
+ end
494
+
495
+ #
496
+ # Collects an Array/Hash full of answers as described in
497
+ # HighLine::Question.gather().
498
+ #
499
+ # Raises EOFError if input is exhausted.
500
+ #
501
+ def gather( )
502
+ @gather = @question.gather
503
+ @answers = [ ]
504
+ original_question = @question
505
+
506
+ @question.gather = false
507
+
508
+ case @gather
509
+ when Integer
510
+ @answers << ask(@question)
511
+ @gather -= 1
512
+
513
+ original_question.question = ""
514
+ until @gather.zero?
515
+ @question = original_question
516
+ @answers << ask(@question)
517
+ @gather -= 1
518
+ end
519
+ when String, Regexp
520
+ @answers << ask(@question)
521
+
522
+ original_question.question = ""
523
+ until (@gather.is_a?(String) and @answers.last.to_s == @gather) or
524
+ (@gather.is_a?(Regexp) and @answers.last.to_s =~ @gather)
525
+ @question = original_question
526
+ @answers << ask(@question)
527
+ end
528
+
529
+ @answers.pop
530
+ when Hash
531
+ @answers = { }
532
+ @gather.keys.sort.each do |key|
533
+ @question = original_question
534
+ @key = key
535
+ @answers[key] = ask(@question)
536
+ end
537
+ end
538
+
539
+ @answers
540
+ end
541
+
542
+ #
543
+ # Read a line of input from the input stream and process whitespace as
544
+ # requested by the Question object.
545
+ #
546
+ # If Question's _readline_ property is set, that library will be used to
547
+ # fetch input. *WARNING*: This ignores the currently set input stream.
548
+ #
549
+ # Raises EOFError if input is exhausted.
550
+ #
551
+ def get_line( )
552
+ if @question.readline
553
+ require "readline" # load only if needed
554
+
555
+ # capture say()'s work in a String to feed to readline()
556
+ old_output = @output
557
+ @output = StringIO.new
558
+ say(@question)
559
+ question = @output.string
560
+ @output = old_output
561
+
562
+ # prep auto-completion
563
+ completions = @question.selection.abbrev
564
+ Readline.completion_proc = lambda { |string| completions[string] }
565
+
566
+ # work-around ugly readline() warnings
567
+ old_verbose = $VERBOSE
568
+ $VERBOSE = nil
569
+ answer = @question.change_case(
570
+ @question.remove_whitespace(
571
+ Readline.readline(question, true) ) )
572
+ $VERBOSE = old_verbose
573
+
574
+ answer
575
+ else
576
+ raise EOFError, "The input stream is exhausted." if @input.eof?
577
+
578
+ @question.change_case(@question.remove_whitespace(@input.gets))
579
+ end
580
+ end
581
+
582
+ #
583
+ # Return a line or character of input, as requested for this question.
584
+ # Character input will be returned as a single character String,
585
+ # not an Integer.
586
+ #
587
+ # This question's _first_answer_ will be returned instead of input, if set.
588
+ #
589
+ # Raises EOFError if input is exhausted.
590
+ #
591
+ def get_response( )
592
+ return @question.first_answer if @question.first_answer?
593
+
594
+ if @question.character.nil?
595
+ if @question.echo == true and @question.limit.nil?
596
+ get_line
597
+ else
598
+ raw_no_echo_mode if stty = CHARACTER_MODE == "stty"
599
+
600
+ line = ""
601
+ begin
602
+ while character = (stty ? @input.getc : get_character(@input))
603
+ line << character.chr
604
+ # looking for carriage return (decimal 13) or
605
+ # newline (decimal 10) in raw input
606
+ break if character == 13 or character == 10 or
607
+ (@question.limit and line.size == @question.limit)
608
+ @output.print(@question.echo) if @question.echo != false
609
+ end
610
+ if @question.overwrite
611
+ @output.print("\r#{ERASE_LINE}")
612
+ @output.flush
613
+ else
614
+ say("\n")
615
+ end
616
+ ensure
617
+ restore_mode if stty
618
+ end
619
+
620
+ @question.change_case(@question.remove_whitespace(line))
621
+ end
622
+ elsif @question.character == :getc
623
+ @question.change_case(@input.getc.chr)
624
+ else
625
+ response = get_character(@input).chr
626
+ if @question.overwrite
627
+ @output.print("\r#{ERASE_LINE}")
628
+ @output.flush
629
+ else
630
+ echo = if @question.echo == true
631
+ response
632
+ elsif @question.echo != false
633
+ @question.echo
634
+ else
635
+ ""
636
+ end
637
+ say("#{echo}\n")
638
+ end
639
+ @question.change_case(response)
640
+ end
641
+ end
642
+
643
+ #
644
+ # Page print a series of at most _page_at_ lines for _output_. After each
645
+ # page is printed, HighLine will pause until the user presses enter/return
646
+ # then display the next page of data.
647
+ #
648
+ # Note that the final page of _output_ is *not* printed, but returned
649
+ # instead. This is to support any special handling for the final sequence.
650
+ #
651
+ def page_print( output )
652
+ lines = output.scan(/[^\n]*\n?/)
653
+ while lines.size > @page_at
654
+ @output.puts lines.slice!(0...@page_at).join
655
+ @output.puts
656
+ # Return last line if user wants to abort paging
657
+ return (["...\n"] + lines.slice(-2,1)).join unless continue_paging?
658
+ end
659
+ return lines.join
660
+ end
661
+
662
+ #
663
+ # Ask user if they wish to continue paging output. Allows them to type "q" to
664
+ # cancel the paging process.
665
+ #
666
+ def continue_paging?
667
+ command = HighLine.new(@input, @output).ask(
668
+ "-- press enter/return to continue or q to stop -- "
669
+ ) { |q| q.character = true }
670
+ command !~ /\A[qQ]\Z/ # Only continue paging if Q was not hit.
671
+ end
672
+
673
+ #
674
+ # Wrap a sequence of _lines_ at _wrap_at_ characters per line. Existing
675
+ # newlines will not be affected by this process, but additional newlines
676
+ # may be added.
677
+ #
678
+ def wrap( lines )
679
+ wrapped = [ ]
680
+ lines.each do |line|
681
+ while line =~ /([^\n]{#{@wrap_at + 1},})/
682
+ search = $1.dup
683
+ replace = $1.dup
684
+ if index = replace.rindex(" ", @wrap_at)
685
+ replace[index, 1] = "\n"
686
+ replace.sub!(/\n[ \t]+/, "\n")
687
+ line.sub!(search, replace)
688
+ else
689
+ line[@wrap_at, 0] = "\n"
690
+ end
691
+ end
692
+ wrapped << line
693
+ end
694
+ return wrapped.join
695
+ end
696
+
697
+ #
698
+ # Returns the length of the passed +string_with_escapes+, minus and color
699
+ # sequence escapes.
700
+ #
701
+ def actual_length( string_with_escapes )
702
+ string_with_escapes.gsub(/\e\[\d{1,2}m/, "").length
703
+ end
704
+ end