#!/usr/bin/env ruby

#=== Summary
#
#A command-line front-end to the Ruby debugger, <tt>ruby-debug</tt>, the
#Fast Ruby Debugger.
#
#Command invocation:
#  
#   rdebug [options] [--] [script-options] ruby-script-to-debug
#   rdebug [options] [script-options] [--client]
#   rdebug [--version | --help]
#
#=== Options
#
#<tt>-A | --annotate</tt> <i>level</i>::
#    Set gdb-style annotation to <i>level</i>, a number.  Additional
#    information is output automatically when program state is
#    changed. This can be used by front-ends such as GNU Emacs to post
#    this updated information without having to poll for it.
#
#<tt>--client</tt>::
#    Connect to a remote debugger. Used with another rdebug invocation
#    using <tt>--server</tt>.  See also <tt>--host</tt> and
#    <tt>--cport</tt> options
#
#<tt>--cport=</tt><i>port</i>::
#    Use port <i>port</i> for access to debugger control.
#
#<tt>-d | --debug</tt>::
#    Set $DEBUG true.
#
#<tt>--emacs</tt>::
#    Activates full GNU Emacs mode. Is the equivalent of setting the
#    options <tt>--emacs-basic --annotate=3 --no-stop --no-control
#    --post-mortem</tt>.
#
#<tt>--emacs-basic</tt>::
#     Activates GNU Emacs mode. Debugger prompts are prefaced with two
#     octal 032 characters.
#
#<tt>-h | --host=</tt><i>host</i>::
#     Use host name <i>host</i> for remote debugging.
#
#<tt>-I | --include</tt> <i>path</i>
#     Add <i>path</i> to <tt>$LOAD_PATH</tt>
#
#<tt>-m | --post-mortem</tt>::
#     Activate post-mortem mode.
#
#<tt>--no-control</tt>::
#      Do not automatically start control thread.
#
#<tt>--no-stop</tt>::
#      Do not stop when script is loaded.
#
#<tt>-p | --port=PORT</tt>::
#      Host name used for remote debugging.
#
#<tt>-r | --require</tt><i>script</i>::
#      Require the library, before executing your script.
#
#<tt>--script</tt> <i>file</i>::
#      Run debugger script file <i>file</i>
#
#<tt>-x | --trace</tt>::
#      Show lines before executing them.
#
#<tt>--no-quit</tt>::
#      Do not quit when script terminates. Instead rerun the
#      program.
#
#<tt>--version</tt>::
#      Show the version number and exit.
#
#<tt>--verbose</tt>::
#      Turn on verbose mode.
#
#<tt>--v</tt>::
#      Print the version number, then turn on verbose mode if
#      a script name is given. If no script name is given
#     just exit after printing the version number.
#
#<tt>--nx</tt>::
#      Don’t execute commands  found in any initialization
#      files, e.g. <tt>.rdebugrc</tt>.
#
#<tt>--keep-frame-binding</tt>::
#      Keep frame bindings.
#
#<tt>--script=</tt><i>file</i>::
#      Name of the script file to run 
#
#<tt>-s | --server</tt>::
#      Listen for remote connections. Another rdebug session
#      accesses using the <tt>--client</tt>  option. See also the
#      <tt>--host</tt>, <tt>--port</tt> and <tt>--cport</tt> options
#
#<tt>-w | --wait</tt>::
#      Wait for a client connection; implies <tt>-s</tt> option.
#
#<tt>--help</tt>::
#      Show invocation help and exit.

require 'rubygems'
require 'optparse'
require 'ostruct'
require 'ruby-debug'

def debug_program(options)
  # Make sure Ruby script syntax checks okay.
  # Otherwise we get a load message that looks like rdebug has 
  # a problem. 
  output = `ruby -c #{Debugger::PROG_SCRIPT} 2>&1`
  if $?.exitstatus != 0 and RUBY_PLATFORM !~ /mswin/
    puts output
    exit $?.exitstatus 
  end
  print "\032\032starting\n" if Debugger.annotate and Debugger.annotate > 2

  # Record where we are we can know if the call stack has been
  # truncated or not.
  Debugger.start_sentinal=caller(0)[1]

  bt = Debugger.debug_load(Debugger::PROG_SCRIPT, options.stop, false)
  if bt
    if options.post_mortem
      Debugger.handle_post_mortem(bt)
    else
      print bt.backtrace.map{|l| "\t#{l}"}.join("\n"), "\n"
      print "Uncaught exception: #{bt}\n"
    end
  end
end

# Do a shell-like path lookup for prog_script and return the results.
# If we can't find anything return prog_script.
def whence_file(prog_script)
  if prog_script.index(File::SEPARATOR)
    # Don't search since this name has path separator components
    return prog_script
  end
  for dirname in ENV['PATH'].split(File::PATH_SEPARATOR) do
    prog_script_try = File.join(dirname, prog_script)
    return prog_script_try if File.exist?(prog_script_try)
  end
  # Failure
  return prog_script
end

options = OpenStruct.new(
  'annotate'           => Debugger.annotate,
  'client'             => false,
  'control'            => true,
  'cport'              => Debugger::PORT + 1,
  'frame_bind'         => false,
  'host'               => nil,
  'quit'               => true,
  'no_rewrite_program' => false,
  'stop'               => true,
  'nx'                 => false,
  'port'               => Debugger::PORT,
  'post_mortem'        => false,
  'restart_script'     => nil,
  'script'             => nil,
  'server'             => false,
  'tracing'            => false,
  'verbose_long'       => false,
  'wait'               => false
)

def process_options(options)
  program = File.basename($0)
  opts = OptionParser.new do |opts|
    opts.banner = <<EOB
#{program} #{Debugger::VERSION}
Usage: #{program} [options] <script.rb> -- <script.rb parameters>
EOB
    opts.separator ""
    opts.separator "Options:"
    opts.on("-A", "--annotate LEVEL", Integer, "Set annotation level") do 
      |annotate|
        Debugger.annotate = annotate
    end
    opts.on("-c", "--client", "Connect to remote debugger") do 
      options.client = true
    end
    opts.on("--cport PORT", Integer, "Port used for control commands") do 
      |cport|
      options.cport = cport
    end
    opts.on("-d", "--debug", "Set $DEBUG=true") {$DEBUG = true}
    opts.on("--emacs LEVEL", Integer,
            "Activates full Emacs support at annotation level LEVEL") do 
      |level|
      Debugger.annotate = level.to_i
      ENV['EMACS'] = '1'
      ENV['COLUMNS'] = '120' if ENV['COLUMNS'].to_i < 120
      options.control = false
      options.quit = false
      options.post_mortem = true
    end
    opts.on('--emacs-basic', 'Activates basic Emacs mode') do 
      ENV['EMACS'] = '1'
    end
    opts.on('-h', '--host HOST', 'Host name used for remote debugging') do
      |host|
      options.host = host
    end
    opts.on('-I', '--include PATH', String, 'Add PATH to $LOAD_PATH') do |path|
      $LOAD_PATH.unshift(path)
    end
    opts.on('--keep-frame-binding', 'Keep frame bindings') do 
      options.frame_bind = true
    end
    opts.on('-m', '--post-mortem', 'Activate post-mortem mode') do 
      options.post_mortem = true
    end
    opts.on('--no-control', 'Do not automatically start control thread') do 
      options.control = false
    end
    opts.on('--no-quit', 'Do not quit when script finishes') do
      options.quit = false
    end
    opts.on('--no-rewrite-program',
            'Do not set $0 to the program being debugged') do 
      options.no_rewrite_program = true
    end
    opts.on('--no-stop', 'Do not stop when script is loaded') do 
      options.stop = false
    end
    opts.on('-nx', 'Not run debugger initialization files (e.g. .rdebugrc') do
      options.nx = true
    end
    opts.on('-p', '--port PORT', Integer, 'Port used for remote debugging') do 
      |port|
      options.port = port
    end
    opts.on('-r', '--require SCRIPT', String,
            'Require the library, before executing your script') do |name|
      if name == 'debug'
        puts "ruby-debug is not compatible with Ruby's 'debug' library. This option is ignored."
      else
        require name
      end
    end
    opts.on('--restart-script FILE', String, 
            'Name of the script file to run. Erased after read') do 
      |restart_script|
      options.restart_script = restart_script
      unless File.exists?(options.restart_script)
        puts "Script file '#{options.restart_script}' is not found"
        exit
      end
    end
    opts.on('--script FILE', String, 'Name of the script file to run') do 
      |script|
      options.script = script
      unless File.exists?(options.script)
        puts "Script file '#{options.script}' is not found"
        exit
      end
    end
    opts.on('-s', '--server', 'Listen for remote connections') do 
      options.server = true
    end
    opts.on('-w', '--wait', 'Wait for a client connection, implies -s option') do
      options.wait = true
    end
    opts.on('-x', '--trace', 'Turn on line tracing') {options.tracing = true}
    opts.separator ''
    opts.separator 'Common options:'
    opts.on_tail('--help', 'Show this message') do
      puts opts
      exit
    end
    opts.on_tail('--version', 
                 'Print the version') do
      puts "ruby-debug #{Debugger::VERSION}"
      exit
    end
    opts.on('--verbose', 'Turn on verbose mode') do
      $VERBOSE = true
      options.verbose_long = true
    end
    opts.on_tail('-v', 
                 'Print version number, then turn on verbose mode') do
      puts "ruby-debug #{Debugger::VERSION}"
      $VERBOSE = true
    end
  end
  return opts
end

# What file is used for debugger startup commands.
unless defined?(OPTS_INITFILE)
  if RUBY_PLATFORM =~ /mswin/
    # Of course MS Windows has to be different
    OPTS_INITFILE = 'rdbopt.ini'
    HOME_DIR =  (ENV['HOME'] || 
                 ENV['HOMEDRIVE'].to_s + ENV['HOMEPATH'].to_s).to_s
  else
    OPTS_INITFILE = '.rdboptrc'
    HOME_DIR = ENV['HOME'].to_s
  end
end
begin
  initfile = File.join(HOME_DIR, OPTS_INITFILE)
  eval(File.read(initfile)) if 
    File.exist?(initfile)
rescue
end

opts = process_options(options)
begin
  if not defined? Debugger::ARGV
    Debugger::ARGV = ARGV.clone
  end
  rdebug_path = File.expand_path($0)
  if RUBY_PLATFORM =~ /mswin/
    rdebug_path += '.cmd' unless rdebug_path =~ /\.cmd$/i
  end
  Debugger::RDEBUG_SCRIPT = rdebug_path
  Debugger::RDEBUG_FILE = __FILE__
  Debugger::INITIAL_DIR = Dir.pwd
  opts.parse! ARGV
rescue StandardError => e
  puts opts
  puts
  puts e.message
  exit(-1)
end

if options.client
  Debugger.start_client(options.host, options.port)
else
  if ARGV.empty?
    exit if $VERBOSE and not options.verbose_long
    puts opts
    puts
    puts 'Must specify a script to run'
    exit(-1)
  end
  
  # save script name
  prog_script = ARGV.shift
  prog_script = whence_file(prog_script) unless File.exist?(prog_script)
  Debugger::PROG_SCRIPT = File.expand_path prog_script
  
  # install interruption handler
  trap('INT') { Debugger.interrupt_last }
  
  # set options
  Debugger.wait_connection = options.wait
  Debugger.keep_frame_binding = options.frame_bind
  
  if options.server
    # start remote mode
    Debugger.start_remote(options.host, [options.port, options.cport], 
                          options.post_mortem) do
      # load initrc script
      Debugger.run_init_script(StringIO.new) unless options.nx
    end
    debug_program(options)
  else
    # Set up trace hook for debugger
    Debugger.start
    # start control thread
    Debugger.start_control(options.host, options.cport) if options.control

    # load initrc script (e.g. .rdebugrc)
    Debugger.run_init_script(StringIO.new) unless options.nx
    
    # run restore-settings startup script if specified
    if options.restart_script
      require 'fileutils'
      Debugger.run_script(options.restart_script)
      FileUtils.rm(options.restart_script)
    end

    # run startup script if specified
    if options.script
      Debugger.run_script(options.script)
    end

    # activate post-mortem
    Debugger.post_mortem if options.post_mortem
    options.stop = false if options.tracing
    Debugger.tracing = options.tracing

    if !options.quit
      if Debugger.started?
        until Debugger.stop do end
      end
      begin
        debug_program(options)
      rescue SyntaxError
        puts $!.backtrace.map{|l| "\t#{l}"}.join("\n")
        puts "Uncaught Syntax Error\n"
      rescue
        print $!.backtrace.map{|l| "\t#{l}"}.join("\n"), "\n"
        print "Uncaught exception: #{$!}\n"
      end
      print "The program finished.\n" unless 
        Debugger.annotate.to_i > 1 # annotate has its own way
      interface = Debugger::LocalInterface.new
      # Not sure if ControlCommandProcessor is really the right
      # thing to use. CommandProcessor requires a state.
      processor = Debugger::ControlCommandProcessor.new(interface)
      processor.process_commands
    else
      debug_program(options)
    end
  end
end
