malysz87-highline 1.5.9 → 1.5.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,514 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # question.rb
4
+ #
5
+ # Created by James Edward Gray II on 2005-04-26.
6
+ # Copyright 2005 Gray Productions. All rights reserved.
7
+ #
8
+ # This is Free Software. See LICENSE and COPYING for details.
9
+
10
+ require "optparse"
11
+ require "date"
12
+ require "pathname"
13
+
14
+ class HighLine
15
+ #
16
+ # Question objects contain all the details of a single invocation of
17
+ # HighLine.ask(). The object is initialized by the parameters passed to
18
+ # HighLine.ask() and then queried to make sure each step of the input
19
+ # process is handled according to the users wishes.
20
+ #
21
+ class Question
22
+ # An internal HighLine errors. User code does not need to trap this.
23
+ class NoAutoCompleteMatch < StandardError
24
+ # do nothing, just creating a unique error type
25
+ end
26
+
27
+ class NotEnoughAnswers < StandardError
28
+ # do nothing, just creating a unique error type
29
+ end
30
+
31
+ #
32
+ # Create an instance of HighLine::Question. Expects a _question_ to ask
33
+ # (can be <tt>""</tt>) and an _answer_type_ to convert the answer to.
34
+ # The _answer_type_ parameter must be a type recongnized by
35
+ # Question.convert(). If given, a block is yeilded the new Question
36
+ # object to allow custom initializaion.
37
+ #
38
+ def initialize( question, answer_type )
39
+ # initialize instance data
40
+ @question = question
41
+ @answer_type = answer_type
42
+
43
+ @many_answers = nil
44
+ @at_least = 1
45
+ @character = nil
46
+ @limit = nil
47
+ @echo = true
48
+ @readline = false
49
+ @whitespace = :strip
50
+ @case = nil
51
+ @default = nil
52
+ @validate = nil
53
+ @above = nil
54
+ @below = nil
55
+ @in = nil
56
+ @confirm = nil
57
+ @gather = false
58
+ @first_answer = nil
59
+ @directory = Pathname.new(File.expand_path(File.dirname($0)))
60
+ @glob = "*"
61
+ @responses = Hash.new
62
+ @overwrite = false
63
+
64
+ # allow block to override settings
65
+ yield self if block_given?
66
+
67
+ # finalize responses based on settings
68
+ build_responses
69
+ end
70
+
71
+
72
+ # The ERb template of the question to be asked.
73
+ attr_accessor :question
74
+ # The type that will be used to convert this answer.
75
+ attr_accessor :answer_type
76
+ #
77
+ # Can be set to +true+ to use HighLine's cross-platform character reader
78
+ # instead of fetching an entire line of input. (Note: HighLine's
79
+ # character reader *ONLY* supports STDIN on Windows and Unix.) Can also
80
+ # be set to <tt>:getc</tt> to use that method on the input stream.
81
+ #
82
+ # *WARNING*: The _echo_ and _overwrite_ attributes for a question are
83
+ # ignored when using the <tt>:getc</tt> method.
84
+ #
85
+ attr_accessor :character
86
+ #
87
+ # Allows you to read many answers
88
+ #
89
+ #
90
+ # *WARNING*: This option forces a character by character read.
91
+ #
92
+ attr_accessor :many_answers
93
+ #
94
+ # Allows you to set a character limit for input.
95
+ #
96
+ attr_accessor :at_least
97
+ #
98
+ # Specify minimum number of answers that user has to give. It
99
+ # should be used only with :many_answers = true
100
+ #
101
+ attr_accessor :limit
102
+ #
103
+ # Can be set to +true+ or +false+ to control whether or not input will
104
+ # be echoed back to the user. A setting of +true+ will cause echo to
105
+ # match input, but any other true value will be treated as to String to
106
+ # echo for each character typed.
107
+ #
108
+ # This requires HighLine's character reader. See the _character_
109
+ # attribute for details.
110
+ #
111
+ # *Note*: When using HighLine to manage echo on Unix based systems, we
112
+ # recommend installing the termios gem. Without it, it's possible to type
113
+ # fast enough to have letters still show up (when reading character by
114
+ # character only).
115
+ #
116
+ attr_accessor :echo
117
+ #
118
+ # Use the Readline library to fetch input. This allows input editing as
119
+ # well as keeping a history. In addition, tab will auto-complete
120
+ # within an Array of choices or a file listing.
121
+ #
122
+ # *WARNING*: This option is incompatible with all of HighLine's
123
+ # character reading modes and it causes HighLine to ignore the
124
+ # specified _input_ stream.
125
+ #
126
+ attr_accessor :readline
127
+ #
128
+ # Used to control whitespace processing for the answer to this question.
129
+ # See HighLine::Question.remove_whitespace() for acceptable settings.
130
+ #
131
+ attr_accessor :whitespace
132
+ #
133
+ # Used to control character case processing for the answer to this question.
134
+ # See HighLine::Question.change_case() for acceptable settings.
135
+ #
136
+ attr_accessor :case
137
+ # Used to provide a default answer to this question.
138
+ attr_accessor :default
139
+ #
140
+ # If set to a Regexp, the answer must match (before type conversion).
141
+ # Can also be set to a Proc which will be called with the provided
142
+ # answer to validate with a +true+ or +false+ return.
143
+ #
144
+ attr_accessor :validate
145
+ # Used to control range checks for answer.
146
+ attr_accessor :above, :below
147
+ # If set, answer must pass an include?() check on this object.
148
+ attr_accessor :in
149
+ #
150
+ # Asks a yes or no confirmation question, to ensure a user knows what
151
+ # they have just agreed to. If set to +true+ the question will be,
152
+ # "Are you sure? " Any other true value for this attribute is assumed
153
+ # to be the question to ask. When +false+ or +nil+ (the default),
154
+ # answers are not confirmed.
155
+ #
156
+ attr_accessor :confirm
157
+ #
158
+ # When set, the user will be prompted for multiple answers which will
159
+ # be collected into an Array or Hash and returned as the final answer.
160
+ #
161
+ # You can set _gather_ to an Integer to have an Array of exactly that
162
+ # many answers collected, or a String/Regexp to match an end input which
163
+ # will not be returned in the Array.
164
+ #
165
+ # Optionally _gather_ can be set to a Hash. In this case, the question
166
+ # will be asked once for each key and the answers will be returned in a
167
+ # Hash, mapped by key. The <tt>@key</tt> variable is set before each
168
+ # question is evaluated, so you can use it in your question.
169
+ #
170
+ attr_accessor :gather
171
+ #
172
+ # When set to a non *nil* value, this will be tried as an answer to the
173
+ # question. If this answer passes validations, it will become the result
174
+ # without the user ever being prompted. Otherwise this value is discarded,
175
+ # and this Question is resolved as a normal call to HighLine.ask().
176
+ #
177
+ attr_writer :first_answer
178
+ #
179
+ # The directory from which a user will be allowed to select files, when
180
+ # File or Pathname is specified as an _answer_type_. Initially set to
181
+ # <tt>Pathname.new(File.expand_path(File.dirname($0)))</tt>.
182
+ #
183
+ attr_accessor :directory
184
+ #
185
+ # The glob pattern used to limit file selection when File or Pathname is
186
+ # specified as an _answer_type_. Initially set to <tt>"*"</tt>.
187
+ #
188
+ attr_accessor :glob
189
+ #
190
+ # A Hash that stores the various responses used by HighLine to notify
191
+ # the user. The currently used responses and their purpose are as
192
+ # follows:
193
+ #
194
+ # <tt>:ambiguous_completion</tt>:: Used to notify the user of an
195
+ # ambiguous answer the auto-completion
196
+ # system cannot resolve.
197
+ # <tt>:ask_on_error</tt>:: This is the question that will be
198
+ # redisplayed to the user in the event
199
+ # of an error. Can be set to
200
+ # <tt>:question</tt> to repeat the
201
+ # original question.
202
+ # <tt>:invalid_type</tt>:: The error message shown when a type
203
+ # conversion fails.
204
+ # <tt>:no_completion</tt>:: Used to notify the user that their
205
+ # selection does not have a valid
206
+ # auto-completion match.
207
+ # <tt>:not_in_range</tt>:: Used to notify the user that a
208
+ # provided answer did not satisfy
209
+ # the range requirement tests.
210
+ # <tt>:not_valid</tt>:: The error message shown when
211
+ # validation checks fail.
212
+ #
213
+ attr_reader :responses
214
+ #
215
+ # When set to +true+ the question is asked, but output does not progress to
216
+ # the next line. The Cursor is moved back to the beginning of the question
217
+ # line and it is cleared so that all the contents of the line disappear from
218
+ # the screen.
219
+ #
220
+ attr_accessor :overwrite
221
+
222
+ #
223
+ # Returns the provided _answer_string_ or the default answer for this
224
+ # Question if a default was set and the answer is empty.
225
+ #
226
+ def answer_or_default( answer_string )
227
+ if answer_string.length == 0 and not @default.nil?
228
+ @default
229
+ else
230
+ answer_string
231
+ end
232
+ end
233
+
234
+ #
235
+ # Called late in the initialization process to build intelligent
236
+ # responses based on the details of this Question object.
237
+ #
238
+ def build_responses( )
239
+ ### WARNING: This code is quasi-duplicated in ###
240
+ ### Menu.update_responses(). Check there too when ###
241
+ ### making changes! ###
242
+ append_default unless default.nil?
243
+ @responses = { :ambiguous_completion =>
244
+ "Ambiguous choice. " +
245
+ "Please choose from #{@answer_type.inspect}.",
246
+ :ask_on_error =>
247
+ "? ",
248
+ :invalid_type =>
249
+ "You must enter a valid #{@answer_type}.",
250
+ :no_completion =>
251
+ "You must choose from " +
252
+ "#{@answer_type.inspect}.",
253
+ :not_enough_answers =>
254
+ "You must give at least #{@at_least} answers",
255
+ :not_in_range =>
256
+ "Your answer isn't within the expected range " +
257
+ "(#{expected_range}).",
258
+ :not_valid =>
259
+ "Your answer isn't valid" + (@validate.is_a?(Regexp) ? " (must match #{@validate.inspect})." : "") }.merge(@responses)
260
+ ### WARNING: This code is quasi-duplicated in ###
261
+ ### Menu.update_responses(). Check there too when ###
262
+ ### making changes! ###
263
+ end
264
+
265
+ #
266
+ # Returns the provided _answer_string_ after changing character case by
267
+ # the rules of this Question. Valid settings for whitespace are:
268
+ #
269
+ # +nil+:: Do not alter character case.
270
+ # (Default.)
271
+ # <tt>:up</tt>:: Calls upcase().
272
+ # <tt>:upcase</tt>:: Calls upcase().
273
+ # <tt>:down</tt>:: Calls downcase().
274
+ # <tt>:downcase</tt>:: Calls downcase().
275
+ # <tt>:capitalize</tt>:: Calls capitalize().
276
+ #
277
+ # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
278
+ #
279
+ def change_case( answer_string )
280
+ if [:up, :upcase].include?(@case)
281
+ answer_string.upcase
282
+ elsif [:down, :downcase].include?(@case)
283
+ answer_string.downcase
284
+ elsif @case == :capitalize
285
+ answer_string.capitalize
286
+ else
287
+ answer_string
288
+ end
289
+ end
290
+
291
+ # This method converts answers to proper type
292
+ def convert( answer_string )
293
+ if @many_answers
294
+ answer = []
295
+ answer_string.split.each do | single_answer |
296
+ answer << convert_single_answer(single_answer)
297
+ end
298
+ if answer.size < @at_least
299
+ raise NotEnoughAnswers
300
+ end
301
+ else
302
+ answer = convert_single_answer(answer_string)
303
+ end
304
+ answer
305
+ end
306
+
307
+ # Returns a english explination of the current range settings.
308
+ def expected_range( )
309
+ expected = [ ]
310
+
311
+ expected << "above #{@above}" unless @above.nil?
312
+ expected << "below #{@below}" unless @below.nil?
313
+ expected << "included in #{@in.inspect}" unless @in.nil?
314
+
315
+ case expected.size
316
+ when 0 then ""
317
+ when 1 then expected.first
318
+ when 2 then expected.join(" and ")
319
+ else expected[0..-2].join(", ") + ", and #{expected.last}"
320
+ end
321
+ end
322
+
323
+ # Returns _first_answer_, which will be unset following this call.
324
+ def first_answer( )
325
+ @first_answer
326
+ ensure
327
+ @first_answer = nil
328
+ end
329
+
330
+ # Returns true if _first_answer_ is set.
331
+ def first_answer?( )
332
+ not @first_answer.nil?
333
+ end
334
+
335
+ #
336
+ # Returns +true+ if the _answer_object_ is greater than the _above_
337
+ # attribute, less than the _below_ attribute and included?()ed in the
338
+ # _in_ attribute. Otherwise, +false+ is returned. Any +nil+ attributes
339
+ # are not checked.
340
+ #
341
+ def in_range?( answer_object )
342
+ if !@many_answers
343
+ answer_object = [answer_object]
344
+ end
345
+ answer_object.each do | single_answer_object |
346
+ return false unless
347
+ (@above.nil? or single_answer_object > @above) and
348
+ (@below.nil? or single_answer_object < @below) and
349
+ (@in.nil? or @in.include?(single_answer_object))
350
+ end
351
+ return true
352
+ end
353
+
354
+ #
355
+ # Returns the provided _answer_string_ after processing whitespace by
356
+ # the rules of this Question. Valid settings for whitespace are:
357
+ #
358
+ # +nil+:: Do not alter whitespace.
359
+ # <tt>:strip</tt>:: Calls strip(). (Default.)
360
+ # <tt>:chomp</tt>:: Calls chomp().
361
+ # <tt>:collapse</tt>:: Collapses all whitspace runs to a
362
+ # single space.
363
+ # <tt>:strip_and_collapse</tt>:: Calls strip(), then collapses all
364
+ # whitspace runs to a single space.
365
+ # <tt>:chomp_and_collapse</tt>:: Calls chomp(), then collapses all
366
+ # whitspace runs to a single space.
367
+ # <tt>:remove</tt>:: Removes all whitespace.
368
+ #
369
+ # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
370
+ #
371
+ # This process is skipped, for single character input.
372
+ #
373
+ def remove_whitespace( answer_string )
374
+ if @whitespace.nil?
375
+ answer_string
376
+ elsif [:strip, :chomp].include?(@whitespace)
377
+ answer_string.send(@whitespace)
378
+ elsif @whitespace == :collapse
379
+ answer_string.gsub(/\s+/, " ")
380
+ elsif [:strip_and_collapse, :chomp_and_collapse].include?(@whitespace)
381
+ result = answer_string.send(@whitespace.to_s[/^[a-z]+/])
382
+ result.gsub(/\s+/, " ")
383
+ elsif @whitespace == :remove
384
+ answer_string.gsub(/\s+/, "")
385
+ else
386
+ answer_string
387
+ end
388
+ end
389
+
390
+ #
391
+ # Returns an Array of valid answers to this question. These answers are
392
+ # only known when _answer_type_ is set to an Array of choices, File, or
393
+ # Pathname. Any other time, this method will return an empty Array.
394
+ #
395
+ def selection( )
396
+ if @answer_type.is_a?(Array)
397
+ @answer_type
398
+ elsif [File, Pathname].include?(@answer_type)
399
+ Dir[File.join(@directory.to_s, @glob)].map do |file|
400
+ File.basename(file)
401
+ end
402
+ else
403
+ [ ]
404
+ end
405
+ end
406
+
407
+ # Stringifies the question to be asked.
408
+ def to_str( )
409
+ @question
410
+ end
411
+
412
+ #
413
+ # Returns +true+ if the provided _answer_string_ is accepted by the
414
+ # _validate_ attribute or +false+ if it's not.
415
+ #
416
+ # It's important to realize that an answer is validated after whitespace
417
+ # and case handling.
418
+ #
419
+ def valid_answer?( answer_string )
420
+ return true if @validate.nil?
421
+ answers = []
422
+ if @many_answers
423
+ answers = answer_string.split
424
+ else
425
+ answers << answer_string
426
+ end
427
+ answers.each do |single_answer_string|
428
+ return false unless
429
+ (@validate.is_a?(Regexp) and single_answer_string =~ @validate) or
430
+ (@validate.is_a?(Proc) and @validate[single_answer_string])
431
+ end
432
+ return true
433
+ end
434
+
435
+ private
436
+
437
+ # Convert single answer
438
+ # Transforms the given _answer_string_ into the expected type for this
439
+ # Question. Currently supported conversions are:
440
+ #
441
+ # <tt>[...]</tt>:: Answer must be a member of the passed Array.
442
+ # Auto-completion is used to expand partial
443
+ # answers.
444
+ # <tt>lambda {...}</tt>:: Answer is passed to lambda for conversion.
445
+ # Date:: Date.parse() is called with answer.
446
+ # DateTime:: DateTime.parse() is called with answer.
447
+ # File:: The entered file name is auto-completed in
448
+ # terms of _directory_ + _glob_, opened, and
449
+ # returned.
450
+ # Float:: Answer is converted with Kernel.Float().
451
+ # Integer:: Answer is converted with Kernel.Integer().
452
+ # +nil+:: Answer is left in String format. (Default.)
453
+ # Pathname:: Same as File, save that a Pathname object is
454
+ # returned.
455
+ # String:: Answer is converted with Kernel.String().
456
+ # Regexp:: Answer is fed to Regexp.new().
457
+ # Symbol:: The method to_sym() is called on answer and
458
+ # the result returned.
459
+ # <i>any other Class</i>:: The answer is passed on to
460
+ # <tt>Class.parse()</tt>.
461
+ #
462
+ # This method throws ArgumentError, if the conversion cannot be
463
+ # completed for any reason.
464
+ #
465
+
466
+ def convert_single_answer( answer_string )
467
+ if @answer_type.nil?
468
+ answer_string
469
+ elsif [Float, Integer, String].include?(@answer_type)
470
+ Kernel.send(@answer_type.to_s.to_sym, answer_string)
471
+ elsif @answer_type == Symbol
472
+ answer_string.to_sym
473
+ elsif @answer_type == Regexp
474
+ Regexp.new(answer_string)
475
+ elsif @answer_type.is_a?(Array) or [File, Pathname].include?(@answer_type)
476
+ # cheating, using OptionParser's Completion module
477
+ choices = selection
478
+ choices.extend(OptionParser::Completion)
479
+ answer = choices.complete(answer_string)
480
+ if answer.nil?
481
+ raise NoAutoCompleteMatch
482
+ end
483
+ if @answer_type.is_a?(Array)
484
+ answer.last
485
+ elsif @answer_type == File
486
+ File.open(File.join(@directory.to_s, answer.last))
487
+ else
488
+ Pathname.new(File.join(@directory.to_s, answer.last))
489
+ end
490
+ elsif [Date, DateTime].include?(@answer_type) or @answer_type.is_a?(Class)
491
+ @answer_type.parse(answer_string)
492
+ elsif @answer_type.is_a?(Proc)
493
+ @answer_type[answer_string]
494
+ end
495
+ end
496
+
497
+ #
498
+ # Adds the default choice to the end of question between <tt>|...|</tt>.
499
+ # Trailing whitespace is preserved so the function of HighLine.say() is
500
+ # not affected.
501
+ #
502
+ def append_default( )
503
+ if @question =~ /([\t ]+)\Z/
504
+ @question << "|#{@default}|#{$1}"
505
+ elsif @question == ""
506
+ @question << "|#{@default}| "
507
+ elsif @question[-1, 1] == "\n"
508
+ @question[-2, 0] = " |#{@default}|"
509
+ else
510
+ @question << " |#{@default}|"
511
+ end
512
+ end
513
+ end
514
+ end
@@ -0,0 +1,130 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # system_extensions.rb
4
+ #
5
+ # Created by James Edward Gray II on 2006-06-14.
6
+ # Copyright 2006 Gray Productions. All rights reserved.
7
+ #
8
+ # This is Free Software. See LICENSE and COPYING for details.
9
+
10
+ class HighLine
11
+ module SystemExtensions
12
+ module_function
13
+
14
+ #
15
+ # This section builds character reading and terminal size functions
16
+ # to suit the proper platform we're running on. Be warned: Here be
17
+ # dragons!
18
+ #
19
+ begin
20
+ # Cygwin will look like Windows, but we want to treat it like a Posix OS:
21
+ raise LoadError, "Cygwin is a Posix OS." if RUBY_PLATFORM =~ /\bcygwin\b/i
22
+
23
+ require "Win32API" # See if we're on Windows.
24
+
25
+ CHARACTER_MODE = "Win32API" # For Debugging purposes only.
26
+
27
+ #
28
+ # Windows savvy getc().
29
+ #
30
+ # *WARNING*: This method ignores <tt>input</tt> and reads one
31
+ # character from +STDIN+!
32
+ #
33
+ def get_character( input = STDIN )
34
+ Win32API.new("crtdll", "_getch", [ ], "L").Call
35
+ end
36
+
37
+ # A Windows savvy method to fetch the console columns, and rows.
38
+ def terminal_size
39
+ m_GetStdHandle = Win32API.new( 'kernel32',
40
+ 'GetStdHandle',
41
+ ['L'],
42
+ 'L' )
43
+ m_GetConsoleScreenBufferInfo = Win32API.new(
44
+ 'kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L'
45
+ )
46
+
47
+ format = 'SSSSSssssSS'
48
+ buf = ([0] * format.size).pack(format)
49
+ stdout_handle = m_GetStdHandle.call(0xFFFFFFF5)
50
+
51
+ m_GetConsoleScreenBufferInfo.call(stdout_handle, buf)
52
+ bufx, bufy, curx, cury, wattr,
53
+ left, top, right, bottom, maxx, maxy = buf.unpack(format)
54
+ return right - left + 1, bottom - top + 1
55
+ end
56
+ rescue LoadError # If we're not on Windows try...
57
+ begin
58
+ require "termios" # Unix, first choice.
59
+
60
+ CHARACTER_MODE = "termios" # For Debugging purposes only.
61
+
62
+ #
63
+ # Unix savvy getc(). (First choice.)
64
+ #
65
+ # *WARNING*: This method requires the "termios" library!
66
+ #
67
+ def get_character( input = STDIN )
68
+ old_settings = Termios.getattr(input)
69
+
70
+ new_settings = old_settings.dup
71
+ new_settings.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
72
+ new_settings.c_cc[Termios::VMIN] = 1
73
+
74
+ begin
75
+ Termios.setattr(input, Termios::TCSANOW, new_settings)
76
+ input.getc
77
+ ensure
78
+ Termios.setattr(input, Termios::TCSANOW, old_settings)
79
+ end
80
+ end
81
+ rescue LoadError # If our first choice fails, default.
82
+ CHARACTER_MODE = "stty" # For Debugging purposes only.
83
+
84
+ #
85
+ # Unix savvy getc(). (Second choice.)
86
+ #
87
+ # *WARNING*: This method requires the external "stty" program!
88
+ #
89
+ def get_character( input = STDIN )
90
+ raw_no_echo_mode
91
+
92
+ begin
93
+ input.getc
94
+ ensure
95
+ restore_mode
96
+ end
97
+ end
98
+
99
+ #
100
+ # Switched the input mode to raw and disables echo.
101
+ #
102
+ # *WARNING*: This method requires the external "stty" program!
103
+ #
104
+ def raw_no_echo_mode
105
+ @state = `stty -g`
106
+ system "stty raw -echo cbreak isig"
107
+ end
108
+
109
+ #
110
+ # Restores a previously saved input mode.
111
+ #
112
+ # *WARNING*: This method requires the external "stty" program!
113
+ #
114
+ def restore_mode
115
+ system "stty #{@state}"
116
+ end
117
+ end
118
+
119
+ # A Unix savvy method to fetch the console columns, and rows.
120
+ def terminal_size
121
+ if /solaris/ =~ RUBY_PLATFORM and
122
+ `stty` =~ /\brows = (\d+).*\bcolumns = (\d+)/
123
+ [$2, $1].map { |c| x.to_i }
124
+ else
125
+ `stty size`.split.map { |x| x.to_i }.reverse
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end