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:

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

friendly_exceptions[RW]

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
irb[RW]

Access IRB instance from an interactive shell.

log[W]

Set the QueuedLogger instance for the Interpreter.

params[RW]

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.

parent[RW]

Plugin instance that instantiated the Interpreter.

plugins[RW]

Hash of plugin tokens to plugin instances for this Interpreter.

project[RW]

Project path for this Interpreter. If no path is available, nil.

Public Class Methods

invoke(recipe, opts={}) click to toggle source

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

add_method_missing_to(object) click to toggle source

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
dist() click to toggle source

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
euid() click to toggle source

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
euid?() click to toggle source

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
get(key) click to toggle source

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
include_in(object, *methods) click to toggle source

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(recipe) click to toggle source

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
log(value=nil) click to toggle source

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
noop(value) click to toggle source

Set noop (no-operation mode) to value. Alias for preview.

# File lib/automateit/interpreter.rb, line 325
def noop(value)
  self.noop = value
end
noop=(value) click to toggle source

Set noop (no-operation mode) to value. Alias for preview=.

# File lib/automateit/interpreter.rb, line 330
def noop=(value)
  self.preview = value
end
noop?() click to toggle source

Are we in noop (no-operation) mode? Alias for preview?.

# File lib/automateit/interpreter.rb, line 335
def noop?
  preview?
end
preview(value) click to toggle source

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
preview=(value) click to toggle source

Set preview mode to +value.

# File lib/automateit/interpreter.rb, line 320
def preview=(value)
  @preview = value
end
preview?() click to toggle source

Is Interpreter running in preview mode?

# File lib/automateit/interpreter.rb, line 289
def preview?
  @preview
end
preview_for(message, &block) click to toggle source

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(key, value) click to toggle source

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(opts={}) click to toggle source

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.

Calls superclass method 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
superuser?() click to toggle source

Does the current user have superuser (root) privileges?

# File lib/automateit/interpreter.rb, line 381
def superuser?
  euid.zero?
end
writing(value) click to toggle source

Set writing to value. This is the opposite of preview.

# File lib/automateit/interpreter.rb, line 340
def writing(value)
  self.writing = value
end
writing=(value) click to toggle source

Set writing to value. This is the opposite of preview=.

# File lib/automateit/interpreter.rb, line 345
def writing=(value)
  self.preview = !value
end
writing?() click to toggle source

Is Interpreter writing? This is the opposite of preview?.

# File lib/automateit/interpreter.rb, line 350
def writing?
  !preview?
end

Private Instance Methods

_expose_plugin_methods(plugin) click to toggle source
# 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
_instantiate_plugin(klass) click to toggle source
# 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
_instantiate_plugins() click to toggle source
# 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