lib/byebug/runner.rb



require 'optparse'
require 'English'
require 'byebug/core'
require 'byebug/version'
require 'byebug/helpers/parse'
require 'byebug/option_setter'
require 'byebug/processors/control_processor'

module Byebug
  #
  # Responsible for starting the debugger when started from the command line.
  #
  class Runner
    include Helpers::ParseHelper

    #
    # Error class signaling absence of a script to debug.
    #
    class NoScript < StandardError; end

    #
    # Error class signaling a non existent script to debug.
    #
    class NonExistentScript < StandardError; end

    #
    # Error class signaling a script with invalid Ruby syntax.
    #
    class InvalidScript < StandardError; end

    #
    # Special working modes that don't actually start the debugger.
    #
    attr_reader :help, :version, :remote

    #
    # Signals that we should exit after the debugged program is finished.
    #
    attr_accessor :quit

    #
    # Signals that we should stop before program starts
    #
    attr_accessor :stop

    #
    # @param stop [Boolean] Whether the runner should stop right before
    # starting the program.
    #
    # @param quit [Boolean] Whether the runner should quit right after
    # finishing the program.
    #
    def initialize(stop = true, quit = true)
      @stop = stop
      @quit = quit
    end

    def help=(text)
      @help ||= text

      interface.puts("\n#{text}\n")
    end

    def version=(number)
      @version ||= number

      interface.puts("\n  Running byebug #{number}\n")
    end

    def remote=(host_and_port)
      @remote ||= Byebug.parse_host_and_port(host_and_port)
    end

    #
    # Usage banner.
    #
    def banner
      <<-EOB.gsub(/^ {8}/, '')

          byebug #{Byebug::VERSION}

          Usage: byebug [options] <script.rb> -- <script.rb parameters>

      EOB
    end

    #
    # Starts byebug to debug a program.
    #
    def run
      prepare_options.order!($ARGV)
      return if version || help

      if remote
        Byebug.start_client(*remote)
        return
      end

      setup_cmd_line_args

      loop do
        debug_program

        break if quit

        ControlProcessor.new.process_commands
      end
    end

    attr_writer :interface

    def interface
      @interface ||= LocalInterface.new
    end

    #
    # Processes options passed from the command line.
    #
    def prepare_options
      OptionParser.new(banner, 25) do |opts|
        opts.banner = banner

        OptionSetter.new(self, opts).setup
      end
    end

    #
    # Extracts debugged program from command line args.
    #
    def setup_cmd_line_args
      Byebug.mode = :standalone

      fail(NoScript, 'You must specify a program to debug...') if $ARGV.empty?

      program = which($ARGV.shift)
      program = which($ARGV.shift) if program == which('ruby')
      fail(NonExistentScript, "The script doesn't exist") unless program

      $PROGRAM_NAME = program
    end

    #
    # Debugs a script only if syntax checks okay.
    #
    def debug_program
      ok = syntax_valid?(File.read($PROGRAM_NAME))
      fail(InvalidScript, 'The script has incorrect syntax') unless ok

      error = Byebug.debug_load($PROGRAM_NAME, stop)
      puts "#{error}\n#{error.backtrace}" if error
    end

    #
    # Cross-platform way of finding an executable in the $PATH.
    # Borrowed from: http://stackoverflow.com/questions/2108727
    #
    def which(cmd)
      return File.expand_path(cmd) if File.exist?(cmd)

      exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
      ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
        exts.each do |ext|
          exe = File.join(path, "#{cmd}#{ext}")
          return exe if File.executable?(exe) && !File.directory?(exe)
        end
      end

      nil
    end
  end
end