Class: AutomateIt::Interpreter

Inherits:
Common show all
Includes:
Nitpick
Defined in:
lib/automateit/interpreter.rb

Overview

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.

Constant Summary

Constants included from Constants

Constants::HELPERS_DIR, Constants::INSTALL_DIR, Constants::PERROR, Constants::PEXEC, Constants::PNOTE, Constants::WARNING_BOILERPLATE

Instance Attribute Summary collapse

Attributes inherited from Common

#interpreter

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Nitpick

included

Methods included from Nitpick::ClassMethods

#nitpick

Methods inherited from Common

#initialize, #nitpick

Constructor Details

This class inherits a constructor from AutomateIt::Common

Instance Attribute Details

#friendly_exceptionsObject

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


120
121
122
# File 'lib/automateit/interpreter.rb', line 120

def friendly_exceptions
  @friendly_exceptions
end

#irbObject

Access IRB instance from an interactive shell.



91
92
93
# File 'lib/automateit/interpreter.rb', line 91

def irb
  @irb
end

#log(value = nil) ⇒ Object

Get or set the QueuedLogger instance for the Interpreter, a special wrapper around the Ruby Logger.



274
275
276
277
278
279
280
# File 'lib/automateit/interpreter.rb', line 274

def log(value=nil)
  if value.nil?
    return defined?(@log) ? @log : nil
  else
    @log = value
  end
end

#paramsObject

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.



99
100
101
# File 'lib/automateit/interpreter.rb', line 99

def params
  @params
end

#pluginsObject

Hash of plugin tokens to plugin instances for this Interpreter.



221
222
223
# File 'lib/automateit/interpreter.rb', line 221

def plugins
  @plugins
end

#projectObject

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



94
95
96
# File 'lib/automateit/interpreter.rb', line 94

def project
  @project
end

Class Method Details

.invoke(recipe, opts = {}) ⇒ Object

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



387
388
389
390
# 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

Instance Method Details

#add_method_missing_to(object) ⇒ Object

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.



593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# 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

#distObject

Path of this project’s “dist” directory. If a project isn’t available or the directory doesn’t exist, this will throw a NotImplementedError.



496
497
498
499
500
501
502
503
504
505
506
507
# 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

#euidObject

Return the effective user id.



365
366
367
368
369
370
371
372
373
374
375
376
377
378
# 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?Boolean

Does this platform provide euid (Effective User ID)?

Returns:

  • (Boolean)


355
356
357
358
359
360
361
362
# File 'lib/automateit/interpreter.rb', line 355

def euid?
  begin
    euid
    return true
  rescue
    return false
  end
end

#get(key) ⇒ Object

Retrieve a #params entry.

Example:

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


539
540
541
# File 'lib/automateit/interpreter.rb', line 539

def get(key)
  params[key.to_sym]
end

#include_in(object, *methods) ⇒ Object

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.



562
563
564
565
566
567
568
569
570
571
572
573
574
575
# 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) ⇒ Object

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

Raises:

  • (Errno::ENOENT)


401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# 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

#noop(value) ⇒ Object

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



325
326
327
# File 'lib/automateit/interpreter.rb', line 325

def noop(value)
  self.noop = value
end

#noop=(value) ⇒ Object

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



330
331
332
# File 'lib/automateit/interpreter.rb', line 330

def noop=(value)
  self.preview = value
end

#noop?Boolean

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

Returns:

  • (Boolean)


335
336
337
# File 'lib/automateit/interpreter.rb', line 335

def noop?
  preview?
end

#preview(value) ⇒ Object

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



284
285
286
# File 'lib/automateit/interpreter.rb', line 284

def preview(value)
  self.preview = value
end

#preview=(value) ⇒ Object

Set preview mode to +value.



320
321
322
# File 'lib/automateit/interpreter.rb', line 320

def preview=(value)
  @preview = value
end

#preview?Boolean

Is Interpreter running in preview mode?

Returns:

  • (Boolean)


289
290
291
# File 'lib/automateit/interpreter.rb', line 289

def preview?
  @preview
end

#preview_for(message, &block) ⇒ Object

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


310
311
312
313
314
315
316
317
# 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) ⇒ Object

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"


523
524
525
526
527
528
529
530
531
532
# 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 = {}) ⇒ Object

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.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# 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?Boolean

Does the current user have superuser (root) privileges?

Returns:

  • (Boolean)


381
382
383
# File 'lib/automateit/interpreter.rb', line 381

def superuser?
  euid.zero?
end

#writing(value) ⇒ Object

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



340
341
342
# File 'lib/automateit/interpreter.rb', line 340

def writing(value)
  self.writing = value
end

#writing=(value) ⇒ Object

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



345
346
347
# File 'lib/automateit/interpreter.rb', line 345

def writing=(value)
  self.preview = !value
end

#writing?Boolean

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

Returns:

  • (Boolean)


350
351
352
# File 'lib/automateit/interpreter.rb', line 350

def writing?
  !preview?
end