Class AutomateIt::Interpreter
In: lib/automateit/interpreter.rb
Parent: Common

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:

  • cd — AutomateIt::ShellManager#cd
  • chmod — AutomateIt::ShellManager#chmod
  • chmod_R — AutomateIt::ShellManager#chmod_R
  • chown — AutomateIt::ShellManager#chown
  • chown_R — AutomateIt::ShellManager#chown_R
  • chperm — AutomateIt::ShellManager#chperm
  • cp — AutomateIt::ShellManager#cp
  • cp_r — AutomateIt::ShellManager#cp_r
  • edit — AutomateIt::EditManager#edit
  • download — AutomateIt::DownloadManager#download
  • hosts_tagged_with — AutomateIt::TagManager#hosts_tagged_with
  • install — AutomateIt::ShellManager#install
  • ln — AutomateIt::ShellManager#ln
  • ln_s — AutomateIt::ShellManager#ln_s
  • 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
  • mv — AutomateIt::ShellManager#mv
  • pwd — AutomateIt::ShellManager#pwd
  • render — AutomateIt::TemplateManager#render
  • rm — AutomateIt::ShellManager#rm
  • rm_r — AutomateIt::ShellManager#rm_r
  • rm_rf — AutomateIt::ShellManager#rm_rf
  • rmdir — AutomateIt::ShellManager#rmdir
  • sh — AutomateIt::ShellManager#sh
  • 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.

Methods

add_method_missing_to   dist   euid   euid?   get   include_in   invoke   invoke   log   noop   noop=   noop?   preview   preview=   preview?   preview_for   set   setup   superuser?   writing   writing=   writing?  

Included Modules

Nitpick

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.
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

Create an Interpreter with the specified opts and invoke the recipe. The opts are passed to setup for parsing.

[Source]

# File lib/automateit/interpreter.rb, line 388
    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.

[Source]

# File lib/automateit/interpreter.rb, line 605
    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 = "def method_missing<%=chain ? '_with_automateit' : ''%>(method, *args, &block)\n### puts \"mm+a(%s, %s)\" % [method, args.inspect]\nif @__automateit.respond_to?(method)\n@__automateit.send(method, *args, &block)\nelse\n<%-if chain-%>\nmethod_missing_without_automateit(method, *args, &block)\n<%-else-%>\nsuper\n<%-end-%>\nend\nend\n<%-if chain-%>\n@__method_missing_without_automateit = self.method(:method_missing)\n\ndef method_missing_without_automateit(*args)\n### puts \"mm-a %s\" % args.inspect\n@__method_missing_without_automateit.call(*args)\nend\n\ndef method_missing(*args)\n### puts \"mm %s\" % args.inspect\nmethod_missing_with_automateit(*args)\nend\n<%-end-%>\n"

      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.

[Source]

# File lib/automateit/interpreter.rb, line 506
    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.

[Source]

# File lib/automateit/interpreter.rb, line 366
    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)?

[Source]

# File lib/automateit/interpreter.rb, line 356
    def euid?
      begin
        euid
        return true
      rescue
        return false
      end
    end

Retrieve a params entry.

Example:

 params[:foo] = "bar"  # => "bar"
 get :foo              # => "bar"

[Source]

# File lib/automateit/interpreter.rb, line 550
    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.

[Source]

# File lib/automateit/interpreter.rb, line 573
    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 "def \#{method}(*args, &block)\n@__automateit.send(:\#{method}, *args, &block)\nend\n"
      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

[Source]

# File lib/automateit/interpreter.rb, line 402
    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 to another helper
              # TODO Figure out if we can steal the Rails equivalent of this complex code

              # Capture initial stack in case we add a debug/breakpoint after this
              stack = caller

              # Extract trace for recipe after the Interpreter#invoke call
              preresult = []
              match_interpreter = %r{.*/lib/automateit/interpreter.rb:\d+:in .*}
              began_interpreter = false
              ended_interpreter = false
              e.backtrace.reverse.each do |line|
                unless began_interpreter
                  began_interpreter |= line =~ match_interpreter
                end
                if began_interpreter and not ended_interpreter
                  ended_interpreter |= line !~ match_interpreter
                end
                #IK# p [began_interpreter, ended_interpreter, line]
                preresult.unshift(line) if began_interpreter && ended_interpreter
              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}:\d+:in `invoke'/
              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.

[Source]

# File lib/automateit/interpreter.rb, line 275
    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.

[Source]

# File lib/automateit/interpreter.rb, line 326
    def noop(value)
      self.noop = value
    end

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

[Source]

# File lib/automateit/interpreter.rb, line 331
    def noop=(value)
      self.preview = value
    end

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

[Source]

# File lib/automateit/interpreter.rb, line 336
    def noop?
      preview?
    end

Set preview mode to value. See warnings in ShellManager to learn how to correctly write code for preview mode.

[Source]

# File lib/automateit/interpreter.rb, line 285
    def preview(value)
      self.preview = value
    end

Set preview mode to +value.

[Source]

# File lib/automateit/interpreter.rb, line 321
    def preview=(value)
      @preview = value
    end

Is Interpreter running in preview mode?

[Source]

# File lib/automateit/interpreter.rb, line 290
    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

[Source]

# File lib/automateit/interpreter.rb, line 311
    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"

[Source]

# File lib/automateit/interpreter.rb, line 533
    def set(key, value)
      key = key.to_sym
      params[key] = value
      eval "def \#{key}\nreturn params[:\#{key}]\nend\n"
      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.
  • :logQueuedLogger 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.

[Source]

# 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?

[Source]

# File lib/automateit/interpreter.rb, line 382
    def superuser?
      euid.zero?
    end

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

[Source]

# File lib/automateit/interpreter.rb, line 341
    def writing(value)
      self.writing = value
    end

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

[Source]

# File lib/automateit/interpreter.rb, line 346
    def writing=(value)
      self.preview = !value
    end

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

[Source]

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

[Validate]