automateit 0.70923

Sign up to get free protection for your applications and to get access to all the features.
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