linecook 0.6.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/History +139 -0
  2. data/HowTo/Control Virtual Machines +106 -0
  3. data/HowTo/Generate Scripts +263 -0
  4. data/HowTo/Run Scripts +87 -0
  5. data/HowTo/Setup Virtual Machines +76 -0
  6. data/License.txt +1 -1
  7. data/README +78 -59
  8. data/bin/linecook +12 -5
  9. data/bin/linecook_run +45 -0
  10. data/bin/linecook_scp +50 -0
  11. data/lib/linecook.rb +1 -3
  12. data/lib/linecook/attributes.rb +49 -12
  13. data/lib/linecook/commands.rb +9 -4
  14. data/lib/linecook/commands/build.rb +69 -0
  15. data/lib/linecook/commands/command.rb +13 -3
  16. data/lib/linecook/commands/command_error.rb +6 -0
  17. data/lib/linecook/commands/env.rb +74 -8
  18. data/lib/linecook/commands/helper.rb +271 -24
  19. data/lib/linecook/commands/init.rb +10 -6
  20. data/lib/linecook/commands/package.rb +36 -18
  21. data/lib/linecook/commands/run.rb +66 -0
  22. data/lib/linecook/commands/snapshot.rb +114 -0
  23. data/lib/linecook/commands/ssh.rb +39 -0
  24. data/lib/linecook/commands/start.rb +34 -0
  25. data/lib/linecook/commands/state.rb +32 -0
  26. data/lib/linecook/commands/stop.rb +22 -0
  27. data/lib/linecook/commands/vbox_command.rb +130 -0
  28. data/lib/linecook/cookbook.rb +112 -55
  29. data/lib/linecook/package.rb +293 -109
  30. data/lib/linecook/proxy.rb +19 -0
  31. data/lib/linecook/recipe.rb +321 -62
  32. data/lib/linecook/template.rb +7 -101
  33. data/lib/linecook/test.rb +196 -141
  34. data/lib/linecook/test/command_parser.rb +75 -0
  35. data/lib/linecook/test/file_test.rb +153 -35
  36. data/lib/linecook/test/shell_test.rb +176 -0
  37. data/lib/linecook/utils.rb +25 -7
  38. data/lib/linecook/version.rb +4 -4
  39. data/templates/Rakefile +44 -47
  40. data/templates/_gitignore +1 -1
  41. data/templates/attributes/project_name.rb +4 -4
  42. data/templates/config/ssh +15 -0
  43. data/templates/files/help.txt +1 -0
  44. data/templates/helpers/project_name/assert_content_equal.erb +15 -0
  45. data/templates/helpers/project_name/create_dir.erb +9 -0
  46. data/templates/helpers/project_name/create_file.erb +8 -0
  47. data/templates/helpers/project_name/install_file.erb +8 -0
  48. data/templates/packages/abox.yml +4 -0
  49. data/templates/recipes/abox.rb +22 -0
  50. data/templates/recipes/abox_test.rb +14 -0
  51. data/templates/templates/todo.txt.erb +3 -0
  52. data/templates/test/project_name_test.rb +19 -0
  53. data/templates/test/test_helper.rb +14 -0
  54. metadata +43 -41
  55. data/cookbook +0 -0
  56. data/lib/linecook/commands/helpers.rb +0 -28
  57. data/lib/linecook/commands/vbox.rb +0 -85
  58. data/lib/linecook/helper.rb +0 -117
  59. data/lib/linecook/shell.rb +0 -11
  60. data/lib/linecook/shell/posix.rb +0 -145
  61. data/lib/linecook/shell/test.rb +0 -254
  62. data/lib/linecook/shell/unix.rb +0 -117
  63. data/lib/linecook/shell/utils.rb +0 -138
  64. data/templates/README +0 -90
  65. data/templates/files/file.txt +0 -1
  66. data/templates/helpers/project_name/echo.erb +0 -5
  67. data/templates/recipes/project_name.rb +0 -20
  68. data/templates/scripts/project_name.yml +0 -7
  69. data/templates/templates/template.txt.erb +0 -3
  70. data/templates/vbox/setup/virtual_box +0 -86
  71. data/templates/vbox/ssh/id_rsa +0 -27
  72. 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
@@ -1,103 +1,362 @@
1
- require 'linecook/template'
2
1
  require 'linecook/attributes'
3
- require 'linecook/package'
2
+ require 'linecook/proxy'
4
3
  require 'linecook/utils'
4
+ require 'erb'
5
+ require 'stringio'
5
6
 
6
7
  module Linecook
7
- class Recipe < Template
8
- class << self
9
- def build(env)
10
- package = Package.new(env)
11
-
12
- package.recipes.each do |recipe_name, target_name|
13
- new(target_name, env).evaluate(recipe_name)
14
- end
15
-
16
- package.close
17
- package
18
- end
19
- end
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
- alias target erbout
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, env={})
26
- @target_name = target_name
27
- @package = Package.init(env)
28
- @attributes = Attributes.new(@package.env)
29
- @erbout = @package.build(target_name)
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
- def source_path(*relative_path)
33
- path = File.join(*relative_path)
34
- @package.manifest[path] or raise "no such file in manifest: #{path.inspect}"
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
- def target_path(source_path)
38
- @package.build_path(source_path) ||
39
- @package.register(source_path, File.join("#{target_name}.d", File.basename(source_path)))
84
+ # Returns true if _target_ is closed.
85
+ def closed?
86
+ _target_.closed?
40
87
  end
41
88
 
42
- def target_file(name, content=nil)
43
- tempfile = @package.build File.join("#{target_name}.d", name)
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
- tempfile << content if content
46
- yield(tempfile) if block_given?
107
+ if block_given?
108
+ attributes.instance_eval(&block)
109
+ end
47
110
 
48
- target_path tempfile.path
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.current
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
- def attributes(attributes_name)
56
- path = source_path('attributes', "#{attributes_name}.rb")
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
- @attributes.instance_eval(File.read(path), path)
59
- @attributes.reset(false)
60
- self
179
+ target_path target_name
61
180
  end
62
181
 
63
- def helpers(helper_name)
64
- require Utils.underscore(helper_name)
65
- extend Utils.constantize(helper_name)
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
- def evaluate(recipe_name=target_name)
69
- path = source_path('recipes', "#{recipe_name}.rb")
70
- instance_eval(File.read(path), path)
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
- def file_path(file_name)
75
- path = source_path('files', file_name)
76
- target_path path
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
- def capture_path(name, &block)
80
- content = capture(false) { instance_eval(&block) }
81
- target_file(name, content)
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
- def recipe_path(recipe_name, target_name = recipe_name)
85
- source_path =
86
- @package.built?(target_name) ?
87
- @package.source_path(target_name) :
88
- Recipe.new(target_name, @package).evaluate(recipe_name).target.path
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
- target_path source_path
329
+ self
91
330
  end
92
331
 
93
- def template_path(template_name, locals={})
94
- path = source_path('templates', "#{template_name}.erb")
95
- target_file template_name, Template.build(File.read(path), locals, path)
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
- def close
99
- @package.close
100
- @package.registry
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
@@ -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
- class << self
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 closed?
100
- erbout.closed?
8
+ def initialize(filename)
9
+ @erb = ERB.new File.read(filename)
10
+ @erb.filename = filename
101
11
  end
102
12
 
103
- def result(&block)
104
- instance_eval(&block) if block
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