automateit 0.70923
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +1 -0
- data/CHANGES.txt +100 -0
- data/Hoe.rake +35 -0
- data/Manifest.txt +111 -0
- data/README.txt +44 -0
- data/Rakefile +284 -0
- data/TESTING.txt +57 -0
- data/TODO.txt +26 -0
- data/TUTORIAL.txt +390 -0
- data/bin/ai +3 -0
- data/bin/aifield +82 -0
- data/bin/aitag +128 -0
- data/bin/automateit +117 -0
- data/docs/friendly_errors.txt +50 -0
- data/docs/previews.txt +86 -0
- data/env.sh +4 -0
- data/examples/basic/Rakefile +26 -0
- data/examples/basic/config/automateit_env.rb +16 -0
- data/examples/basic/config/fields.yml +3 -0
- data/examples/basic/config/tags.yml +13 -0
- data/examples/basic/dist/README.txt +9 -0
- data/examples/basic/dist/myapp_server.erb +30 -0
- data/examples/basic/install.log +15 -0
- data/examples/basic/lib/README.txt +10 -0
- data/examples/basic/recipes/README.txt +4 -0
- data/examples/basic/recipes/install.rb +53 -0
- data/examples/basic/recipes/uninstall.rb +6 -0
- data/gpl.txt +674 -0
- data/lib/automateit.rb +66 -0
- data/lib/automateit/account_manager.rb +106 -0
- data/lib/automateit/account_manager/linux.rb +171 -0
- data/lib/automateit/account_manager/passwd.rb +69 -0
- data/lib/automateit/account_manager/portable.rb +136 -0
- data/lib/automateit/address_manager.rb +165 -0
- data/lib/automateit/address_manager/linux.rb +80 -0
- data/lib/automateit/address_manager/portable.rb +37 -0
- data/lib/automateit/cli.rb +80 -0
- data/lib/automateit/common.rb +65 -0
- data/lib/automateit/constants.rb +33 -0
- data/lib/automateit/edit_manager.rb +292 -0
- data/lib/automateit/error.rb +10 -0
- data/lib/automateit/field_manager.rb +103 -0
- data/lib/automateit/interpreter.rb +641 -0
- data/lib/automateit/package_manager.rb +242 -0
- data/lib/automateit/package_manager/apt.rb +63 -0
- data/lib/automateit/package_manager/egg.rb +64 -0
- data/lib/automateit/package_manager/gem.rb +179 -0
- data/lib/automateit/package_manager/portage.rb +69 -0
- data/lib/automateit/package_manager/yum.rb +65 -0
- data/lib/automateit/platform_manager.rb +47 -0
- data/lib/automateit/platform_manager/darwin.rb +30 -0
- data/lib/automateit/platform_manager/debian.rb +26 -0
- data/lib/automateit/platform_manager/freebsd.rb +25 -0
- data/lib/automateit/platform_manager/gentoo.rb +26 -0
- data/lib/automateit/platform_manager/lsb.rb +40 -0
- data/lib/automateit/platform_manager/struct.rb +78 -0
- data/lib/automateit/platform_manager/uname.rb +29 -0
- data/lib/automateit/platform_manager/windows.rb +33 -0
- data/lib/automateit/plugin.rb +7 -0
- data/lib/automateit/plugin/base.rb +32 -0
- data/lib/automateit/plugin/driver.rb +218 -0
- data/lib/automateit/plugin/manager.rb +232 -0
- data/lib/automateit/project.rb +460 -0
- data/lib/automateit/root.rb +14 -0
- data/lib/automateit/service_manager.rb +79 -0
- data/lib/automateit/service_manager/chkconfig.rb +39 -0
- data/lib/automateit/service_manager/rc_update.rb +37 -0
- data/lib/automateit/service_manager/sysv.rb +126 -0
- data/lib/automateit/service_manager/update_rcd.rb +35 -0
- data/lib/automateit/shell_manager.rb +261 -0
- data/lib/automateit/shell_manager/base_link.rb +67 -0
- data/lib/automateit/shell_manager/link.rb +24 -0
- data/lib/automateit/shell_manager/portable.rb +421 -0
- data/lib/automateit/shell_manager/symlink.rb +32 -0
- data/lib/automateit/shell_manager/which.rb +25 -0
- data/lib/automateit/tag_manager.rb +63 -0
- data/lib/automateit/tag_manager/struct.rb +101 -0
- data/lib/automateit/tag_manager/tag_parser.rb +91 -0
- data/lib/automateit/tag_manager/yaml.rb +29 -0
- data/lib/automateit/template_manager.rb +55 -0
- data/lib/automateit/template_manager/base.rb +172 -0
- data/lib/automateit/template_manager/erb.rb +17 -0
- data/lib/ext/metaclass.rb +17 -0
- data/lib/ext/object.rb +18 -0
- data/lib/hashcache.rb +22 -0
- data/lib/helpful_erb.rb +63 -0
- data/lib/nested_error.rb +33 -0
- data/lib/queued_logger.rb +68 -0
- data/lib/tempster.rb +239 -0
- data/misc/index_gem_repository.rb +303 -0
- data/misc/setup_egg.rb +12 -0
- data/misc/setup_gem_dependencies.sh +7 -0
- data/misc/setup_rubygems.sh +21 -0
- data/misc/which.cmd +6 -0
- data/spec/extras/automateit_service_sysv_test +50 -0
- data/spec/extras/scratch.rb +15 -0
- data/spec/extras/simple_recipe.rb +8 -0
- data/spec/integration/account_manager_spec.rb +218 -0
- data/spec/integration/address_manager_linux_spec.rb +119 -0
- data/spec/integration/address_manager_portable_spec.rb +30 -0
- data/spec/integration/cli_spec.rb +215 -0
- data/spec/integration/examples_spec.rb +54 -0
- data/spec/integration/examples_spec_editor.rb +71 -0
- data/spec/integration/package_manager_spec.rb +104 -0
- data/spec/integration/platform_manager_spec.rb +69 -0
- data/spec/integration/service_manager_sysv_spec.rb +115 -0
- data/spec/integration/shell_manager_spec.rb +471 -0
- data/spec/integration/template_manager_erb_spec.rb +31 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/unit/edit_manager_spec.rb +162 -0
- data/spec/unit/field_manager_spec.rb +79 -0
- data/spec/unit/hashcache_spec.rb +28 -0
- data/spec/unit/interpreter_spec.rb +98 -0
- data/spec/unit/platform_manager_spec.rb +44 -0
- data/spec/unit/plugins_spec.rb +253 -0
- data/spec/unit/tag_manager_spec.rb +189 -0
- data/spec/unit/template_manager_erb_spec.rb +137 -0
- metadata +249 -0
- 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,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
|