linecook 0.6.2 → 1.0.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.
- data/History +139 -0
- data/HowTo/Control Virtual Machines +106 -0
- data/HowTo/Generate Scripts +263 -0
- data/HowTo/Run Scripts +87 -0
- data/HowTo/Setup Virtual Machines +76 -0
- data/License.txt +1 -1
- data/README +78 -59
- data/bin/linecook +12 -5
- data/bin/linecook_run +45 -0
- data/bin/linecook_scp +50 -0
- data/lib/linecook.rb +1 -3
- data/lib/linecook/attributes.rb +49 -12
- data/lib/linecook/commands.rb +9 -4
- data/lib/linecook/commands/build.rb +69 -0
- data/lib/linecook/commands/command.rb +13 -3
- data/lib/linecook/commands/command_error.rb +6 -0
- data/lib/linecook/commands/env.rb +74 -8
- data/lib/linecook/commands/helper.rb +271 -24
- data/lib/linecook/commands/init.rb +10 -6
- data/lib/linecook/commands/package.rb +36 -18
- data/lib/linecook/commands/run.rb +66 -0
- data/lib/linecook/commands/snapshot.rb +114 -0
- data/lib/linecook/commands/ssh.rb +39 -0
- data/lib/linecook/commands/start.rb +34 -0
- data/lib/linecook/commands/state.rb +32 -0
- data/lib/linecook/commands/stop.rb +22 -0
- data/lib/linecook/commands/vbox_command.rb +130 -0
- data/lib/linecook/cookbook.rb +112 -55
- data/lib/linecook/package.rb +293 -109
- data/lib/linecook/proxy.rb +19 -0
- data/lib/linecook/recipe.rb +321 -62
- data/lib/linecook/template.rb +7 -101
- data/lib/linecook/test.rb +196 -141
- data/lib/linecook/test/command_parser.rb +75 -0
- data/lib/linecook/test/file_test.rb +153 -35
- data/lib/linecook/test/shell_test.rb +176 -0
- data/lib/linecook/utils.rb +25 -7
- data/lib/linecook/version.rb +4 -4
- data/templates/Rakefile +44 -47
- data/templates/_gitignore +1 -1
- data/templates/attributes/project_name.rb +4 -4
- data/templates/config/ssh +15 -0
- data/templates/files/help.txt +1 -0
- data/templates/helpers/project_name/assert_content_equal.erb +15 -0
- data/templates/helpers/project_name/create_dir.erb +9 -0
- data/templates/helpers/project_name/create_file.erb +8 -0
- data/templates/helpers/project_name/install_file.erb +8 -0
- data/templates/packages/abox.yml +4 -0
- data/templates/recipes/abox.rb +22 -0
- data/templates/recipes/abox_test.rb +14 -0
- data/templates/templates/todo.txt.erb +3 -0
- data/templates/test/project_name_test.rb +19 -0
- data/templates/test/test_helper.rb +14 -0
- metadata +43 -41
- data/cookbook +0 -0
- data/lib/linecook/commands/helpers.rb +0 -28
- data/lib/linecook/commands/vbox.rb +0 -85
- data/lib/linecook/helper.rb +0 -117
- data/lib/linecook/shell.rb +0 -11
- data/lib/linecook/shell/posix.rb +0 -145
- data/lib/linecook/shell/test.rb +0 -254
- data/lib/linecook/shell/unix.rb +0 -117
- data/lib/linecook/shell/utils.rb +0 -138
- data/templates/README +0 -90
- data/templates/files/file.txt +0 -1
- data/templates/helpers/project_name/echo.erb +0 -5
- data/templates/recipes/project_name.rb +0 -20
- data/templates/scripts/project_name.yml +0 -7
- data/templates/templates/template.txt.erb +0 -3
- data/templates/vbox/setup/virtual_box +0 -86
- data/templates/vbox/ssh/id_rsa +0 -27
- data/templates/vbox/ssh/id_rsa.pub +0 -1
@@ -0,0 +1,19 @@
|
|
1
|
+
module Linecook
|
2
|
+
# A proxy used to chain method calls back to a recipe.
|
3
|
+
class Proxy
|
4
|
+
def initialize(recipe)
|
5
|
+
@recipe = recipe
|
6
|
+
end
|
7
|
+
|
8
|
+
# Proxies to recipe.chain.
|
9
|
+
def method_missing(*args, &block)
|
10
|
+
@recipe.chain(*args, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns an empty string, such that the proxy makes no text when it is
|
14
|
+
# accidentally put into a target by a helper.
|
15
|
+
def to_s
|
16
|
+
''
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/linecook/recipe.rb
CHANGED
@@ -1,103 +1,362 @@
|
|
1
|
-
require 'linecook/template'
|
2
1
|
require 'linecook/attributes'
|
3
|
-
require 'linecook/
|
2
|
+
require 'linecook/proxy'
|
4
3
|
require 'linecook/utils'
|
4
|
+
require 'erb'
|
5
|
+
require 'stringio'
|
5
6
|
|
6
7
|
module Linecook
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
8
|
+
# Recipe is the context in which recipes are evaluated (literally). Recipe
|
9
|
+
# uses compiled ERB snippets to build text using method calls. For example:
|
10
|
+
#
|
11
|
+
# module Helper
|
12
|
+
# # This is an ERB template compiled to write to a Recipe.
|
13
|
+
# #
|
14
|
+
# # compiler = ERB::Compiler.new('<>')
|
15
|
+
# # compiler.put_cmd = "write"
|
16
|
+
# # compiler.insert_cmd = "write"
|
17
|
+
# # compiler.compile("echo '<%= args.join(' ') %>'\n")
|
18
|
+
# #
|
19
|
+
# def echo(*args)
|
20
|
+
# write "echo '"; write(( args.join(' ') ).to_s); write "'\n"
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# package = Package.new
|
25
|
+
# recipe = package.setup_recipe
|
26
|
+
#
|
27
|
+
# recipe.extend Helper
|
28
|
+
# recipe.instance_eval do
|
29
|
+
# echo 'a', 'b c'
|
30
|
+
# echo 'X Y'.downcase, :z
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# "\n" + recipe.result
|
34
|
+
# # => %{
|
35
|
+
# # echo 'a b c'
|
36
|
+
# # echo 'x y z'
|
37
|
+
# # }
|
38
|
+
#
|
39
|
+
class Recipe
|
40
|
+
|
41
|
+
# The recipe package
|
42
|
+
attr_reader :_package_
|
43
|
+
|
44
|
+
# The recipe target
|
45
|
+
attr_reader :_target_
|
46
|
+
|
47
|
+
# The target name of self in package
|
48
|
+
attr_reader :_target_name_
|
49
|
+
|
50
|
+
# The recipe proxy
|
51
|
+
attr_reader :_proxy_
|
20
52
|
|
21
|
-
|
53
|
+
# The current target for self set as needed during captures; equal to
|
54
|
+
# _target_ otherwise.
|
55
|
+
attr_reader :target
|
22
56
|
|
57
|
+
# The current target_name for self set as needed during captures; equal to
|
58
|
+
# _target_name_ otherwise.
|
23
59
|
attr_reader :target_name
|
24
60
|
|
25
|
-
def initialize(target_name,
|
26
|
-
@
|
27
|
-
@
|
28
|
-
@
|
29
|
-
@
|
61
|
+
def initialize(package, target_name, mode)
|
62
|
+
@_package_ = package
|
63
|
+
@_target_name_ = target_name
|
64
|
+
@_target_ = package.setup_tempfile(target_name, mode)
|
65
|
+
@_proxy_ = Proxy.new(self)
|
66
|
+
|
67
|
+
@target_name = @_target_name_
|
68
|
+
@target = @_target_
|
69
|
+
|
70
|
+
@chain = false
|
71
|
+
@attributes = {}
|
72
|
+
@indents = []
|
73
|
+
@outdents = []
|
30
74
|
end
|
31
75
|
|
32
|
-
|
33
|
-
|
34
|
-
|
76
|
+
# Closes _target_ and returns self.
|
77
|
+
def close
|
78
|
+
unless closed?
|
79
|
+
_target_.close
|
80
|
+
end
|
81
|
+
self
|
35
82
|
end
|
36
83
|
|
37
|
-
|
38
|
-
|
39
|
-
|
84
|
+
# Returns true if _target_ is closed.
|
85
|
+
def closed?
|
86
|
+
_target_.closed?
|
40
87
|
end
|
41
88
|
|
42
|
-
|
43
|
-
|
89
|
+
# Returns the current contents of target, or the contents of _target_ if
|
90
|
+
# closed? is true.
|
91
|
+
def result
|
92
|
+
if closed?
|
93
|
+
_package_.content(_target_name_)
|
94
|
+
else
|
95
|
+
target.flush
|
96
|
+
target.rewind
|
97
|
+
target.read
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Loads the specified attributes file and merges the results into attrs. A
|
102
|
+
# block may be given to specify attrs as well; it will be evaluated in the
|
103
|
+
# context of an Attributes instance.
|
104
|
+
def attributes(attributes_name=nil, &block)
|
105
|
+
attributes = _package_.load_attributes(attributes_name)
|
44
106
|
|
45
|
-
|
46
|
-
|
107
|
+
if block_given?
|
108
|
+
attributes.instance_eval(&block)
|
109
|
+
end
|
47
110
|
|
48
|
-
|
111
|
+
@attributes = Utils.deep_merge(@attributes, attributes.to_hash)
|
112
|
+
@attrs = nil
|
113
|
+
self
|
49
114
|
end
|
50
115
|
|
116
|
+
# Returns the package env merged over any attrs specified by attributes.
|
117
|
+
# The attrs hash should be treated as if it were read-only because changes
|
118
|
+
# could alter the package env and thereby spill over into other recipes.
|
51
119
|
def attrs
|
52
|
-
@attributes.
|
120
|
+
@attrs ||= Utils.deep_merge(@attributes, _package_.env)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Looks up and extends self with the specified helper.
|
124
|
+
def helpers(helper_name)
|
125
|
+
extend _package_.load_helper(helper_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns an expression that evaluates to the package dir, assuming that
|
129
|
+
# $0 evaluates to the full path to the current recipe.
|
130
|
+
def package_dir
|
131
|
+
"${0%/#{target_name}}"
|
132
|
+
end
|
133
|
+
|
134
|
+
# The path to the named target as it should be referenced in the final
|
135
|
+
# script, specifically target_name joined to package_dir.
|
136
|
+
def target_path(target_name)
|
137
|
+
File.join(package_dir, target_name)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Registers the specified file into package and returns the target_path to
|
141
|
+
# the file.
|
142
|
+
def file_path(file_name, target_name=file_name, mode=0600)
|
143
|
+
_package_.build_file(target_name, file_name, mode)
|
144
|
+
target_path target_name
|
145
|
+
end
|
146
|
+
|
147
|
+
# Looks up, builds, and registers the specified template and returns the
|
148
|
+
# target_path to the resulting file.
|
149
|
+
def template_path(template_name, target_name=template_name, mode=0600, locals={'attrs' => attrs})
|
150
|
+
_package_.build_template(target_name, template_name, mode, locals)
|
151
|
+
target_path target_name
|
152
|
+
end
|
153
|
+
|
154
|
+
# Looks up, builds, and registers the specified recipe and returns the
|
155
|
+
# target_path to the resulting file.
|
156
|
+
def recipe_path(recipe_name, target_name=recipe_name, mode=0700)
|
157
|
+
_package_.build_recipe(target_name, recipe_name, mode)
|
158
|
+
target_path target_name
|
53
159
|
end
|
54
160
|
|
55
|
-
|
56
|
-
|
161
|
+
# Captures the output for a block, registers it, and returns the
|
162
|
+
# target_path to the resulting file. The current target and target_name
|
163
|
+
# are updated for the duration of the block.
|
164
|
+
def capture_path(target_name, mode=0600)
|
165
|
+
tempfile = _package_.setup_tempfile(target_name, mode)
|
166
|
+
capture_block(tempfile) do
|
167
|
+
current = @target_name
|
168
|
+
|
169
|
+
begin
|
170
|
+
@target_name = target_name
|
171
|
+
yield
|
172
|
+
ensure
|
173
|
+
@target_name = current
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
tempfile.close
|
57
178
|
|
58
|
-
|
59
|
-
@attributes.reset(false)
|
60
|
-
self
|
179
|
+
target_path target_name
|
61
180
|
end
|
62
181
|
|
63
|
-
|
64
|
-
|
65
|
-
|
182
|
+
# Captures output to the target for the duration of a block. Returns the
|
183
|
+
# capture target.
|
184
|
+
def capture_block(target=StringIO.new)
|
185
|
+
current = @target
|
186
|
+
|
187
|
+
begin
|
188
|
+
@target = target
|
189
|
+
yield
|
190
|
+
ensure
|
191
|
+
@target = current
|
192
|
+
end
|
193
|
+
|
194
|
+
target
|
195
|
+
end
|
196
|
+
|
197
|
+
# Captures and returns output for the duration of a block by redirecting
|
198
|
+
# target to a temporary buffer.
|
199
|
+
def capture_str
|
200
|
+
capture_block { yield }.string
|
201
|
+
end
|
202
|
+
|
203
|
+
# Writes input to target using 'write'. Returns self.
|
204
|
+
def write(input)
|
205
|
+
target.write input
|
206
|
+
self
|
66
207
|
end
|
67
208
|
|
68
|
-
|
69
|
-
|
70
|
-
|
209
|
+
# Writes input to target using 'puts'. Returns self.
|
210
|
+
def writeln(input)
|
211
|
+
target.puts input
|
71
212
|
self
|
72
213
|
end
|
73
214
|
|
74
|
-
|
75
|
-
|
76
|
-
|
215
|
+
# Truncates the contents of target starting at the first match of pattern
|
216
|
+
# and returns the resulting match data. If a block is given then rewrite
|
217
|
+
# yields the match data to the block and returns the block result.
|
218
|
+
#
|
219
|
+
# ==== Notes
|
220
|
+
#
|
221
|
+
# Rewrites can be computationally expensive because they require the
|
222
|
+
# current target to be flushed, rewound, and read in it's entirety. In
|
223
|
+
# practice the performance of rewrite is almost never an issue because
|
224
|
+
# recipe output is usually small in size.
|
225
|
+
#
|
226
|
+
# If performance becomes an issue, then wrap the rewritten bits in a
|
227
|
+
# capture block to reassign the current target to a StringIO (which is
|
228
|
+
# much faster to rewrite), and to limit the scope of the rewritten text.
|
229
|
+
def rewrite(pattern)
|
230
|
+
if match = pattern.match(result)
|
231
|
+
start = match.begin(0)
|
232
|
+
target.pos = start
|
233
|
+
target.truncate start
|
234
|
+
end
|
235
|
+
|
236
|
+
block_given? ? yield(match) : match
|
77
237
|
end
|
78
238
|
|
79
|
-
|
80
|
-
|
81
|
-
|
239
|
+
# Strips whitespace from the end of target and returns the stripped
|
240
|
+
# whitespace, or an empty string if no whitespace is available.
|
241
|
+
def rstrip
|
242
|
+
match = rewrite(/\s+\z/)
|
243
|
+
match ? match[0] : ''
|
82
244
|
end
|
83
245
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
246
|
+
# Indents the output of the block. Indents may be nested. To prevent a
|
247
|
+
# section from being indented, enclose it within outdent which resets
|
248
|
+
# indentation to nothing for the duration of a block.
|
249
|
+
#
|
250
|
+
# Example:
|
251
|
+
#
|
252
|
+
# writeln 'a'
|
253
|
+
# indent do
|
254
|
+
# writeln 'b'
|
255
|
+
# outdent do
|
256
|
+
# writeln 'c'
|
257
|
+
# indent do
|
258
|
+
# writeln 'd'
|
259
|
+
# end
|
260
|
+
# writeln 'c'
|
261
|
+
# end
|
262
|
+
# writeln 'b'
|
263
|
+
# end
|
264
|
+
# writeln 'a'
|
265
|
+
#
|
266
|
+
# "\n" + result
|
267
|
+
# # => %q{
|
268
|
+
# # a
|
269
|
+
# # b
|
270
|
+
# # c
|
271
|
+
# # d
|
272
|
+
# # c
|
273
|
+
# # b
|
274
|
+
# # a
|
275
|
+
# # }
|
276
|
+
#
|
277
|
+
def indent(indent=' ')
|
278
|
+
@indents << @indents.last.to_s + indent
|
279
|
+
str = capture_str { yield }
|
280
|
+
@indents.pop
|
281
|
+
|
282
|
+
unless str.empty?
|
283
|
+
str.gsub!(/^/, indent)
|
284
|
+
|
285
|
+
if @indents.empty?
|
286
|
+
@outdents.each do |flag|
|
287
|
+
str.gsub!(/#{flag}(\d+):(.*?)#{flag}/m) do
|
288
|
+
$2.gsub!(/^.{#{$1.to_i}}/, '')
|
289
|
+
end
|
290
|
+
end
|
291
|
+
@outdents.clear
|
292
|
+
end
|
293
|
+
|
294
|
+
writeln str
|
295
|
+
end
|
296
|
+
|
297
|
+
self
|
298
|
+
end
|
299
|
+
|
300
|
+
# Resets indentation to nothing for a section of text indented by indent.
|
301
|
+
#
|
302
|
+
# === Notes
|
303
|
+
#
|
304
|
+
# Outdent works by setting a text flag around the outdented section; the
|
305
|
+
# flag and indentation is later stripped out using regexps. For that
|
306
|
+
# reason, be sure flag is not something that will appear anywhere else in
|
307
|
+
# the section.
|
308
|
+
#
|
309
|
+
# The default flag is like ':outdent_N:' where N is a big random number.
|
310
|
+
def outdent(flag=nil)
|
311
|
+
current_indent = @indents.last
|
312
|
+
|
313
|
+
if current_indent.nil?
|
314
|
+
yield
|
315
|
+
else
|
316
|
+
flag ||= ":outdent_#{rand(10000000)}:"
|
317
|
+
@outdents << flag
|
318
|
+
|
319
|
+
write "#{flag}#{current_indent.length}:#{rstrip}"
|
320
|
+
@indents << ''
|
321
|
+
|
322
|
+
yield
|
323
|
+
|
324
|
+
@indents.pop
|
325
|
+
|
326
|
+
write "#{flag}#{rstrip}"
|
327
|
+
end
|
89
328
|
|
90
|
-
|
329
|
+
self
|
91
330
|
end
|
92
331
|
|
93
|
-
|
94
|
-
|
95
|
-
|
332
|
+
# Sets chain? to true and calls the method (thereby allowing the method to
|
333
|
+
# invoke chain-specific behavior). Chain is invoked via the chain_proxy
|
334
|
+
# which is returned by helper methods.
|
335
|
+
def chain(method_name, *args, &block)
|
336
|
+
@chain = true
|
337
|
+
send(method_name, *args, &block)
|
96
338
|
end
|
97
339
|
|
98
|
-
|
99
|
-
|
100
|
-
@
|
340
|
+
# Returns true if the current context was invoked through chain.
|
341
|
+
def chain?
|
342
|
+
@chain
|
343
|
+
end
|
344
|
+
|
345
|
+
# Sets chain to false and returns the proxy.
|
346
|
+
def chain_proxy
|
347
|
+
@chain = false
|
348
|
+
_proxy_
|
349
|
+
end
|
350
|
+
|
351
|
+
# Captures a block of output and concats to the named callback.
|
352
|
+
def callback(name)
|
353
|
+
target = _package_.callbacks[name]
|
354
|
+
capture_block(target) { yield }
|
355
|
+
end
|
356
|
+
|
357
|
+
# Writes the specified callback to the current target.
|
358
|
+
def write_callback(name)
|
359
|
+
write _package_.callbacks[name].string
|
101
360
|
end
|
102
361
|
end
|
103
362
|
end
|
data/lib/linecook/template.rb
CHANGED
@@ -1,111 +1,17 @@
|
|
1
|
-
require 'ostruct'
|
2
|
-
require 'stringio'
|
3
1
|
require 'erb'
|
2
|
+
require 'ostruct'
|
4
3
|
|
5
4
|
module Linecook
|
6
5
|
class Template
|
7
|
-
|
8
|
-
def build(template, locals, template_path=nil)
|
9
|
-
ERB.new(template).result(OpenStruct.new(locals).send(:binding))
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
attr_reader :erbout
|
14
|
-
|
15
|
-
def initialize
|
16
|
-
@erbout = StringIO.new
|
17
|
-
end
|
18
|
-
|
19
|
-
# Returns self (not the underlying erbout storage that actually receives
|
20
|
-
# the output lines). In the ERB context, this method directs erb outputs
|
21
|
-
# to Template#concat and into the redirect mechanism.
|
22
|
-
def _erbout
|
23
|
-
self
|
24
|
-
end
|
25
|
-
|
26
|
-
# Sets the underlying erbout storage to input.
|
27
|
-
def _erbout=(input)
|
28
|
-
end
|
29
|
-
|
30
|
-
# Concatenates the specified input to the underlying erbout storage.
|
31
|
-
def concat(input)
|
32
|
-
erbout << input
|
33
|
-
self
|
34
|
-
end
|
35
|
-
|
36
|
-
def capture(strip=true)
|
37
|
-
current, redirect = erbout, StringIO.new
|
38
|
-
|
39
|
-
begin
|
40
|
-
@erbout = redirect
|
41
|
-
yield
|
42
|
-
ensure
|
43
|
-
@erbout = current
|
44
|
-
end
|
45
|
-
|
46
|
-
str = redirect.string
|
47
|
-
str.strip! if strip
|
48
|
-
str
|
49
|
-
end
|
50
|
-
|
51
|
-
def indent(indent=' ', &block)
|
52
|
-
capture(&block).split("\n").each do |line|
|
53
|
-
concat "#{indent}#{line}\n"
|
54
|
-
end
|
55
|
-
self
|
56
|
-
end
|
57
|
-
|
58
|
-
def nest(*nestings)
|
59
|
-
options = nestings.last.kind_of?(Hash) ? nestings.pop : {}
|
60
|
-
indent = options[:indent] || " "
|
61
|
-
line_sep = options[:line_sep] || "\n"
|
62
|
-
|
63
|
-
content = capture { yield }
|
64
|
-
return content if nestings.empty?
|
65
|
-
|
66
|
-
depth = nestings.length
|
67
|
-
lines = [indent * depth + content.gsub(/#{line_sep}/, line_sep + indent * depth)]
|
68
|
-
|
69
|
-
nestings.reverse_each do |(start_line, end_line)|
|
70
|
-
depth -= 1
|
71
|
-
lines.unshift(indent * depth + start_line)
|
72
|
-
lines << (indent * depth + end_line)
|
73
|
-
end
|
74
|
-
|
75
|
-
concat lines.join(line_sep)
|
76
|
-
end
|
77
|
-
|
78
|
-
def rstrip(n=10)
|
79
|
-
yield if block_given?
|
80
|
-
|
81
|
-
pos = erbout.pos
|
82
|
-
n = pos if pos < n
|
83
|
-
start = pos - n
|
84
|
-
|
85
|
-
erbout.pos = start
|
86
|
-
tail = erbout.read(n).rstrip
|
87
|
-
|
88
|
-
erbout.pos = start
|
89
|
-
erbout.truncate start
|
90
|
-
|
91
|
-
tail.length == 0 && start > 0 ? rstrip(n * 2) : concat(tail)
|
92
|
-
end
|
93
|
-
|
94
|
-
def close
|
95
|
-
erbout.close unless closed?
|
96
|
-
self
|
97
|
-
end
|
6
|
+
attr_reader :erb
|
98
7
|
|
99
|
-
def
|
100
|
-
|
8
|
+
def initialize(filename)
|
9
|
+
@erb = ERB.new File.read(filename)
|
10
|
+
@erb.filename = filename
|
101
11
|
end
|
102
12
|
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
erbout.flush
|
107
|
-
erbout.rewind
|
108
|
-
erbout.read
|
13
|
+
def build(locals={})
|
14
|
+
erb.result OpenStruct.new(locals).send(:binding)
|
109
15
|
end
|
110
16
|
end
|
111
17
|
end
|