rudy 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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