Class: PuppetDebugger::Cli

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Support
Defined in:
lib/puppet-debugger/cli.rb

Constant Summary collapse

OUT_SYMBOL =
' => '

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support

#bolt_pdb_client, #do_initialize, #generate_ast, #initialize_from_scope, #known_resource_types, #lib_dirs, #manifest_file, #mod_finder, #parse_error, #parser, #puppet_debugger_lib_dir, #puppet_eval, #puppet_lib_dir, #static_responder_list

Methods included from Support::Loader

#create_loader, #loaders

Methods included from Support::Node

#convert_remote_node, #create_node, #create_real_node, #get_remote_node, #node, #remote_node_name, #remote_node_name=, #set_node, #set_node_from_name, #set_remote_node_name

Methods included from Support::Scope

#catalog, #create_scope, #get_catalog_text, #scope, #scope_vars, #set_catalog, #set_scope

Methods included from Support::Facts

#default_facter_version, #default_facterdb_filter, #default_facts, #dynamic_facterdb_filter, #facter_os_name, #facter_os_version, #facter_version, #node_facts, #server_facts, #set_facts

Methods included from Support::Environment

#bolt_modules, #create_environment, #create_node_environment, #default_manifests_dir, #default_modules_paths, #default_puppet_env_name, #default_site_manifest, #environment_loaders, #modules_paths, #puppet_env_name, #puppet_environment, #set_environment

Methods included from Support::Compilier

#compiler, #create_compiler, #set_compiler

Constructor Details

#initialize(options = {}) ⇒ Cli

Returns a new instance of Cli.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/puppet-debugger/cli.rb', line 25

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

Instance Attribute Details

#benchObject

Returns the value of attribute bench.



20
21
22
# File 'lib/puppet-debugger/cli.rb', line 20

def bench
  @bench
end

#extra_promptObject

Returns the value of attribute extra_prompt.



20
21
22
# File 'lib/puppet-debugger/cli.rb', line 20

def extra_prompt
  @extra_prompt
end

#hooksObject (readonly)

Returns the value of attribute hooks.



21
22
23
# File 'lib/puppet-debugger/cli.rb', line 21

def hooks
  @hooks
end

#html_modeObject

Returns the value of attribute html_mode.



20
21
22
# File 'lib/puppet-debugger/cli.rb', line 20

def html_mode
  @html_mode
end

#in_bufferObject

Returns the value of attribute in_buffer.



20
21
22
# File 'lib/puppet-debugger/cli.rb', line 20

def in_buffer
  @in_buffer
end

#log_levelObject

Returns the value of attribute log_level.



20
21
22
# File 'lib/puppet-debugger/cli.rb', line 20

def log_level
  @log_level
end

#optionsObject (readonly)

Returns the value of attribute options.



21
22
23
# File 'lib/puppet-debugger/cli.rb', line 21

def options
  @options
end

#out_bufferObject

Returns the value of attribute out_buffer.



20
21
22
# File 'lib/puppet-debugger/cli.rb', line 20

def out_buffer
  @out_buffer
end

#settingsObject

Returns the value of attribute settings.



20
21
22
# File 'lib/puppet-debugger/cli.rb', line 20

def settings
  @settings
end

#source_fileObject (readonly)

Returns the value of attribute source_file.



21
22
23
# File 'lib/puppet-debugger/cli.rb', line 21

def source_file
  @source_file
end

#source_line_numObject (readonly)

Returns the value of attribute source_line_num.



21
22
23
# File 'lib/puppet-debugger/cli.rb', line 21

def source_line_num
  @source_line_num
end

Class Method Details



219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/puppet-debugger/cli.rb', line 219

def self.print_repl_desc
  <<~OUT
    Ruby Version: #{RUBY_VERSION}
    Puppet Version: #{Puppet.version}
    Puppet Debugger Version: #{PuppetDebugger::VERSION}
    Created by: NWOps <[email protected]>
    Type "commands" for a list of debugger commands
    or "help" to show the help screen.


  OUT
end

.start(options = { scope: nil }) ⇒ Object

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

Parameters:

  • puppet (Hash)

    scope object



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/puppet-debugger/cli.rb', line 300

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 }) ⇒ Object

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

Parameters:

  • must (Hash)

    contain at least the puppet scope object

  • play (Hash)

    a customizable set of options

  • content (Hash)

    a customizable set of options

  • source_file (Hash)

    a customizable set of options

  • source_line (Hash)

    a customizable set of options

  • in_buffer (Hash)

    a customizable set of options

  • out_buffer (Hash)

    a customizable set of options

  • scope (Hash)

    a customizable set of options



285
286
287
288
289
290
291
292
293
294
# File 'lib/puppet-debugger/cli.rb', line 285

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

Instance Method Details

#command_completionProc

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

Returns:

  • (Proc)

    the proc used in the command completion for readline



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/puppet-debugger/cli.rb', line 63

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) ⇒ Boolean

Returns:

  • (Boolean)


124
125
126
# File 'lib/puppet-debugger/cli.rb', line 124

def contains_resources?(result)
  !Array(result).flatten.find { |r| r.class.to_s =~ /Puppet::Pops::Types::PResourceType/ }.nil?
end

#expand_resource_type(types) ⇒ Array

Returns - returns a formatted array.

Parameters:

  • types (Array)
    • an array or string

Returns:

  • (Array)
    • returns a formatted array



120
121
122
# File 'lib/puppet-debugger/cli.rb', line 120

def expand_resource_type(types)
  Array(types).flatten.map { |t| contains_resources?(t) ? to_resource_declaration(t) : t }
end

#handle_input(input) ⇒ Object

this method handles all input and expects a string of text.

Parameters:

  • input (String)
    • the input content to parse or run

Raises:

  • (ArgumentError)


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
# File 'lib/puppet-debugger/cli.rb', line 172

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) ⇒ Object

Parameters:

  • output (String)
    • the content to output



159
160
161
162
163
164
165
166
167
168
# File 'lib/puppet-debugger/cli.rb', line 159

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_wordsObject

returns a cached list of key words



85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/puppet-debugger/cli.rb', line 85

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) ⇒ Boolean

tries to determine if the input is going to be a multiline input by reading the parser error message

Returns:

  • (Boolean)
    • return true if this is a multiline input, false otherwise



235
236
237
238
239
240
241
242
# File 'lib/puppet-debugger/cli.rb', line 235

def multiline_input?(data)
  case data.message
  when /Syntax error at end of/i
    true
  else
    false
  end
end

#normalize_output(result) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/puppet-debugger/cli.rb', line 128

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

#pagerTTY::Pager

Returns the pager object, disable if CI or testing is present.

Returns:

  • (TTY::Pager)

    the pager object, disable if CI or testing is present



146
147
148
# File 'lib/puppet-debugger/cli.rb', line 146

def pager
  @pager ||= TTY::Pager.new(output: out_buffer, enabled: pager_enabled?)
end

#pager_enabled?Boolean

Returns:

  • (Boolean)


150
151
152
# File 'lib/puppet-debugger/cli.rb', line 150

def pager_enabled?
  ENV['CI'].nil?
end

#read_loopObject

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



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/puppet-debugger/cli.rb', line 249

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_listObject



141
142
143
# File 'lib/puppet-debugger/cli.rb', line 141

def responder_list
  Pluginator.find(PuppetDebugger)
end

#to_resource_declaration(type) ⇒ Object

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



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/puppet-debugger/cli.rb', line 100

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