class PuppetDebugger::Cli

Constants

OUT_SYMBOL

Attributes

bench[RW]
extra_prompt[RW]
hooks[R]
html_mode[RW]
in_buffer[RW]
log_level[RW]
options[R]
out_buffer[RW]
settings[RW]
source_file[R]
source_line_num[R]

Public Class Methods

new(options = {}) click to toggle source
# File lib/puppet-debugger/cli.rb, line 21
def initialize(options = {})
  do_initialize if Puppet[:codedir].nil?
  Puppet.settings[:name] = :debugger
  @options = options
  Puppet[:static_catalogs] = false unless Puppet.settings[:static_catalogs].nil?
  set_remote_node_name(options[:node_name])
  initialize_from_scope(options[:scope])
  set_catalog(options[:catalog])
  @log_level = 'notice'
  @out_buffer = options[:out_buffer] || $stdout
  @html_mode = options[:html_mode] || false
  @source_file = options[:source_file] || nil
  @source_line_num = options[:source_line] || nil
  @in_buffer = options[:in_buffer] || $stdin
  Readline.input = @in_buffer
  Readline.output = @out_buffer
  Readline.completion_append_character = ''
  Readline.basic_word_break_characters = ' '
  Readline.completion_proc = command_completion
  AwesomePrint.defaults = {
    html: @html_mode,
    sort_keys: true,
    indent: 2
  }
  # Catch control-c sequences
  trap('SIGINT') do
    # control-d
    exit 0
  end
  trap('INT') do
    # control-c
    # handle_output('Type exit or use control-d')
  end
end
print_repl_desc() click to toggle source
start(options = { scope: nil }) click to toggle source

start reads from stdin or from a file if from stdin, the repl will process the input and exit if from a file, the repl will process the file and continue to prompt @param [Hash] puppet scope object

# File lib/puppet-debugger/cli.rb, line 296
def self.start(options = { scope: nil })
  opts = Trollop.options do
    opt :play, 'Url or file to load from', required: false, type: String
    opt :run_once, 'Evaluate and quit', required: false, default: false
    opt :node_name, 'Remote Node to grab facts from', required: false, type: String
    opt :catalog, 'Import a catalog file to inspect', required: false, type: String
    opt :quiet, 'Do not display banner', required: false, default: false
  end
  if !STDIN.tty? && !STDIN.closed?
    options[:run_once] = true
    options[:quiet] = true
  end
  options = opts.merge(options)
  options[:play] = options[:play].path if options[:play].respond_to?(:path)
  repl_obj = PuppetDebugger::Cli.new(options)
  repl_obj.out_buffer.puts print_repl_desc unless options[:quiet]
  if options[:play]
    repl_obj.handle_input("play #{options[:play]}")
  elsif (ARGF.filename == '-') && (!STDIN.tty? && !STDIN.closed?)
    # when the user supplied a file content using stdin, aka. cat,pipe,echo or redirection
    input = ARGF.read
    repl_obj.handle_input(input)
  elsif ARGF.filename != '-'
    # when the user supplied a file name without using the args (stdin)
    path = File.expand_path(ARGF.filename)
    repl_obj.handle_input("play #{path}")
  end
  # helper code to make tests exit the loop
  repl_obj.read_loop unless options[:run_once]
end
start_without_stdin(options = { scope: nil }) click to toggle source

used to start a debugger session without attempting to read from stdin or this is primarily used by the debug::break() module function and the puppet debugger face @param [Hash] must contain at least the puppet scope object @option play [String] - must be a path to a file @option content [String] - play back the string content passed in @option source_file [String] - the file from which the breakpoint was used @option source_line [Integer] - the line in the sourcefile from which the breakpoint was used @option in_buffer [IO] - the input buffer to read from @option out_buffer [IO] - the output buffer to write to @option scope [Scope] - the puppet scope

# File lib/puppet-debugger/cli.rb, line 281
def self.start_without_stdin(options = { scope: nil })
  options[:play] = options[:play].path if options[:play].respond_to?(:path)
  repl_obj = PuppetDebugger::Cli.new(options)
  repl_obj.out_buffer.puts print_repl_desc unless options[:quiet]
  repl_obj.handle_input(options[:content]) if options[:content]
  # TODO: make the output optional so we can have different output destinations
  repl_obj.handle_input('whereami') if options[:source_file] && options[:source_line]
  repl_obj.handle_input("play #{options[:play]}") if options[:play]
  repl_obj.read_loop unless options[:run_once]
end

Public Instance Methods

command_completion() click to toggle source

@return [Proc] the proc used in the command completion for readline if a plugin keyword is found lets return keywords using the plugin's command completion otherwise return the default set of keywords and filter out based on input

# File lib/puppet-debugger/cli.rb, line 59
def command_completion
  proc do |input|
    words = Readline.line_buffer.split(Readline.basic_word_break_characters)
    next key_words.grep(/^#{Regexp.escape(input)}/) if words.empty?

    first_word = words.shift
    plugins = PuppetDebugger::InputResponders::Commands.plugins.find_all do |p|
      p::COMMAND_WORDS.find { |word| word.start_with?(first_word) }
    end
    if (plugins.count == 1) && /\A#{first_word}\s/.match(Readline.line_buffer)
      plugins.first.command_completion(words)
    else
      key_words.grep(/^#{Regexp.escape(input)}/)
    end
  end
end
contains_resources?(result) click to toggle source
# File lib/puppet-debugger/cli.rb, line 120
def contains_resources?(result)
  !Array(result).flatten.find { |r| r.class.to_s =~ /Puppet::Pops::Types::PResourceType/ }.nil?
end
expand_resource_type(types) click to toggle source

@return [Array] - returns a formatted array @param types [Array] - an array or string

# File lib/puppet-debugger/cli.rb, line 116
def expand_resource_type(types)
  Array(types).flatten.map { |t| contains_resources?(t) ? to_resource_declaration(t) : t }
end
handle_input(input) click to toggle source

this method handles all input and expects a string of text. @param input [String] - the input content to parse or run

# File lib/puppet-debugger/cli.rb, line 168
def handle_input(input)
  raise ArgumentError unless input.instance_of?(String)

  output =
    begin
      case input.strip
      when PuppetDebugger::InputResponders::Commands.command_list_regex
        args = input.split(' ')
        command = args.shift
        plugin = PuppetDebugger::InputResponders::Commands.plugin_from_command(command)
        plugin.execute(args, self) || ''
      when '_'
        " => #{@last_item}"
      else
        result = puppet_eval(input)
        @last_item = result
        o = normalize_output(result)
        o.nil? ? '' : o.ai
      end
    rescue PuppetDebugger::Exception::InvalidCommand => e
      e.message.fatal
    rescue LoadError => e
      e.message.fatal
    rescue Errno::ETIMEDOUT => e
      e.message.fatal
    rescue ArgumentError => e
      e.message.fatal
    rescue Puppet::ResourceError => e
      e.message.fatal
    rescue Puppet::Error => e
      e.message.fatal
    rescue Puppet::ParseErrorWithIssue => e
      e.message.fatal
    rescue PuppetDebugger::Exception::FatalError => e
      handle_output(e.message.fatal)
      exit 1 # this can sometimes causes tests to fail
    rescue PuppetDebugger::Exception::Error => e
      e.message.fatal
    rescue ::RuntimeError => e
      handle_output(e.message.fatal)
      exit 1
    end
  output = OUT_SYMBOL + output unless output.empty?
  handle_output(output)
  exec_hook :after_output, out_buffer, self, self
end
handle_output(output) click to toggle source

@param output [String] - the content to output @summary outputs the output to the output buffer

uses the pager if the screen height is less than the height of the
output content
Disabled if CI or testing is being done
# File lib/puppet-debugger/cli.rb, line 155
def handle_output(output)
  return if output.nil?

  if pager_enabled? && output.lines.count >= TTY::Screen.height
    output << "\n"
    pager.page(output)
  else
    out_buffer.puts(output) unless output.empty?
  end
end
key_words() click to toggle source

returns a cached list of key words

# File lib/puppet-debugger/cli.rb, line 81
def key_words
  # because dollar signs don't work we can't display a $ sign in the keyword
  # list so its not explicitly clear what the keyword
  variables = scope.to_hash.keys
  # prepend a :: to topscope variables
  scoped_vars = variables.map { |k, _v| scope.compiler.topscope.exist?(k) ? "$::#{k}" : "$#{k}" }
  PuppetDebugger::InputResponders::Functions.instance.debugger = self
  funcs = PuppetDebugger::InputResponders::Functions.instance.func_list
  PuppetDebugger::InputResponders::Datatypes.instance.debugger = self
  (scoped_vars + funcs + static_responder_list +
    PuppetDebugger::InputResponders::Datatypes.instance.all_data_types).uniq.sort
end
multiline_input?(data) click to toggle source

tries to determine if the input is going to be a multiline input by reading the parser error message @return [Boolean] - return true if this is a multiline input, false otherwise

# File lib/puppet-debugger/cli.rb, line 231
def multiline_input?(data)
  case data.message
  when /Syntax error at end of/i
    true
  else
    false
  end
end
normalize_output(result) click to toggle source
# File lib/puppet-debugger/cli.rb, line 124
def normalize_output(result)
  if contains_resources?(result)
    output = expand_resource_type(result)
    # the results might be wrapped into an array
    # if the only output is a resource then return it
    # otherwise it is multiple items or an actually array
    return output.first if output.count == 1

    return output
  end
  result
end
pager() click to toggle source

@return [TTY::Pager] the pager object, disable if CI or testing is present

# File lib/puppet-debugger/cli.rb, line 142
def pager
  @pager ||= TTY::Pager.new(output: out_buffer, enabled: pager_enabled?)
end
pager_enabled?() click to toggle source
# File lib/puppet-debugger/cli.rb, line 146
def pager_enabled?
  ENV['CI'].nil?
end
read_loop() click to toggle source

reads input from stdin, since readline requires a tty we cannot read from other sources as readline requires a file object we parse the string after each input to determine if the input is a multiline_input entry. If it is multiline we run through the loop again and concatenate the input

# File lib/puppet-debugger/cli.rb, line 245
def read_loop
  line_number = 1
  full_buffer = ''
  while buf = Readline.readline("#{line_number}:#{extra_prompt}>> ", true)
    begin
      full_buffer += buf
      # unless this is puppet code, otherwise skip repl keywords
      unless PuppetDebugger::InputResponders::Commands.command_list_regex.match(buf)
        line_number = line_number.next
        begin
          parser.parse_string(full_buffer)
        rescue Puppet::ParseErrorWithIssue => e
          if multiline_input?(e)
            out_buffer.print '  '
            full_buffer += "\n"
            next
          end
        end
      end
      handle_input(full_buffer)
      full_buffer = ''
    end
  end
end
responder_list() click to toggle source
# File lib/puppet-debugger/cli.rb, line 137
def responder_list
  Pluginator.find(PuppetDebugger)
end
to_resource_declaration(type) click to toggle source

looks up the type in the catalog by using the type and title and returns the resource in ral format

# File lib/puppet-debugger/cli.rb, line 96
def to_resource_declaration(type)
  if type.respond_to?(:type_name) && type.respond_to?(:title)
    title = type.title
    type_name = type.type_name
  elsif type_result = /(\w+)\['?(\w+)'?\]/.match(type.to_s)
    # not all types have a type_name and title so we
    # output to a string and parse the results
    title = type_result[2]
    type_name = type_result[1]
  else
    return type
  end
  res = scope.catalog.resource(type_name, title)
  return res.to_ral if res
  # don't return anything or returns nil if item is not in the catalog
end