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,270 @@
1
+ #---
2
+ # TODO: Use Matrix to give a more accurate annoyance factor
3
+ # TODO: Add trivia questions
4
+ #+++
5
+
6
+ require 'timeout'
7
+ require 'sysinfo'
8
+ require 'highline'
9
+
10
+ # = Annoy
11
+ #
12
+ # Like your annoying friend that asks you questions all the time.
13
+ #
14
+ class Annoy
15
+
16
+ attr_accessor :factor
17
+ attr_accessor :flavor
18
+ attr_accessor :answer
19
+ attr_accessor :writer
20
+ attr_accessor :period
21
+ attr_accessor :system
22
+
23
+ @@operators = {
24
+ :low => %w(+ -),
25
+ :medium => %w(* -),
26
+ :high => %w(& * -),
27
+ :insane => %w(** << | & *)
28
+ }.freeze
29
+
30
+ @@strlen = {
31
+ :low => 2,
32
+ :medium => 3,
33
+ :high => 4,
34
+ :insane => 32
35
+ }.freeze
36
+
37
+ @@randsize = {
38
+ :low => 10,
39
+ :medium => 14,
40
+ :high => 50,
41
+ :insane => 1000
42
+ }.freeze
43
+
44
+ @@period = 60.freeze # max seconds to wait
45
+ @@flavors = [:numeric, :string].freeze
46
+ @@skip = false # skip questions
47
+
48
+ # Calling this method tells Annoy to not prompt for
49
+ # a response. All questions will return true.
50
+ def Annoy.enable_skip; @@skip = true; end
51
+ # Tells annoy to prompt for a response.
52
+ def Annoy.disable_skip; @@skip = false; end
53
+ # Returns true of Annoy is in skip mode
54
+ def Annoy.skip?; @@skip; end
55
+
56
+ # * +factor+ annoyance factor, one of :low (default), :medium, :high, :insane
57
+ # * +flavor+ annoyance flavor, one of :rand (default), :numeric, string
58
+ # * +writer+ an IO object to write to. Default: STDERR
59
+ # * +period+ the amount of time to wait in seconds. Default: 60
60
+ def initialize(opts={:factor=>:medium, :flavor=>:rand, :writer=>STDOUT, :period=>nil})
61
+ @factor = opts[:factor]
62
+ @flavor = Annoy.get_flavor(opts[:flavor])
63
+ @writer = opts[:writer]
64
+ @period = opts[:period] || @@period
65
+ unless Annoy.respond_to?("#{@flavor}_question")
66
+ raise "Hey, hey, hey. I don't know that flavor! (#{@flavor})"
67
+ end
68
+ end
69
+
70
+ # Generates and returns a question. The correct response is available
71
+ # as +@answer+.
72
+ def question
73
+ q, @answer =Annoy.question(@factor, @flavor)
74
+ q
75
+ end
76
+
77
+ # A wrapper for string_question and numberic_question
78
+ def Annoy.question(factor=:medium, flavor=:rand)
79
+ raise "Come on, you ruined the flavor!" unless flavor
80
+ Annoy.send("#{flavor}_question", factor)
81
+ end
82
+
83
+ # Generates a random string
84
+ def Annoy.string_question(factor=:medium)
85
+ # Strings don't need to be evaluated so the answer is the
86
+ # same as the question.
87
+ str = strand @@strlen[factor]
88
+ [str,str]
89
+ end
90
+
91
+ # * Generates a rudimentary numeric equation in the form: (Integer OPERATOR Integer).
92
+ # * Returns [equation, answer]
93
+ def Annoy.numeric_question(factor=:medium)
94
+ equation = answer = 0
95
+ while answer < 10
96
+ vals = [rand(@@randsize[factor])+1,
97
+ @@operators[factor][ rand(@@operators[factor].size) ],
98
+ rand(@@randsize[factor])+1 ]
99
+ equation = "(%d %s %d)" % vals
100
+ answer = eval(equation)
101
+ end
102
+ [equation, answer]
103
+ end
104
+
105
+ # Prints a question to +writer+ and waits for a response on STDIN.
106
+ # It checks whether STDIN is connected a tty so it doesn't block on gets
107
+ # when there's no human around to annoy. It will return <b>TRUE</b> when
108
+ # STDIN is NOT connected to a tty (when STDIN.tty? returns false).
109
+ # * +msg+ The message to print. Default: "Please confirm."
110
+ # Returns true when the answer is correct, otherwise false.
111
+ def Annoy.challenge?(msg="Please confirm.", factor=:medium, flavor=:rand, writer=STDOUT, period=nil)
112
+ return true unless STDIN.tty? # Humans only!
113
+ return true if Annoy.skip?
114
+ begin
115
+ success = Timeout::timeout(period || @@period) do
116
+ flavor = Annoy.get_flavor(flavor)
117
+ question, answer = Annoy.question(factor, flavor)
118
+ msg = "#{msg} To continue, #{Annoy.verb(flavor)} #{question}: "
119
+ #writer.print msg
120
+ #if ![:medium, :high, :insane].member?(factor) && flavor == :numeric
121
+ #writer.print "(#{answer}) "
122
+ #writer.flush
123
+ #end
124
+ #response = Annoy.get_response(writer)
125
+
126
+ trap("SIGINT") { raise Annoy::GiveUp }
127
+
128
+ highline = HighLine.new
129
+ response = highline.ask(msg) { |q|
130
+ q.echo = '*' # Don't display response
131
+ q.overwrite = true # Erase the question afterwards
132
+ q.whitespace = :strip # Remove whitespace from the response
133
+ q.answer_type = Integer if flavor == :numeric
134
+ }
135
+
136
+ ret = (response == answer)
137
+ writer.puts "Incorrect" unless ret
138
+ ret
139
+ end
140
+ rescue Annoy::GiveUp => ex
141
+ writer.puts $/, "Giving up!"
142
+ false
143
+ rescue Timeout::Error => ex
144
+ writer.puts $/, "Times up!"
145
+ false
146
+ end
147
+ end
148
+
149
+ # Runs a challenge with the message, "Are you sure?"
150
+ # See: Annoy.challenge?
151
+ def Annoy.are_you_sure?(factor=:medium, flavor=:rand, writer=STDOUT)
152
+ Annoy.challenge?("Are you sure?", factor, flavor, writer)
153
+ end
154
+
155
+ # Runs a challenge with the message, "Proceed?"
156
+ # See: Annoy.challenge?
157
+ def Annoy.proceed?(factor=:medium, flavor=:rand, writer=STDOUT)
158
+ Annoy.challenge?("Proceed?", factor, flavor, writer)
159
+ end
160
+
161
+ # See: Annoy.challenge?
162
+ # Uses the value of @flavor, @factor, and @writer
163
+ def challenge?(msg="Please confirm.")
164
+ Annoy.challenge?(msg, @factor, @flavor, @writer)
165
+ end
166
+
167
+ # See: Annoy.pose_question
168
+ # Uses the value of @writer
169
+ def pose_question(msg, regexp)
170
+ Annoy.pose_question(msg, regexp, @writer)
171
+ end
172
+
173
+ # Prints a question to writer and waits for a response on STDIN.
174
+ # It checks whether STDIN is connected a tty so it doesn't block on gets.
175
+ # when there's no human around to annoy. It will return <b>TRUE</b> when
176
+ # STDIN is NOT connected to a tty.
177
+ # * +msg+ The question to pose to the user
178
+ # * +regexp+ The regular expression to match the answer.
179
+ def Annoy.pose_question(msg, regexp, writer=STDOUT, period=nil)
180
+ return true unless STDIN.tty? # Only ask a question if there's a human
181
+ return true if Annoy.skip?
182
+ begin
183
+ success = Timeout::timeout(period || @@period) do
184
+ regexp &&= Regexp.new regexp
185
+ writer.print msg
186
+ writer.flush if writer.respond_to?(:flush)
187
+ response = Annoy.get_response
188
+ regexp.match(response)
189
+ end
190
+ rescue Timeout::Error => ex
191
+ writer.puts $/, "Times up!"
192
+ false
193
+ end
194
+ end
195
+
196
+
197
+ private
198
+ def Annoy.get_response(writer=STDOUT)
199
+ return true unless STDIN.tty? # Humans only
200
+ return true if Annoy.skip?
201
+ # TODO: Count the number of keystrokes to prevent copy/paste.
202
+ # We can probably use Highline.
203
+ # We likely need to be more specific but this will do for now.
204
+ #if ::SystemInfo.new.os == :unix
205
+ # begin
206
+ # response = []
207
+ # char = nil
208
+ # system("stty raw -echo") # Raw mode, no echo
209
+ # while char != "\r" || response.size > 5
210
+ # char = STDIN.getc.chr
211
+ # writer.print char
212
+ # writer.flush
213
+ # response << char
214
+ # end
215
+ # writer.print "\n\r"
216
+ # response = response.join('')
217
+ # rescue => ex
218
+ # ensure
219
+ # system("stty -raw echo") # Reset terminal mode
220
+ # end
221
+ #else
222
+ response = (STDIN.gets || "")
223
+ #end
224
+ response.chomp.gsub(/["']/, '')
225
+ end
226
+ # Returns a verb appropriate to the flavor.
227
+ # * :numeric => resolve
228
+ # * :string => type
229
+ def Annoy.verb(flavor)
230
+ case flavor
231
+ when :numeric then "resolve"
232
+ when :string then "type"
233
+ else
234
+ nil
235
+ end
236
+ end
237
+
238
+ #
239
+ # Generates a string of random alphanumeric characters.
240
+ # * +len+ is the length, an Integer. Default: 8
241
+ # * +safe+ in safe-mode, ambiguous characters are removed (default: true):
242
+ # i l o 1 0
243
+ def Annoy.strand( len=8, safe=true )
244
+ chars = ("a".."z").to_a + ("0".."9").to_a
245
+ chars.delete_if { |v| %w(i l o 1 0).member?(v) } if safe
246
+ str = ""
247
+ 1.upto(len) { |i| str << chars[rand(chars.size-1)] }
248
+ str
249
+ end
250
+
251
+ # * +f+ a prospective flavor name
252
+ def Annoy.get_flavor(f)
253
+ f.to_sym == :rand ? flavor_rand : f.to_sym
254
+ end
255
+
256
+ # Return a random flavor
257
+ def Annoy.flavor_rand
258
+ @@flavors[rand(@@flavors.size)]
259
+ end
260
+
261
+
262
+ end
263
+
264
+ # = Annoy::GiveUp
265
+ #
266
+ # This is what happens when you don't answer Annoy's questions.
267
+ class Annoy::GiveUp < RuntimeError
268
+ end
269
+
270
+
@@ -4,8 +4,15 @@
4
4
  # See: man terminfo
5
5
  #+++
6
6
 
7
-
7
+ #
8
+ #
9
+ #
8
10
  class String
11
+ @@print_with_attributes = true
12
+ def String.disable_colour; @@print_with_attributes = false; end
13
+ def String.disable_color; @@print_with_attributes = false; end
14
+ def String.enable_colour; @@print_with_attributes = true; end
15
+ def String.enable_color; @@print_with_attributes = true; end
9
16
 
10
17
  # +col+, +bgcol+, and +attribute+ are symbols corresponding
11
18
  # to Console::COLOURS, Console::BGCOLOURS, and Console::ATTRIBUTES.
@@ -14,6 +21,7 @@ class String
14
21
  # "MONKEY_JUNK".colour(:blue, :white, :blink) # => "\e[34;47;5mMONKEY_JUNK\e[39;49;0m"
15
22
  #
16
23
  def colour(col, bgcol = nil, attribute = nil)
24
+ return self unless @@print_with_attributes
17
25
  Console.style(col, bgcol, attribute) +
18
26
  self +
19
27
  Console.style(:default, :default, :default)
@@ -22,6 +30,7 @@ class String
22
30
 
23
31
  # See colour
24
32
  def bgcolour(bgcol = :default)
33
+ return self unless @@print_with_attributes
25
34
  Console.style(nil, bgcol, nil) +
26
35
  self +
27
36
  Console.style(nil, :default, nil)
@@ -30,11 +39,16 @@ class String
30
39
 
31
40
  # See colour
32
41
  def att(a = :default)
42
+ return self unless @@print_with_attributes
33
43
  Console.style(nil, nil, a) +
34
44
  self +
35
45
  Console.style(nil, nil, :default)
36
46
  end
37
47
 
48
+ # Shortcut for att(:bright)
49
+ def bright
50
+ att(:bright)
51
+ end
38
52
 
39
53
  # Print the string at +x+ +y+. When +minus+ is any true value
40
54
  # the length of the string is subtracted from the value of x
@@ -46,7 +60,8 @@ class String
46
60
  Console.print_at(self, args)
47
61
  end
48
62
 
49
- # Returns the string with escape code attributes removed.
63
+ # Returns the string with ANSI escape codes removed.
64
+ #
50
65
  # NOTE: The non-printable attributes count towards the string size.
51
66
  # You can use this method to get the "visible" size:
52
67
  #
@@ -54,11 +69,13 @@ class String
54
69
  # "\e[34;47;5mMONKEY_JUNK\e[39;49;0m".size # => 31
55
70
  #
56
71
  def noatt
57
- gsub(/\e\[[\d;]*m/, '')
72
+ gsub(/\e\[?[0-9;]*[mc]?/, '')
58
73
  end
74
+ alias :noansi :noatt
75
+
59
76
  end
60
77
 
61
- class Object
78
+ class Object #:nodoc:all
62
79
 
63
80
  # Executes tput +capnam+ with +args+. Returns true if tcap gives
64
81
  # 0 exit status and false otherwise.
@@ -82,7 +99,7 @@ end
82
99
 
83
100
 
84
101
 
85
- module Console
102
+ module Console #:nodoc:all
86
103
  extend self
87
104
  require 'timeout'
88
105
  require 'thread'
@@ -127,6 +144,10 @@ module Console
127
144
  :random => 40 + rand(10).to_i
128
145
  }.freeze unless defined? BGCOLOURS
129
146
 
147
+ def valid_colour?(colour)
148
+ COLOURS.has_key? colour
149
+ end
150
+ alias :valid_color? :valid_colour?
130
151
 
131
152
  def print_left(str, props={})
132
153
  props[:x] ||= 0
@@ -159,8 +180,8 @@ module Console
159
180
  end
160
181
  def print_at(str, props={})
161
182
  print_at_lamb = lambda {
162
- #props[:x] ||= 0
163
- #props[:y] ||= 0
183
+ props[:x] ||= 0
184
+ props[:y] ||= 0
164
185
  props[:minus] = false unless props.has_key?(:minus)
165
186
  props[:x] = props[:x]-str.noatt.size if props[:x] && props[:minus] # Subtract the str length from the position
166
187
  Cursor.save
@@ -196,7 +217,7 @@ module Console
196
217
  end
197
218
  end
198
219
 
199
- module Cursor
220
+ module Cursor #:nodoc:all
200
221
  extend self
201
222
 
202
223
  # Returns [x,y] for the current cursor position.
@@ -291,7 +312,7 @@ module Cursor
291
312
 
292
313
  end
293
314
 
294
- class Window
315
+ class Window #:nodoc:all
295
316
  attr_accessor :row, :col, :width, :height, :text, :fg, :bg
296
317
  attr_reader :threads
297
318
 
@@ -0,0 +1,305 @@
1
+ # escape.rb - escape/unescape library for several formats
2
+ #
3
+ # Copyright (C) 2006,2007 Tanaka Akira <akr@fsij.org>
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice, this
9
+ # list of conditions and the following disclaimer.
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote products
14
+ # derived from this software without specific prior written permission.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
21
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
24
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
25
+ # OF SUCH DAMAGE.
26
+
27
+ # Escape module provides several escape functions.
28
+ # * URI
29
+ # * HTML
30
+ # * shell command
31
+ module Escape #:nodoc:all
32
+ module_function
33
+
34
+ class StringWrapper
35
+ class << self
36
+ alias new_no_dup new
37
+ def new(str)
38
+ new_no_dup(str.dup)
39
+ end
40
+ end
41
+
42
+ def initialize(str)
43
+ @str = str
44
+ end
45
+
46
+ def to_s
47
+ @str.dup
48
+ end
49
+
50
+ def inspect
51
+ "\#<#{self.class}: #{@str}>"
52
+ end
53
+
54
+ def ==(other)
55
+ other.class == self.class && @str == other.instance_variable_get(:@str)
56
+ end
57
+ alias eql? ==
58
+
59
+ def hash
60
+ @str.hash
61
+ end
62
+ end
63
+
64
+ class ShellEscaped < StringWrapper
65
+ end
66
+
67
+ # Escape.shell_command composes
68
+ # a sequence of words to
69
+ # a single shell command line.
70
+ # All shell meta characters are quoted and
71
+ # the words are concatenated with interleaving space.
72
+ # It returns an instance of ShellEscaped.
73
+ #
74
+ # Escape.shell_command(["ls", "/"]) #=> #<Escape::ShellEscaped: ls />
75
+ # Escape.shell_command(["echo", "*"]) #=> #<Escape::ShellEscaped: echo '*'>
76
+ #
77
+ # Note that system(*command) and
78
+ # system(Escape.shell_command(command)) is roughly same.
79
+ # There are two exception as follows.
80
+ # * The first is that the later may invokes /bin/sh.
81
+ # * The second is an interpretation of an array with only one element:
82
+ # the element is parsed by the shell with the former but
83
+ # it is recognized as single word with the later.
84
+ # For example, system(*["echo foo"]) invokes echo command with an argument "foo".
85
+ # But system(Escape.shell_command(["echo foo"])) invokes "echo foo" command without arguments (and it probably fails).
86
+ def shell_command(*command)
87
+ command = [command].flatten.compact # Delano
88
+ s = command.map {|word| shell_single_word(word) }.join(' ')
89
+ ShellEscaped.new_no_dup(s)
90
+ end
91
+
92
+ # Escape.shell_single_word quotes shell meta characters.
93
+ # It returns an instance of ShellEscaped.
94
+ #
95
+ # The result string is always single shell word, even if
96
+ # the argument is "".
97
+ # Escape.shell_single_word("") returns #<Escape::ShellEscaped: ''>.
98
+ #
99
+ # Escape.shell_single_word("") #=> #<Escape::ShellEscaped: ''>
100
+ # Escape.shell_single_word("foo") #=> #<Escape::ShellEscaped: foo>
101
+ # Escape.shell_single_word("*") #=> #<Escape::ShellEscaped: '*'>
102
+ def shell_single_word(str)
103
+ return unless str
104
+ str &&= str.to_s # Delano fix
105
+ if str.empty?
106
+ ShellEscaped.new_no_dup("''")
107
+ elsif %r{\A[0-9A-Za-z+,./:=@_-]+\z} =~ str
108
+ ShellEscaped.new(str)
109
+ else
110
+ result = ''
111
+ str.scan(/('+)|[^']+/) {
112
+ if $1
113
+ result << %q{\'} * $1.length
114
+ else
115
+ result << "'#{$&}'"
116
+ end
117
+ }
118
+ ShellEscaped.new_no_dup(result)
119
+ end
120
+ end
121
+
122
+ class PercentEncoded < StringWrapper
123
+ end
124
+
125
+ # Escape.uri_segment escapes URI segment using percent-encoding.
126
+ # It returns an instance of PercentEncoded.
127
+ #
128
+ # Escape.uri_segment("a/b") #=> #<Escape::PercentEncoded: a%2Fb>
129
+ #
130
+ # The segment is "/"-splitted element after authority before query in URI, as follows.
131
+ #
132
+ # scheme://authority/segment1/segment2/.../segmentN?query#fragment
133
+ #
134
+ # See RFC 3986 for details of URI.
135
+ def uri_segment(str)
136
+ # pchar - pct-encoded = unreserved / sub-delims / ":" / "@"
137
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
138
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
139
+ s = str.gsub(%r{[^A-Za-z0-9\-._~!$&'()*+,;=:@]}n) {
140
+ '%' + $&.unpack("H2")[0].upcase
141
+ }
142
+ PercentEncoded.new_no_dup(s)
143
+ end
144
+
145
+ # Escape.uri_path escapes URI path using percent-encoding.
146
+ # The given path should be a sequence of (non-escaped) segments separated by "/".
147
+ # The segments cannot contains "/".
148
+ # It returns an instance of PercentEncoded.
149
+ #
150
+ # Escape.uri_path("a/b/c") #=> #<Escape::PercentEncoded: a/b/c>
151
+ # Escape.uri_path("a?b/c?d/e?f") #=> #<Escape::PercentEncoded: a%3Fb/c%3Fd/e%3Ff>
152
+ #
153
+ # The path is the part after authority before query in URI, as follows.
154
+ #
155
+ # scheme://authority/path#fragment
156
+ #
157
+ # See RFC 3986 for details of URI.
158
+ #
159
+ # Note that this function is not appropriate to convert OS path to URI.
160
+ def uri_path(str)
161
+ s = str.gsub(%r{[^/]+}n) { uri_segment($&) }
162
+ PercentEncoded.new_no_dup(s)
163
+ end
164
+
165
+
166
+ def html_form_fast(pairs, sep='&')
167
+ s = pairs.map {|k, v|
168
+ # query-chars - pct-encoded - x-www-form-urlencoded-delimiters =
169
+ # unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
170
+ # query-char - pct-encoded = unreserved / sub-delims / ":" / "@" / "/" / "?"
171
+ # query-char = pchar / "/" / "?" = unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?"
172
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
173
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
174
+ # x-www-form-urlencoded-delimiters = "&" / "+" / ";" / "="
175
+ k = k.gsub(%r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n) {
176
+ '%' + $&.unpack("H2")[0].upcase
177
+ }
178
+ v = v.gsub(%r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n) {
179
+ '%' + $&.unpack("H2")[0].upcase
180
+ }
181
+ "#{k}=#{v}"
182
+ }.join(sep)
183
+ PercentEncoded.new_no_dup(s)
184
+ end
185
+
186
+
187
+ # Escape.html_form composes HTML form key-value pairs as a x-www-form-urlencoded encoded string.
188
+ # It returns an instance of PercentEncoded.
189
+ #
190
+ # Escape.html_form takes an array of pair of strings or
191
+ # an hash from string to string.
192
+ #
193
+ # Escape.html_form([["a","b"], ["c","d"]]) #=> #<Escape::PercentEncoded: a=b&c=d>
194
+ # Escape.html_form({"a"=>"b", "c"=>"d"}) #=> #<Escape::PercentEncoded: a=b&c=d>
195
+ #
196
+ # In the array form, it is possible to use same key more than once.
197
+ # (It is required for a HTML form which contains
198
+ # checkboxes and select element with multiple attribute.)
199
+ #
200
+ # Escape.html_form([["k","1"], ["k","2"]]) #=> #<Escape::PercentEncoded: k=1&k=2>
201
+ #
202
+ # If the strings contains characters which must be escaped in x-www-form-urlencoded,
203
+ # they are escaped using %-encoding.
204
+ #
205
+ # Escape.html_form([["k=","&;="]]) #=> #<Escape::PercentEncoded: k%3D=%26%3B%3D>
206
+ #
207
+ # The separator can be specified by the optional second argument.
208
+ #
209
+ # Escape.html_form([["a","b"], ["c","d"]], ";") #=> #<Escape::PercentEncoded: a=b;c=d>
210
+ #
211
+ # See HTML 4.01 for details.
212
+ def html_form(pairs, sep='&')
213
+ r = ''
214
+ first = true
215
+ pairs.each {|k, v|
216
+ # query-chars - pct-encoded - x-www-form-urlencoded-delimiters =
217
+ # unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
218
+ # query-char - pct-encoded = unreserved / sub-delims / ":" / "@" / "/" / "?"
219
+ # query-char = pchar / "/" / "?" = unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?"
220
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
221
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
222
+ # x-www-form-urlencoded-delimiters = "&" / "+" / ";" / "="
223
+ r << sep if !first
224
+ first = false
225
+ k.each_byte {|byte|
226
+ ch = byte.chr
227
+ if %r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n =~ ch
228
+ r << "%" << ch.unpack("H2")[0].upcase
229
+ else
230
+ r << ch
231
+ end
232
+ }
233
+ r << '='
234
+ v.each_byte {|byte|
235
+ ch = byte.chr
236
+ if %r{[^0-9A-Za-z\-\._~:/?@!\$'()*,]}n =~ ch
237
+ r << "%" << ch.unpack("H2")[0].upcase
238
+ else
239
+ r << ch
240
+ end
241
+ }
242
+ }
243
+ PercentEncoded.new_no_dup(r)
244
+ end
245
+
246
+ class HTMLEscaped < StringWrapper
247
+ end
248
+
249
+
250
+ HTML_TEXT_ESCAPE_HASH = {
251
+ '&' => '&amp;',
252
+ '<' => '&lt;',
253
+ '>' => '&gt;',
254
+ }
255
+
256
+
257
+ # Escape.html_text escapes a string appropriate for HTML text using character references.
258
+ # It returns an instance of HTMLEscaped.
259
+ #
260
+ # It escapes 3 characters:
261
+ # * '&' to '&amp;'
262
+ # * '<' to '&lt;'
263
+ # * '>' to '&gt;'
264
+ #
265
+ # Escape.html_text("abc") #=> #<Escape::HTMLEscaped: abc>
266
+ # Escape.html_text("a & b < c > d") #=> #<Escape::HTMLEscaped: a &amp; b &lt; c &gt; d>
267
+ #
268
+ # This function is not appropriate for escaping HTML element attribute
269
+ # because quotes are not escaped.
270
+ def html_text(str)
271
+ s = str.gsub(/[&<>]/) {|ch| HTML_TEXT_ESCAPE_HASH[ch] }
272
+ HTMLEscaped.new_no_dup(s)
273
+ end
274
+
275
+
276
+ HTML_ATTR_ESCAPE_HASH = {
277
+ '&' => '&amp;',
278
+ '<' => '&lt;',
279
+ '>' => '&gt;',
280
+ '"' => '&quot;',
281
+ }
282
+
283
+
284
+ class HTMLAttrValue < StringWrapper
285
+ end
286
+
287
+ # Escape.html_attr_value encodes a string as a double-quoted HTML attribute using character references.
288
+ # It returns an instance of HTMLAttrValue.
289
+ #
290
+ # Escape.html_attr_value("abc") #=> #<Escape::HTMLAttrValue: "abc">
291
+ # Escape.html_attr_value("a&b") #=> #<Escape::HTMLAttrValue: "a&amp;b">
292
+ # Escape.html_attr_value("ab&<>\"c") #=> #<Escape::HTMLAttrValue: "ab&amp;&lt;&gt;&quot;c">
293
+ # Escape.html_attr_value("a'c") #=> #<Escape::HTMLAttrValue: "a'c">
294
+ #
295
+ # It escapes 4 characters:
296
+ # * '&' to '&amp;'
297
+ # * '<' to '&lt;'
298
+ # * '>' to '&gt;'
299
+ # * '"' to '&quot;'
300
+ #
301
+ def html_attr_value(str)
302
+ s = '"' + str.gsub(/[&<>"]/) {|ch| HTML_ATTR_ESCAPE_HASH[ch] } + '"'
303
+ HTMLAttrValue.new_no_dup(s)
304
+ end
305
+ end