keybox 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,462 @@
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 error. 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
+ #
28
+ # Create an instance of HighLine::Question. Expects a _question_ to ask
29
+ # (can be <tt>""</tt>) and an _answer_type_ to convert the answer to.
30
+ # The _answer_type_ parameter must be a type recongnized by
31
+ # Question.convert(). If given, a block is yeilded the new Question
32
+ # object to allow custom initializaion.
33
+ #
34
+ def initialize( question, answer_type )
35
+ # initialize instance data
36
+ @question = question
37
+ @answer_type = answer_type
38
+
39
+ @character = nil
40
+ @limit = nil
41
+ @echo = true
42
+ @readline = false
43
+ @whitespace = :strip
44
+ @case = nil
45
+ @default = nil
46
+ @validate = nil
47
+ @above = nil
48
+ @below = nil
49
+ @in = nil
50
+ @confirm = nil
51
+ @gather = false
52
+ @first_answer = nil
53
+ @directory = Pathname.new(File.expand_path(File.dirname($0)))
54
+ @glob = "*"
55
+ @responses = Hash.new
56
+ @overwrite = false
57
+
58
+ # allow block to override settings
59
+ yield self if block_given?
60
+
61
+ # finalize responses based on settings
62
+ build_responses
63
+ end
64
+
65
+ # The ERb template of the question to be asked.
66
+ attr_accessor :question
67
+ # The type that will be used to convert this answer.
68
+ attr_accessor :answer_type
69
+ #
70
+ # Can be set to +true+ to use HighLine's cross-platform character reader
71
+ # instead of fetching an entire line of input. (Note: HighLine's
72
+ # character reader *ONLY* supports STDIN on Windows and Unix.) Can also
73
+ # be set to <tt>:getc</tt> to use that method on the input stream.
74
+ #
75
+ # *WARNING*: The _echo_ and _overwrite_ attributes for a question are
76
+ # ignored when using the <tt>:getc</tt> method.
77
+ #
78
+ attr_accessor :character
79
+ #
80
+ # Allows you to set a character limit for input.
81
+ #
82
+ # *WARNING*: This option forces a character by character read.
83
+ #
84
+ attr_accessor :limit
85
+ #
86
+ # Can be set to +true+ or +false+ to control whether or not input will
87
+ # be echoed back to the user. A setting of +true+ will cause echo to
88
+ # match input, but any other true value will be treated as to String to
89
+ # echo for each character typed.
90
+ #
91
+ # This requires HighLine's character reader. See the _character_
92
+ # attribute for details.
93
+ #
94
+ # *Note*: When using HighLine to manage echo on Unix based systems, we
95
+ # recommend installing the termios gem. Without it, it's possible to type
96
+ # fast enough to have letters still show up (when reading character by
97
+ # character only).
98
+ #
99
+ attr_accessor :echo
100
+ #
101
+ # Use the Readline library to fetch input. This allows input editing as
102
+ # well as keeping a history. In addition, tab will auto-complete
103
+ # within an Array of choices or a file listing.
104
+ #
105
+ # *WARNING*: This option is incompatible with all of HighLine's
106
+ # character reading modes and it causes HighLine to ignore the
107
+ # specified _input_ stream.
108
+ #
109
+ attr_accessor :readline
110
+ #
111
+ # Used to control whitespace processing for the answer to this question.
112
+ # See HighLine::Question.remove_whitespace() for acceptable settings.
113
+ #
114
+ attr_accessor :whitespace
115
+ #
116
+ # Used to control character case processing for the answer to this question.
117
+ # See HighLine::Question.change_case() for acceptable settings.
118
+ #
119
+ attr_accessor :case
120
+ # Used to provide a default answer to this question.
121
+ attr_accessor :default
122
+ #
123
+ # If set to a Regexp, the answer must match (before type conversion).
124
+ # Can also be set to a Proc which will be called with the provided
125
+ # answer to validate with a +true+ or +false+ return.
126
+ #
127
+ attr_accessor :validate
128
+ # Used to control range checks for answer.
129
+ attr_accessor :above, :below
130
+ # If set, answer must pass an include?() check on this object.
131
+ attr_accessor :in
132
+ #
133
+ # Asks a yes or no confirmation question, to ensure a user knows what
134
+ # they have just agreed to. If set to +true+ the question will be,
135
+ # "Are you sure? " Any other true value for this attribute is assumed
136
+ # to be the question to ask. When +false+ or +nil+ (the default),
137
+ # answers are not confirmed.
138
+ #
139
+ attr_accessor :confirm
140
+ #
141
+ # When set, the user will be prompted for multiple answers which will
142
+ # be collected into an Array or Hash and returned as the final answer.
143
+ #
144
+ # You can set _gather_ to an Integer to have an Array of exactly that
145
+ # many answers collected, or a String/Regexp to match an end input which
146
+ # will not be returned in the Array.
147
+ #
148
+ # Optionally _gather_ can be set to a Hash. In this case, the question
149
+ # will be asked once for each key and the answers will be returned in a
150
+ # Hash, mapped by key. The <tt>@key</tt> variable is set before each
151
+ # question is evaluated, so you can use it in your question.
152
+ #
153
+ attr_accessor :gather
154
+ #
155
+ # When set to a non *nil* value, this will be tried as an answer to the
156
+ # question. If this answer passes validations, it will become the result
157
+ # without the user ever being prompted. Otherwise this value is discarded,
158
+ # and this Question is resolved as a normal call to HighLine.ask().
159
+ #
160
+ attr_writer :first_answer
161
+ #
162
+ # The directory from which a user will be allowed to select files, when
163
+ # File or Pathname is specified as an _answer_type_. Initially set to
164
+ # <tt>Pathname.new(File.expand_path(File.dirname($0)))</tt>.
165
+ #
166
+ attr_accessor :directory
167
+ #
168
+ # The glob pattern used to limit file selection when File or Pathname is
169
+ # specified as an _answer_type_. Initially set to <tt>"*"</tt>.
170
+ #
171
+ attr_accessor :glob
172
+ #
173
+ # A Hash that stores the various responses used by HighLine to notify
174
+ # the user. The currently used responses and their purpose are as
175
+ # follows:
176
+ #
177
+ # <tt>:ambiguous_completion</tt>:: Used to notify the user of an
178
+ # ambiguous answer the auto-completion
179
+ # system cannot resolve.
180
+ # <tt>:ask_on_error</tt>:: This is the question that will be
181
+ # redisplayed to the user in the event
182
+ # of an error. Can be set to
183
+ # <tt>:question</tt> to repeat the
184
+ # original question.
185
+ # <tt>:invalid_type</tt>:: The error message shown when a type
186
+ # conversion fails.
187
+ # <tt>:no_completion</tt>:: Used to notify the user that their
188
+ # selection does not have a valid
189
+ # auto-completion match.
190
+ # <tt>:not_in_range</tt>:: Used to notify the user that a
191
+ # provided answer did not satisfy
192
+ # the range requirement tests.
193
+ # <tt>:not_valid</tt>:: The error message shown when
194
+ # validation checks fail.
195
+ #
196
+ attr_reader :responses
197
+ #
198
+ # When set to +true+ the question is asked, but output does not progress to
199
+ # the next line. The Cursor is moved back to the beginning of the question
200
+ # line and it is cleared so that all the contents of the line disappear from
201
+ # the screen.
202
+ #
203
+ attr_accessor :overwrite
204
+
205
+ #
206
+ # Returns the provided _answer_string_ or the default answer for this
207
+ # Question if a default was set and the answer is empty.
208
+ #
209
+ def answer_or_default( answer_string )
210
+ if answer_string.length == 0 and not @default.nil?
211
+ @default
212
+ else
213
+ answer_string
214
+ end
215
+ end
216
+
217
+ #
218
+ # Called late in the initialization process to build intelligent
219
+ # responses based on the details of this Question object.
220
+ #
221
+ def build_responses( )
222
+ ### WARNING: This code is quasi-duplicated in ###
223
+ ### Menu.update_responses(). Check there too when ###
224
+ ### making changes! ###
225
+ append_default unless default.nil?
226
+ @responses = { :ambiguous_completion =>
227
+ "Ambiguous choice. " +
228
+ "Please choose one of #{@answer_type.inspect}.",
229
+ :ask_on_error =>
230
+ "? ",
231
+ :invalid_type =>
232
+ "You must enter a valid #{@answer_type}.",
233
+ :no_completion =>
234
+ "You must choose one of " +
235
+ "#{@answer_type.inspect}.",
236
+ :not_in_range =>
237
+ "Your answer isn't within the expected range " +
238
+ "(#{expected_range}).",
239
+ :not_valid =>
240
+ "Your answer isn't valid (must match " +
241
+ "#{@validate.inspect})." }.merge(@responses)
242
+ ### WARNING: This code is quasi-duplicated in ###
243
+ ### Menu.update_responses(). Check there too when ###
244
+ ### making changes! ###
245
+ end
246
+
247
+ #
248
+ # Returns the provided _answer_string_ after changing character case by
249
+ # the rules of this Question. Valid settings for whitespace are:
250
+ #
251
+ # +nil+:: Do not alter character case.
252
+ # (Default.)
253
+ # <tt>:up</tt>:: Calls upcase().
254
+ # <tt>:upcase</tt>:: Calls upcase().
255
+ # <tt>:down</tt>:: Calls downcase().
256
+ # <tt>:downcase</tt>:: Calls downcase().
257
+ # <tt>:capitalize</tt>:: Calls capitalize().
258
+ #
259
+ # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
260
+ #
261
+ def change_case( answer_string )
262
+ if [:up, :upcase].include?(@case)
263
+ answer_string.upcase
264
+ elsif [:down, :downcase].include?(@case)
265
+ answer_string.downcase
266
+ elsif @case == :capitalize
267
+ answer_string.capitalize
268
+ else
269
+ answer_string
270
+ end
271
+ end
272
+
273
+ #
274
+ # Transforms the given _answer_string_ into the expected type for this
275
+ # Question. Currently supported conversions are:
276
+ #
277
+ # <tt>[...]</tt>:: Answer must be a member of the passed Array.
278
+ # Auto-completion is used to expand partial
279
+ # answers.
280
+ # <tt>lambda {...}</tt>:: Answer is passed to lambda for conversion.
281
+ # Date:: Date.parse() is called with answer.
282
+ # DateTime:: DateTime.parse() is called with answer.
283
+ # File:: The entered file name is auto-completed in
284
+ # terms of _directory_ + _glob_, opened, and
285
+ # returned.
286
+ # Float:: Answer is converted with Kernel.Float().
287
+ # Integer:: Answer is converted with Kernel.Integer().
288
+ # +nil+:: Answer is left in String format. (Default.)
289
+ # Pathname:: Same as File, save that a Pathname object is
290
+ # returned.
291
+ # String:: Answer is converted with Kernel.String().
292
+ # Regexp:: Answer is fed to Regexp.new().
293
+ # Symbol:: The method to_sym() is called on answer and
294
+ # the result returned.
295
+ # <i>any other Class</i>:: The answer is passed on to
296
+ # <tt>Class.parse()</tt>.
297
+ #
298
+ # This method throws ArgumentError, if the conversion cannot be
299
+ # completed for any reason.
300
+ #
301
+ def convert( answer_string )
302
+ if @answer_type.nil?
303
+ answer_string
304
+ elsif [Float, Integer, String].include?(@answer_type)
305
+ Kernel.send(@answer_type.to_s.to_sym, answer_string)
306
+ elsif @answer_type == Symbol
307
+ answer_string.to_sym
308
+ elsif @answer_type == Regexp
309
+ Regexp.new(answer_string)
310
+ elsif @answer_type.is_a?(Array) or [File, Pathname].include?(@answer_type)
311
+ # cheating, using OptionParser's Completion module
312
+ choices = selection
313
+ choices.extend(OptionParser::Completion)
314
+ answer = choices.complete(answer_string)
315
+ if answer.nil?
316
+ raise NoAutoCompleteMatch
317
+ end
318
+ if @answer_type.is_a?(Array)
319
+ answer.last
320
+ elsif @answer_type == File
321
+ File.open(File.join(@directory.to_s, answer.last))
322
+ else
323
+ Pathname.new(File.join(@directory.to_s, answer.last))
324
+ end
325
+ elsif [Date, DateTime].include?(@answer_type) or @answer_type.is_a?(Class)
326
+ @answer_type.parse(answer_string)
327
+ elsif @answer_type.is_a?(Proc)
328
+ @answer_type[answer_string]
329
+ end
330
+ end
331
+
332
+ # Returns a english explination of the current range settings.
333
+ def expected_range( )
334
+ expected = [ ]
335
+
336
+ expected << "above #{@above}" unless @above.nil?
337
+ expected << "below #{@below}" unless @below.nil?
338
+ expected << "included in #{@in.inspect}" unless @in.nil?
339
+
340
+ case expected.size
341
+ when 0 then ""
342
+ when 1 then expected.first
343
+ when 2 then expected.join(" and ")
344
+ else expected[0..-2].join(", ") + ", and #{expected.last}"
345
+ end
346
+ end
347
+
348
+ # Returns _first_answer_, which will be unset following this call.
349
+ def first_answer( )
350
+ @first_answer
351
+ ensure
352
+ @first_answer = nil
353
+ end
354
+
355
+ # Returns true if _first_answer_ is set.
356
+ def first_answer?( )
357
+ not @first_answer.nil?
358
+ end
359
+
360
+ #
361
+ # Returns +true+ if the _answer_object_ is greater than the _above_
362
+ # attribute, less than the _below_ attribute and included?()ed in the
363
+ # _in_ attribute. Otherwise, +false+ is returned. Any +nil+ attributes
364
+ # are not checked.
365
+ #
366
+ def in_range?( answer_object )
367
+ (@above.nil? or answer_object > @above) and
368
+ (@below.nil? or answer_object < @below) and
369
+ (@in.nil? or @in.include?(answer_object))
370
+ end
371
+
372
+ #
373
+ # Returns the provided _answer_string_ after processing whitespace by
374
+ # the rules of this Question. Valid settings for whitespace are:
375
+ #
376
+ # +nil+:: Do not alter whitespace.
377
+ # <tt>:strip</tt>:: Calls strip(). (Default.)
378
+ # <tt>:chomp</tt>:: Calls chomp().
379
+ # <tt>:collapse</tt>:: Collapses all whitspace runs to a
380
+ # single space.
381
+ # <tt>:strip_and_collapse</tt>:: Calls strip(), then collapses all
382
+ # whitspace runs to a single space.
383
+ # <tt>:chomp_and_collapse</tt>:: Calls chomp(), then collapses all
384
+ # whitspace runs to a single space.
385
+ # <tt>:remove</tt>:: Removes all whitespace.
386
+ #
387
+ # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
388
+ #
389
+ # This process is skipped, for single character input.
390
+ #
391
+ def remove_whitespace( answer_string )
392
+ if @whitespace.nil?
393
+ answer_string
394
+ elsif [:strip, :chomp].include?(@whitespace)
395
+ answer_string.send(@whitespace)
396
+ elsif @whitespace == :collapse
397
+ answer_string.gsub(/\s+/, " ")
398
+ elsif [:strip_and_collapse, :chomp_and_collapse].include?(@whitespace)
399
+ result = answer_string.send(@whitespace.to_s[/^[a-z]+/])
400
+ result.gsub(/\s+/, " ")
401
+ elsif @whitespace == :remove
402
+ answer_string.gsub(/\s+/, "")
403
+ else
404
+ answer_string
405
+ end
406
+ end
407
+
408
+ #
409
+ # Returns an Array of valid answers to this question. These answers are
410
+ # only known when _answer_type_ is set to an Array of choices, File, or
411
+ # Pathname. Any other time, this method will return an empty Array.
412
+ #
413
+ def selection( )
414
+ if @answer_type.is_a?(Array)
415
+ @answer_type
416
+ elsif [File, Pathname].include?(@answer_type)
417
+ Dir[File.join(@directory.to_s, @glob)].map do |file|
418
+ File.basename(file)
419
+ end
420
+ else
421
+ [ ]
422
+ end
423
+ end
424
+
425
+ # Stringifies the question to be asked.
426
+ def to_str( )
427
+ @question
428
+ end
429
+
430
+ #
431
+ # Returns +true+ if the provided _answer_string_ is accepted by the
432
+ # _validate_ attribute or +false+ if it's not.
433
+ #
434
+ # It's important to realize that an answer is validated after whitespace
435
+ # and case handling.
436
+ #
437
+ def valid_answer?( answer_string )
438
+ @validate.nil? or
439
+ (@validate.is_a?(Regexp) and answer_string =~ @validate) or
440
+ (@validate.is_a?(Proc) and @validate[answer_string])
441
+ end
442
+
443
+ private
444
+
445
+ #
446
+ # Adds the default choice to the end of question between <tt>|...|</tt>.
447
+ # Trailing whitespace is preserved so the function of HighLine.say() is
448
+ # not affected.
449
+ #
450
+ def append_default( )
451
+ if @question =~ /([\t ]+)\Z/
452
+ @question << "|#{@default}|#{$1}"
453
+ elsif @question == ""
454
+ @question << "|#{@default}| "
455
+ elsif @question[-1, 1] == "\n"
456
+ @question[-2, 0] = " |#{@default}|"
457
+ else
458
+ @question << " |#{@default}|"
459
+ end
460
+ end
461
+ end
462
+ end