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