#!/usr/bin/perl use IO::Zlib; use IO::Uncompress::Bunzip2; use Getopt::Long; use Encode; # on xterm, Mac OS X Terminal, and probably others: # first digit: 3 = change foreground, 4 = change background # 9 = light foreground, 10 = light background # second digit: 0=black,1=red,2=green,3=yellow,4=blue,5=magenta,6=cyan,7=white # or a single digit: 0=plain, 1=bold, 4=underline, 7=reverse fg/bg my $MATCH_COLOR = "\e[31m"; my $FILENAME_COLOR = "\e[41m"; my $BINARY_COLOR = "\e[90m"; my $IS_A_DIRECTORY_COLOR = "\e[90m"; my $COLOR_END = "\e[0m"; # you can remove this line if it's causing problems; this is just in case I # accidentally set this to run as CGI instead of downloading die "Not a CGI script" if $ENV{SERVER_NAME}; my($show_help, $show_version); sub HELP_MESSAGE { my $msg = <\$with_number, 'H|with-filename'=>\$opt_H, 'h|no-filename'=>\$opt_h, 'l|files-with-matches|files-with-match'=>\$files_only, 'L|files-without-match|files-without-matches'=>\$files_without_only, 'binary-files=s'=>\$binary_files, 'z|compressed-text'=>\$opt_z, 'u|utf-8|utf8'=>\$use_utf8, 'r|R|recursive'=>\$should_recurse, 'c|color|colour'=>\$color, 'section=s'=>\$section_pattern, 'section-after=s'=>\$section_after_pattern, 'follow-links'=>\$follow_links, 'e=s' => \@patterns, 'i|ignore-case'=>\$opt_i, 'v|invert-match=s'=>\@inverted_patterns, 'A|include-hidden'=>\$include_hidden, 'I|include=s'=>\@include, 'X|exclude=s'=>\@exclude, 'exclude-dir=s'=>\@exclude_dir, 'max-chars=i'=>\$max_chars, 'help'=>\$show_help, 'version'=>\$show_version); $SIG{INFO} = sub {$print_next_line = 1;}; if($show_help) { VERSION_MESSAGE(); HELP_MESSAGE(); exit 1; } if($show_version) { VERSION_MESSAGE(); exit 1; } warn "-h and -H are mutually exclusive\n" if $opt_H && $opt_h; warn "-l and -L are mutually exclusive\n" if $files_only && $files_without_only; warn "-n, -h, -H, --max-chars, --section, and --section-after have no effect if\nlines aren't being displayed\n" if ($with_number || $opt_h || $opt_H || $max_chars >= 1 || defined($section_pattern) || defined($section_after_pattern)) && ($files_only || $files_without_only); warn "Minimum --max-chars is 7" if $max_chars > 0 && $max_chars < 7; ($COLOR_END, $IS_A_DIRECTORY_COLOR, $BINARY_COLOR) = ('', '', '') if !$color; if(@patterns == 0 && @inverted_patterns == 0) { if(!@ARGV) { VERSION_MESSAGE(); HELP_MESSAGE(); exit 1; } @patterns = shift @ARGV; } @patterns = map {decode('utf8', $_)} @patterns if $use_utf8; @patterns = map {$opt_i ? qr/$_/i : qr/$_/} @patterns; @inverted_patterns = map {decode('utf8', $_)} @inverted_patterns if $use_utf8; @inverted_patterns = map {$opt_i ? qr/$_/i : qr/$_/} @inverted_patterns; if(@exclude_dir != 0 || @exclude != 0 || @include != 0 || $include_hidden || $follow_links) { $should_recurse = 1; } if(!$include_hidden) { push @exclude, ".*"; push @exclude_dir, ".*"; } if(@include == 0) { @include = ("*"); } $with_filename = (@ARGV > 1) || $should_recurse; $with_filename = 1 if $opt_H; $with_filename = 0 if $opt_h; if(@ARGV == 0) { if($should_recurse) { @ARGV = ("."); } else { @ARGV = ("-"); } } sub globToRE { local $_; $_ = $_[0]; s/[^0-9a-zA-Z*\[\]\-?]/\\$&/g; s/\*/.*/g; s/\?/./g; s/\[\\\^/[^/g; # note: [?] and [*] probably won't work correctly return "^$_\$"; } sub matchesAGlob { local $_; $_ = shift; #s/.*\///g; for my $glob (@_) { my $re = globToRE($glob); return 1 if /$re/; } return 0; } my $bytenum; sub doMatch { no warnings 'utf8'; # for matching binary files with -u $_ = decode 'utf8', $_ if $use_utf8; for my $pattern (@patterns) { my $match = /$pattern/; return 0 unless $match; {use bytes; $bytenum = length $`;} } for my $pattern (@inverted_patterns) { my $match = /$pattern/; return 0 if $match; } return 1; } sub doFile { local $_; my $name = $_[0]; if(-d $name) { if($print_next_line) { print STDERR "Current file: $name (directory)\n"; $print_next_line = 0; } if(!$should_recurse) { print STDERR "$IS_A_DIRECTORY_COLOR$name is a directory$COLOR_END\n"; return; } my $dh; if(not opendir $dh, $name) { print STDERR "Cannot open directory $name\n"; return; } foreach my $fn (readdir($dh)) { next if !$follow_links && -l "$name/$fn"; next if $fn =~ /^\.\.?$/; if(-d "$name/$fn") { next if matchesAGlob($fn, @exclude_dir); } else { next unless matchesAGlob($fn, @include); next if matchesAGlob($fn, @exclude); } doFile("$name/$fn"); } return; } #elsif(!-f $name && $name ne '-') { #print STDERR "$name is not a normal file\n"; #return; #} my $fh; if($opt_z && $name =~ /\.t?gz$/) { $fh = IO::Zlib->new($name, "rb"); if(not $fh) { print STDERR "Cannot open gzipped file $name\n"; return; } } elsif($opt_z && $name =~ /\.bz2$/) { $fh = new IO::Uncompress::Bunzip2 $name; if(not $fh) { print STDERR "Cannot open bzip2'd file $name\n"; return; } } elsif($name ne '-' && -B $name && ($binary_files eq "without-match" || $binary_files eq "ignore")) { return; } elsif($name ne '-' && -B $name && $binary_files eq "binary") { if($print_next_line) { print STDERR "Current file: $name (binary)\n"; $print_next_line = 0; } local $/; if(not open $fh, '<', $name) { print STDERR "Cannot open binary file $name\n"; return; } binmode $fh; $_ = <$fh>; my $match = doMatch(); if($files_only) { print "$name\n" if $match; } elsif($files_without_only) { print "$name\n" unless $match; } else { print "${BINARY_COLOR}Binary file $name matches at byte $bytenum$COLOR_END\n" if $match; } close $fh; return; } else { if($name eq '-') { open $fh, '-' or die "Can't open standard input\n"; } else { if(not open $fh, '<', $name) { print STDERR "Cannot open file $name\n"; return; } } } my $matched_somewhere = 0; my $section = ''; my $section_next = 0; while(<$fh>) { if($print_next_line) { print STDERR "Current line: $name: $.: $_\n"; $print_next_line = 0; } my $is_section_header = 0; if($section_next || (defined($section_pattern) && /$section_pattern/)) { $section = $_; chomp $section; $section_next = 0; $is_section_header = 1; } if(defined($section_after_pattern) && /$section_after_pattern/) { $section_next = 1; } my $match = doMatch(); next unless $match; if($files_only) { print "$name\n"; return; } elsif($files_without_only) { $matched_somewhere = 1; } else { if($max_chars >= 7 && length($_)-1 > $max_chars) { chomp; my ($mstart, $mlength) = (0, 0); if(@patterns) { /$patterns[0]/ and ($mstart, $mlength) = (length $`, length $&); } my $llength = $max_chars - 6; my $lstart = int($mstart + ($mlength - $llength)/2); if($lstart <= 0) { $_ = substr($_, 0, $max_chars-3) . '...'; } elsif($lstart + $llength >= length($_)-1) { $_ = '...' . substr($_, -($max_chars-3)); } else { $_ = '...' . substr($_, $lstart, $llength) . '...'; } $_ .= "\n"; } if($color) { print "$FILENAME_COLOR"; for my $pattern (@patterns) { s/$pattern/$MATCH_COLOR$&$COLOR_END/; } } print "$name: " if $with_filename; print "$section: " if $section ne '' && !$is_section_header; print "$.: " if $with_number; print $COLOR_END if $color; $_ = encode 'utf8', $_ if $use_utf8; print; } } if($files_without_only) { print "$name\n" unless $matched_somewhere; } close $fh; } for my $filename (@ARGV) { doFile($filename); }