#!/usr/local/bin/php -Cq
<?php
/*
*
*   program:  pmatch
*   author:   Monte Ohrt <monte@ispi.net>
*   version:  (see $version property)
*   date:     September 26, 2002
*   purpose:  pmatch acts much like grep, but with
*             many enhancements such as:
*             * show only portion of line that matched
*             * show surrounding lines of match
*             * trim whitespace from matching lines
*             * syntax highlighting
*             * PCRE regex engine
*
*   examples (from shell prompt):
*
*   // search for string "foo" in *.txt files
*   pmatch "foo" *.txt
*   // search for {$...} in *.tpl files,
*   // show two lines above and below matches
*   pmatch -C 2 "{\\$.*}" *.tpl
*   // search for {$...} in *.tpl files,
*   // only show file name,
*   // show 5 characters before and after match
*   pmatch -F -P 5 "{\\$.*}" *.tpl
*
*/

class pmatch {

	var $version = '1.5.3';

    var $show_filename = true;
    var $show_only_filename = false;
    var $show_line_numbers = false;
    var $show_negative_match = false;
    var $show_filepath = true;
    var $found_pattern = false;
    var $show_entire_line = true;
    var $show_only_match = false;
    var $show_only_count = false;
    var $ignore_case = false;
    var $supress_errors = false;
    var $greedy_match = false;
    var $trim_output = true;
	var $word_boundary = false;
	var $regex_match = true;
	var $context_lines = 0;
	var $highlight = true;
	var $context_chars = null;
	
    var $_filenames = array();
	var $_argv_pattern = null;
	var $_pcre_pattern = null;
	var $_pattern_opts = null;
	var $_lines = array();
	var $_results = array();
	var $_filename = null;
	var $_filenames = array();
	var $_filename_count = array();
	var $_found = false;
	
	var $text_attrs = array(
		"reset" => "\033[0m",
		"bright" => "\033[1m",
		"dim" => "\033[2m",
		"underscore" => "\033[4m",
		"blink" => "\033[5m",
		"reverse" => "\033[7m",
		"hidden" => "\033[8m",
		"fg_black" => "\033[30m",
		"fg_red" => "\033[31m",
		"fg_green" => "\033[32m",
		"fg_yellow" => "\033[33m",
		"fg_blue" => "\033[34m",
		"fg_magenta" => "\033[35m",
		"fg_cyan" => "\033[36m",
		"fg_white" => "\033[37m",
		"bg_black" => "\033[40m",
		"bg_red" => "\033[41m",
		"bg_green" => "\033[42m",
		"bg_yellow" => "\033[43m",
		"bg_blue" => "\033[44m",
		"bg_magenta" => "\033[45m",
		"bg_cyan" => "\033[46m",
		"bg_white" => "\033[47m"			
	);

	function pmatch() {
		// class constructor
	}
	
	function show_usage() {
        echo "\npmatch, the PHP regular expression file matcher, version ".$this->version."\n";
        echo "Monte Ohrt <monte@ispi.net>\n\n";
        echo "usage: pmatch [ -CcFGHhilnPsTvWX ] 'pattern' [ file ] ...\n";
        echo "-C N       : show N lines above and below matched line\n";
        echo "-c         : show only line count for each file\n";
        echo "-F         : hide file path\n";
        echo "-G         : greedy matching\n";
        echo "-H         : disable syntax highlighting\n";
        echo "-h         : supress filenames\n";
        echo "-i         : ignore case\n";
        echo "-l         : show only filenames\n";
        echo "-n         : show line numbers\n";
        echo "-P [N]     : show only first match, with N surrounding chars\n";
        echo "-M         : show only matching chars\n";
        echo "-s         : supress error messages about missing or unreadable files\n";
        echo "-T         : do not trim whitespace from begin/end of lines\n";
        echo "-v         : show lines that do NOT match pattern\n";
        echo "-W         : match pattern as a whole word only\n";
        echo "-X         : do literal match (no regex)\n";
        echo "\nsee PCRE manual for more information\n";
	}

	function parse_arguments() {
		
		global $argv;
						
		if(count($argv) < 2) {
			return false;
		}
		
    	for ($x=1; $x < count($argv); $x++) {

        	if(substr($argv[$x],0,1) == '-' && $argv[$x-1] != '--') {

            	$flags = preg_split('!!',$argv[$x],-1,PREG_SPLIT_NO_EMPTY);

            	foreach($flags as $flag) {

                	switch($flag) {

                    	case "-":
                        	break;

                    	case "C":
                        	$this->context_lines = $argv[$x+1];
							$this->show_entire_line = true;
							$x++;
                        	break;

                    	case "F":
                        	$this->show_filepath = false;
                        	break;

                    	case "h":
                        	$this->show_filename = false;
                        	break;

                    	case "H":
                        	$this->highlight = false;
                        	break;

                    	case "l":
                        	$this->show_only_filename = true;
                        	break;

                    	case "i":
                        	$this->ignore_case = true;
                        	break;

                    	case "G":
                        	$this->greedy_match = true;
                        	break;

                    	case "P":
                        	$this->show_entire_line = false;
                        	$this->context_lines = 0;
							if(substr($argv[$x+1],0,1) != '-') {
								$this->context_chars = (int) $argv[$x+1];
								$x++;
							}
                        	break;

                    	case "M":
                        	$this->show_entire_line = false;
                        	$this->show_only_match = true;
                        	break;

                    	case "c";
                        	$this->show_only_count = true;
                        	break;

                    	case "n";
                        	$this->show_line_numbers = true;
                        	break;

                    	case "s";
                        	$this->supress_errors = true;
                        	break;

                    	case "v";
                        	$this->show_negative_match = true;
                        	$this->show_entire_line = true;
                        	break;

                    	case "T";
                        	$this->trim_output = false;
                        	break;

                    	case "W";
                        	$this->word_boundary = true;
                        	break;

                    	case "X";
                        	$this->regex_match = false;
                        	break;

                    	default:
                        	echo "unknown flag: $flag\n";
                        	$this->show_usage();
                        	exit(1);

                	}

            	}
        	} else {			
				// beginning of filename list
				$this->_argv_pattern = str_replace('\PLUS', '+', $argv[$x]);
				if(isset($argv[$x+1])) {
					$this->_filenames = array_slice($argv,$x+1);
				}
				break;
			}
		}
		return true;
	}
	
	function process_pattern() {
		if($this->regex_match) {
            $this->_argv_pattern = str_replace(array('/'),array('\/'),$this->_argv_pattern);
		} else {
            $this->_argv_pattern = preg_quote($this->_argv_pattern,'/');
		}
		if($this->word_boundary) {
			$this->_argv_pattern = '\b'.$this->_argv_pattern.'\b';
		}

        if(!$this->greedy_match)
            $this->_pattern_opts .= 'U';                
        if($this->ignore_case)
            $this->_pattern_opts .= 'i';

		if(substr($this->_argv_pattern,0,1) == '^') {
			$this->_pcre_pattern = '/';
		} else {
			$this->_pcre_pattern = '/^.*';
		}
		$this->_pcre_pattern .= $this->_argv_pattern;
		if(substr($this->_argv_pattern,-1) == '$') {
			$this->_pcre_pattern .= '/';
		} else {
			$this->_pcre_pattern .= '.*$/';
		}
		
		$this->_pcre_pattern .= $this->_pattern_opts;
		
		return true;
	}
	
	function process_input() {
		if($this->_filename == 'php://stdin') {
			// input from stdin
			$this->show_filename = false;
			$fd = popen('cat /dev/fd/0','r');
			$this->scan_input_lines($fd);
			pclose($fd);
			return true;		
		} else {
			// input from file
            if(!@file_exists($this->_filename)) {
                if(!$this->supress_errors) {
                    trigger_error("no such file: $this->_filename");
				}
                return false;					
			} elseif(!@is_readable($this->_filename)) {
                if(!$this->supress_errors) {
                    trigger_error("cannot read file: $this->_filename");
				}
                return false;
            }
			$fsize = filesize($this->_filename);
			$fd = fopen($this->_filename,'r');
			$this->scan_input_lines($fd);
			fclose($fd);
			return true;
		}
	}
	
	function scan_input_lines(&$fd) {
		$_counter = 1;
		$_post_counter = 0;
		while(!feof($fd)) {
			$_curr_line = fgets($fd, 4096);
			if($_curr_line == '') {
				// EOF
				break;
			} else {
				$_curr_line = chop($_curr_line);
			}
			if($this->match_line($_curr_line)) {
				if($this->show_only_count) {
					$this->_filename_count[$this->_filename]++;
				} elseif($this->show_only_filename)  {
					$this->display_filename($this->_filename,"\n");
					return;
				} else {
					while(!empty($_prev_lines)) {
						$this->display_line(array_shift($_prev_lines),array_shift($_prev_counter),false);
					}
					$this->display_line($_curr_line,$_counter,true);
					$_post_counter = $this->context_lines;
				}
				$this->_found = true;
			} else {
				if($_post_counter > 0) {
					$this->display_line($_curr_line,$_counter,false);
					$_post_counter--;
				} else {
					$_prev_lines[] = $_curr_line;
					$_prev_counter[] = $_counter;
					if(count($_prev_lines) > $this->context_lines) {
						array_shift($_prev_lines);
						array_shift($_prev_counter);
					}
				}
			}
			$_counter++;
		}		
	}
	
	function match_line(&$_line_contents) {
        if($this->show_negative_match) {
            return (!preg_match($this->_pcre_pattern, $_line_contents));
		} else {
            return (preg_match($this->_pcre_pattern, $_line_contents));
		}		
	}
	
	function display_line(&$_curr_line, &$_counter, $_match_line) {
		$this->display_filename($this->_filename);
		if($this->show_line_numbers) {
			echo $this->highlight(array('bright','fg_blue'),$_counter . ':');
		}
		if(!$_match_line) {
			echo $this->trim($_curr_line);
		} else {
			if($this->show_only_match) {
				preg_match_all('/('.$this->_argv_pattern.')/' .
				$this->_pattern_opts, $_curr_line, $_match);
				foreach($_match[1] as $_curr_match) {
					echo $this->highlight(array("bright","fg_green"),$_curr_match);
				}
			} elseif($this->show_entire_line) {
				echo $this->trim(preg_replace('/('.$this->_argv_pattern.')/' . $this->_pattern_opts, $this->highlight(array("bright","fg_green"),"$1"), $_curr_line));
			} else {
				preg_match('/('.$this->_argv_pattern.')/' . $this->_pattern_opts, $_curr_line, $_match);
				if(!$this->context_chars) {
					echo $this->trim($this->highlight(array("bright","fg_green"), $_match[0]));				
				} else {
					$strpos = strpos($_curr_line, $_match[0]);
					echo 	$this->trim(substr($_curr_line, $strpos - $this->context_chars, $this->context_chars)) .
							$this->trim($this->highlight(array("bright","fg_green"), $_match[0])) .
							$this->trim(substr($_curr_line, $strpos + strlen($_match[0]), $this->context_chars));		
				}
			}
		}
		echo "\n";
		return true;
	}

	function display_filename_count() {
		foreach($this->_filename_count as $_filename => $_count) {
		$this->display_filename($_filename);
			echo $_count;
			echo "\n";
		}
		return true;
	}	
	
	function display_filename($_filename, $suffix = ':') {
		if($this->show_filename) {
			if($this->show_filepath) {
				echo $this->highlight(array('bright','fg_blue'), $_filename . $suffix);		
			} else {
				echo $this->highlight(array('bright','fg_blue'), basename($_filename) . $suffix);					
			}
		}
	}

	function highlight($styles, $text) {
		if(!$this->highlight) {
			return $text;
		}
		foreach($styles as $style) {
			$_ret .= $this->text_attrs[$style];
		}
		$_ret .= $text;
		$_ret .= $this->text_attrs['reset'];
		return $_ret;
	}

	function trim(&$text) {
		if($this->trim_output) {
			return trim($text);
		} else {
			return $text;
		}
	}

}

$pmatch = new pmatch;

if(!$pmatch->parse_arguments()) {
	$pmatch->show_usage();
	exit(1);
}
if(!$pmatch->process_pattern()) {
	$pmatch->show_usage();
	exit(1);
}

if(empty($pmatch->_filenames)) {
	$pmatch->_filenames = array('php://stdin');
}	

foreach($pmatch->_filenames as $pmatch->_filename) {
	$pmatch->process_input();
	if($pmatch->show_only_count) {
		$pmatch->display_filename_count();
	}
}

exit($this->_found);
?>
