Class | AutomateIt::Interpreter |
In: |
lib/automateit/interpreter.rb
|
Parent: | Common |
The Interpreter runs AutomateIt commands.
The TUTORIAL.txt file provides hands-on examples for using the Interpreter.
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:
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.
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. |
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 388 def self.invoke(recipe, opts={}) opts[:project] ||= File.join(File.dirname(recipe), "..") AutomateIt.new(opts).invoke(recipe) end
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 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.
# 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.
# 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
Retrieve a params entry.
Example:
params[:foo] = "bar" # => "bar" get :foo # => "bar"
# 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.
# 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
# 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.
# 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 preview mode to value. See warnings in ShellManager to learn how to correctly write code for preview mode.
# File lib/automateit/interpreter.rb, line 285 def preview(value) self.preview = value end
Is Interpreter running in preview mode?
# 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
# 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"
# 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:
Options for internal use:
# 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 382 def superuser? euid.zero? end
Is Interpreter writing? This is the opposite of preview?.
# File lib/automateit/interpreter.rb, line 351 def writing? !preview? end