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.
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