automateit 0.70923

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 (119) hide show
  1. data.tar.gz.sig +1 -0
  2. data/CHANGES.txt +100 -0
  3. data/Hoe.rake +35 -0
  4. data/Manifest.txt +111 -0
  5. data/README.txt +44 -0
  6. data/Rakefile +284 -0
  7. data/TESTING.txt +57 -0
  8. data/TODO.txt +26 -0
  9. data/TUTORIAL.txt +390 -0
  10. data/bin/ai +3 -0
  11. data/bin/aifield +82 -0
  12. data/bin/aitag +128 -0
  13. data/bin/automateit +117 -0
  14. data/docs/friendly_errors.txt +50 -0
  15. data/docs/previews.txt +86 -0
  16. data/env.sh +4 -0
  17. data/examples/basic/Rakefile +26 -0
  18. data/examples/basic/config/automateit_env.rb +16 -0
  19. data/examples/basic/config/fields.yml +3 -0
  20. data/examples/basic/config/tags.yml +13 -0
  21. data/examples/basic/dist/README.txt +9 -0
  22. data/examples/basic/dist/myapp_server.erb +30 -0
  23. data/examples/basic/install.log +15 -0
  24. data/examples/basic/lib/README.txt +10 -0
  25. data/examples/basic/recipes/README.txt +4 -0
  26. data/examples/basic/recipes/install.rb +53 -0
  27. data/examples/basic/recipes/uninstall.rb +6 -0
  28. data/gpl.txt +674 -0
  29. data/lib/automateit.rb +66 -0
  30. data/lib/automateit/account_manager.rb +106 -0
  31. data/lib/automateit/account_manager/linux.rb +171 -0
  32. data/lib/automateit/account_manager/passwd.rb +69 -0
  33. data/lib/automateit/account_manager/portable.rb +136 -0
  34. data/lib/automateit/address_manager.rb +165 -0
  35. data/lib/automateit/address_manager/linux.rb +80 -0
  36. data/lib/automateit/address_manager/portable.rb +37 -0
  37. data/lib/automateit/cli.rb +80 -0
  38. data/lib/automateit/common.rb +65 -0
  39. data/lib/automateit/constants.rb +33 -0
  40. data/lib/automateit/edit_manager.rb +292 -0
  41. data/lib/automateit/error.rb +10 -0
  42. data/lib/automateit/field_manager.rb +103 -0
  43. data/lib/automateit/interpreter.rb +641 -0
  44. data/lib/automateit/package_manager.rb +242 -0
  45. data/lib/automateit/package_manager/apt.rb +63 -0
  46. data/lib/automateit/package_manager/egg.rb +64 -0
  47. data/lib/automateit/package_manager/gem.rb +179 -0
  48. data/lib/automateit/package_manager/portage.rb +69 -0
  49. data/lib/automateit/package_manager/yum.rb +65 -0
  50. data/lib/automateit/platform_manager.rb +47 -0
  51. data/lib/automateit/platform_manager/darwin.rb +30 -0
  52. data/lib/automateit/platform_manager/debian.rb +26 -0
  53. data/lib/automateit/platform_manager/freebsd.rb +25 -0
  54. data/lib/automateit/platform_manager/gentoo.rb +26 -0
  55. data/lib/automateit/platform_manager/lsb.rb +40 -0
  56. data/lib/automateit/platform_manager/struct.rb +78 -0
  57. data/lib/automateit/platform_manager/uname.rb +29 -0
  58. data/lib/automateit/platform_manager/windows.rb +33 -0
  59. data/lib/automateit/plugin.rb +7 -0
  60. data/lib/automateit/plugin/base.rb +32 -0
  61. data/lib/automateit/plugin/driver.rb +218 -0
  62. data/lib/automateit/plugin/manager.rb +232 -0
  63. data/lib/automateit/project.rb +460 -0
  64. data/lib/automateit/root.rb +14 -0
  65. data/lib/automateit/service_manager.rb +79 -0
  66. data/lib/automateit/service_manager/chkconfig.rb +39 -0
  67. data/lib/automateit/service_manager/rc_update.rb +37 -0
  68. data/lib/automateit/service_manager/sysv.rb +126 -0
  69. data/lib/automateit/service_manager/update_rcd.rb +35 -0
  70. data/lib/automateit/shell_manager.rb +261 -0
  71. data/lib/automateit/shell_manager/base_link.rb +67 -0
  72. data/lib/automateit/shell_manager/link.rb +24 -0
  73. data/lib/automateit/shell_manager/portable.rb +421 -0
  74. data/lib/automateit/shell_manager/symlink.rb +32 -0
  75. data/lib/automateit/shell_manager/which.rb +25 -0
  76. data/lib/automateit/tag_manager.rb +63 -0
  77. data/lib/automateit/tag_manager/struct.rb +101 -0
  78. data/lib/automateit/tag_manager/tag_parser.rb +91 -0
  79. data/lib/automateit/tag_manager/yaml.rb +29 -0
  80. data/lib/automateit/template_manager.rb +55 -0
  81. data/lib/automateit/template_manager/base.rb +172 -0
  82. data/lib/automateit/template_manager/erb.rb +17 -0
  83. data/lib/ext/metaclass.rb +17 -0
  84. data/lib/ext/object.rb +18 -0
  85. data/lib/hashcache.rb +22 -0
  86. data/lib/helpful_erb.rb +63 -0
  87. data/lib/nested_error.rb +33 -0
  88. data/lib/queued_logger.rb +68 -0
  89. data/lib/tempster.rb +239 -0
  90. data/misc/index_gem_repository.rb +303 -0
  91. data/misc/setup_egg.rb +12 -0
  92. data/misc/setup_gem_dependencies.sh +7 -0
  93. data/misc/setup_rubygems.sh +21 -0
  94. data/misc/which.cmd +6 -0
  95. data/spec/extras/automateit_service_sysv_test +50 -0
  96. data/spec/extras/scratch.rb +15 -0
  97. data/spec/extras/simple_recipe.rb +8 -0
  98. data/spec/integration/account_manager_spec.rb +218 -0
  99. data/spec/integration/address_manager_linux_spec.rb +119 -0
  100. data/spec/integration/address_manager_portable_spec.rb +30 -0
  101. data/spec/integration/cli_spec.rb +215 -0
  102. data/spec/integration/examples_spec.rb +54 -0
  103. data/spec/integration/examples_spec_editor.rb +71 -0
  104. data/spec/integration/package_manager_spec.rb +104 -0
  105. data/spec/integration/platform_manager_spec.rb +69 -0
  106. data/spec/integration/service_manager_sysv_spec.rb +115 -0
  107. data/spec/integration/shell_manager_spec.rb +471 -0
  108. data/spec/integration/template_manager_erb_spec.rb +31 -0
  109. data/spec/spec_helper.rb +23 -0
  110. data/spec/unit/edit_manager_spec.rb +162 -0
  111. data/spec/unit/field_manager_spec.rb +79 -0
  112. data/spec/unit/hashcache_spec.rb +28 -0
  113. data/spec/unit/interpreter_spec.rb +98 -0
  114. data/spec/unit/platform_manager_spec.rb +44 -0
  115. data/spec/unit/plugins_spec.rb +253 -0
  116. data/spec/unit/tag_manager_spec.rb +189 -0
  117. data/spec/unit/template_manager_erb_spec.rb +137 -0
  118. metadata +249 -0
  119. metadata.gz.sig +0 -0
@@ -0,0 +1,292 @@
1
+ # == EditManager
2
+ #
3
+ # The EditManager provides a way of editing files and strings
4
+ # programmatically.
5
+ #
6
+ # See documentation for EditManager::EditSession.
7
+ class AutomateIt::EditManager < AutomateIt::Plugin::Manager
8
+ alias_methods :edit
9
+
10
+ # Creates an editing session. See documentation for EditManager::EditSession.
11
+ def edit(*opts, &block) dispatch(*opts, &block) end
12
+ end
13
+
14
+ # == EditManager::BaseDriver
15
+ #
16
+ # Base class for all EditManager drivers.
17
+ class AutomateIt::EditManager::BaseDriver < AutomateIt::Plugin::Driver
18
+ end
19
+
20
+ # == EditManager::Simple
21
+ #
22
+ # Provides a way to edit files and strings.
23
+ #
24
+ # See documentation for EditSession.
25
+ class AutomateIt::EditManager::Simple < AutomateIt::EditManager::BaseDriver
26
+ depends_on :nothing
27
+
28
+ def suitability(method, *args) # :nodoc:
29
+ 1
30
+ end
31
+
32
+ # Creates an editing session. See documentation for EditSession#edit.
33
+ def edit(*opts, &block)
34
+ AutomateIt::EditManager::EditSession.new(:interpreter => @interpreter).edit(*opts, &block)
35
+ end
36
+ end # class Base
37
+
38
+ # == EditSession
39
+ #
40
+ # EditSession provides a way to edit files and strings.
41
+ #
42
+ # For example, here's how to edit a string from the Interpreter:
43
+ #
44
+ # edit(:text => "# hello") do
45
+ # uncomment "llo"
46
+ # append "world"
47
+ # end
48
+ # # => "hello\nworld"
49
+ #
50
+ # The above example edits a text string containing "# hello". The editing
51
+ # session uncomments the line containing "llo" and then appends a line with the
52
+ # word "world". The edited result is returned, containing two lines: "hello"
53
+ # and "world".
54
+ #
55
+ # The edit session only makes changes if they're needed. In the above example,
56
+ # once the "hello" line is uncommented, the "uncomment" command won't do
57
+ # anything. Similarly, once the word "world" has been appended, it won't be
58
+ # appended again. So if you re-edit the resulting string, it won't be changed
59
+ # because it's already in the desired state.
60
+ #
61
+ # This approach simplifies editing because you only need to specify the
62
+ # commands that are needed to change the file, and the session will figure out
63
+ # which ones to run.
64
+ class AutomateIt::EditManager::EditSession < AutomateIt::Common
65
+ # Create an EditSession.
66
+ #
67
+ # Options:
68
+ # * :interpreter -- AutomateIt Interpreter, required. Will be automatically
69
+ # set if you use AutomateIt::Interpreter#edit.
70
+ def initialize(*args)
71
+ super(*args)
72
+ interpreter.add_method_missing_to(self)
73
+ end
74
+
75
+ # Edit a file or string.
76
+ #
77
+ # Requires a filename argument or options hash -- e.g.,.
78
+ # <tt>edit("foo")</tt> and <tt>edit(:file => "foo")</tt> will both edit a
79
+ # file called +foo+.
80
+ #
81
+ # Options:
82
+ # * :file -- File to edit.
83
+ # * :text -- String to edit.
84
+ # * :params -- Hash to make available to editor session.
85
+ # * :create -- Create the file if it doesn't exist? Defaults to false.
86
+ # * :mode, :user, :group -- Set permissions on generated file, see ShellManager#chperm
87
+ #
88
+ # Edit a string:
89
+ #
90
+ # edit(:text => "foo") do
91
+ # replace "o", "@"
92
+ # end
93
+ # # => "f@@"
94
+ #
95
+ # Edit a file and pass parameters to the editing session:
96
+ #
97
+ # edit(:file => "myfile", :params => {:greet => "world"} do
98
+ # prepend "MyHeader"
99
+ # append "Hello "+params[:greet]
100
+ # end
101
+ #
102
+ # Edit a file, create it and set permissions if necessary:
103
+ #
104
+ # edit("/tmp/foo", :create => true, :mode => 0600, :user => :root) do
105
+ # prepend "Hello world!"
106
+ # end
107
+ def edit(*a, &block)
108
+ args, opts = args_and_opts(*a)
109
+ if args.first
110
+ @filename = args.first
111
+ else
112
+ raise ArgumentError.new("no file or text specified for editing") unless opts[:file] or opts[:text]
113
+ @filename = opts.delete(:file)
114
+ @contents = opts.delete(:text)
115
+ end
116
+ @params = opts.delete(:params) || {}
117
+ @comment_prefix = "# "
118
+ @comment_suffix = ""
119
+ begin
120
+ @contents ||= _read || ""
121
+ rescue Errno::ENOENT => e
122
+ if opts[:create]
123
+ @contents = ""
124
+ else
125
+ raise e
126
+ end
127
+ end
128
+ @original_contents = @contents.clone
129
+
130
+ raise ArgumentError.new("no block given") unless block
131
+ instance_eval(&block)
132
+ if @filename
133
+ _write if different?
134
+
135
+ chperm_opts = {}
136
+ for key in [:owner, :user, :group, :mode]
137
+ chperm_opts[key] = opts[key] if opts[key]
138
+ end
139
+ chperm(@filename, chperm_opts) unless chperm_opts.empty?
140
+
141
+ return different?
142
+ else
143
+ return contents
144
+ end
145
+ end
146
+
147
+ # File that was read for editing.
148
+ attr_accessor :filename
149
+
150
+ # Current contents of the editing buffer.
151
+ attr_accessor :contents
152
+
153
+ # Original contents of the editing buffer before any changes were made.
154
+ attr_accessor :original_contents
155
+
156
+ # Hash of parameters to make available to the editing session.
157
+ attr_accessor :params
158
+
159
+ # Comment prefix, e.g., "/*"
160
+ attr_accessor :comment_prefix
161
+
162
+ # Comment suffix, e.g., "*/"
163
+ attr_accessor :comment_suffix
164
+
165
+ # Prepend +line+ to the top of the buffer, but only if it's not in this
166
+ # file already.
167
+ #
168
+ # Options:
169
+ # * :unless -- Look for this String or Regexp instead and don't prepend
170
+ # if it matches.
171
+ #
172
+ # Example:
173
+ # # Buffer's contents are 'add this line'
174
+ #
175
+ # # This will prepend a line because they're not identical.
176
+ # prepend("add this line")
177
+ #
178
+ # # Won't prepend line because Regexp matches exisint line in buffer.
179
+ # prepend("add this line", :unless => /add\s*this\*line/)
180
+ def prepend(line, opts={})
181
+ query = Regexp.new(opts[:unless] || line)
182
+ query = Regexp.escape(query) if query.is_a?(String)
183
+ return if contains?(query)
184
+ @contents = "%s\n%s" % [line, @contents]
185
+ end
186
+
187
+ # Append +line+ to the bottom of the buffer, but only if it's not in
188
+ # this file already.
189
+ #
190
+ # Options:
191
+ # * :unless -- Look for this String or Regexp instead and don't append
192
+ # if it matches.
193
+ #
194
+ # See example for #prepend.
195
+ def append(line, opts={})
196
+ query = opts[:unless] || line
197
+ if query.is_a?(String)
198
+ query = Regexp.new(Regexp.escape(query))
199
+ end
200
+ return if contains?(query)
201
+ @contents = "%s\n%s\n" % [@contents.chomp, line]
202
+ end
203
+
204
+ # Does the buffer contain anything that matches the String or Regexp +query+?
205
+ def contains?(query)
206
+ if query.is_a?(String)
207
+ query = Regexp.new(Regexp.escape(query))
208
+ end
209
+ ! @contents.match(query).nil?
210
+ end
211
+
212
+ # Delete lines matching the String or Regexp +query+
213
+ def delete(query, opts={})
214
+ query = Regexp.escape(query) if query.is_a?(String)
215
+ query = Regexp.new(query+"\n?")
216
+ @contents.gsub!(query, "")
217
+ end
218
+
219
+ # Specify the comment style's +prefix+ and +suffix+.
220
+ #
221
+ # Example:
222
+ # # C style comments
223
+ # comment_style "/*", "*/"
224
+ def comment_style(prefix, suffix="")
225
+ @comment_prefix = prefix
226
+ @comment_suffix = suffix
227
+ end
228
+
229
+ # Comment out lines matching the String or Regexp +query+.
230
+ def comment(query, opts={})
231
+ query = Regexp.escape(query) if query.is_a?(String)
232
+ query = Regexp.new("^([^\n]*%s[^\n]*)(\n*)" % query)
233
+ return false unless @contents.match(query)
234
+ @contents.gsub!(query, "%s%s%s%s" % [@comment_prefix, $1, @comment_suffix, $2])
235
+ end
236
+
237
+ # Uncomment lines matching the String or Regexp +query+.
238
+ def uncomment(query, opts={})
239
+ query = Regexp.escape(query) if query.is_a?(String)
240
+ query = Regexp.new("^(%s)([^\n]*%s[^\n]*)(%s)(\n*)" % [@comment_prefix, query, @comment_suffix])
241
+ return false unless @contents.match(query)
242
+ @contents.gsub!(query, "%s%s" % [$2, $4])
243
+ end
244
+
245
+ # Replace contents matching the String or Regexp +query+ with the +string+.
246
+ def replace(query, string, opts={})
247
+ if query.is_a?(String)
248
+ query = Regexp.new(Regexp.escape(query))
249
+ end
250
+ @contents.gsub!(query, string)
251
+ end
252
+
253
+ # Manipulate the buffer. The result of your block will replace the
254
+ # buffer. This is very useful for complex edits.
255
+ #
256
+ # Example:
257
+ # manipulate do |buffer|
258
+ # buffer.gsub(/foo/, "bar")
259
+ # end
260
+ def manipulate(&block) # :yields: buffer
261
+ @contents = block.call(@contents)
262
+ end
263
+
264
+ # Is the buffer currently different than its original contents?
265
+ def different?
266
+ @contents != @original_contents
267
+ end
268
+
269
+ # Read contents from #filename. Called by the #edit command to load text
270
+ # into the buffer.
271
+ def _read
272
+ @contents = \
273
+ if writing? or (preview? and @filename and File.exists?(@filename))
274
+ File.read(@filename)
275
+ else
276
+ nil
277
+ end
278
+ end
279
+ protected :_read
280
+
281
+ # Write contents to #filename. Used by the #edit command to write the buffer
282
+ # to a file.
283
+ def _write
284
+ log.info(PNOTE+"Edited '#{@filename}'")
285
+ if preview?
286
+ true
287
+ else
288
+ File.open(@filename, "w+"){|writer| writer.write(@contents)}
289
+ end
290
+ end
291
+ protected :_write
292
+ end # class EditSession
@@ -0,0 +1,10 @@
1
+ module AutomateIt
2
+ # == AutomateIt::Error
3
+ #
4
+ # Wraps errors while preserving their cause. Used by the Interpreter to
5
+ # display user-friendly error messages.
6
+ #
7
+ # See NestedError for class API.
8
+ class Error < ::NestedError
9
+ end
10
+ end
@@ -0,0 +1,103 @@
1
+ # == FieldManager
2
+ #
3
+ # The FieldManager provides a way of accessing a hash of constants. These are
4
+ # useful for storing configuration data seperately from recipes. # Fields are
5
+ # typically stored in a Project's <tt>config/fields.yml</tt> file.
6
+ #
7
+ # Fields can also be queried from the Unix shell using +aifield+, run
8
+ # <tt>aifield --help</tt> for details.
9
+ class AutomateIt::FieldManager < AutomateIt::Plugin::Manager
10
+ alias_methods :lookup
11
+
12
+ # Lookup a field.
13
+ #
14
+ # For example, consider a <tt>field.yml</tt> that contains YAML like:
15
+ # foo: bar
16
+ # my_app:
17
+ # my_key: my_value
18
+ #
19
+ # With the above file, we can query the fields like this:
20
+ # lookup(:foo) # => "bar"
21
+ # lookup("foo") # => "bar"
22
+ # lookup("my_app#my_key") # => "my_value"
23
+ # lookup("my_app#my_branch") # => "my_value"
24
+ #
25
+ # You can get a reference to the entire hash:
26
+ # lookup("*")
27
+ #
28
+ # If a field isn't found, a IndexError is raised.
29
+ def lookup(search=nil) dispatch(search) end
30
+ end
31
+
32
+ # == FieldManager::BaseDriver
33
+ #
34
+ # Base class for all FieldManager drivers.
35
+ class AutomateIt::FieldManager::BaseDriver < AutomateIt::Plugin::Driver
36
+ end
37
+
38
+ # == FieldManager::Struct
39
+ #
40
+ # A FileManager driver that queries a data structure.
41
+ class AutomateIt::FieldManager::Struct < AutomateIt::FieldManager::BaseDriver
42
+ depends_on :nothing
43
+
44
+ def suitability(method, *args) # :nodoc:
45
+ return 1
46
+ end
47
+
48
+ # Options:
49
+ # * :struct -- Hash to use as the fields data structure.
50
+ def setup(opts={})
51
+ super(opts)
52
+
53
+ if opts[:struct]
54
+ @struct = opts[:struct]
55
+ else
56
+ @struct ||= {}
57
+ end
58
+ end
59
+
60
+ # See FieldManager#lookup
61
+ def lookup(search=nil)
62
+ return @struct if search.nil? or search == "*"
63
+ ref = @struct
64
+ for key in search.to_s.split("#")
65
+ ref = ref[key]
66
+ end
67
+ if ref
68
+ return ref
69
+ else
70
+ raise IndexError.new("can't find value for: #{search}")
71
+ end
72
+ end
73
+ end
74
+
75
+ # == FieldManager::YAML
76
+ #
77
+ # A FieldManager driver that reads its data structure from a file.
78
+ class AutomateIt::FieldManager::YAML < AutomateIt::FieldManager::Struct
79
+ depends_on :nothing
80
+
81
+ def suitability(method, *args) # :nodoc:
82
+ return 5
83
+ end
84
+
85
+ # Options:
86
+ # * :file -- Filename to read data structure from. Contents will be
87
+ # parsed with ERB and then handed to YAML.
88
+ def setup(opts={})
89
+ if filename = opts.delete(:file)
90
+ contents = _read(filename)
91
+ binder = interpreter.send(:binding)
92
+ output = HelpfulERB.new(contents, filename).result(binder)
93
+
94
+ opts[:struct] = ::YAML::load(output)
95
+ end
96
+ super(opts)
97
+ end
98
+
99
+ def _read(filename)
100
+ return File.read(filename)
101
+ end
102
+ private :_read
103
+ end
@@ -0,0 +1,641 @@
1
+ module AutomateIt
2
+ # == Interpreter
3
+ #
4
+ # The Interpreter runs AutomateIt commands.
5
+ #
6
+ # The TUTORIAL.txt[link:files/TUTORIAL_txt.html] file provides hands-on examples
7
+ # for using the Interpreter.
8
+ #
9
+ # === Aliased methods
10
+ #
11
+ # The Interpreter provides shortcut aliases for certain plugin commands.
12
+ #
13
+ # For example, the following commands will run the same method:
14
+ #
15
+ # shell_manager.sh "ls"
16
+ #
17
+ # sh "ls"
18
+ #
19
+ # The full set of aliased methods:
20
+ #
21
+ # * cd -- AutomateIt::ShellManager#cd
22
+ # * chmod -- AutomateIt::ShellManager#chmod
23
+ # * chmod_R -- AutomateIt::ShellManager#chmod_R
24
+ # * chown -- AutomateIt::ShellManager#chown
25
+ # * chown_R -- AutomateIt::ShellManager#chown_R
26
+ # * chperm -- AutomateIt::ShellManager#chperm
27
+ # * cp -- AutomateIt::ShellManager#cp
28
+ # * cp_r -- AutomateIt::ShellManager#cp_r
29
+ # * edit -- AutomateIt::EditManager#edit
30
+ # * hosts_tagged_with -- AutomateIt::TagManager#hosts_tagged_with
31
+ # * install -- AutomateIt::ShellManager#install
32
+ # * ln -- AutomateIt::ShellManager#ln
33
+ # * ln_s -- AutomateIt::ShellManager#ln_s
34
+ # * ln_sf -- AutomateIt::ShellManager#ln_sf
35
+ # * lookup -- AutomateIt::FieldManager#lookup
36
+ # * mkdir -- AutomateIt::ShellManager#mkdir
37
+ # * mkdir_p -- AutomateIt::ShellManager#mkdir_p
38
+ # * mktemp -- AutomateIt::ShellManager#mktemp
39
+ # * mktempdir -- AutomateIt::ShellManager#mktempdir
40
+ # * mktempdircd -- AutomateIt::ShellManager#mktempdircd
41
+ # * mv -- AutomateIt::ShellManager#mv
42
+ # * pwd -- AutomateIt::ShellManager#pwd
43
+ # * render -- AutomateIt::TemplateManager#render
44
+ # * rm -- AutomateIt::ShellManager#rm
45
+ # * rm_r -- AutomateIt::ShellManager#rm_r
46
+ # * rm_rf -- AutomateIt::ShellManager#rm_rf
47
+ # * rmdir -- AutomateIt::ShellManager#rmdir
48
+ # * sh -- AutomateIt::ShellManager#sh
49
+ # * tagged? -- AutomateIt::TagManager#tagged?
50
+ # * tags -- AutomateIt::TagManager#tags
51
+ # * tags_for -- AutomateIt::TagManager#tags_for
52
+ # * touch -- AutomateIt::ShellManager#touch
53
+ # * umask -- AutomateIt::ShellManager#umask
54
+ # * which -- AutomateIt::ShellManager#which
55
+ # * which! -- AutomateIt::ShellManager#which!
56
+ #
57
+ # [[[ <a name="embedding"> ]]]
58
+ # === Embedding the Interpreter
59
+ # [[[ </a> ]]]
60
+ #
61
+ # The AutomateIt Interpreter can be embedded inside a Ruby program:
62
+ #
63
+ # require 'rubygems'
64
+ # require 'automateit'
65
+ #
66
+ # interpreter = AutomateIt.new
67
+ #
68
+ # # Use the interpreter as an object:
69
+ # interpreter.sh "ls -la"
70
+ #
71
+ # # Have it execute a recipe:
72
+ # interpreter.invoke "myrecipe.rb"
73
+ #
74
+ # # Or execute recipes within a block
75
+ # interpreter.instance_eval do
76
+ # puts superuser?
77
+ # sh "ls -la"
78
+ # end
79
+ #
80
+ # See the #include_in and #add_method_missing_to methods for instructions on
81
+ # how to more easily dispatch commands from your program to the Interpreter
82
+ # instance.
83
+ class Interpreter < Common
84
+ # Plugin instance that instantiated the Interpreter.
85
+ attr_accessor :parent
86
+ private :parent
87
+ private :parent=
88
+
89
+ # Access IRB instance from an interactive shell.
90
+ attr_accessor :irb
91
+
92
+ # Project path for this Interpreter. If no path is available, nil.
93
+ attr_accessor :project
94
+
95
+ # Hash of parameters to make available to the Interpreter. Mostly useful
96
+ # when needing to pass arguments to an embedded Interpreter before doing an
97
+ # #instance_eval.
98
+ attr_accessor :params
99
+
100
+ # The Interpreter throws friendly error messages by default that make it
101
+ # easier to see what's wrong with a recipe. These friendly messages display
102
+ # the cause, a snapshot of the problematic code, shortened paths, and only
103
+ # the relevant stack frames.
104
+ #
105
+ # However, if there's a bug in the AutomateIt internals, these friendly
106
+ # messages may inadvertently hide the cause, and it may be necessary to
107
+ # turn them off to figure out what's wrong.
108
+ #
109
+ # To turn off friendly exceptions:
110
+ #
111
+ # # From a recipe or the AutomateIt interactive shell:
112
+ # self.friendly_exceptions = false
113
+ #
114
+ # # For an embedded interpreter at instantiation:
115
+ # AutomateIt.new(:friendly_exceptions => false)
116
+ #
117
+ # # From the UNIX command line when invoking a recipe:
118
+ # automateit --trace myrecipe.rb
119
+ attr_accessor :friendly_exceptions
120
+
121
+ # Setup the Interpreter. This method is also called from Interpreter#new.
122
+ #
123
+ # Options for users:
124
+ # * :verbosity -- Alias for :log_level
125
+ # * :log_level -- Log level to use, defaults to Logger::INFO.
126
+ # * :preview -- Turn on preview mode, defaults to false.
127
+ # * :project -- Set project path.
128
+ # * :friendly_exceptions -- Throw user-friendly exceptions that make it
129
+ # easier to see errors in recipes, defaults to true.
130
+ #
131
+ # Options for internal use:
132
+ # * :parent -- Parent plugin instance.
133
+ # * :log -- QueuedLogger instance.
134
+ # * :guessed_project -- Boolean of whether the project path was guessed. If
135
+ # guessed, won't throw exceptions if project wasn't found at the
136
+ # specified path. If not guessed, will throw exception in such a
137
+ # situation.
138
+ def setup(opts={})
139
+ super(opts.merge(:interpreter => self))
140
+
141
+ self.params ||= {}
142
+
143
+ if opts[:irb]
144
+ @irb = opts[:irb]
145
+ end
146
+
147
+ if opts[:parent]
148
+ @parent = opts[:parent]
149
+ end
150
+
151
+ if opts[:log]
152
+ @log = opts[:log]
153
+ elsif not defined?(@log) or @log.nil?
154
+ @log = QueuedLogger.new($stdout)
155
+ @log.level = Logger::INFO
156
+ end
157
+
158
+ if opts[:log_level] or opts[:verbosity]
159
+ @log.level = opts[:log_level] || opts[:verbosity]
160
+ end
161
+
162
+ if opts[:preview].nil? # can be false
163
+ self.preview = false unless preview?
164
+ else
165
+ self.preview = opts[:preview]
166
+ end
167
+
168
+ if opts[:friendly_exceptions].nil?
169
+ @friendly_exceptions = true unless defined?(@friendly_exceptions)
170
+ else
171
+ @friendly_exceptions = opts[:friendly_exceptions]
172
+ end
173
+
174
+ # Instantiate core plugins so they're available to the project
175
+ _instantiate_plugins
176
+
177
+ if project_path = opts[:project] || ENV["AUTOMATEIT_PROJECT"] || ENV["AIP"]
178
+ # Only load a project if we find its env file
179
+ env_file = File.join(project_path, "config", "automateit_env.rb")
180
+ if File.exists?(env_file)
181
+ @project = File.expand_path(project_path)
182
+ log.debug(PNOTE+"Loading project from path: #{@project}")
183
+
184
+ tag_file = File.join(@project, "config", "tags.yml")
185
+ if File.exists?(tag_file)
186
+ log.debug(PNOTE+"Loading project tags: #{tag_file}")
187
+ tag_manager[:yaml].setup(:file => tag_file)
188
+ end
189
+
190
+ field_file = File.join(@project, "config", "fields.yml")
191
+ if File.exists?(field_file)
192
+ log.debug(PNOTE+"Loading project fields: #{field_file}")
193
+ field_manager[:yaml].setup(:file => field_file)
194
+ end
195
+
196
+ lib_files = Dir[File.join(@project, "lib", "*.rb")] + Dir[File.join(@project, "lib", "**", "init.rb")]
197
+ lib_files.each do |lib|
198
+ log.debug(PNOTE+"Loading project library: #{lib}")
199
+ invoke(lib)
200
+ end
201
+
202
+ # Instantiate project's plugins so they're available to the environment
203
+ _instantiate_plugins
204
+
205
+ if File.exists?(env_file)
206
+ log.debug(PNOTE+"Loading project env: #{env_file}")
207
+ invoke(env_file)
208
+ end
209
+ elsif not opts[:guessed_project]
210
+ raise ArgumentError.new("Couldn't find project at: #{project_path}")
211
+ end
212
+ end
213
+ end
214
+
215
+ # Hash of plugin tokens to plugin instances for this Interpreter.
216
+ attr_accessor :plugins
217
+
218
+ def _instantiate_plugins
219
+ @plugins ||= {}
220
+ # If a parent is defined, use it to prep the list and avoid re-instantiating it.
221
+ if defined?(@parent) and @parent and Plugin::Manager === @parent
222
+ @plugins[@parent.class.token] = @parent
223
+ end
224
+ plugin_classes = AutomateIt::Plugin::Manager.classes.reject{|t| t == @parent if @parent}
225
+ for klass in plugin_classes
226
+ _instantiate_plugin(klass)
227
+ end
228
+ end
229
+ private :_instantiate_plugins
230
+
231
+ def _instantiate_plugin(klass)
232
+ token = klass.token
233
+ unless plugin = @plugins[token]
234
+ plugin = @plugins[token] = klass.new(:interpreter => self)
235
+ #puts "!!! ip #{token}"
236
+ unless respond_to?(token.to_sym)
237
+ self.class.send(:define_method, token) do
238
+ @plugins[token]
239
+ end
240
+ end
241
+ _expose_plugin_methods(plugin)
242
+ end
243
+ plugin.instantiate_drivers
244
+ end
245
+ private :_instantiate_plugin
246
+
247
+ def _expose_plugin_methods(plugin)
248
+ return unless plugin.class.aliased_methods
249
+ plugin.class.aliased_methods.each do |method|
250
+ #puts "!!! epm #{method}"
251
+ unless respond_to?(method.to_sym)
252
+ # Must use instance_eval because methods created with define_method
253
+ # can't accept block as argument. This is a known Ruby 1.8 bug.
254
+ self.instance_eval <<-EOB
255
+ def #{method}(*args, &block)
256
+ @plugins[:#{plugin.class.token}].send(:#{method}, *args, &block)
257
+ end
258
+ EOB
259
+ end
260
+ end
261
+ end
262
+ private :_expose_plugin_methods
263
+
264
+ # Set the QueuedLogger instance for the Interpreter.
265
+ attr_writer :log
266
+
267
+ # Get or set the QueuedLogger instance for the Interpreter, a special
268
+ # wrapper around the Ruby Logger.
269
+ def log(value=nil)
270
+ if value.nil?
271
+ return defined?(@log) ? @log : nil
272
+ else
273
+ @log = value
274
+ end
275
+ end
276
+
277
+ # Set preview mode to +value+. See warnings in ShellManager to learn how to
278
+ # correctly write code for preview mode.
279
+ def preview(value)
280
+ self.preview = value
281
+ end
282
+
283
+ # Is Interpreter running in preview mode?
284
+ def preview?
285
+ @preview
286
+ end
287
+
288
+ # Preview a block of custom commands. When in preview mode, displays the
289
+ # +message+ but doesn't execute the +block+. When not previewing, will
290
+ # execute the block and not display the +message+.
291
+ #
292
+ # For example:
293
+ #
294
+ # preview_for("FOO") do
295
+ # puts "BAR"
296
+ # end
297
+ #
298
+ # In preview mode, this displays:
299
+ #
300
+ # => FOO
301
+ #
302
+ # When not previewing, displays:
303
+ #
304
+ # BAR
305
+ def preview_for(message, &block)
306
+ if preview?
307
+ log.info(message)
308
+ :preview
309
+ else
310
+ block.call
311
+ end
312
+ end
313
+
314
+ # Set preview mode to +value.
315
+ def preview=(value)
316
+ @preview = value
317
+ end
318
+
319
+ # Set noop (no-operation mode) to +value+. Alias for #preview.
320
+ def noop(value)
321
+ self.noop = value
322
+ end
323
+
324
+ # Set noop (no-operation mode) to +value+. Alias for #preview=.
325
+ def noop=(value)
326
+ self.preview = value
327
+ end
328
+
329
+ # Are we in noop (no-operation) mode? Alias for #preview?.
330
+ def noop?
331
+ preview?
332
+ end
333
+
334
+ # Set writing to +value+. This is the opposite of #preview.
335
+ def writing(value)
336
+ self.writing = value
337
+ end
338
+
339
+ # Set writing to +value+. This is the opposite of #preview=.
340
+ def writing=(value)
341
+ self.preview = !value
342
+ end
343
+
344
+ # Is Interpreter writing? This is the opposite of #preview?.
345
+ def writing?
346
+ !preview?
347
+ end
348
+
349
+ # Does this platform provide euid (Effective User ID)?
350
+ def euid?
351
+ begin
352
+ euid
353
+ return true
354
+ rescue
355
+ return false
356
+ end
357
+ end
358
+
359
+ # Return the effective user id.
360
+ def euid
361
+ begin
362
+ return Process.euid
363
+ rescue NoMethodError => e
364
+ output = `id -u 2>&1`
365
+ raise e unless output and $?.exitstatus.zero?
366
+ begin
367
+ return output.match(/(\d+)/)[1].to_i
368
+ rescue IndexError
369
+ raise e
370
+ end
371
+ end
372
+
373
+ end
374
+
375
+ # Does the current user have superuser (root) privileges?
376
+ def superuser?
377
+ euid.zero?
378
+ end
379
+
380
+ # Create an Interpreter with the specified +opts+ and invoke
381
+ # the +recipe+. The opts are passed to #setup for parsing.
382
+ def self.invoke(recipe, opts={})
383
+ opts[:project] ||= File.join(File.dirname(recipe), "..")
384
+ AutomateIt.new(opts).invoke(recipe)
385
+ end
386
+
387
+ # Invoke the +recipe+. The recipe may be expressed as a relative or fully
388
+ # qualified path. When invoked within a project, the recipe can also be the
389
+ # name of a recipe.
390
+ #
391
+ # Example:
392
+ # invoke "/tmp/recipe.rb" # Run "/tmp/recipe.rb"
393
+ # invoke "recipe.rb" # Run "./recipe.rb". If not found and in a
394
+ # # project, will try running "recipes/recipe.rb"
395
+ # invoke "recipe" # Run "recipes/recipe.rb" in a project
396
+ def invoke(recipe)
397
+ filenames = [recipe]
398
+ filenames << File.join(project, "recipes", recipe) if project
399
+ filenames << File.join(project, "recipes", recipe + ".rb") if project
400
+
401
+ for filename in filenames
402
+ log.debug(PNOTE+" invoking "+filename)
403
+ if File.exists?(filename)
404
+ data = File.read(filename)
405
+ begin
406
+ return instance_eval(data, filename, 1)
407
+ rescue Exception => e
408
+ if @friendly_exceptions
409
+ # TODO Extract this routine and its companion in HelpfulERB
410
+
411
+ # Capture initial stack in case we add a debug/breakpoint after this
412
+ stack = caller
413
+
414
+ # Extract trace for recipe after the Interpreter#invoke call
415
+ preresult = []
416
+ for line in e.backtrace
417
+ # Stop at the Interpreter#invoke call
418
+ break if line == stack.first
419
+ preresult << line
420
+ end
421
+
422
+ # Extract the recipe filename
423
+ preresult.last.match(/^([^:]+):(\d+):in `invoke'/)
424
+ recipe = $1
425
+
426
+ # Extract trace for most recent block
427
+ result = []
428
+ for line in preresult
429
+ # Ignore manager wrapper and dispatch methods
430
+ next if line =~ %r{lib/automateit/.+manager\.rb:\d+:in `.+'$}
431
+ result << line
432
+ # Stop at the first mention of this recipe
433
+ break if line =~ /^#{recipe}/
434
+ end
435
+
436
+ # Extract line number
437
+ if e.is_a?(SyntaxError)
438
+ line_number = e.message.match(/^[^:]+:(\d+):/)[1].to_i
439
+ else
440
+ result.last.match(/^([^:]+):(\d+):in `invoke'/)
441
+ line_number = $2.to_i
442
+ end
443
+
444
+ msg = "Problem with recipe '#{recipe}' at line #{line_number}\n"
445
+
446
+ # Extract recipe text
447
+ begin
448
+ lines = File.read(recipe).split(/\n/)
449
+
450
+ min = line_number - 7
451
+ min = 0 if min < 0
452
+
453
+ max = line_number + 1
454
+ max = lines.size if max > lines.size
455
+
456
+ width = max.to_s.size
457
+
458
+ for i in min..max
459
+ n = i+1
460
+ marker = n == line_number ? "*" : ""
461
+ msg << "\n%2s %#{width}i %s" % [marker, n, lines[i]]
462
+ end
463
+
464
+ msg << "\n"
465
+ rescue Exception => e
466
+ # Ignore
467
+ end
468
+
469
+ msg << "\n(#{e.exception.class}) #{e.message}"
470
+
471
+ # Append shortened trace
472
+ for line in result
473
+ msg << "\n "+line
474
+ end
475
+
476
+ # Remove project path
477
+ msg.gsub!(/#{@project}\/?/, '') if @project
478
+
479
+ raise AutomateIt::Error.new(msg, e)
480
+ else
481
+ raise e
482
+ end
483
+ end
484
+ end
485
+ end
486
+ raise Errno::ENOENT.new(recipe)
487
+ end
488
+
489
+ # Path of this project's "dist" directory. If a project isn't available or
490
+ # the directory doesn't exist, this will throw a NotImplementedError.
491
+ def dist
492
+ if @project
493
+ result = File.join(@project, "dist/")
494
+ if File.directory?(result)
495
+ return result
496
+ else
497
+ raise NotImplementedError.new("can't find dist directory at: #{result}")
498
+ end
499
+ else
500
+ raise NotImplementedError.new("can't use dist without a project")
501
+ end
502
+ end
503
+
504
+ # Set value to share throughout the Interpreter. Use this instead of
505
+ # globals so that different Interpreters don't see each other's variables.
506
+ # Creates a method that returns the value and also adds a #params entry.
507
+ #
508
+ # Example:
509
+ # set :asdf, 9 # => 9
510
+ # asdf # => 9
511
+ #
512
+ # This is best used for frequently-used variables, like paths. For
513
+ # infrequently-used variables, use #lookup and #params. A good place to use
514
+ # the #set is in the Project's <tt>config/automateit_env.rb</tt> file so
515
+ # that paths are exposed to all recipes like this:
516
+ #
517
+ # set :helpers, project+"/helpers"
518
+ def set(key, value)
519
+ key = key.to_sym
520
+ params[key] = value
521
+ eval <<-HERE
522
+ def #{key}
523
+ return params[:#{key}]
524
+ end
525
+ HERE
526
+ value
527
+ end
528
+
529
+ # Retrieve a #params entry.
530
+ #
531
+ # Example:
532
+ # params[:foo] = "bar" # => "bar"
533
+ # get :foo # => "bar"
534
+ def get(key)
535
+ params[key.to_sym]
536
+ end
537
+
538
+ # Creates wrapper methods in +object+ to dispatch calls to an Interpreter instance.
539
+ #
540
+ # *WARNING*: This will overwrite all methods and variables in the target +object+ that have the same names as the Interpreter's methods. You should considerer specifying the +methods+ to limit the number of methods included to minimize surprises due to collisions. If +methods+ is left blank, will create wrappers for all Interpreter methods.
541
+ #
542
+ # For example, include an Interpreter instance into a Rake session, which will override the FileUtils commands with AutomateIt equivalents:
543
+ #
544
+ # # Rakefile
545
+ #
546
+ # require 'automateit'
547
+ # @ai = AutomateIt.new
548
+ # @ai.include_in(self, %w(preview? sh)) # Include #preview? and #sh methods
549
+ #
550
+ # task :default do
551
+ # puts preview? # Uses Interpreter#preview?
552
+ # sh "id" # Uses Interpreter#sh, not FileUtils#sh
553
+ # cp "foo", "bar" # Uses FileUtils#cp, not Interpreter#cp
554
+ # end
555
+ #
556
+ # For situations where you don't want to override any existing methods, consider using #add_method_missing_to.
557
+ def include_in(object, *methods)
558
+ methods = [methods].flatten
559
+ methods = unique_methods.reject{|t| t =~ /^_/} if methods.empty?
560
+
561
+ object.instance_variable_set(:@__automateit, self)
562
+
563
+ for method in methods
564
+ object.instance_eval <<-HERE
565
+ def #{method}(*args, &block)
566
+ @__automateit.send(:#{method}, *args, &block)
567
+ end
568
+ HERE
569
+ end
570
+ end
571
+
572
+ # Creates #method_missing in +object+ that dispatches calls to an Interpreter instance. If a #method_missing is already present, it will be preserved as a fall-back using #alias_method_chain.
573
+ #
574
+ # For example, add #method_missing to a Rake session to provide direct access to Interpreter instance's methods whose names don't conflict with the names existing variables and methods:
575
+ #
576
+ # # Rakefile
577
+ #
578
+ # require 'automateit'
579
+ # @ai = AutomateIt.new
580
+ # @ai.add_method_missing_to(self)
581
+ #
582
+ # task :default do
583
+ # puts preview? # Uses Interpreter#preview?
584
+ # sh "id" # Uses FileUtils#sh, not Interpreter#sh
585
+ # end
586
+ #
587
+ # For situations where it's necessary to override existing methods, such as the +sh+ call in the example, consider using #include_in.
588
+ def add_method_missing_to(object)
589
+ object.instance_variable_set(:@__automateit, self)
590
+ chain = object.respond_to?(:method_missing)
591
+
592
+ # XXX The solution below is evil and ugly, but I don't know how else to solve this. The problem is that I want to *only* alter the +object+ instance, and NOT its class. Unfortunately, #alias_method and #alias_method_chain only operate on classes, not instances, which makes them useless for this task.
593
+
594
+ template = <<-HERE
595
+ def method_missing<%=chain ? '_with_automateit' : ''%>(method, *args, &block)
596
+ ### puts "mm+a(%s, %s)" % [method, args.inspect]
597
+ if @__automateit.respond_to?(method)
598
+ @__automateit.send(method, *args, &block)
599
+ else
600
+ <%-if chain-%>
601
+ method_missing_without_automateit(method, *args, &block)
602
+ <%-else-%>
603
+ super
604
+ <%-end-%>
605
+ end
606
+ end
607
+ <%-if chain-%>
608
+ @__method_missing_without_automateit = self.method(:method_missing)
609
+
610
+ def method_missing_without_automateit(*args)
611
+ ### puts "mm-a %s" % args.inspect
612
+ @__method_missing_without_automateit.call(*args)
613
+ end
614
+
615
+ def method_missing(*args)
616
+ ### puts "mm %s" % args.inspect
617
+ method_missing_with_automateit(*args)
618
+ end
619
+ <%-end-%>
620
+ HERE
621
+
622
+ text = ::HelpfulERB.new(template).result(binding)
623
+ object.instance_eval(text)
624
+ end
625
+
626
+ # Use to manage nitpick message for debugging AutomateIt internals.
627
+ #
628
+ # Arguments:
629
+ # * nil -- Returns boolean of whether nitpick messages will be displayed.
630
+ # * Boolean -- Sets nitpick state.
631
+ # * String or Symbol -- Displays nitpick message if state is on.
632
+ def nitpick(value=nil)
633
+ case value
634
+ when NilClass: @nitpick
635
+ when TrueClass, FalseClass: @nitpick = value
636
+ when String, Symbol: puts "%% #{value}" if @nitpick
637
+ else raise TypeError.new("Unknown nitpick type: #{value.class}")
638
+ end
639
+ end
640
+ end
641
+ end