#!/bin/sh
exec ruby -w -x $0 ${1+"$@"} # -*- ruby -*-
#!ruby -w
# vim: set filetype=ruby : set sw=2

# An extended grep, with extended functionality including full regular
# expressions, contextual output, highlighting, detection and exclusion of
# nontext files, and complex matching criteria.

# $Id: glark,v 1.61 2004/04/13 17:57:33 jeugenepace Exp $

require "English"

$stdout.sync = true             # unbuffer
$stderr.sync = true             # unbuffer

$PACKAGE = "glark"
$VERSION = "1.7.0"
# $DEBUGGING = false


# -------------------------------------------------------
# ANSI colou?r
# -------------------------------------------------------

#     Attribute codes:
#         00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed
#     Text color codes:
#         30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white
#     Background color codes:
#         40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white

module ANSIColor

  VERSION = 1.0

  def color(code, &blk)
    ANSIColor.color(code, self, &blk)
  end

  def ANSIColor.color(code, obj = self, &blk)
    # this is the Module self ^^^^^^^^
    result = "\e[#{code}m"
    if blk
      result << blk.call
      result << "\e[0m"
    elsif obj.kind_of?(String)
      result << obj
      result << "\e[0m"
    end
    result
  end
  
  @@ATTRIBUTES = Hash[
    'none'       => '0', 
    'reset'      => '0',
    'bold'       => '1',
    'underscore' => '4',
    'underline'  => '4',
    'blink'      => '5',
    'reverse'    => '7',
    'concealed'  => '8',
    'black'      => '30',
    'red'        => '31',
    'green'      => '32',
    'yellow'     => '33',
    'blue'       => '34',
    'magenta'    => '35',
    'cyan'       => '36',
    'white'      => '37',
    'on_black'   => '40',
    'on_red'     => '41',
    'on_green'   => '42',
    'on_yellow'  => '43',
    'on_blue'    => '44',
    'on_magenta' => '45',
    'on_cyan'    => '46',
    'on_white'   => '47',
  ]

  @@ATTRIBUTES.each do |name, val|
    cd = <<-EODEF
      def ANSIColor.#{name}(&blk)
        ANSIColor.color(#{val}, &blk)
      end

      def #{name}(&blk)
        color(#{val}, &blk)
      end

    EODEF

    # puts cd

    eval cd
  end

  # for String
  alias negative reverse

  def ANSIColor.attributes
    @@ATTRIBUTES.keys
  end
  
  # returns the code for the given color string, which is in the format:
  # [foreground] on [background]. Note that the foreground and background sections
  # can have modifiers (attributes). Examples:
  #     black
  #     blue on white
  #     bold green on yellow
  #     underscore bold magenta on cyan
  #     underscore red on cyan

  def ANSIColor.code(str)
    fg, bg = str.split(/\s*\bon_?\s*/)
    (fg ? foreground(fg) : "") + (bg ? background(bg) : "")
  end

  # returns the code for the given background color(s)
  def ANSIColor.background(bgcolor)
    make_code("on_" + bgcolor)
  end

  # returns the code for the given foreground color(s)
  def ANSIColor.foreground(fgcolor)
    make_code(fgcolor)
  end

  protected

  def ANSIColor.make_code(str)
    str ||= ""
    str.split.collect do |s|
      if attr = @@ATTRIBUTES[s]
        "\e[#{attr}m"
      else
        $stderr.puts "WARNING: ANSIColor::make_code(" + str + "): unknown color: " + s
        return ""
      end
    end.join("")
  end

end


class String
  alias oldReverse reverse
  include ANSIColor
  alias reverse oldReverse
end


# -------------------------------------------------------
# Logging
# -------------------------------------------------------

# Very minimal logging output. If verbose is set, this displays the method and
# line number whence called. It can be a mixin to a class, which displays the
# class and method from where it called. If not in a class, it displays only the
# method.

# All kids love log.
class Log

  $LOGGING_LEVEL = nil

  VERSION = "1.0.1"
  
  module Severity
    DEBUG = 0
    INFO  = 1
    WARN  = 2
    ERROR = 3
    FATAL = 4
  end

  include Log::Severity

  def initialize
    $LOGGING_LEVEL   = @level = FATAL
    @ignored_files   = {}
    @ignored_methods = {}
    @ignored_classes = {}
    @width           = 0
    @output          = $stdout
    @fmt             = "[%s:%04d] {%s}"
    @autoalign       = false
    @colors          = []
    @colorize_line   = false
  end
    
  def verbose=(v)
    @level = v ? DEBUG : FATAL
  end

  def level=(lvl)
    @level = lvl
  end

  # Assigns output to the given stream.
  def output=(io)
    @output = io
  end

  # sets whether to colorize the entire line, or just the message.
  def colorize_line=(col)
    @colorize_line = col
  end

  # Assigns output to a file with the given name. Returns the file; client
  # is responsible for closing it.
  def outfile=(f)
    @output = if f.kind_of?(IO) then f else File.new(f, "w") end
  end

  # Creates a printf format for the given widths, for aligning output.
  def set_widths(file_width, line_width, func_width)
    @fmt = "[%#{file_width}s:%#{line_width}d] {%#{func_width}s}"
  end

  def ignore_file(fname)
    @ignored_files[fname] = true
  end
  
  def ignore_method(methname)
    @ignored_methods[methname] = true
  end
  
  def ignore_class(classname)
    @ignored_classes[classname] = true
  end

  def log_file(fname)
    @ignored_files.delete(fname)
  end
  
  def log_method(methname)
    @ignored_methods.delete(methname)
  end
  
  def log_class(classname)
    @ignored_classes.delete(classname)
  end

  # Sets auto-align of the {function} section.  set_widths will likely
  # result in nicer output.
  def autoalign
    @autoalign = true
  end

  def debug(msg = "", depth = 1, &blk)
    log(msg, DEBUG, depth + 1, &blk)
  end

  def info(msg = "", depth = 1, &blk)
    log(msg, INFO, depth + 1, &blk)
  end

  def warn(msg = "", depth = 1, &blk)
    log(msg, WARN, depth + 1, &blk)
  end

  def error(msg = "", depth = 1, &blk)
    log(msg, ERROR, depth + 1, &blk)
  end

  def fatal(msg = "", depth = 1, &blk)
    log(msg, FATAL, depth + 1, &blk)
  end

  # Logs the given message.
  def log(msg = "", level = DEBUG, depth = 1, cname = nil, &blk)
    if level >= @level
      c = caller(depth)[0]
      c.index(/(.*):(\d+)(?::in \`(.*)\')?/)
      file, line, func = $1, $2, $3
      file.sub!(/.*\//, "")

      if cname
        func = cname + "#" + func
      end

      if @ignored_files[file] || (cname && @ignored_classes[cname]) || @ignored_methods[func]
        # skip this one.
      elsif @autoalign
        print_autoaligned(file, line, func, msg, level, &blk)
      else
        print_formatted(file, line, func, msg, level, &blk)
      end
    end
  end

  # Shows the current stack.
  def stack(msg = "", level = DEBUG, depth = 1, cname = nil, &blk)
    if level >= @level
      stk = caller(depth)
      for c in stk
        c.index(/(.*):(\d+)(?::in \`(.*)\')?/)
        file, line, func = $1, $2, $3
        file.sub!(/.*\//, "")

        func ||= "???"

        if cname
          func = cname + "#" + func
        end
        
        if @ignored_files[file] || (cname && @ignored_classes[cname]) || @ignored_methods[func]
        # skip this one.
        elsif @autoalign
          print_autoaligned(file, line, func, msg, level, &blk)
        else
          print_formatted(file, line, func, msg, level, &blk)
        end
        msg = ""
      end
    end
  end

  def print_autoaligned(file, line, func, msg, level, &blk)
    @width = [ @width, func.length ].max
    hdr = sprintf "[%s:%04d] {%-*s} ", file, line, @width
    print(hdr, msg, level)
  end

  def print_formatted(file, line, func, msg, level, &blk)
    hdr = sprintf @fmt, file, line, func
    print(hdr, msg, level, &blk)
  end
  
  def print(hdr, msg, level, &blk)
    if blk
      x = blk.call
      if x.kind_of?(String)
        msg = x
      else
        return
      end
    end

    if @colors[level]
      if @colorize_line
        @output.puts @colors[level] + hdr + " " + msg.to_s.chomp + ANSIColor.reset
      else
        @output.puts hdr + " " + @colors[level] + msg.to_s.chomp + ANSIColor.reset
      end
    else
      @output.puts hdr + " " + msg.to_s.chomp
    end      
  end

  def set_color(level, color)
    # log("#{level}, #{color}")
    @colors[level] = ANSIColor::code(color)
  end

  # by default, class methods delegate to a single app-wide log.

  @@log = Log.new

  def Log.verbose=(v)
    @@log.verbose = v
  end

  def Log.level=(lvl)
    @@log.level = lvl
  end

  # Assigns output to the given stream.
  def Log.output=(io)
    @@log.output = io
  end

  # sets whether to colorize the entire line, or just the message.
  def Log.colorize_line=(col)
    @@log.colorize_line = col
  end

  # Assigns output to a file with the given name. Returns the file; client
  # is responsible for closing it.
  def Log.outfile=(fname)
    @@log.outfile = fname
  end

  # Creates a printf format for the given widths, for aligning output.
  def Log.set_widths(file_width, line_width, func_width)
    @@log.set_widths(file_width, line_width, func_width)
  end

  def Log.ignore_file(fname)
    @@log.ignore_file(fname)
  end
  
  def Log.ignore_method(methname)
    @@ignored_methods[methname] = true
  end
  
  def Log.ignore_class(classname)
    @@ignored_classes[classname] = true
  end

  def Log.log_file(fname)
    @@log.log_file(fname)
  end
  
  def Log.log_method(methname)
    @@log.log_method(methname)
  end
  
  def Log.log_class(classname)
    @@log.log_class(classname)
  end

  # Sets auto-align of the {function} section.  set_widths will likely
  # result in nicer output.
  def Log.autoalign
    @@log.autoalign
  end

  def Log.debug(msg = "", depth = 1, &blk)
    @@log.log(msg, DEBUG, depth + 1, &blk)
  end

  def Log.info(msg = "", depth = 1, &blk)
    @@log.log(msg, INFO, depth + 1, &blk)
  end

  def Log.warn(msg = "", depth = 1, &blk)
    @@log.log(msg, WARN, depth + 1, &blk)
  end

  def Log.error(msg = "", depth = 1, &blk)
    @@log.log(msg, ERROR, depth + 1, &blk)
  end

  def Log.fatal(msg = "", depth = 1, &blk)
    @@log.log(msg, FATAL, depth + 1, &blk)
  end

  # Logs the given message.
  def Log.log(msg = "", level = DEBUG, depth = 1, cname = nil, &blk)
    @@log.log(msg, level, depth + 1, cname, &blk)
  end

  def Log.set_color(level, color)
    @@log.set_color(level, color)
  end

  def Log.stack(msg = "", level = DEBUG, depth = 1, cname = nil, &blk)
    @@log.stack(msg, level, depth, cname, &blk)
  end

end


class AppLog < Log
  include Log::Severity

end


module Loggable

  # Logs the given message, including the class whence invoked.
  def log(msg = "", level = Log::DEBUG, depth = 1, &blk)
    AppLog.log(msg, level, depth + 1, self.class.to_s)
  end

  def debug(msg = "", depth = 1, &blk)
    AppLog.log(msg, Log::DEBUG, depth + 1, self.class.to_s, &blk)
  end

  def info(msg = "", depth = 1, &blk)
    AppLog.log(msg, Log::INFO, depth + 1, self.class.to_s, &blk)
  end

  def warn(msg = "", depth = 1, &blk)
    AppLog.log(msg, Log::WARN, depth + 1, self.class.to_s, &blk)
  end

  def error(msg = "", depth = 1, &blk)
    AppLog.log(msg, Log::ERROR, depth + 1, self.class.to_s, &blk)
  end

  def fatal(msg = "", depth = 1, &blk)
    AppLog.log(msg, Log::FATAL, depth + 1, self.class.to_s, &blk)
  end

  def stack(msg = "", level = Log::DEBUG, depth = 1, &blk)
    AppLog.stack(msg, level, depth + 1, self.class.to_s, &blk)
  end

end


# -------------------------------------------------------
# Input file
# -------------------------------------------------------

# A thing that can be grepped.
class InputFile
  include Loggable

  attr_reader :lines, :fname, :stati, :count, :output

  # cross-platform end of line:   DOS  UNIX  MAC
  ANY_END_OF_LINE = Regexp.new(/(?:\r\n|\n|\r)/)
  
  def initialize(fname, lines)
    @fname = fname
    @lines = lines
    @stati = Array.new          # index = line number, value = context character
    if $options && $options.count
      @count = 0
    else
      @count = nil
    end
    @output = $options && $options.grep_output ? GrepOutputFormat.new(self) : GlarkOutputFormat.new(self)
    @extracted = nil
    @regions   = nil
    @modlines  = nil
  end

  def has_context
    $options.after != 0 || $options.before != 0
  end

  def set_status(from, to, ch, force = false)
    from.upto(to) do |ln|
      if force or not @stati[ln]
        @stati[ln] = ch
      end
    end
  end

  def mark_as_match(start_line, end_line = start_line)
    if $options.grep_output
      end_line = start_line
    end

    $options.exit_status = $options.invert_match ? 1 : 0

    if @count
      @count += 1
    else
      st = [0, start_line - $options.before].max
      set_status(st,           start_line - 1,            "-")
      set_status(start_line,   end_line,                  ":",  true)
      set_status(end_line + 1, end_line + $options.after, "+")
    end
  end

  def write_matches
    @output.write_matches
  end

  def write_all
    @output.write_all
  end

  def write_non_matches
    @output.write_non_matches
  end

  def calculate_regions
  end

  # Returns the lines for this file, separated by end of line sequences.
  def get_lines
    if $/ == "\n"
      @lines
    else
      @extracted = []
      
      # This is much easier. Just resplit the whole thing at end of line sequences
      
      eoline = "\n"             # should be OS-dependent
      srclines = @lines
      reallines = @lines.join("").split(ANY_END_OF_LINE)
      
      # "\n" after all but the last line
      (0 ... (reallines.length - 1)).each do |lnum|
        @extracted << reallines[lnum] + eoline
      end
      @extracted << reallines[-1]
      @extracted
    end
  end

  # Returns the given line for this file. For this method, a line ends with a
  # CR, as opposed to the "lines" method, which ends with $/.
  def get_line(lnum)
    log "lnum: #{lnum}"
    get_lines()[lnum]
  end

  # returns the range that is represented by the region number
  def get_range(rnum)
    unless @regions
      srclines = @modlines ? @modlines : @lines

      if $/ == "\n"
        @regions = (0 ... srclines.length).collect { |lnum| lnum .. lnum }
      else
        @regions = []           # keys = region number; values = range of lines

        lstart = 0
        srclines.each do |line|
          lend = lstart
          log "considering <<#{line.gsub(/\n/, '\\n')}>>"
          log "line.chomped: <<#{line.chomp.gsub(/\n/, '\\n')}>>"
          line.scan(ANY_END_OF_LINE).each do |cr|
            log "cr: #{cr}"
            lend += 1
          end

#          log "adding region #{lstart} .. #{lend}"
          @regions << Range.new(lstart, lend - 1)

          lstart = lend
        end
        
#         @regions.each_with_index do |range, idx|
#           log "region[#{idx}] = #{range}"
#         end
      end
    end

    @regions[rnum]
  end
end



# -------------------------------------------------------
# Output format
# -------------------------------------------------------

class OutputFormat
  include Loggable

  attr_reader :formatted

  def initialize(infile)
    @infile = infile
    @show_file_name = $files.size > 0 && ($files.size > 1 || FileTester.type($files[0]) == FileTester::DIRECTORY) && $options.show_file_names
    @formatted = []
  end
  
  # prints the line, and adjusts for the fact that in our world, lines are
  # 0-indexed, whereas they are displayed as if 1-indexed.
  def print_line(lnum, ch = nil)
    log "lnum #{lnum}, ch: '#{ch}'"
    begin
      lnums = @infile.get_range(lnum)
      log "lnums(#{lnum}): #{lnums}"
      if lnums
        lnums.each do |ln|
          if $options.show_line_numbers
            printf "%5d ", ln + 1
          end
          if ch && has_context
            printf "%s ", ch
          end
          puts @formatted[ln] || @infile.get_line(ln)
        end
      end
    rescue => e
      # puts e
      # puts e.backtrace
    end
  end

  def has_context
    $options.after != 0 || $options.before != 0
  end

  def write_matches
    if $options.count
      write_count
    else
      lastln = nil
      0.upto(@infile.stati.size) do |ln|
        if @infile.stati[ln]
          print_line(ln, @infile.stati[ln]) 
          lastln = ln
        elsif has_context && $options.show_break && ln > 0 && ln + 1 < @infile.stati.size && @infile.stati[ln - 1]
          puts "  ---"
        end
      end
    end
  end

  def write_non_matches
    if $options.count
      write_non_count
    else
      (0 ... @infile.lines.length).each do |ln|
        unless @infile.stati[ln] && @infile.stati[ln] == ":"
          print_line(ln) 
        end
      end
    end
  end

  def write_all
    (0 ... @infile.lines.length).each do |ln|
      print_line(ln) 
    end
  end

end


# -------------------------------------------------------
# Glark output format
# -------------------------------------------------------

class GlarkOutputFormat < OutputFormat

  def show_file_header
    print $options.file_highlight if $options.highlight
    print @infile.fname, ":"
    print ANSIColor.reset if $options.highlight
    print "\n"
  end

  def write_count
    puts "    " + @infile.count.to_s
  end

  def write_non_count
    puts "    " + (@infile.lines.size - @infile.count).to_s
  end

  def write_matches
    show_file_header if @show_file_name
    super
  end

  def write_non_matches
    show_file_header if @show_file_name
    super
  end

  def write_all
    show_file_header if @show_file_name
    super
  end

end


# -------------------------------------------------------
# Grep output format
# -------------------------------------------------------

# This matches grep, mostly. It is for running within emacs, thus,
# it does not support context or highlighting.
class GrepOutputFormat < OutputFormat

  def write_count
    print @infile.fname, ":" if @show_file_name
    puts @infile.count
  end

  def write_non_count
    print @infile.fname, ":" if @show_file_name
    puts @infile.lines.length - @infile.count
  end

  # prints the line, and adjusts for the fact that in our world, lines are
  # 0-indexed, whereas they are displayed as if 1-indexed.
  def print_line(lnum, ch = nil)
    print @infile.fname, ":" if @show_file_name
    if $options.show_line_numbers
      printf "%d: ", lnum + 1
    end
    print @formatted[lnum] || @infile.get_line(lnum)
  end

end



# -------------------------------------------------------
# Binary file
# -------------------------------------------------------

class BinaryFile < InputFile

  def write_matches
    if $options.count
      write_count
    else
      puts "Binary file " + @fname + " matches"
    end
  end

  def write_non_matches
    if $options.count
      write_non_count
    else
      puts "Binary file " + @fname + " matches"
    end
  end

end


# -------------------------------------------------------
# File tester
# -------------------------------------------------------

class FileTester 
  include Loggable

  BINARY     = "binary"
  DIRECTORY  = "directory"
  NONE       = "none"
  TEXT       = "text"
  UNKNOWN    = "unknown"
  UNREADABLE = "unreadable"

  # the percentage of characters that we allow to be odd in a text file
  @@ODD_FACTOR = 0.3

  # how many bytes (characters) of a file we test
  @@TEST_LENGTH = 1024

  # extensions associated with files that are always text:
  @@KNOWN_TEXT = %w{ 
    c
    cpp
    css
    h
    f
    for
    fpp
    hpp
    html
    java
    mk
    php
    pl
    pm
    rb
    rbw
    txt
  }

  # extensions associated with files that are never text:
  @@KNOWN_NONTEXT = %w{ 
    Z
    a
    bz2
    elc
    gif
    gz
    jar
    jpeg
    jpg
    o
    obj
    pdf
    png
    ps
    tar
    zip
  }

  def FileTester.ascii?(c)
    # from ctype.h
    return (c.to_i & ~0x7f) == 0
  end

  def FileTester.type(file)
    if File.exists?(file)
      if File.stat(file).file?
        if File.readable?(file)
          if FileTester.text?(file)
            TEXT
          else
            BINARY
          end
        else
          UNREADABLE
        end
      elsif File.stat(file).directory?
        DIRECTORY
      else
        UNKNOWN
      end
    else
      NONE
    end
  end

  def FileTester.is_text(ext)
    @@KNOWN_TEXT << ext
    @@KNOWN_NONTEXT.delete(ext)
  end

  def FileTester.is_nontext(ext)
    @@KNOWN_NONTEXT << ext
    @@KNOWN_TEXT.delete(ext)
  end

  def FileTester.text_extensions
    @@KNOWN_TEXT
  end

  def FileTester.nontext_extensions
    @@KNOWN_NONTEXT
  end

  def FileTester.text?(file)
    # Don't waste our time if it doesn't even exist:
    return false unless File.exists?(file)
    
    if file.index(/\.(\w+)\s*$/)
      suffix = $1
      return true  if @@KNOWN_TEXT.include?(suffix)
      return false if @@KNOWN_NONTEXT.include?(suffix)
    end
    
    ntested = 0
    nodd = 0

    Log.log "reading #{file}"

    File.open(file) do |f|
      buf = f.read(@@TEST_LENGTH)
      if buf
        Log.log "got buf; length = #{buf.length}"

        # split returns strings, whereas we want characters (bytes)
        chars = buf.split(//, buf.length).collect { |w| w[0] }

        # using the limit parameter to split results in the last character being
        # "0" (nil), so remove it

        if chars.size > 1 and chars[-1].to_i == 0
          chars = chars[0 .. -2]
        end
        
        chars.each do |ch|
          ntested += 1

          # never allow null in a text file
          return false if ch.to_i == 0
          
          nodd += 1 unless FileTester.ascii?(ch)
        end
      else
        # file had length of 0:
        return UNKNOWN
      end
    end
    return FileTester.summary(nodd, ntested)
  end

  def FileTester.summary(nodd, ntested)
    return nodd < ntested * @@ODD_FACTOR
  end

end


# -------------------------------------------------------
# IO
# -------------------------------------------------------

class IO

  $-w = false

  # Reads the stream into an array. It works even when $/ == nil, which
  # works around a problem in Ruby 1.8.1.
  def readlines
    contents = []
    while ((line = gets) && line.length > 0)
      contents << line
    end
    contents
  end

  $-w = true

end


# -------------------------------------------------------
# Glark
# -------------------------------------------------------

# The main processor.
class Glark 
  include Loggable
  
  def initialize(func)
    @func = func
  end

  def search_file(fname, lines)
    log "searching #{fname} for #{@func}" if $options.verbose
    p = InputFile.new(fname, lines)
    @func.process(p)
  end

  def search_binary_file(fname, lines)
    log "searching binary file #{fname} for #{@func}" if $options.verbose
    bf = BinaryFile.new(fname, lines)
    @func.process(bf)
  end
  
  def search(name)
    log "searching #{name} for #{@func}"

    if name == "-" 
      log "reading standard input..."
      $stderr.print "reading standard input...\n" unless $options.quiet
      search_file(name, $stdin.readlines)
    else
      type = FileTester.type(name)
      case type
      when FileTester::TEXT
        if $options.basename && !$options.basename.match(File.basename(name))
          log "skipping file: #{name}"
        elsif $options.fullname && !$options.fullname.match(name)
          log "skipping file: #{name}"
        else
          log "searching text"
          # readlines doesn't work with $/ == nil, so we'll use gets instead.
          # this has been fixed in the CVS version of Ruby (on 26 Dec 2003).
          text = []
          File.open(name) do |f|
            while ((line = f.gets) && line.length > 0)
              text << line
            end
          end
          log "got text #{text.length}"
          search_file(name, text)
        end
      when FileTester::BINARY
        if $options.basename && !$options.basename.match(File.basename(name))
          log "skipping file: #{name}"
        elsif $options.fullname && !$options.fullname.match(name)
          log "skipping file: #{name}"
        else
          log "handling binary"
          # Log.print "not a text file: #{name}\n"
          
          case $options.binary_files
          when "without-match"
            log "skipping binary file #{name}"
            
          when "binary"
            search_binary_file(name, IO.readlines(name))
            
          when "text"
            log "processing binary file #{name} as text"
            search_file(name, IO.readlines(name))
          end
        end
      when FileTester::UNREADABLE
        log "skipping unreadable"
        $stderr.print "file not readable: #{name}\n" unless $options.quiet
      when FileTester::NONE
        log "skipping none"
        $stderr.print "WARNING: no such file: #{name}\n" unless $options.quiet
      when FileTester::UNKNOWN
        log "skipping unknown"
        $stderr.print "WARNING: unknown file type: #{name}\n" unless $options.quiet
      when FileTester::DIRECTORY
        log "processing directory"
        case $options.directory
        when "read"
          log "directory: #{$options.directory}"
          $stderr.print "glark: #{name}: Is a directory\n" unless $options.quiet
        when "recurse"
          log "recursing into directory #{name}"
          begin
            entries = Dir.entries(name).reject { |x| x == "." || x == ".." }
            entries.each do |e|
              search(name + "/" + e)
            end
          rescue Errno::EACCES => e
            $stderr.print "WARNING: directory not readable: #{name}\n" unless $options.quiet
          end
        when "skip"
          log "skipping directory #{name}"
        else
          log "directory: #{$options.directory}"
        end
      else
        print "unknown type #{type}"
      end
    end
  end
end



# -------------------------------------------------------
# Env
# -------------------------------------------------------

# Returns the home directory, for both Unix and Windows.

module Env

  def Env.home_directory
    if hm = ENV["HOME"]
      return hm
    else
      hd = ENV["HOMEDRIVE"]
      hp = ENV["HOMEPATH"]
      if hd || hp
        return (hd || "") + (hp || "\\")
      else
        return nil
      end
    end
  end

  # matches single and double quoted strings:
  REGEXP = /                    # either:
              ([\"\'])          #     start with a quote, and save it ($1)
              (                 #     save this ($2)
                (?:             #         either (and don't save this):
                    \\.         #             any escaped character
                  |             #         or
                    [^\1\\]     #             anything that is not a quote ($1), and is not a backslash
                )*              #         as many as we can get
              )                 #         end of $2
              \1                #     end with the same quote we started with
            |                   # or
              (\S+)             #     plain old nonwhitespace ($3)
           /x
      
  # amazing that ruby-mode (Emacs) handled all that.
  
  
  # reads the environment variable, splitting it according to its quoting.
  def Env.split(varname)
    if v = ENV[varname]
      v.scan(REGEXP).collect { |x| x[1] || x[2] }
    else
      []
    end
  end

end


# -------------------------------------------------------
# Function Object
# -------------------------------------------------------

# A function object, which can be applied (processed) against a InputFile.
class FuncObj
  
  attr_accessor :match_line_number, :file, :matches

  def initialize
    @match_line_number = nil
    @matches = Array.new
  end

  def add_match(lnum)
    @matches.push(lnum)
  end

  def start_position
    match_line_number
  end

  def end_position
    start_position
  end

  def reset_file(file)
    @match_line_number = nil
    @file              = file
    @matches           = Array.new
  end

  def range(var, count)
    if var
      if var.index(/([\.\d]+)%/)
        count * $1.to_f / 100
      else
        var.to_f
      end
    else
      nil
    end
  end

  def process(infile)
    got_match = false
    reset_file(infile.fname)

    rgstart = range($options.range_start, infile.lines.size)
    rgend   = range($options.range_end,   infile.lines.size)

    nmatches = 0
    (0 ... infile.lines.size).each do |lnum|
      if ((!rgstart || lnum >= rgstart) && 
          (!rgend   || lnum <= rgend)   &&
          evaluate(infile.lines[lnum], lnum, infile))
        mark_as_match(infile)
        got_match = true
        nmatches += 1
        break if $options.num_matches && nmatches >= $options.num_matches
      end
    end
    
    if $options.file_names_only
      if got_match != $options.invert_match
        print infile.fname
        if $options.write_null
          print "\0"
        else
          print "\n"
        end
      end
    elsif $options.filter
      if $options.invert_match
        infile.write_non_matches
      elsif got_match
        infile.write_matches
      end
    else
      infile.write_all
    end
  end

  def mark_as_match(infile)
    infile.mark_as_match(start_position)
  end

  def to_s
    return inspect
  end
  
end


# -------------------------------------------------------
# Regular expression function object
# -------------------------------------------------------

# Applies a regular expression against a InputFile.
class RegexpFuncObj < FuncObj
  include Loggable

  attr_reader :re

  def initialize(re)
    @re = re
    @file = nil
    super()
  end

  def inspect
    @re.inspect
  end

  def match?(line)
    @re.match(line)
  end

  def evaluate(line, lnum, file)
    log "evaluating <<<#{line[0 .. -2]}>>>"
    if md = match?(line)
      log "matched regular expression #{@re}"
      
      if $options.extract_matches
        if md.kind_of?(MatchData)
          log "replacing line"
          line.replace(md[-1] + "\n")
          # line.gsub!(@re) { |m| Log.log "replacing with #{m}"; m }
        else
          log "--not does not work with -v"
        end
      else
        log "NOT replacing line"
      end
      
      @match_line_number = lnum
      # highlight what the regular expression matched
      if $options.highlight
        str = file.output.formatted[lnum] || file.get_line(lnum)
        # must use the block form
        file.output.formatted[lnum] = str.gsub(@re) { |m| $options.text_highlight + m + ANSIColor.reset }
      end
      add_match(lnum)
      return true
    else
      return false
    end
  end
  
  def explain(level = 0)
    " " * level + to_s + "\n"
  end
  
end


# -------------------------------------------------------
# Regular expression extension
# -------------------------------------------------------

# Negates the given expression.
class NegatedRegexp < Regexp

  def match(str)
    !super
  end

end

class Regexp

  # Handles negation, whole words, and ignore case (Ruby no longer supports
  # /foo/i, as of 1.8).
  
  def Regexp.create(pattern, negated = false, ignorecase = false, wholewords = false, wholelines = false)
    # we handle a ridiculous number of possibilities here:
    #     /foobar/     -- "foobar"
    #     /foo/bar/    -- "foo", then slash, then "bar"
    #     /foo\/bar/   -- same as above
    #     /foo/bar/i   -- same as above, case insensitive
    #     /foo/bari    -- "/foo/bari" exactly
    #     /foo/bar\/i  -- "/foo/bar/i" exactly
    #     foo/bar/     -- "foo/bar/" exactly
    #     foo/bar/     -- "foo/bar/" exactly

    if pattern.sub!(/^!(?=\/)/, "")
      Log.log "expression is negated"
      negated = true
    end

    if pattern.index(/^\/(.*[^\\])\/i$/)
      pattern    = $1
      ignorecase = true
    elsif pattern.index(/^\/(.*[^\\])\/$/)
      pattern    = $1
    elsif pattern.index(/^(\/.*)$/)
      pattern    = $1
    elsif pattern.index(/^(.*\/)$/)
      pattern    = $1
    end
    
    if wholewords
      # sanity check:

      # match "\w", A-Za-z0-9_, 
      stword  = pattern.index(/^[\[\(]*(?:\\w|\w)/)
      endword = pattern[-1, 1] =~ /\w/ || pattern[-2, 2] =~ /\w\?\*/

      re = Regexp.new('(?:                 # one of the following:
                           \\w             #   - \w for regexp
                         |                 # 
                           \w              #   - a literal A-Z, a-z, 0-9, or _
                         |                 # 
                           (?:             #   - one of the following:
                               \[[^\]]*    #         LB, with no RB until:
                               (?:         #      - either of:
                                   \\w     #         - "\w"
                                 |         # 
                                   \w      #         - a literal A-Z, a-z, 0-9, or _
                               )           #      
                               [^\]]*\]    #      - anything (except RB) to the next RB
                           )               #
                       )                   #
                       (?:                 # optionally, one of the following:
                           \*              #   - "*"
                         |                 # 
                           \+              #   - "+"
                         |                 #
                           \?              #   - "?"
                         |                 #
                           \{\d*,\d*\}     #   - "{3,4}", "{,4}, "{,123}" (also matches the invalid {,})
                       )?                  #
                       $                   # the end.
                      ', 
                      Regexp::EXTENDED)
      endword = pattern.index(re)

      if stword && endword
        # good
      else
        msg = "WARNING: pattern '#{pattern}' does not "
        if stword
          msg += "end"
        elsif endword
          msg += "begin"
        else
          msg += "begin and end"
        end
        msg += " on a word boundary."
        $stderr.puts msg
      end
      pattern = '\b' + pattern + '\b'
    elsif wholelines
      pattern = '^'  + pattern + '$'
    end
    
    # log "pattern", pattern
    # log "ignorecase", ignorecase
    
    reclass = negated ? NegatedRegexp : Regexp
    if ignorecase
      regex = reclass.new(pattern, Regexp::IGNORECASE)
    else
      regex = reclass.new(pattern)
    end

    regex
  end
end


# -------------------------------------------------------
# Compound expression function object
# -------------------------------------------------------

# Associates a pair of expressions.
class CompoundExpression < FuncObj

  def initialize(op1, op2)
    @op1, @op2 = op1, op2
    @file = nil
    super()
  end

  def reset_file(file)
    @op1.reset_file(file)
    @op2.reset_file(file)
    super
  end

  def start_position
    return @last_start
  end
  
end


# -------------------------------------------------------
# Or expression function object
# -------------------------------------------------------

# Evaluates both expressions.
class OrExpression < CompoundExpression

  def evaluate(line, lnum, file)
    # log self, "evaluating <<<#{line[0 .. -2]}>>>"

    m1 = @op1.evaluate(line, lnum, file)
    m2 = @op2.evaluate(line, lnum, file)

    if comp(m1, m2)
      if m1
        @last_start = @op1.start_position
        @last_end   = @op1.end_position
      end
      if m2
        @last_start = @op2.start_position
        @last_end   = @op2.end_position
      end
      
      @match_line_number = lnum
      add_match(lnum)
      return true
    else
      return false
    end
  end
  
  def inspect
    "(" + @op1.to_s + " or " + @op2.to_s + ")"
  end

  def end_position
    return @last_end
  end

  def explain(level = 0)
    str  = " " * level + "either:\n"
    str += @op1.explain(level + 4)
    str += " " * level + operator + "\n"
    str += @op2.explain(level + 4)
    str
  end
  
end


# -------------------------------------------------------
# Inclusive or expression function object
# -------------------------------------------------------

# Evaluates both expressions, and is satisfied when either return true.
class InclusiveOrExpression < OrExpression

  def comp(m1, m2)
    m1 || m2
  end
  
  def operator
    "or"
  end

end


# -------------------------------------------------------
# Exclusive or expression function object
# -------------------------------------------------------

# Evaluates both expressions, and is satisfied when only one returns true.
class ExclusiveOrExpression < OrExpression

  def comp(m1, m2)
    m1 ^ m2
  end

  def operator
    "xor"
  end

end


# -------------------------------------------------------
# And expression function object
# -------------------------------------------------------

# Evaluates both expressions, and is satisfied when both return true.
class AndExpression < CompoundExpression
  
  def initialize(dist, op1, op2)
    @dist = dist
    super(op1, op2)
  end

  def mark_as_match(infile)
    infile.mark_as_match(start_position, end_position)
  end

  def match_within_distance(op, lnum)
    op.matches.size > 0 and (op.matches[-1] - lnum <= @dist)
  end

  def inspect
    str = "("+ @op1.to_s
    if @dist == 0
      str += " same line as "
    elsif @dist.kind_of?(Float) && @dist.infinite?
      str += " same file as "
    else 
      str += " within " + @dist.to_s + " lines of "
    end
    str += @op2.to_s + ")"
    str
  end

  def reset_match(op, lnum)
    op.matches.reverse.each do |m|
      if lnum - m <= @dist
        @last_start = m
        return true
      else
        return false
      end
    end
    return true
  end

  def match?(line, lnum, file)
    m1 = @op1.evaluate(line, lnum, file)
    m2 = @op2.evaluate(line, lnum, file)

    if m1 and match_within_distance(@op2, lnum)
      return reset_match(@op2, lnum)
    elsif m2 and match_within_distance(@op1, lnum)
      return reset_match(@op1, lnum)
    else
      return false
    end
  end

  def end_position
    [@op1.end_position, @op2.end_position].max
  end

  def evaluate(line, lnum, file)
    # log self, "evaluating line #{lnum}: #{line[0 .. -2]}"

    if match?(line, lnum, file)
      @match_line_number = lnum
      return true
    else
      return false
    end
  end

  def explain(level = 0)
    str = ""
    if @dist == 0
      str += " " * level + "on the same line:\n"
    elsif @dist.kind_of?(Float) && @dist.infinite?
      str += " " * level + "in the same file:\n"
    else 
      lnstr = @dist == 1 ? "line" : "lines"
      str += " " * level + "within #{@dist} #{lnstr} of each other:\n"
    end
    str += @op1.explain(level + 4)
    str += " " * level + "and\n"
    str += @op2.explain(level + 4)
    str
  end
  
end


# -------------------------------------------------------
# Expression function object creator
# -------------------------------------------------------

class ExpressionCreator
  include Loggable

  attr_reader :expr

  def initialize(arg, args)
    @current = arg
    @args    = args
    @expr    = create_expression
  end

  def create_regular_expression(negated = false)
    pat = @current.dup

    # this check is because they may have omitted the pattern, e.g.:
    #   % glark *.cpp
    if File.exists?(pat) and !$options.quiet
      $stderr.print "WARNING: pattern '#{pat}' exists as a file.\n"
      $stderr.print "    Pattern may have been omitted.\n"
    end

    regex = Regexp.create(pat, negated, $options.nocase, $options.whole_words, $options.whole_lines)
    RegexpFuncObj.new(regex)
  end 

  # creates two expressions and returns them.
  def create_expressions
    @current = @args.shift
    a1 = create_expression

    @current = @args.shift
    a2 = create_expression
    
    [ a1, a2 ]
  end

  def consume_not_expression
    @current = @args.shift
    expr = create_regular_expression(true)
    unless expr
      $stderr.print "ERROR: 'not' expression takes one argument\n"
      exit 2
    end

    # explicit end tag is optional:
    @args.shift if @args[0] == "--end-of-not"
    expr
  end
  
  def consume_or_expression
    a1, a2 = create_expressions
    unless a1 && a2
      $stderr.print "ERROR: 'or' expression takes two arguments\n"
      exit 2
    end

    # explicit end tag is optional:
    @args.shift if @args[0] == "--end-of-or"
    InclusiveOrExpression.new(a1, a2)
  end

  def consume_xor_expression
    a1, a2 = create_expressions
    unless a1 && a2
      $stderr.print "ERROR: 'xor' expression takes two arguments\n"
      exit 2
    end

    # explicit end tag is optional:
    @args.shift if @args[0] == "--end-of-xor"
    ExclusiveOrExpression.new(a1, a2)
  end

  def consume_and_expression
    if @current == "-a"
      dist = @args.shift
    # future version will support --and=NUM, with --and (no following =) defaulting to 0
    # elsif @current == "--and"
      # dist = "0"
    elsif @current.index(/^(?:\-\-and=)(\-?\d+)$/)
      dist = $1
    else
      dist = @args.shift
    end
    
    # check to ensure that this is numeric
    if !dist || (dist.to_i != $options.infinite_distance && !dist.index(/^\d+$/))
      $stderr.print "ERROR: invalid distance for 'and' expression: '#{dist}'\n" 
      $stderr.print "    expecting an integer, or #{$options.infinite_distance} for 'infinite'\n" 
      exit 2
    end

    if dist.to_i == $options.infinite_distance
      dist = 1.0 / 0.0            # infinity
    else
      dist = dist.to_i
    end

    a1, a2 = create_expressions
    unless a1 && a2
      $stderr.print "ERROR: 'and' expression takes two arguments\n"
      exit 2
    end
    # explicit end tag is optional:
    @args.shift if @args[0] == "--end-of-and"
    AndExpression.new(dist, a1, a2)
  end

  def create_expression
    if @current
      log "processing arg #{@current}"
      case @current
      when "--or", "-o"
        return consume_or_expression
      when "--xor"
        return consume_xor_expression
      when /^\-\-and/, /^\-a/
        return consume_and_expression
      when /^--/
        $stderr.print "option not understood: #{@current}"
        exit 2
      else
        $stderr.print "assuming the last argument #{@current} is a pattern\n" if $options.verbose
        return create_regular_expression
      end
    else
      return nil
    end
  end

end


# -------------------------------------------------------
# Help
# -------------------------------------------------------

class GlarkHelp

  def initialize
    puts "Usage: glark [options] expression file..."
    puts "Search for expression in each file or standard input."
    puts "Example: glark --and=3 'try' 'catch' *.java"
    puts ""

    puts "Input:"
    puts "  -0[nnn]                        Use \\nnn as the input record separator"
    puts "  -d, --directories=ACTION       Process directories as read, skip, or recurse"
    puts "      --binary-files=TYPE        Treat binary files as TYPE"
    puts "      --basename, --name EXPR    Search only files with base names matching EXPR"
    puts "      --fullname, --path EXPR    Search only files with full names matching EXPR"
    puts "  -M, --exclude-matching         Ignore files with names matching the expression"
    puts "  -r, --recurse                  Recurse through directories"
    puts ""

    puts "Matching:"
    puts "  -a, --and=NUM EXPR1 EXPR2      Match both expressions, within NUM lines"
    puts "  -b, --before NUM[%]            Restrict the search to the top % or lines"
    puts "  -f, --after NUM[%]             Restrict the search to after the given location"
    puts "  -i, --ignore-case              Ignore case for matching regular expressions"
    puts "  -m, --match-limit=NUM          Find only the first NUM matches in each file"
    puts "  -o, --or EXPR1 EXPR2           Match either of the two expressions"
    puts "  -R, --range NUM[%] NUM[%]      Restrict the search to the given range of lines"
    puts "  -v, --invert-match             Show lines not matching the expression"
    puts "  -w, --word, --word-regexp      Put word boundaries around each pattern"
    puts "  -x, --line-regexp              Select entire line matching pattern"
    puts "      --xor EXPR1 EXPR2          Match either expression, but not both"
    puts ""

    puts "Output:"
    puts "  -A, --after-context=NUM        Print NUM lines of trailing context"
    puts "  -B, --before-context=NUM       Print NUM lines of leading context"
    puts "  -C, -NUM, --context[=NUM]      Output NUM lines of context"
    puts "  -c, --count                    Display only the match count per file"
    puts "  -F, --file-color COLOR         Specify the highlight color for file names"
    puts "      --no-filter                Display the entire file"
    puts "  -g, --grep                     Produce output like the grep default"
    puts "  -h, --no-filename              Do not display the names of matching files"
    puts "  -H, --with-filename            Display the names of matching files"
    puts "  -l, --files-with-matches       Print only names of matching file"
    puts "  -L, --files-without-match      Print only names of file not matching"
    puts "  -n, --line-number              Display line numbers"
    puts "  -N, --no-line-number           Do not display line numbers"
    puts "  -T, --text-color COLOR         Specify the highlight color for text"
    puts "  -u, --highlight                Enable highlighting"
    puts "  -U, --no-highlight             Disable highlighting"
    puts "  -y, --extract-matches          Display only the matching region, not the entire line"
    puts "  -Z, --null                     In -l mode, write file names followed by NULL"
    puts ""

    puts "Debugging/Errors:"
    puts "      --explain                  Write the expression in a more legible format"
    puts "  -q, --quiet                    Suppress warnings"
    puts "  -Q, --no-quiet                 Enable warnings"
    puts "  -s, --no-messages              Suppress warnings"
    puts "  -V, --version                  Display version information"
    puts "      --verbose                  Display normally suppressed output"

    puts ""
    puts "See the man page for more information."
  end

end


# -------------------------------------------------------
# Options
# -------------------------------------------------------

class GlarkOptions
  include Loggable

  attr_accessor :after
  attr_accessor :before
  attr_accessor :binary_files
  attr_accessor :count
  attr_accessor :directory
  attr_accessor :exclude_matching
  attr_accessor :exit_status
  attr_accessor :explain
  attr_accessor :expr
  attr_accessor :extract_matches
  attr_accessor :file_highlight
  attr_accessor :file_names_only
  attr_accessor :filter
  attr_accessor :grep_output
  attr_accessor :highlight
  attr_accessor :infinite_distance
  attr_accessor :invert_match
  attr_accessor :basename
  attr_accessor :fullname
  attr_accessor :nocase
  attr_accessor :num_matches
  attr_accessor :package
  attr_accessor :local_config_files
  attr_accessor :quiet
  attr_accessor :range_end
  attr_accessor :range_start
  attr_accessor :show_file_names
  attr_accessor :show_line_numbers
  attr_accessor :text_highlight
  attr_accessor :text_highlights
  attr_accessor :verbose
  attr_accessor :version
  attr_accessor :whole_lines
  attr_accessor :whole_words
  attr_accessor :write_null
  attr_accessor :show_break

  def initialize(package = "undef", version = "1.2.3.4")
    $options = self

    @after             = 0          # lines of context before the match
    @before            = 0          # lines of context after the match
    @binary_files      = "binary"   # 
    @count             = false      # just count the lines
    @directory         = "read"     # read, skip, or recurse, a la grep
    @expr              = nil        # the expression to be evaluated
    @exclude_matching  = false      # exclude files whose names match the expression
    @exit_status       = 1          # 0 == matches, 1 == no matches, 2 == error
    @explain           = false      # display a legible version of the expression
    @extract_matches   = false      # whether to show _only_ the part that matched
    @file_names_only   = false      # display only the file names
    @filter            = true       # display only matches
    @grep_output       = false      # emulate grep output
    @highlight         = true       # highlight matches (using ANSI codes)

    @infinite_distance = -1         # signifies no limit to the distance between
                                    # matches, i.e., anywhere within the entire file is valid.

    @invert_match      = false      # display non-matching lines
    @basename          = nil        # match files with this basename
    @fullname          = nil        # match files with this full name
    @nocase            = false      # match case
    @num_matches       = nil        # the maximum number of matches to display per file
    @package           = package
    @local_config_files = false
    @quiet             = false      # minimize warnings
    @range_end         = nil        # range to stop searching; nil => the entire file
    @range_start       = nil        # range to begin searching; nil => the entire file
    @show_line_numbers = true       # display numbers of matching lines
    @show_file_names   = true       # show the names of matching files
    @verbose           = nil        # display debugging output
    @version           = version
    @whole_lines       = false      # true means patterns must match the entire line
    @whole_words       = false      # true means all patterns are '\b'ed front and back
    @write_null        = false      # in @file_names_only mode, write '\0' instead of '\n'

    @show_break        = false

    # default highlighting
    @text_highlight    = ANSIColor::code("black on yellow")
    @text_highlights   = [ ANSIColor::code("black on yellow") ]
    @file_highlight    = ANSIColor::code("reverse bold")
  end

  def run(args)
    @args = args
    
    log ""

    if hd = Env.home_directory
      homerc = hd + "/.glarkrc"
      read_rcfile(homerc)
    end

    if @local_config_files
      dir = File.expand_path(".")
      log "starting with #{dir}"
      while dir != "/" && dir != hd
        rcfile = dir + "/.glarkrc"
        log "looking for #{rcfile}"
        if File.exists?(rcfile)
          read_rcfile(rcfile)
          break
        else
          log "not found #{rcfile}"
          dir = File.dirname(dir)
        end
      end
    end

    read_environment_variable

    # honor thy EMACS; go to grep mode
    set_grep_output if ENV["EMACS"]

    read_options
    validate

    if @verbose
      methods.sort.each do |meth|
        # call the accessor for every setter method
        if meth.index(/^(\w+)=$/)
          acc = $1
          m = method(acc)
          log "#{acc}: #{m.call}" + ANSIColor.reset
        end
      end
    end
  end

  def read_rcfile(fname)
    log ""

    log "reading RC file: #{fname}"
    if File.exists?(fname)
      IO.readlines(fname).each do |line|
        line.sub!(/\s*#.*/, "")
        line.chomp!
        name, value = line.split(/\s*[=:]\s*/)
        next unless name && value

        case name
        when "after-context"
          @after = value.to_i
        when "before-context"
          @before = value.to_i
        when "binary-files"
          @binary_files = value
        when "context"
          @after = @before = value == "all" ? -1 : value.to_i
        when "expression"
          # this should be more intelligent than just splitting on whitespace:
          @expr = ExpressionCreator.new(value.split(/\s+/))
        when "file-color"
          @file_highlight = make_highlight(name, value)
        when "filter"
          @filter = to_boolean(value)
        when "grep"
          set_grep_output if to_boolean(value)
        when "highlight"
          @highlight = to_boolean(value)
        when "ignore-case"
          @nocase = to_boolean(value)
        when "known-nontext-files"
          value.split(/\s+/).each do |ext|
            FileTester.is_nontext(ext)
          end
        when "known-text-files"
          value.split(/\s+/).each do |ext|
            FileTester.is_text(ext)
          end
        when "local-config-files"
          @local_config_files = to_boolean(value)
        when "show-break"
          @show_break = to_boolean(value)
        when "quiet"
          @quiet = to_boolean(value)
        when "text-color"
          @text_highlight = make_highlight(name, value)
        when "verbose"
          @verbose = to_boolean(value) ? 1 : nil
          AppLog.verbose = @verbose
        when "verbosity"
          @verbose = value.to_i
          AppLog.verbose = @verbose
        end
      end
    end
  end
  
  # creates a color for the given option, based on its value
  def make_highlight(opt, value)
    if value
      return ANSIColor::code(value)
    else
      $stderr.print "ERROR: " + opt + " requires a color\n"
      exit 2
    end
  end

  # returns whether the value matches a true value, such as "yes", "true", or "on".
  def to_boolean(value)
    [ "yes", "true", "on" ].include?(value.downcase)
  end

  def read_environment_variable
    # process the environment variable
    options = Env.split("GLARKOPTS")
    log "options: #{options.join(', ')}"
    while options.length > 0
      opt = options.shift
      process_option(opt, options)
    end
  end

  # sets output a la grep
  def set_grep_output
    @highlight         = false
    @show_line_numbers = false
    @after             = 0
    @before            = 0
    @grep_output       = true
  end

  def read_options
    log ""
    nargs = @args.size
    args = @args.dup

    @expr = nil

    while @args.length > 0
      arg = @args.shift
      break if @expr = process_option(arg, @args)
    end

    unless @expr
      # were any options processed?

      # A lone option of "-v" means version, if there was nothing else on the
      # command line. For grep compatibility, "-v" with an expression has to
      # mean an inverted match.

      if nargs == 1 && args[0] == "-v"
        show_version
      elsif nargs > 0
        $stderr.print "No expression provided.\n"
      end
      
      $stderr.print "Usage: glark [options] expression file...\n"
      $stderr.print "Try `glark --help' for more information.\n"
      exit 1
    end
  end

  def process_option(opt, args)
    log "processing option #{opt}"
    case opt

    when /^-0(\d{0,3})/
      log "got record separator"
      if $1.size.zero?
        $/ = "\n\n"
      else
        val = $1.oct
        begin
          $/ = $1.oct.chr
        rescue RangeError => e
          # out of range (e.g., 777) means nil:
          $/ = nil
        end
      end
      log "record separator set to #{$/}"

      # after (context)
    when "-A"
      @after = args.shift.to_i
    when /^--after-context=(\d+)/
      @after = $1.to_i

      # before (context)
    when "-B"
      @before = args.shift.to_i
    when /^--before-context=(\d+)/
      @before = $1.to_i

      # after (range)
    when "-f", "--after"
      @range_start = args.shift

      # before (range)
    when "-b", "--before"
      @range_end = args.shift

      # range
    when "-R", "--range"
      @range_start, @range_end = args.shift, args.shift

      # context
    when "-C"
      nxt = args.shift
      # keep it if it is a number, else use the default
      if nxt =~ /^\d+/
        @before = @after = nxt.to_i
      else
        @before = @after = 2
        args.unshift(nxt)
      end
    when /^--context(=(\d+))?/
      @after = @before = if $2 then $2.to_i else 2 end
    when /^-([1-9]\d*)$/
      @after = @before = $1.to_i
      log "@after = #{@after}; @before = #{@before}"

      # highlighting
    when "-u", "--highlight"
      @highlight = true
    when "-U", "--no-highlight"
      @highlight = false
      
      # version
    when "-V", "--version"
      show_version

      # verbose
    when /^--verbos(?:e|ity)(?:=(\d+))?/
      @verbose = $1 ? $1.to_i : 1
      log "setting verbose to #{@verbose}"
      AppLog.verbose = @verbose

    when "-v", "--invert-match"
      @invert_match = true
      @exit_status  = 0
    when "-i", "--ignore-case"
      @nocase = true

      # filter
    when "--filter"
      @filter = true

      # filter
    when /--no-?filter/
      @filter = false

      # grep
    when "-g", "--grep"
      set_grep_output

      # help
    when "-?", "--help"
      GlarkHelp.new
      exit 0

      # regexp explanation
    when "--explain"
      @explain = true

      # line numbers
    when "-N", "--no-line-number"
      @show_line_numbers = false
    when "-n", "--line-number"
      @show_line_numbers = true

      # quiet
    when "-q", "-s", "--quiet", "--messages"
      @quiet = true
    when "-Q", "-S", "--no-quiet", "--no-messages"
      @quiet = false

    when "-m", "--match-limit"
      @num_matches = args.shift.to_i
      
      # whole words
    when "-w", "--word", "--word-regexp"
      @whole_words = true

      # whole lines
    when "-x", "--line-regexp"
      @whole_lines = true
      
      # file names only
    when "-l", "--files-with-matches"
      @file_names_only = true
      @invert_match = false
    when  "-L", "--files-without-match"
      @file_names_only = true
      @invert_match = true

      # For selecting by file name, like find(1) --name
    when "--name", "--basename"
      @basename = Regexp.create(args.shift)
      # Make this a regexp.  If they want globs they can use find.

      # For selecting by file name, like find(1) --path
    when "--path", "--fullname"
      @fullname = Regexp.create(args.shift)
      # Make this a regexp.  If they want globs they can use find.

      # colors
    when "-T", "--text-color"
      @text_highlight = make_highlight(opt, args.shift)
    when "-F", "--file-color"
      @file_highlight = make_highlight(opt, args.shift)

    when "-c", "--count"
      @count = true

    when "-Z", "--null"
      @write_null = true

    when "-M", "--exclude-matching"
      @exclude_matching = true
      
    when "-d"
      @directory = args.shift
    when /^--directories=(\w+)/
      @directory = $1

    when "-r", "--recurse"
      @directory = "recurse"

    when "-o", "-a"
      ec = ExpressionCreator.new(opt, args)
      @expr = ec.expr
      return @expr               # we are done.

    when "-H", /^--with-?filenames?$/
      @show_file_names = true
      
    when "-h", /^--no-?filenames?$/
      @show_file_names = false
      
    when /^--binary-files?=\"?(\w+)\"?/
      @binary_files = $1
      log "set binary_files to #{@binary_files}"

    when "-y", "--extract-matches"
      log "set extract matches"
      @extract_matches = true
      
    when /^(\-(?:[1-9]\d*|\w))(.+)/
      # handles -13wo (-13, -w, -o)

      opt, rest = $1, "-" + $2
      log "opt, rest = #{opt}, #{rest}"
      args.unshift(rest)
      log "args = #{args}"
      return process_option(opt, args)

    when "--config"
      printf "%s: %s\n", "after", @after
      printf "%s: %s\n", "basename", @basename
      printf "%s: %s\n", "before", @before
      printf "%s: %s\n", "binary_files", @binary_files
      printf "%s: %s\n", "count", @count
      printf "%s: %s\n", "directory", @directory
      printf "%s: %s\n", "exclude_matching", @exclude_matching
      printf "%s: %s\n", "explain", @explain
      printf "%s: %s\n", "expr", @expr
      printf "%s: %s\n", "extract_matches", @extract_matches
      printf "%s: %sfilename%s\n", "file_highlight", @file_highlight, ANSIColor.reset
      printf "%s: %s\n", "file_names_only", @file_names_only
      printf "%s: %s\n", "filter", @filter
      printf "%s: %s\n", "fullname", @fullname
      printf "%s: %s\n", "grep_output", @grep_output
      printf "%s: %s\n", "highlight", @highlight
      printf "%s: %s\n", "infinite_distance", @infinite_distance
      printf "%s: %s\n", "invert_match", @invert_match
      printf "%s: %s\n", "known_nontext_files", FileTester.nontext_extensions.join(", ")
      printf "%s: %s\n", "known_text_files", FileTester.text_extensions.join(", ")
      printf "%s: %s\n", "local_config_files", @local_config_files
      printf "%s: %s\n", "nocase", @nocase
      printf "%s: %s\n", "num_matches", @num_matches
      printf "%s: %s\n", "package", @package
      printf "%s: %s\n", "quiet", @quiet
      printf "%s: %s\n", "range_end", @range_end
      printf "%s: %s\n", "range_start", @range_start
      printf "%s: %s\n", "show_break", @show_break
      printf "%s: %s\n", "show_file_names", @show_file_names
      printf "%s: %s\n", "show_line_numbers", @show_line_numbers
      printf "%s: %stext%s\n", "text_highlight", @text_highlight, ANSIColor.reset
      printf "%s: %stext%s\n", "text_highlights", @text_highlights, ANSIColor.reset
      printf "%s: %s\n", "verbose", @verbose
      printf "%s: %s\n", "version", @version
      printf "%s: %s\n", "whole_lines", @whole_lines
      printf "%s: %s\n", "whole_words", @whole_words
      printf "%s: %s\n", "write_null", @write_null
      printf "%s: %s\n", "ruby version", RUBY_VERSION
      exit
      
      # the expression
    else
      log "not an option: #{opt}"
      if args
        ec = ExpressionCreator.new(opt, args)
        @expr = ec.expr
        return @expr            # we are done.
      end
    end
    return nil                  # we're not done.
  end

  # check options for collisions/data validity
  def validate
    if @range_start && @range_end
      pctre = Regexp.new(/([\.\d]+)%/)
      smd = pctre.match(@range_start)
      emd = pctre.match(@range_end)
      if !smd == !emd
        if smd
          if smd[1].to_f > emd[1].to_f
            puts "ERROR: range start (#{smd}) follows range end (#{emd})"
            exit 2
          end
        elsif @range_start.to_i > @range_end.to_i
          puts "ERROR: range start (#{@range_start}) follows range end (#{@range_end})"
          exit 2
        end
      end
    end
  end

  def show_version
    puts @package + ", version " + @version
    puts "Written by Jeff Pace (jpace@incava.org)."
    puts "Released under the Lesser GNU Public License."
    exit 0
  end
  
end


# -------------------------------------------------------
# main()
# -------------------------------------------------------

begin
  AppLog.set_widths(15, 5, -30)
  AppLog.set_color(Log::WARN, "reverse")

  AppLog.log "loading options"
  GlarkOptions.new($PACKAGE, $VERSION).run(ARGV)
  AppLog.log "done loading options"

  # To get rid of the annoying stack trace on ctrl-C:
  trap("INT") { abort }

  puts $options.expr.explain if $options.explain

  glark = Glark.new($options.expr)
  $files = if ARGV.size > 0 then
    ARGV.collect do |f|
      f.split(File::PATH_SEPARATOR)
    end.flatten
  else 
    [ '-' ]
  end
  
  $files.each do |f|
    if $options.exclude_matching
      if $options.expr.re.match(f)
        AppLog.log "skipping file #{f} with matching name"
        next
      else
        AppLog.log "not skipping file #{f}"
      end
    end
    glark.search(f) 
  end
rescue => e
  # show only the message, not the stack trace:
  $stderr.puts "error: #{e}"
  #$$$ raise only during debugging:
  # raise
end
