malysz87-highline 1.5.2 → 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,515 @@
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 (must match " +
260
+ "#{@validate.inspect})." }.merge(@responses)
261
+ ### WARNING: This code is quasi-duplicated in ###
262
+ ### Menu.update_responses(). Check there too when ###
263
+ ### making changes! ###
264
+ end
265
+
266
+ #
267
+ # Returns the provided _answer_string_ after changing character case by
268
+ # the rules of this Question. Valid settings for whitespace are:
269
+ #
270
+ # +nil+:: Do not alter character case.
271
+ # (Default.)
272
+ # <tt>:up</tt>:: Calls upcase().
273
+ # <tt>:upcase</tt>:: Calls upcase().
274
+ # <tt>:down</tt>:: Calls downcase().
275
+ # <tt>:downcase</tt>:: Calls downcase().
276
+ # <tt>:capitalize</tt>:: Calls capitalize().
277
+ #
278
+ # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
279
+ #
280
+ def change_case( answer_string )
281
+ if [:up, :upcase].include?(@case)
282
+ answer_string.upcase
283
+ elsif [:down, :downcase].include?(@case)
284
+ answer_string.downcase
285
+ elsif @case == :capitalize
286
+ answer_string.capitalize
287
+ else
288
+ answer_string
289
+ end
290
+ end
291
+
292
+ # This method converts answers to proper type
293
+ def convert( answer_string )
294
+ if @many_answers
295
+ answer = []
296
+ answer_string.split.each do | single_answer |
297
+ answer << convert_single_answer(single_answer)
298
+ end
299
+ if answer.size < @at_least
300
+ raise NotEnoughAnswers
301
+ end
302
+ else
303
+ answer = convert_single_answer(answer_string)
304
+ end
305
+ answer
306
+ end
307
+
308
+ # Returns a english explination of the current range settings.
309
+ def expected_range( )
310
+ expected = [ ]
311
+
312
+ expected << "above #{@above}" unless @above.nil?
313
+ expected << "below #{@below}" unless @below.nil?
314
+ expected << "included in #{@in.inspect}" unless @in.nil?
315
+
316
+ case expected.size
317
+ when 0 then ""
318
+ when 1 then expected.first
319
+ when 2 then expected.join(" and ")
320
+ else expected[0..-2].join(", ") + ", and #{expected.last}"
321
+ end
322
+ end
323
+
324
+ # Returns _first_answer_, which will be unset following this call.
325
+ def first_answer( )
326
+ @first_answer
327
+ ensure
328
+ @first_answer = nil
329
+ end
330
+
331
+ # Returns true if _first_answer_ is set.
332
+ def first_answer?( )
333
+ not @first_answer.nil?
334
+ end
335
+
336
+ #
337
+ # Returns +true+ if the _answer_object_ is greater than the _above_
338
+ # attribute, less than the _below_ attribute and included?()ed in the
339
+ # _in_ attribute. Otherwise, +false+ is returned. Any +nil+ attributes
340
+ # are not checked.
341
+ #
342
+ def in_range?( answer_object )
343
+ if !@many_answers
344
+ answer_object = [answer_object]
345
+ end
346
+ answer_object.each do | single_answer_object |
347
+ return false unless
348
+ (@above.nil? or single_answer_object > @above) and
349
+ (@below.nil? or single_answer_object < @below) and
350
+ (@in.nil? or @in.include?(single_answer_object))
351
+ end
352
+ return true
353
+ end
354
+
355
+ #
356
+ # Returns the provided _answer_string_ after processing whitespace by
357
+ # the rules of this Question. Valid settings for whitespace are:
358
+ #
359
+ # +nil+:: Do not alter whitespace.
360
+ # <tt>:strip</tt>:: Calls strip(). (Default.)
361
+ # <tt>:chomp</tt>:: Calls chomp().
362
+ # <tt>:collapse</tt>:: Collapses all whitspace runs to a
363
+ # single space.
364
+ # <tt>:strip_and_collapse</tt>:: Calls strip(), then collapses all
365
+ # whitspace runs to a single space.
366
+ # <tt>:chomp_and_collapse</tt>:: Calls chomp(), then collapses all
367
+ # whitspace runs to a single space.
368
+ # <tt>:remove</tt>:: Removes all whitespace.
369
+ #
370
+ # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
371
+ #
372
+ # This process is skipped, for single character input.
373
+ #
374
+ def remove_whitespace( answer_string )
375
+ if @whitespace.nil?
376
+ answer_string
377
+ elsif [:strip, :chomp].include?(@whitespace)
378
+ answer_string.send(@whitespace)
379
+ elsif @whitespace == :collapse
380
+ answer_string.gsub(/\s+/, " ")
381
+ elsif [:strip_and_collapse, :chomp_and_collapse].include?(@whitespace)
382
+ result = answer_string.send(@whitespace.to_s[/^[a-z]+/])
383
+ result.gsub(/\s+/, " ")
384
+ elsif @whitespace == :remove
385
+ answer_string.gsub(/\s+/, "")
386
+ else
387
+ answer_string
388
+ end
389
+ end
390
+
391
+ #
392
+ # Returns an Array of valid answers to this question. These answers are
393
+ # only known when _answer_type_ is set to an Array of choices, File, or
394
+ # Pathname. Any other time, this method will return an empty Array.
395
+ #
396
+ def selection( )
397
+ if @answer_type.is_a?(Array)
398
+ @answer_type
399
+ elsif [File, Pathname].include?(@answer_type)
400
+ Dir[File.join(@directory.to_s, @glob)].map do |file|
401
+ File.basename(file)
402
+ end
403
+ else
404
+ [ ]
405
+ end
406
+ end
407
+
408
+ # Stringifies the question to be asked.
409
+ def to_str( )
410
+ @question
411
+ end
412
+
413
+ #
414
+ # Returns +true+ if the provided _answer_string_ is accepted by the
415
+ # _validate_ attribute or +false+ if it's not.
416
+ #
417
+ # It's important to realize that an answer is validated after whitespace
418
+ # and case handling.
419
+ #
420
+ def valid_answer?( answer_string )
421
+ return true if @validate.nil?
422
+ answers = []
423
+ if @many_answers
424
+ answers = answer_string.split
425
+ else
426
+ answers << answer_string
427
+ end
428
+ answers.each do |single_answer_string|
429
+ return false unless
430
+ (@validate.is_a?(Regexp) and single_answer_string =~ @validate) or
431
+ (@validate.is_a?(Proc) and @validate[single_answer_string])
432
+ end
433
+ return true
434
+ end
435
+
436
+ private
437
+
438
+ # Convert single answer
439
+ # Transforms the given _answer_string_ into the expected type for this
440
+ # Question. Currently supported conversions are:
441
+ #
442
+ # <tt>[...]</tt>:: Answer must be a member of the passed Array.
443
+ # Auto-completion is used to expand partial
444
+ # answers.
445
+ # <tt>lambda {...}</tt>:: Answer is passed to lambda for conversion.
446
+ # Date:: Date.parse() is called with answer.
447
+ # DateTime:: DateTime.parse() is called with answer.
448
+ # File:: The entered file name is auto-completed in
449
+ # terms of _directory_ + _glob_, opened, and
450
+ # returned.
451
+ # Float:: Answer is converted with Kernel.Float().
452
+ # Integer:: Answer is converted with Kernel.Integer().
453
+ # +nil+:: Answer is left in String format. (Default.)
454
+ # Pathname:: Same as File, save that a Pathname object is
455
+ # returned.
456
+ # String:: Answer is converted with Kernel.String().
457
+ # Regexp:: Answer is fed to Regexp.new().
458
+ # Symbol:: The method to_sym() is called on answer and
459
+ # the result returned.
460
+ # <i>any other Class</i>:: The answer is passed on to
461
+ # <tt>Class.parse()</tt>.
462
+ #
463
+ # This method throws ArgumentError, if the conversion cannot be
464
+ # completed for any reason.
465
+ #
466
+
467
+ def convert_single_answer( answer_string )
468
+ if @answer_type.nil?
469
+ answer_string
470
+ elsif [Float, Integer, String].include?(@answer_type)
471
+ Kernel.send(@answer_type.to_s.to_sym, answer_string)
472
+ elsif @answer_type == Symbol
473
+ answer_string.to_sym
474
+ elsif @answer_type == Regexp
475
+ Regexp.new(answer_string)
476
+ elsif @answer_type.is_a?(Array) or [File, Pathname].include?(@answer_type)
477
+ # cheating, using OptionParser's Completion module
478
+ choices = selection
479
+ choices.extend(OptionParser::Completion)
480
+ answer = choices.complete(answer_string)
481
+ if answer.nil?
482
+ raise NoAutoCompleteMatch
483
+ end
484
+ if @answer_type.is_a?(Array)
485
+ answer.last
486
+ elsif @answer_type == File
487
+ File.open(File.join(@directory.to_s, answer.last))
488
+ else
489
+ Pathname.new(File.join(@directory.to_s, answer.last))
490
+ end
491
+ elsif [Date, DateTime].include?(@answer_type) or @answer_type.is_a?(Class)
492
+ @answer_type.parse(answer_string)
493
+ elsif @answer_type.is_a?(Proc)
494
+ @answer_type[answer_string]
495
+ end
496
+ end
497
+
498
+ #
499
+ # Adds the default choice to the end of question between <tt>|...|</tt>.
500
+ # Trailing whitespace is preserved so the function of HighLine.say() is
501
+ # not affected.
502
+ #
503
+ def append_default( )
504
+ if @question =~ /([\t ]+)\Z/
505
+ @question << "|#{@default}|#{$1}"
506
+ elsif @question == ""
507
+ @question << "|#{@default}| "
508
+ elsif @question[-1, 1] == "\n"
509
+ @question[-2, 0] = " |#{@default}|"
510
+ else
511
+ @question << " |#{@default}|"
512
+ end
513
+ end
514
+ end
515
+ 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