keybox 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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