automate-it 0.9.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 (137) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.hgignore +10 -0
  4. data/.loadpath +5 -0
  5. data/.project +17 -0
  6. data/CHANGES.txt +314 -0
  7. data/Hoe.rake +40 -0
  8. data/Manifest.txt +164 -0
  9. data/README.txt +40 -0
  10. data/Rakefile +256 -0
  11. data/TESTING.txt +57 -0
  12. data/TODO.txt +50 -0
  13. data/TUTORIAL.txt +391 -0
  14. data/automate-it.gemspec +25 -0
  15. data/bin/ai +3 -0
  16. data/bin/aifield +75 -0
  17. data/bin/aissh +93 -0
  18. data/bin/aitag +134 -0
  19. data/bin/automateit +133 -0
  20. data/docs/friendly_errors.txt +50 -0
  21. data/docs/previews.txt +86 -0
  22. data/examples/basic/Rakefile +26 -0
  23. data/examples/basic/config/automateit_env.rb +16 -0
  24. data/examples/basic/config/fields.yml +3 -0
  25. data/examples/basic/config/tags.yml +7 -0
  26. data/examples/basic/dist/README.txt +9 -0
  27. data/examples/basic/dist/myapp_server.erb +30 -0
  28. data/examples/basic/install.log +15 -0
  29. data/examples/basic/lib/README.txt +10 -0
  30. data/examples/basic/recipes/README.txt +4 -0
  31. data/examples/basic/recipes/install.rb +61 -0
  32. data/examples/basic/recipes/uninstall.rb +6 -0
  33. data/gpl.txt +674 -0
  34. data/helpers/cpan_wrapper.pl +220 -0
  35. data/helpers/which.cmd +7 -0
  36. data/lib/automateit.rb +55 -0
  37. data/lib/automateit/account_manager.rb +114 -0
  38. data/lib/automateit/account_manager/base.rb +138 -0
  39. data/lib/automateit/account_manager/etc.rb +128 -0
  40. data/lib/automateit/account_manager/nscd.rb +33 -0
  41. data/lib/automateit/account_manager/passwd_expect.rb +40 -0
  42. data/lib/automateit/account_manager/passwd_pty.rb +69 -0
  43. data/lib/automateit/account_manager/posix.rb +138 -0
  44. data/lib/automateit/address_manager.rb +88 -0
  45. data/lib/automateit/address_manager/base.rb +171 -0
  46. data/lib/automateit/address_manager/bsd.rb +28 -0
  47. data/lib/automateit/address_manager/freebsd.rb +59 -0
  48. data/lib/automateit/address_manager/linux.rb +42 -0
  49. data/lib/automateit/address_manager/openbsd.rb +66 -0
  50. data/lib/automateit/address_manager/portable.rb +37 -0
  51. data/lib/automateit/address_manager/sunos.rb +34 -0
  52. data/lib/automateit/cli.rb +85 -0
  53. data/lib/automateit/common.rb +65 -0
  54. data/lib/automateit/constants.rb +35 -0
  55. data/lib/automateit/download_manager.rb +48 -0
  56. data/lib/automateit/edit_manager.rb +321 -0
  57. data/lib/automateit/error.rb +10 -0
  58. data/lib/automateit/field_manager.rb +103 -0
  59. data/lib/automateit/interpreter.rb +631 -0
  60. data/lib/automateit/package_manager.rb +257 -0
  61. data/lib/automateit/package_manager/apt.rb +27 -0
  62. data/lib/automateit/package_manager/cpan.rb +101 -0
  63. data/lib/automateit/package_manager/dpkg.rb +54 -0
  64. data/lib/automateit/package_manager/egg.rb +64 -0
  65. data/lib/automateit/package_manager/gem.rb +201 -0
  66. data/lib/automateit/package_manager/pear.rb +95 -0
  67. data/lib/automateit/package_manager/pecl.rb +80 -0
  68. data/lib/automateit/package_manager/portage.rb +69 -0
  69. data/lib/automateit/package_manager/yum.rb +65 -0
  70. data/lib/automateit/platform_manager.rb +49 -0
  71. data/lib/automateit/platform_manager/darwin.rb +30 -0
  72. data/lib/automateit/platform_manager/debian.rb +26 -0
  73. data/lib/automateit/platform_manager/freebsd.rb +29 -0
  74. data/lib/automateit/platform_manager/gentoo.rb +26 -0
  75. data/lib/automateit/platform_manager/lsb.rb +44 -0
  76. data/lib/automateit/platform_manager/openbsd.rb +28 -0
  77. data/lib/automateit/platform_manager/struct.rb +80 -0
  78. data/lib/automateit/platform_manager/sunos.rb +39 -0
  79. data/lib/automateit/platform_manager/uname.rb +29 -0
  80. data/lib/automateit/platform_manager/windows.rb +40 -0
  81. data/lib/automateit/plugin.rb +7 -0
  82. data/lib/automateit/plugin/base.rb +32 -0
  83. data/lib/automateit/plugin/driver.rb +256 -0
  84. data/lib/automateit/plugin/manager.rb +224 -0
  85. data/lib/automateit/project.rb +493 -0
  86. data/lib/automateit/root.rb +17 -0
  87. data/lib/automateit/service_manager.rb +93 -0
  88. data/lib/automateit/service_manager/chkconfig.rb +39 -0
  89. data/lib/automateit/service_manager/rc_update.rb +37 -0
  90. data/lib/automateit/service_manager/sysv.rb +139 -0
  91. data/lib/automateit/service_manager/update_rcd.rb +35 -0
  92. data/lib/automateit/shell_manager.rb +316 -0
  93. data/lib/automateit/shell_manager/base_link.rb +67 -0
  94. data/lib/automateit/shell_manager/link.rb +24 -0
  95. data/lib/automateit/shell_manager/portable.rb +523 -0
  96. data/lib/automateit/shell_manager/symlink.rb +32 -0
  97. data/lib/automateit/shell_manager/which_base.rb +30 -0
  98. data/lib/automateit/shell_manager/which_unix.rb +16 -0
  99. data/lib/automateit/shell_manager/which_windows.rb +20 -0
  100. data/lib/automateit/tag_manager.rb +127 -0
  101. data/lib/automateit/tag_manager/struct.rb +121 -0
  102. data/lib/automateit/tag_manager/tag_parser.rb +93 -0
  103. data/lib/automateit/tag_manager/yaml.rb +29 -0
  104. data/lib/automateit/template_manager.rb +56 -0
  105. data/lib/automateit/template_manager/base.rb +181 -0
  106. data/lib/automateit/template_manager/erb.rb +17 -0
  107. data/lib/ext/metaclass.rb +17 -0
  108. data/lib/ext/object.rb +18 -0
  109. data/lib/ext/shell_escape.rb +7 -0
  110. data/lib/hashcache.rb +22 -0
  111. data/lib/helpful_erb.rb +63 -0
  112. data/lib/inactive_support.rb +53 -0
  113. data/lib/inactive_support/basic_object.rb +6 -0
  114. data/lib/inactive_support/clean_logger.rb +127 -0
  115. data/lib/inactive_support/core_ext/array/extract_options.rb +19 -0
  116. data/lib/inactive_support/core_ext/blank.rb +50 -0
  117. data/lib/inactive_support/core_ext/class/attribute_accessors.rb +48 -0
  118. data/lib/inactive_support/core_ext/class/inheritable_attributes.rb +140 -0
  119. data/lib/inactive_support/core_ext/enumerable.rb +63 -0
  120. data/lib/inactive_support/core_ext/hash/keys.rb +54 -0
  121. data/lib/inactive_support/core_ext/module/aliasing.rb +70 -0
  122. data/lib/inactive_support/core_ext/numeric/time.rb +91 -0
  123. data/lib/inactive_support/core_ext/string/inflections.rb +153 -0
  124. data/lib/inactive_support/core_ext/symbol.rb +14 -0
  125. data/lib/inactive_support/core_ext/time/conversions.rb +96 -0
  126. data/lib/inactive_support/duration.rb +96 -0
  127. data/lib/inactive_support/inflections.rb +53 -0
  128. data/lib/inactive_support/inflector.rb +282 -0
  129. data/lib/nested_error.rb +33 -0
  130. data/lib/nitpick.rb +33 -0
  131. data/lib/queued_logger.rb +68 -0
  132. data/lib/tempster.rb +250 -0
  133. data/misc/index_gem_repository.rb +304 -0
  134. data/misc/setup_egg.rb +12 -0
  135. data/misc/setup_gem_dependencies.sh +6 -0
  136. data/misc/setup_rubygems.sh +21 -0
  137. metadata +279 -0
@@ -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,631 @@
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
+ # * download -- AutomateIt::DownloadManager#download
31
+ # * hosts_tagged_with -- AutomateIt::TagManager#hosts_tagged_with
32
+ # * install -- AutomateIt::ShellManager#install
33
+ # * ln -- AutomateIt::ShellManager#ln
34
+ # * ln_s -- AutomateIt::ShellManager#ln_s
35
+ # * ln_sf -- AutomateIt::ShellManager#ln_sf
36
+ # * lookup -- AutomateIt::FieldManager#lookup
37
+ # * mkdir -- AutomateIt::ShellManager#mkdir
38
+ # * mkdir_p -- AutomateIt::ShellManager#mkdir_p
39
+ # * mktemp -- AutomateIt::ShellManager#mktemp
40
+ # * mktempdir -- AutomateIt::ShellManager#mktempdir
41
+ # * mktempdircd -- AutomateIt::ShellManager#mktempdircd
42
+ # * mv -- AutomateIt::ShellManager#mv
43
+ # * pwd -- AutomateIt::ShellManager#pwd
44
+ # * render -- AutomateIt::TemplateManager#render
45
+ # * rm -- AutomateIt::ShellManager#rm
46
+ # * rm_r -- AutomateIt::ShellManager#rm_r
47
+ # * rm_rf -- AutomateIt::ShellManager#rm_rf
48
+ # * rmdir -- AutomateIt::ShellManager#rmdir
49
+ # * sh -- AutomateIt::ShellManager#sh
50
+ # * tagged? -- AutomateIt::TagManager#tagged?
51
+ # * tags -- AutomateIt::TagManager#tags
52
+ # * tags_for -- AutomateIt::TagManager#tags_for
53
+ # * touch -- AutomateIt::ShellManager#touch
54
+ # * umask -- AutomateIt::ShellManager#umask
55
+ # * which -- AutomateIt::ShellManager#which
56
+ # * which! -- AutomateIt::ShellManager#which!
57
+ #
58
+ # === Embedding the Interpreter
59
+ #
60
+ # The AutomateIt Interpreter can be embedded inside a Ruby program:
61
+ #
62
+ # require 'rubygems'
63
+ # require 'automateit'
64
+ #
65
+ # interpreter = AutomateIt.new
66
+ #
67
+ # # Use the interpreter as an object:
68
+ # interpreter.sh "ls -la"
69
+ #
70
+ # # Have it execute a recipe:
71
+ # interpreter.invoke "myrecipe.rb"
72
+ #
73
+ # # Or execute recipes within a block
74
+ # interpreter.instance_eval do
75
+ # puts superuser?
76
+ # sh "ls -la"
77
+ # end
78
+ #
79
+ # See the #include_in and #add_method_missing_to methods for instructions on
80
+ # how to more easily dispatch commands from your program to the Interpreter
81
+ # instance.
82
+ class Interpreter < Common
83
+ include Nitpick
84
+
85
+ # Plugin instance that instantiated the Interpreter.
86
+ attr_accessor :parent
87
+ private :parent
88
+ private :parent=
89
+
90
+ # Access IRB instance from an interactive shell.
91
+ attr_accessor :irb
92
+
93
+ # Project path for this Interpreter. If no path is available, nil.
94
+ attr_accessor :project
95
+
96
+ # Hash of parameters to make available to the Interpreter. Mostly useful
97
+ # when needing to pass arguments to an embedded Interpreter before doing an
98
+ # #instance_eval.
99
+ attr_accessor :params
100
+
101
+ # The Interpreter throws friendly error messages by default that make it
102
+ # easier to see what's wrong with a recipe. These friendly messages display
103
+ # the cause, a snapshot of the problematic code, shortened paths, and only
104
+ # the relevant stack frames.
105
+ #
106
+ # However, if there's a bug in the AutomateIt internals, these friendly
107
+ # messages may inadvertently hide the cause, and it may be necessary to
108
+ # turn them off to figure out what's wrong.
109
+ #
110
+ # To turn off friendly exceptions:
111
+ #
112
+ # # From a recipe or the AutomateIt interactive shell:
113
+ # self.friendly_exceptions = false
114
+ #
115
+ # # For an embedded interpreter at instantiation:
116
+ # AutomateIt.new(:friendly_exceptions => false)
117
+ #
118
+ # # From the UNIX command line when invoking a recipe:
119
+ # automateit --trace myrecipe.rb
120
+ attr_accessor :friendly_exceptions
121
+
122
+ # Setup the Interpreter. This method is also called from Interpreter#new.
123
+ #
124
+ # Options for users:
125
+ # * :verbosity -- Alias for :log_level
126
+ # * :log_level -- Log level to use, defaults to Logger::INFO.
127
+ # * :preview -- Turn on preview mode, defaults to false.
128
+ # * :project -- Project directory to use.
129
+ # * :tags -- Array of tags to add to this run.
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
+ # * :friendly_exceptions -- Throw user-friendly exceptions that make it
139
+ # easier to see errors in recipes, defaults to true.
140
+ def setup(opts={})
141
+ super(opts.merge(:interpreter => self))
142
+
143
+ self.params ||= {}
144
+
145
+ if opts[:irb]
146
+ @irb = opts[:irb]
147
+ end
148
+
149
+ if opts[:parent]
150
+ @parent = opts[:parent]
151
+ end
152
+
153
+ if opts[:log]
154
+ @log = opts[:log]
155
+ elsif not defined?(@log) or @log.nil?
156
+ @log = QueuedLogger.new($stdout)
157
+ @log.level = Logger::INFO
158
+ end
159
+
160
+ if opts[:log_level] or opts[:verbosity]
161
+ @log.level = opts[:log_level] || opts[:verbosity]
162
+ end
163
+
164
+ if opts[:preview].nil? # can be false
165
+ self.preview = false unless preview?
166
+ else
167
+ self.preview = opts[:preview]
168
+ end
169
+
170
+ if opts[:friendly_exceptions].nil?
171
+ @friendly_exceptions = true unless defined?(@friendly_exceptions)
172
+ else
173
+ @friendly_exceptions = opts[:friendly_exceptions]
174
+ end
175
+
176
+ # Instantiate core plugins so they're available to the project
177
+ _instantiate_plugins
178
+
179
+ # Add optional run-time tags
180
+ tags.merge(opts[:tags]) if opts[:tags]
181
+
182
+ if project_path = opts[:project] || ENV["AUTOMATEIT_PROJECT"] || ENV["AIP"]
183
+ # Only load a project if we find its env file
184
+ env_file = File.join(project_path, "config", "automateit_env.rb")
185
+ if File.exists?(env_file)
186
+ @project = File.expand_path(project_path)
187
+ log.debug(PNOTE+"Loading project from path: #{@project}")
188
+
189
+ lib_files = Dir[File.join(@project, "lib", "*.rb")] + Dir[File.join(@project, "lib", "**", "init.rb")]
190
+ lib_files.each do |lib|
191
+ log.debug(PNOTE+"Loading project library: #{lib}")
192
+ invoke(lib)
193
+ end
194
+
195
+ tag_file = File.join(@project, "config", "tags.yml")
196
+ if File.exists?(tag_file)
197
+ log.debug(PNOTE+"Loading project tags: #{tag_file}")
198
+ tag_manager[:yaml].setup(:file => tag_file)
199
+ end
200
+
201
+ field_file = File.join(@project, "config", "fields.yml")
202
+ if File.exists?(field_file)
203
+ log.debug(PNOTE+"Loading project fields: #{field_file}")
204
+ field_manager[:yaml].setup(:file => field_file)
205
+ end
206
+
207
+ # Instantiate project's plugins so they're available to the environment
208
+ _instantiate_plugins
209
+
210
+ if File.exists?(env_file)
211
+ log.debug(PNOTE+"Loading project env: #{env_file}")
212
+ invoke(env_file)
213
+ end
214
+ elsif not opts[:guessed_project]
215
+ raise ArgumentError.new("Couldn't find project at: #{project_path}")
216
+ end
217
+ end
218
+ end
219
+
220
+ # Hash of plugin tokens to plugin instances for this Interpreter.
221
+ attr_accessor :plugins
222
+
223
+ def _instantiate_plugins
224
+ @plugins ||= {}
225
+ # If a parent is defined, use it to prep the list and avoid re-instantiating it.
226
+ if defined?(@parent) and @parent and Plugin::Manager === @parent
227
+ @plugins[@parent.class.token] = @parent
228
+ end
229
+ plugin_classes = AutomateIt::Plugin::Manager.classes.reject{|t| t == @parent if @parent}
230
+ for klass in plugin_classes
231
+ _instantiate_plugin(klass)
232
+ end
233
+ end
234
+ private :_instantiate_plugins
235
+
236
+ def _instantiate_plugin(klass)
237
+ token = klass.token
238
+ unless plugin = @plugins[token]
239
+ plugin = @plugins[token] = klass.new(:interpreter => self)
240
+ #puts "!!! ip #{token}"
241
+ unless respond_to?(token.to_sym)
242
+ self.class.send(:define_method, token) do
243
+ @plugins[token]
244
+ end
245
+ end
246
+ _expose_plugin_methods(plugin)
247
+ end
248
+ plugin.instantiate_drivers
249
+ end
250
+ private :_instantiate_plugin
251
+
252
+ def _expose_plugin_methods(plugin)
253
+ return unless plugin.class.aliased_methods
254
+ plugin.class.aliased_methods.each do |method|
255
+ #puts "!!! epm #{method}"
256
+ unless respond_to?(method.to_sym)
257
+ # Must use instance_eval because methods created with define_method
258
+ # can't accept block as argument. This is a known Ruby 1.8 bug.
259
+ self.instance_eval <<-EOB
260
+ def #{method}(*args, &block)
261
+ @plugins[:#{plugin.class.token}].send(:#{method}, *args, &block)
262
+ end
263
+ EOB
264
+ end
265
+ end
266
+ end
267
+ private :_expose_plugin_methods
268
+
269
+ # Set the QueuedLogger instance for the Interpreter.
270
+ attr_writer :log
271
+
272
+ # Get or set the QueuedLogger instance for the Interpreter, a special
273
+ # wrapper around the Ruby Logger.
274
+ def log(value=nil)
275
+ if value.nil?
276
+ return defined?(@log) ? @log : nil
277
+ else
278
+ @log = value
279
+ end
280
+ end
281
+
282
+ # Set preview mode to +value+. See warnings in ShellManager to learn how to
283
+ # correctly write code for preview mode.
284
+ def preview(value)
285
+ self.preview = value
286
+ end
287
+
288
+ # Is Interpreter running in preview mode?
289
+ def preview?
290
+ @preview
291
+ end
292
+
293
+ # Preview a block of custom commands. When in preview mode, displays the
294
+ # +message+ but doesn't execute the +block+. When not previewing, will
295
+ # execute the block and not display the +message+.
296
+ #
297
+ # For example:
298
+ #
299
+ # preview_for("FOO") do
300
+ # puts "BAR"
301
+ # end
302
+ #
303
+ # In preview mode, this displays:
304
+ #
305
+ # => FOO
306
+ #
307
+ # When not previewing, displays:
308
+ #
309
+ # BAR
310
+ def preview_for(message, &block)
311
+ if preview?
312
+ log.info(message)
313
+ :preview
314
+ else
315
+ block.call
316
+ end
317
+ end
318
+
319
+ # Set preview mode to +value.
320
+ def preview=(value)
321
+ @preview = value
322
+ end
323
+
324
+ # Set noop (no-operation mode) to +value+. Alias for #preview.
325
+ def noop(value)
326
+ self.noop = value
327
+ end
328
+
329
+ # Set noop (no-operation mode) to +value+. Alias for #preview=.
330
+ def noop=(value)
331
+ self.preview = value
332
+ end
333
+
334
+ # Are we in noop (no-operation) mode? Alias for #preview?.
335
+ def noop?
336
+ preview?
337
+ end
338
+
339
+ # Set writing to +value+. This is the opposite of #preview.
340
+ def writing(value)
341
+ self.writing = value
342
+ end
343
+
344
+ # Set writing to +value+. This is the opposite of #preview=.
345
+ def writing=(value)
346
+ self.preview = !value
347
+ end
348
+
349
+ # Is Interpreter writing? This is the opposite of #preview?.
350
+ def writing?
351
+ !preview?
352
+ end
353
+
354
+ # Does this platform provide euid (Effective User ID)?
355
+ def euid?
356
+ begin
357
+ euid
358
+ return true
359
+ rescue
360
+ return false
361
+ end
362
+ end
363
+
364
+ # Return the effective user id.
365
+ def euid
366
+ begin
367
+ return Process.euid
368
+ rescue NoMethodError => e
369
+ output = `id -u 2>&1`
370
+ raise e unless output and $?.exitstatus.zero?
371
+ begin
372
+ return output.match(/(\d+)/)[1].to_i
373
+ rescue IndexError
374
+ raise e
375
+ end
376
+ end
377
+
378
+ end
379
+
380
+ # Does the current user have superuser (root) privileges?
381
+ def superuser?
382
+ euid.zero?
383
+ end
384
+
385
+ # Create an Interpreter with the specified +opts+ and invoke
386
+ # the +recipe+. The opts are passed to #setup for parsing.
387
+ def self.invoke(recipe, opts={})
388
+ opts[:project] ||= File.join(File.dirname(recipe), "..")
389
+ AutomateIt.new(opts).invoke(recipe)
390
+ end
391
+
392
+ # Invoke the +recipe+. The recipe may be expressed as a relative or fully
393
+ # qualified path. When invoked within a project, the recipe can also be the
394
+ # name of a recipe.
395
+ #
396
+ # Example:
397
+ # invoke "/tmp/recipe.rb" # Run "/tmp/recipe.rb"
398
+ # invoke "recipe.rb" # Run "./recipe.rb". If not found and in a
399
+ # # project, will try running "recipes/recipe.rb"
400
+ # invoke "recipe" # Run "recipes/recipe.rb" in a project
401
+ def invoke(recipe)
402
+ filenames = [recipe]
403
+ filenames << File.join(project, "recipes", recipe) if project
404
+ filenames << File.join(project, "recipes", recipe + ".rb") if project
405
+
406
+ for filename in filenames
407
+ log.debug(PNOTE+" invoking "+filename)
408
+ if File.exists?(filename)
409
+ data = File.read(filename)
410
+ begin
411
+ return instance_eval(data, filename, 1)
412
+ rescue Exception => e
413
+ if @friendly_exceptions
414
+ # TODO Extract this routine and its companion in HelpfulERB
415
+
416
+ # Capture initial stack in case we add a debug/breakpoint after this
417
+ stack = caller
418
+
419
+ # Extract trace for recipe after the Interpreter#invoke call
420
+ preresult = []
421
+ for line in e.backtrace
422
+ # Stop at the Interpreter#invoke call
423
+ break if line == stack.first
424
+ preresult << line
425
+ end
426
+
427
+ # Extract the recipe filename
428
+ preresult.last.match(/^([^:]+):(\d+):in `invoke'/)
429
+ recipe = $1
430
+
431
+ # Extract trace for most recent block
432
+ result = []
433
+ for line in preresult
434
+ # Ignore manager wrapper and dispatch methods
435
+ next if line =~ %r{lib/automateit/.+manager\.rb:\d+:in `.+'$}
436
+ result << line
437
+ # Stop at the first mention of this recipe
438
+ break if line =~ /^#{recipe}/
439
+ end
440
+
441
+ # Extract line number
442
+ if e.is_a?(SyntaxError)
443
+ line_number = e.message.match(/^[^:]+:(\d+):/)[1].to_i
444
+ else
445
+ result.last.match(/^([^:]+):(\d+):in `invoke'/)
446
+ line_number = $2.to_i
447
+ end
448
+
449
+ msg = "Problem with recipe '#{recipe}' at line #{line_number}\n"
450
+
451
+ # Extract recipe text
452
+ begin
453
+ lines = File.read(recipe).split(/\n/)
454
+
455
+ min = line_number - 7
456
+ min = 0 if min < 0
457
+
458
+ max = line_number + 1
459
+ max = lines.size if max > lines.size
460
+
461
+ width = max.to_s.size
462
+
463
+ for i in min..max
464
+ n = i+1
465
+ marker = n == line_number ? "*" : ""
466
+ msg << "\n%2s %#{width}i %s" % [marker, n, lines[i]]
467
+ end
468
+
469
+ msg << "\n"
470
+ rescue Exception => e
471
+ # Ignore
472
+ end
473
+
474
+ msg << "\n(#{e.exception.class}) #{e.message}"
475
+
476
+ # Append shortened trace
477
+ for line in result
478
+ msg << "\n "+line
479
+ end
480
+
481
+ # Remove project path
482
+ msg.gsub!(/#{@project}\/?/, '') if @project
483
+
484
+ raise AutomateIt::Error.new(msg, e)
485
+ else
486
+ raise e
487
+ end
488
+ end
489
+ end
490
+ end
491
+ raise Errno::ENOENT.new(recipe)
492
+ end
493
+
494
+ # Path of this project's "dist" directory. If a project isn't available or
495
+ # the directory doesn't exist, this will throw a NotImplementedError.
496
+ def dist
497
+ if @project
498
+ result = File.join(@project, "dist/")
499
+ if File.directory?(result)
500
+ return result
501
+ else
502
+ raise NotImplementedError.new("can't find dist directory at: #{result}")
503
+ end
504
+ else
505
+ raise NotImplementedError.new("can't use dist without a project")
506
+ end
507
+ end
508
+
509
+ # Set value to share throughout the Interpreter. Use this instead of
510
+ # globals so that different Interpreters don't see each other's variables.
511
+ # Creates a method that returns the value and also adds a #params entry.
512
+ #
513
+ # Example:
514
+ # set :asdf, 9 # => 9
515
+ # asdf # => 9
516
+ #
517
+ # This is best used for frequently-used variables, like paths. For
518
+ # infrequently-used variables, use #lookup and #params. A good place to use
519
+ # the #set is in the Project's <tt>config/automateit_env.rb</tt> file so
520
+ # that paths are exposed to all recipes like this:
521
+ #
522
+ # set :helpers, project+"/helpers"
523
+ def set(key, value)
524
+ key = key.to_sym
525
+ params[key] = value
526
+ eval <<-HERE
527
+ def #{key}
528
+ return params[:#{key}]
529
+ end
530
+ HERE
531
+ value
532
+ end
533
+
534
+ # Retrieve a #params entry.
535
+ #
536
+ # Example:
537
+ # params[:foo] = "bar" # => "bar"
538
+ # get :foo # => "bar"
539
+ def get(key)
540
+ params[key.to_sym]
541
+ end
542
+
543
+ # Creates wrapper methods in +object+ to dispatch calls to an Interpreter instance.
544
+ #
545
+ # *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.
546
+ #
547
+ # For example, include an Interpreter instance into a Rake session, which will override the FileUtils commands with AutomateIt equivalents:
548
+ #
549
+ # # Rakefile
550
+ #
551
+ # require 'automateit'
552
+ # @ai = AutomateIt.new
553
+ # @ai.include_in(self, %w(preview? sh)) # Include #preview? and #sh methods
554
+ #
555
+ # task :default do
556
+ # puts preview? # Uses Interpreter#preview?
557
+ # sh "id" # Uses Interpreter#sh, not FileUtils#sh
558
+ # cp "foo", "bar" # Uses FileUtils#cp, not Interpreter#cp
559
+ # end
560
+ #
561
+ # For situations where you don't want to override any existing methods, consider using #add_method_missing_to.
562
+ def include_in(object, *methods)
563
+ methods = [methods].flatten
564
+ methods = unique_methods.reject{|t| t.to_s =~ /^_/} if methods.empty?
565
+
566
+ object.instance_variable_set(:@__automateit, self)
567
+
568
+ for method in methods
569
+ object.instance_eval <<-HERE
570
+ def #{method}(*args, &block)
571
+ @__automateit.send(:#{method}, *args, &block)
572
+ end
573
+ HERE
574
+ end
575
+ end
576
+
577
+ # 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.
578
+ #
579
+ # 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:
580
+ #
581
+ # # Rakefile
582
+ #
583
+ # require 'automateit'
584
+ # @ai = AutomateIt.new
585
+ # @ai.add_method_missing_to(self)
586
+ #
587
+ # task :default do
588
+ # puts preview? # Uses Interpreter#preview?
589
+ # sh "id" # Uses FileUtils#sh, not Interpreter#sh
590
+ # end
591
+ #
592
+ # For situations where it's necessary to override existing methods, such as the +sh+ call in the example, consider using #include_in.
593
+ def add_method_missing_to(object)
594
+ object.instance_variable_set(:@__automateit, self)
595
+ chain = object.respond_to?(:method_missing)
596
+
597
+ # 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.
598
+
599
+ template = <<-HERE
600
+ def method_missing<%=chain ? '_with_automateit' : ''%>(method, *args, &block)
601
+ ### puts "mm+a(%s, %s)" % [method, args.inspect]
602
+ if @__automateit.respond_to?(method)
603
+ @__automateit.send(method, *args, &block)
604
+ else
605
+ <%-if chain-%>
606
+ method_missing_without_automateit(method, *args, &block)
607
+ <%-else-%>
608
+ super
609
+ <%-end-%>
610
+ end
611
+ end
612
+ <%-if chain-%>
613
+ @__method_missing_without_automateit = self.method(:method_missing)
614
+
615
+ def method_missing_without_automateit(*args)
616
+ ### puts "mm-a %s" % args.inspect
617
+ @__method_missing_without_automateit.call(*args)
618
+ end
619
+
620
+ def method_missing(*args)
621
+ ### puts "mm %s" % args.inspect
622
+ method_missing_with_automateit(*args)
623
+ end
624
+ <%-end-%>
625
+ HERE
626
+
627
+ text = ::HelpfulERB.new(template).result(binding)
628
+ object.instance_eval(text)
629
+ end
630
+ end
631
+ end