class AutomateIt::Interpreter
Interpreter
¶ ↑
The Interpreter
runs AutomateIt
commands.
The TUTORIAL.txt file provides hands-on examples for using the Interpreter
.
Aliased methods¶ ↑
The Interpreter
provides shortcut aliases for certain plugin commands.
For example, the following commands will run the same method:
shell_manager.sh "ls" sh "ls"
The full set of aliased methods:
-
chmod –
AutomateIt::ShellManager#chmod
-
chmod_R –
AutomateIt::ShellManager#chmod_R
-
chown –
AutomateIt::ShellManager#chown
-
chown_R –
AutomateIt::ShellManager#chown_R
-
chperm –
AutomateIt::ShellManager#chperm
-
edit –
AutomateIt::EditManager#edit
-
download –
AutomateIt::DownloadManager#download
-
hosts_tagged_with –
AutomateIt::TagManager#hosts_tagged_with
-
install –
AutomateIt::ShellManager#install
-
ln_sf –
AutomateIt::ShellManager#ln_sf
-
lookup –
AutomateIt::FieldManager#lookup
-
mkdir –
AutomateIt::ShellManager#mkdir
-
mkdir_p –
AutomateIt::ShellManager#mkdir_p
-
mktemp –
AutomateIt::ShellManager#mktemp
-
mktempdir –
AutomateIt::ShellManager#mktempdir
-
mktempdircd –
AutomateIt::ShellManager#mktempdircd
-
render –
AutomateIt::TemplateManager#render
-
rm_rf –
AutomateIt::ShellManager#rm_rf
-
rmdir –
AutomateIt::ShellManager#rmdir
-
tagged? –
AutomateIt::TagManager#tagged?
-
tags –
AutomateIt::TagManager#tags
-
tags_for –
AutomateIt::TagManager#tags_for
-
touch –
AutomateIt::ShellManager#touch
-
umask –
AutomateIt::ShellManager#umask
-
which –
AutomateIt::ShellManager#which
-
which! –
AutomateIt::ShellManager#which!
Embedding the Interpreter
¶ ↑
The AutomateIt
Interpreter
can be embedded inside a Ruby program:
require 'rubygems' require 'automateit' interpreter = AutomateIt.new # Use the interpreter as an object: interpreter.sh "ls -la" # Have it execute a recipe: interpreter.invoke "myrecipe.rb" # Or execute recipes within a block interpreter.instance_eval do puts superuser? sh "ls -la" end
See the include_in
and add_method_missing_to
methods for instructions on how to more easily dispatch commands from your program to the Interpreter
instance.
Attributes
The Interpreter
throws friendly error messages by default that make it easier to see what’s wrong with a recipe. These friendly messages display the cause, a snapshot of the problematic code, shortened paths, and only the relevant stack frames.
However, if there’s a bug in the AutomateIt
internals, these friendly messages may inadvertently hide the cause, and it may be necessary to turn them off to figure out what’s wrong.
To turn off friendly exceptions:
# From a recipe or the AutomateIt interactive shell: self.friendly_exceptions = false # For an embedded interpreter at instantiation: AutomateIt.new(:friendly_exceptions => false) # From the UNIX command line when invoking a recipe: automateit --trace myrecipe.rb
Access IRB instance from an interactive shell.
Set the QueuedLogger
instance for the Interpreter
.
Hash
of parameters to make available to the Interpreter
. Mostly useful when needing to pass arguments to an embedded Interpreter
before doing an instance_eval.
Plugin
instance that instantiated the Interpreter
.
Hash
of plugin tokens to plugin instances for this Interpreter
.
Project
path for this Interpreter
. If no path is available, nil.
Public Class Methods
Create an Interpreter
with the specified opts
and invoke the recipe
. The opts are passed to setup
for parsing.
# File lib/automateit/interpreter.rb, line 387 def self.invoke(recipe, opts={}) opts[:project] ||= File.join(File.dirname(recipe), "..") AutomateIt.new(opts).invoke(recipe) end
Public Instance Methods
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.
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:
# Rakefile require 'automateit' @ai = AutomateIt.new @ai.add_method_missing_to(self) task :default do puts preview? # Uses Interpreter#preview? sh "id" # Uses FileUtils#sh, not Interpreter#sh end
For situations where it’s necessary to override existing methods, such as the sh
call in the example, consider using include_in
.
# File lib/automateit/interpreter.rb, line 593 def add_method_missing_to(object) object.instance_variable_set(:@__automateit, self) chain = object.respond_to?(:method_missing) # 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. template = <<-HERE def method_missing<%=chain ? '_with_automateit' : ''%>(method, *args, &block) ### puts "mm+a(%s, %s)" % [method, args.inspect] if @__automateit.respond_to?(method) @__automateit.send(method, *args, &block) else <%-if chain-%> method_missing_without_automateit(method, *args, &block) <%-else-%> super <%-end-%> end end <%-if chain-%> @__method_missing_without_automateit = self.method(:method_missing) def method_missing_without_automateit(*args) ### puts "mm-a %s" % args.inspect @__method_missing_without_automateit.call(*args) end def method_missing(*args) ### puts "mm %s" % args.inspect method_missing_with_automateit(*args) end <%-end-%> HERE text = ::HelpfulERB.new(template).result(binding) object.instance_eval(text) end
Path of this project’s “dist” directory. If a project isn’t available or the directory doesn’t exist, this will throw a NotImplementedError.
# File lib/automateit/interpreter.rb, line 496 def dist if @project result = File.join(@project, "dist/") if File.directory?(result) return result else raise NotImplementedError.new("can't find dist directory at: #{result}") end else raise NotImplementedError.new("can't use dist without a project") end end
Return the effective user id.
# File lib/automateit/interpreter.rb, line 365 def euid begin return Process.euid rescue NoMethodError => e output = `id -u 2>&1` raise e unless output and $?.exitstatus.zero? begin return output.match(/(\d+)/)[1].to_i rescue IndexError raise e end end end
Does this platform provide euid (Effective User ID)?
# File lib/automateit/interpreter.rb, line 355 def euid? begin euid return true rescue return false end end
Retrieve a params
entry.
Example:
params[:foo] = "bar" # => "bar" get :foo # => "bar"
# File lib/automateit/interpreter.rb, line 539 def get(key) params[key.to_sym] end
Creates wrapper methods in object
to dispatch calls to an Interpreter
instance.
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.
For example, include an Interpreter
instance into a Rake session, which will override the FileUtils commands with AutomateIt
equivalents:
# Rakefile require 'automateit' @ai = AutomateIt.new @ai.include_in(self, %w(preview? sh)) # Include #preview? and #sh methods task :default do puts preview? # Uses Interpreter#preview? sh "id" # Uses Interpreter#sh, not FileUtils#sh cp "foo", "bar" # Uses FileUtils#cp, not Interpreter#cp end
For situations where you don’t want to override any existing methods, consider using add_method_missing_to
.
# File lib/automateit/interpreter.rb, line 562 def include_in(object, *methods) methods = [methods].flatten methods = unique_methods.reject{|t| t.to_s =~ /^_/} if methods.empty? object.instance_variable_set(:@__automateit, self) for method in methods object.instance_eval <<-HERE def #{method}(*args, &block) @__automateit.send(:#{method}, *args, &block) end HERE end end
Invoke the recipe
. The recipe may be expressed as a relative or fully qualified path. When invoked within a project, the recipe can also be the name of a recipe.
Example:
invoke "/tmp/recipe.rb" # Run "/tmp/recipe.rb" invoke "recipe.rb" # Run "./recipe.rb". If not found and in a # project, will try running "recipes/recipe.rb" invoke "recipe" # Run "recipes/recipe.rb" in a project
# File lib/automateit/interpreter.rb, line 401 def invoke(recipe) filenames = [recipe] filenames << File.join(project, "recipes", recipe) if project filenames << File.join(project, "recipes", recipe + ".rb") if project for filename in filenames log.debug(PNOTE+" invoking "+filename) if File.exists?(filename) data = File.read(filename) begin return instance_eval(data, filename, 1) rescue Exception => e if @friendly_exceptions # TODO Extract this routine and its companion in HelpfulERB # Capture initial stack in case we add a debug/breakpoint after this stack = caller # Extract trace for recipe after the Interpreter#invoke call preresult = [] for line in e.backtrace # Stop at the Interpreter#invoke call break if line == stack.first preresult << line end # Extract the recipe filename preresult.last.match(/^([^:]+):(\d+):in `invoke'/) recipe = $1 # Extract trace for most recent block result = [] for line in preresult # Ignore manager wrapper and dispatch methods next if line =~ %r{lib/automateit/.+manager\.rb:\d+:in `.+'$} result << line # Stop at the first mention of this recipe break if line =~ /^#{recipe}/ end # Extract line number if e.is_a?(SyntaxError) line_number = e.message.match(/^[^:]+:(\d+):/)[1].to_i else result.last.match(/^([^:]+):(\d+):in `invoke'/) line_number = $2.to_i end msg = "Problem with recipe '#{recipe}' at line #{line_number}\n" # Extract recipe text begin lines = File.read(recipe).split(/\n/) min = line_number - 7 min = 0 if min < 0 max = line_number + 1 max = lines.size if max > lines.size width = max.to_s.size for i in min..max n = i+1 marker = n == line_number ? "*" : "" msg << "\n%2s %#{width}i %s" % [marker, n, lines[i]] end msg << "\n" rescue Exception => e # Ignore end msg << "\n(#{e.exception.class}) #{e.message}" # Append shortened trace for line in result msg << "\n "+line end # Remove project path msg.gsub!(/#{@project}\/?/, '') if @project raise AutomateIt::Error.new(msg, e) else raise e end end end end raise Errno::ENOENT.new(recipe) end
Get or set the QueuedLogger
instance for the Interpreter
, a special wrapper around the Ruby Logger
.
# File lib/automateit/interpreter.rb, line 274 def log(value=nil) if value.nil? return defined?(@log) ? @log : nil else @log = value end end
Set noop (no-operation mode) to value
. Alias for preview
.
# File lib/automateit/interpreter.rb, line 325 def noop(value) self.noop = value end
Set noop (no-operation mode) to value
. Alias for preview=
.
# File lib/automateit/interpreter.rb, line 330 def noop=(value) self.preview = value end
Are we in noop (no-operation) mode? Alias for preview?
.
# File lib/automateit/interpreter.rb, line 335 def noop? preview? end
Set preview mode to value
. See warnings in ShellManager
to learn how to correctly write code for preview mode.
# File lib/automateit/interpreter.rb, line 284 def preview(value) self.preview = value end
Set preview mode to +value.
# File lib/automateit/interpreter.rb, line 320 def preview=(value) @preview = value end
Is Interpreter
running in preview mode?
# File lib/automateit/interpreter.rb, line 289 def preview? @preview end
Preview a block of custom commands. When in preview mode, displays the message
but doesn’t execute the block
. When not previewing, will execute the block and not display the message
.
For example:
preview_for("FOO") do puts "BAR" end
In preview mode, this displays:
=> FOO
When not previewing, displays:
BAR
# File lib/automateit/interpreter.rb, line 310 def preview_for(message, &block) if preview? log.info(message) :preview else block.call end end
Set value to share throughout the Interpreter
. Use this instead of globals so that different Interpreters don’t see each other’s variables. Creates a method that returns the value and also adds a params
entry.
Example:
set :asdf, 9 # => 9 asdf # => 9
This is best used for frequently-used variables, like paths. For infrequently-used variables, use lookup and params
. A good place to use the set
is in the Project’s config/automateit_env.rb
file so that paths are exposed to all recipes like this:
set :helpers, project+"/helpers"
# File lib/automateit/interpreter.rb, line 523 def set(key, value) key = key.to_sym params[key] = value eval <<-HERE def #{key} return params[:#{key}] end HERE value end
Setup the Interpreter
. This method is also called from Interpreter#new.
Options for users:
-
:verbosity – Alias for :log_level
-
:log_level – Log level to use, defaults to Logger::INFO.
-
:preview – Turn on preview mode, defaults to false.
-
:project –
Project
directory to use. -
:tags –
Array
of tags to add to this run.
Options for internal use:
-
:parent – Parent plugin instance.
-
:log –
QueuedLogger
instance. -
:guessed_project – Boolean of whether the project path was guessed. If guessed, won’t throw exceptions if project wasn’t found at the specified path. If not guessed, will throw exception in such a situation.
-
:friendly_exceptions – Throw user-friendly exceptions that make it easier to see errors in recipes, defaults to true.
AutomateIt::Common#setup
# File lib/automateit/interpreter.rb, line 140 def setup(opts={}) super(opts.merge(:interpreter => self)) self.params ||= {} if opts[:irb] @irb = opts[:irb] end if opts[:parent] @parent = opts[:parent] end if opts[:log] @log = opts[:log] elsif not defined?(@log) or @log.nil? @log = QueuedLogger.new($stdout) @log.level = Logger::INFO end if opts[:log_level] or opts[:verbosity] @log.level = opts[:log_level] || opts[:verbosity] end if opts[:preview].nil? # can be false self.preview = false unless preview? else self.preview = opts[:preview] end if opts[:friendly_exceptions].nil? @friendly_exceptions = true unless defined?(@friendly_exceptions) else @friendly_exceptions = opts[:friendly_exceptions] end # Instantiate core plugins so they're available to the project _instantiate_plugins # Add optional run-time tags tags.merge(opts[:tags]) if opts[:tags] if project_path = opts[:project] || ENV["AUTOMATEIT_PROJECT"] || ENV["AIP"] # Only load a project if we find its env file env_file = File.join(project_path, "config", "automateit_env.rb") if File.exists?(env_file) @project = File.expand_path(project_path) log.debug(PNOTE+"Loading project from path: #{@project}") lib_files = Dir[File.join(@project, "lib", "*.rb")] + Dir[File.join(@project, "lib", "**", "init.rb")] lib_files.each do |lib| log.debug(PNOTE+"Loading project library: #{lib}") invoke(lib) end tag_file = File.join(@project, "config", "tags.yml") if File.exists?(tag_file) log.debug(PNOTE+"Loading project tags: #{tag_file}") tag_manager[:yaml].setup(:file => tag_file) end field_file = File.join(@project, "config", "fields.yml") if File.exists?(field_file) log.debug(PNOTE+"Loading project fields: #{field_file}") field_manager[:yaml].setup(:file => field_file) end # Instantiate project's plugins so they're available to the environment _instantiate_plugins if File.exists?(env_file) log.debug(PNOTE+"Loading project env: #{env_file}") invoke(env_file) end elsif not opts[:guessed_project] raise ArgumentError.new("Couldn't find project at: #{project_path}") end end end
Does the current user have superuser (root) privileges?
# File lib/automateit/interpreter.rb, line 381 def superuser? euid.zero? end
Set writing to value
. This is the opposite of preview
.
# File lib/automateit/interpreter.rb, line 340 def writing(value) self.writing = value end
Set writing to value
. This is the opposite of preview=
.
# File lib/automateit/interpreter.rb, line 345 def writing=(value) self.preview = !value end
Is Interpreter
writing? This is the opposite of preview?
.
# File lib/automateit/interpreter.rb, line 350 def writing? !preview? end
Private Instance Methods
# File lib/automateit/interpreter.rb, line 252 def _expose_plugin_methods(plugin) return unless plugin.class.aliased_methods plugin.class.aliased_methods.each do |method| #puts "!!! epm #{method}" unless respond_to?(method.to_sym) # Must use instance_eval because methods created with define_method # can't accept block as argument. This is a known Ruby 1.8 bug. self.instance_eval <<-EOB def #{method}(*args, &block) @plugins[:#{plugin.class.token}].send(:#{method}, *args, &block) end EOB end end end
# File lib/automateit/interpreter.rb, line 236 def _instantiate_plugin(klass) token = klass.token unless plugin = @plugins[token] plugin = @plugins[token] = klass.new(:interpreter => self) #puts "!!! ip #{token}" unless respond_to?(token.to_sym) self.class.send(:define_method, token) do @plugins[token] end end _expose_plugin_methods(plugin) end plugin.instantiate_drivers end
# File lib/automateit/interpreter.rb, line 223 def _instantiate_plugins @plugins ||= {} # If a parent is defined, use it to prep the list and avoid re-instantiating it. if defined?(@parent) and @parent and Plugin::Manager === @parent @plugins[@parent.class.token] = @parent end plugin_classes = AutomateIt::Plugin::Manager.classes.reject{|t| t == @parent if @parent} for klass in plugin_classes _instantiate_plugin(klass) end end