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