diff --git a/home/.matrix/bin/ack b/home/.matrix/bin/ack index 88faa74..c12de69 100755 --- a/home/.matrix/bin/ack +++ b/home/.matrix/bin/ack @@ -4,31 +4,76 @@ # Please DO NOT EDIT or send patches for it. # # Please take a look at the source from -# http://github.com/petdance/ack2 +# https://github.com/beyondgrep/ack3 # and submit patches against the individual files # that build ack. # +$App::Ack::STANDALONE = 1; package main; use strict; use warnings; -our $VERSION = '2.12'; # Check http://beyondgrep.com/ for updates +our $VERSION = 'v3.7.0'; # Check https://beyondgrep.com/ for updates -use 5.008008; -use Getopt::Long 2.35 (); -use Carp 1.04 (); +use 5.010001; - -# XXX Don't make this so brute force -# See also: https://github.com/petdance/ack2/issues/89 - -# These are all our globals. +use File::Spec (); +use Getopt::Long (); + + + +# Global command-line options +our $opt_1; +our $opt_A; +our $opt_B; +our $opt_break; +our $opt_color; +our $opt_column; +our $opt_debug; +our $opt_c; +our $opt_f; +our $opt_g; +our $opt_heading; +our $opt_L; +our $opt_l; +our $opt_m; +our $opt_output; +our $opt_passthru; +our $opt_p; +our $opt_range_start; +our $opt_range_end; +our $opt_range_invert; +our $opt_regex; +our $opt_show_filename; +our $opt_show_types; +our $opt_underline; +our $opt_v; + +# Flag if we need any context tracking. +our $is_tracking_context; + +# The regex that we search for in each file. +our $search_re; + +# The regex that matches for things we want to exclude via the --not option. +our $search_not_re; + +# Special /m version of our $search_re. +our $scan_re; + +our @special_vars_used_by_opt_output; + +our $using_ranges; + +# Internal stats for debugging. +our %stats; MAIN: { - $App::Ack::orig_program_name = $0; + $App::Ack::ORIGINAL_PROGRAM_NAME = $0; $0 = join(' ', 'ack', $0); + $App::Ack::ors = "\n"; if ( $App::Ack::VERSION ne $main::VERSION ) { App::Ack::die( "Program/library version mismatch\n\t$0 is $main::VERSION\n\t$INC{'App/Ack.pm'} is $App::Ack::VERSION" ); } @@ -38,91 +83,327 @@ MAIN: { for my $arg ( @ARGV ) { last if ( $arg eq '--' ); - # Get the --thpppt, --bar, --cathy checking out of the way. - $arg =~ /^--th[pt]+t+$/ and App::Ack::_thpppt($arg); - $arg eq '--bar' and App::Ack::_bar(); - $arg eq '--cathy' and App::Ack::_cathy(); + # Get the --thpppt, --bar, --cathy and --man checking out of the way. + $arg =~ /^--th[pt]+t+$/ and App::Ack::thpppt($arg); + $arg eq '--bar' and App::Ack::ackbar(); + $arg eq '--cathy' and App::Ack::cathy(); # See if we want to ignore the environment. (Don't tell Al Gore.) $arg eq '--env' and $env_is_usable = 1; $arg eq '--noenv' and $env_is_usable = 0; } - if ( !$env_is_usable ) { + if ( $env_is_usable ) { + if ( $ENV{ACK_OPTIONS} ) { + App::Ack::warn( 'WARNING: ack no longer uses the ACK_OPTIONS environment variable. Use an ackrc file instead.' ); + } + } + else { my @keys = ( 'ACKRC', grep { /^ACK_/ } keys %ENV ); delete @ENV{@keys}; } - load_colors(); - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); - Getopt::Long::Configure('pass_through', 'no_auto_abbrev'); + # Load colors + my $modules_loaded_ok = eval 'use Term::ANSIColor 1.10 (); 1;'; + if ( $modules_loaded_ok && $App::Ack::is_windows ) { + $modules_loaded_ok = eval 'use Win32::Console::ANSI; 1;'; + } + if ( $modules_loaded_ok ) { + $ENV{ACK_COLOR_MATCH} ||= 'black on_yellow'; + $ENV{ACK_COLOR_FILENAME} ||= 'bold green'; + $ENV{ACK_COLOR_LINENO} ||= 'bold yellow'; + $ENV{ACK_COLOR_COLNO} ||= 'bold yellow'; + } + + App::Ack::ConfigLoader::configure_parser( 'no_auto_abbrev', 'pass_through' ); Getopt::Long::GetOptions( - 'help' => sub { App::Ack::show_help(); exit; }, - 'version' => sub { App::Ack::print_version_statement(); exit; }, - 'man' => sub { App::Ack::show_man(); exit; }, + help => sub { App::Ack::show_help(); exit; }, + version => sub { App::Ack::print( App::Ack::get_version_statement() ); exit; }, + man => sub { App::Ack::show_man(); }, ); - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); if ( !@ARGV ) { App::Ack::show_help(); exit 1; } - main(); + my @arg_sources = App::Ack::ConfigLoader::retrieve_arg_sources(); + + my $opt = App::Ack::ConfigLoader::process_args( @arg_sources ); + + $opt_1 = $opt->{1}; + $opt_A = $opt->{A}; + $opt_B = $opt->{B}; + $opt_break = $opt->{break}; + $opt_c = $opt->{c}; + $opt_color = $opt->{color}; + $opt_column = $opt->{column}; + $opt_debug = $opt->{debug}; + $opt_f = $opt->{f}; + $opt_g = $opt->{g}; + $opt_heading = $opt->{heading}; + $opt_L = $opt->{L}; + $opt_l = $opt->{l}; + $opt_m = $opt->{m}; + $opt_output = $opt->{output}; + $opt_p = $opt->{p}; + $opt_passthru = $opt->{passthru}; + $opt_range_start = $opt->{range_start}; + $opt_range_end = $opt->{range_end}; + $opt_range_invert = $opt->{range_invert}; + $opt_regex = $opt->{regex}; + $opt_show_filename = $opt->{show_filename}; + $opt_show_types = $opt->{show_types}; + $opt_underline = $opt->{underline}; + $opt_v = $opt->{v}; + + if ( $opt_show_types && not( $opt_f || $opt_g ) ) { + App::Ack::die( '--show-types can only be used with -f or -g.' ); + } + + if ( $opt_range_start ) { + ($opt_range_start, undef) = build_regex( $opt_range_start, {} ); + } + if ( $opt_range_end ) { + ($opt_range_end, undef) = build_regex( $opt_range_end, {} ); + } + $using_ranges = $opt_range_start || $opt_range_end; + + $App::Ack::report_bad_filenames = !$opt->{s}; + $App::Ack::ors = $opt->{print0} ? "\0" : "\n"; + + if ( !defined($opt_color) && !$opt_g ) { + my $windows_color = 1; + if ( $App::Ack::is_windows ) { + $windows_color = eval { require Win32::Console::ANSI; }; + } + $opt_color = !App::Ack::output_to_pipe() && $windows_color; + } + $opt_heading //= !App::Ack::output_to_pipe(); + $opt_break //= !App::Ack::output_to_pipe(); + + if ( defined($opt->{H}) || defined($opt->{h}) ) { + $opt_show_filename = $opt->{show_filename} = $opt->{H} && !$opt->{h}; + } + + if ( defined $opt_output ) { + # Expand out \t, \n and \r. + $opt_output =~ s/\\n/\n/g; + $opt_output =~ s/\\r/\r/g; + $opt_output =~ s/\\t/\t/g; + + my @supported_special_variables = ( 1..9, qw( _ . ` & ' + f ) ); + @special_vars_used_by_opt_output = grep { $opt_output =~ /\$$_/ } @supported_special_variables; + + # If the $opt_output contains $&, $` or $', those vars won't be + # captured until they're used at least once in the program. + # Do the eval to make this happen. + for my $i ( @special_vars_used_by_opt_output ) { + if ( $i eq q{&} || $i eq q{'} || $i eq q{`} ) { + no warnings; # They will be undef, so don't warn. + eval qq{"\$$i"}; + } + } + } + + # Set up file filters. + my $files; + if ( $App::Ack::is_filter_mode && !$opt->{files_from} ) { # probably -x + $files = App::Ack::Files->from_stdin(); + $opt_regex //= shift @ARGV; + ($search_re, $scan_re) = build_regex( $opt_regex, $opt ); + $search_not_re = _build_search_not_re( $opt ); + $stats{search_re} = $search_re; + $stats{scan_re} = $scan_re; + $stats{search_not_re} = $search_not_re; + } + else { + if ( $opt_f ) { + # No need to check for regex, since mutex options are handled elsewhere. + } + else { + $opt_regex //= shift @ARGV; + ($search_re, $scan_re) = build_regex( $opt_regex, $opt ); + $search_not_re = _build_search_not_re( $opt ); + $stats{search_re} = $search_re; + $stats{scan_re} = $scan_re; + $stats{search_not_re} = $search_not_re; + } + # XXX What is this checking for? + if ( $search_re && $search_re =~ /\n/ ) { + App::Ack::exit_from_ack( 0 ); + } + my @start; + if ( not defined $opt->{files_from} ) { + @start = @ARGV; + } + if ( !exists($opt->{show_filename}) ) { + unless(@start == 1 && !(-d $start[0])) { + $opt_show_filename = $opt->{show_filename} = 1; + } + } + + if ( defined $opt->{files_from} ) { + $files = App::Ack::Files->from_file( $opt, $opt->{files_from} ); + exit 1 unless $files; + } + else { + @start = ('.') unless @start; + foreach my $target (@start) { + if ( !-e $target && $App::Ack::report_bad_filenames) { + App::Ack::warn( "$target: No such file or directory" ); + } + } + + $opt->{file_filter} = _compile_file_filter($opt, \@start); + $opt->{descend_filter} = _compile_descend_filter($opt); + + $files = App::Ack::Files->from_argv( $opt, \@start ); + } + } + App::Ack::set_up_pager( $opt->{pager} ) if defined $opt->{pager}; + + my $nmatches; + if ( $opt_f || $opt_g ) { + $nmatches = file_loop_fg( $files ); + } + elsif ( $opt_c ) { + $nmatches = file_loop_c( $files ); + } + elsif ( $opt_l || $opt_L ) { + $nmatches = file_loop_lL( $files ); + } + else { + $nmatches = file_loop_normal( $files ); + } + + if ( $opt_debug ) { + require List::Util; + my @stats = qw( search_re scan_re search_not_re prescans linescans filematches linematches ); + my $width = List::Util::max( map { length } @stats ); + + for my $stat ( @stats ) { + App::Ack::warn( sprintf( '%-*.*s = %s', $width, $width, $stat, $stats{$stat} // 'undef' ) ); + } + } + + close $App::Ack::fh; + + App::Ack::exit_from_ack( $nmatches ); } -sub _compile_descend_filter { - my ( $opt ) = @_; +# End of MAIN - my $idirs = $opt->{idirs}; - my $dont_ignore_dirs = $opt->{no_ignore_dirs}; +sub file_loop_fg { + my $files = shift; - # if we have one or more --noignore-dir directives, we can't ignore - # entire subdirectory hierarchies, so we return an "accept all" - # filter and scrutinize the files more in _compile_file_filter - return if $dont_ignore_dirs; - return unless $idirs && @{$idirs}; + my $nmatches = 0; + while ( defined( my $file = $files->next ) ) { + if ( $opt_show_types ) { + App::Ack::show_types( $file ); + } + elsif ( $opt_g ) { + print_line_with_options( undef, $file->name, 0, $App::Ack::ors ); + } + else { + App::Ack::say( $file->name ); + } + ++$nmatches; + last if defined($opt_m) && ($nmatches >= $opt_m); + } + + return $nmatches; +} + + +sub file_loop_c { + my $files = shift; - my %ignore_dirs; + my $total_count = 0; + while ( defined( my $file = $files->next ) ) { + my $matches_for_this_file = count_matches_in_file( $file ); + + if ( not $opt_show_filename ) { + $total_count += $matches_for_this_file; + next; + } - foreach my $idir (@{$idirs}) { - if ( $idir =~ /^(\w+):(.*)/ ) { - if ( $1 eq 'is') { - $ignore_dirs{$2} = 1; + if ( !$opt_l || $matches_for_this_file > 0 ) { + if ( $opt_show_filename ) { + my $display_filename = $file->name; + if ( $opt_color ) { + $display_filename = Term::ANSIColor::colored($display_filename, $ENV{ACK_COLOR_FILENAME}); + } + App::Ack::say( $display_filename, ':', $matches_for_this_file ); } else { - Carp::croak( 'Non-is filters are not yet supported for --ignore-dir' ); + App::Ack::say( $matches_for_this_file ); } } - else { - Carp::croak( qq{Invalid filter specification "$idir"} ); + } + + if ( !$opt_show_filename ) { + App::Ack::say( $total_count ); + } + + return; +} + + +sub file_loop_lL { + my $files = shift; + + my $nmatches = 0; + while ( defined( my $file = $files->next ) ) { + my $is_match = count_matches_in_file( $file, 1 ); + + if ( $opt_L ? !$is_match : $is_match ) { + App::Ack::say( $file->name ); + ++$nmatches; + + last if $opt_1; + last if defined($opt_m) && ($nmatches >= $opt_m); } } - return sub { - return !exists $ignore_dirs{$_} && !exists $ignore_dirs{$File::Next::dir}; - }; + return $nmatches; } -sub _compile_file_filter { - my ( $opt, $start ) = @_; - my $ifiles = $opt->{ifiles}; - $ifiles ||= []; +sub _compile_descend_filter { + my ( $opt ) = @_; - my $ifiles_filters = App::Ack::Filter::Collection->new(); + my $idirs = 0; + my $dont_ignore_dirs = 0; - foreach my $filter_spec (@{$ifiles}) { - if ( $filter_spec =~ /^(\w+):(.+)/ ) { - my ($how,$what) = ($1,$2); - my $filter = App::Ack::Filter->create_filter($how, split(/,/, $what)); - $ifiles_filters->add($filter); + for my $filter (@{$opt->{idirs} || []}) { + if ($filter->is_inverted()) { + $dont_ignore_dirs++; } else { - Carp::croak( qq{Invalid filter specification "$filter_spec"} ); + $idirs++; } } + # If we have one or more --noignore-dir directives, we can't ignore + # entire subdirectory hierarchies, so we return an "accept all" + # filter and scrutinize the files more in _compile_file_filter. + return if $dont_ignore_dirs; + return unless $idirs; + + $idirs = $opt->{idirs}; + + return sub { + my $file = App::Ack::File->new($File::Next::dir); + return !grep { $_->filter($file) } @{$idirs}; + }; +} + +sub _compile_file_filter { + my ( $opt, $start ) = @_; + + my $ifiles_filters = $opt->{ifiles}; + my $filters = $opt->{'filters'} || []; my $direct_filters = App::Ack::Filter::Collection->new(); my $inverse_filters = App::Ack::Filter::Collection->new(); @@ -139,58 +420,58 @@ sub _compile_file_filter { my %is_member_of_starting_set = map { (get_file_id($_) => 1) } @{$start}; - my $ignore_dir_list = $opt->{idirs}; - my $dont_ignore_dir_list = $opt->{no_ignore_dirs}; + my @ignore_dir_filter = @{$opt->{idirs} || []}; + my @is_inverted = map { $_->is_inverted() } @ignore_dir_filter; + # This depends on InverseFilter->invert returning the original filter (for optimization). + @ignore_dir_filter = map { $_->is_inverted() ? $_->invert() : $_ } @ignore_dir_filter; + my $dont_ignore_dir_filter = grep { $_ } @is_inverted; + my $previous_dir = ''; + my $previous_dir_ignore_result; - my %ignore_dir_set; - my %dont_ignore_dir_set; - - foreach my $filter (@{ $ignore_dir_list }) { - if ( $filter =~ /^(\w+):(.*)/ ) { - if ( $1 eq 'is' ) { - $ignore_dir_set{ $2 } = 1; - } else { - Carp::croak( 'Non-is filters are not yet supported for --ignore-dir' ); + return sub { + if ( $opt_g ) { + if ( $File::Next::name =~ /$search_re/o ) { + return 0 if $opt_v; } - } else { - Carp::croak( qq{Invalid filter specification "$filter"} ); - } - } - foreach my $filter (@{ $dont_ignore_dir_list }) { - if ( $filter =~ /^(\w+):(.*)/ ) { - if ( $1 eq 'is' ) { - $dont_ignore_dir_set{ $2 } = 1; - } else { - Carp::croak( 'Non-is filters are not yet supported for --ignore-dir' ); + else { + return 0 if !$opt_v; } - } else { - Carp::croak( qq{Invalid filter specification "$filter"} ); } - } - - return sub { # ack always selects files that are specified on the command # line, regardless of filetype. If you want to ack a JPEG, # and say "ack foo whatever.jpg" it will do it for you. return 1 if $is_member_of_starting_set{ get_file_id($File::Next::name) }; - if ( $dont_ignore_dir_list ) { - my ( undef, $dirname ) = File::Spec->splitpath($File::Next::name); - my @dirs = File::Spec->splitdir($dirname); + if ( $dont_ignore_dir_filter ) { + if ( $previous_dir eq $File::Next::dir ) { + if ( $previous_dir_ignore_result ) { + return 0; + } + } + else { + my @dirs = File::Spec->splitdir($File::Next::dir); + + my $is_ignoring = 0; - my $is_ignoring = 0; + for ( my $i = 0; $i < @dirs; $i++) { + my $dir_rsrc = App::Ack::File->new(File::Spec->catfile(@dirs[0 .. $i])); - foreach my $dir ( @dirs ) { - if ( $ignore_dir_set{ $dir } ) { - $is_ignoring = 1; + my $j = 0; + for my $filter (@ignore_dir_filter) { + if ( $filter->filter($dir_rsrc) ) { + $is_ignoring = !$is_inverted[$j]; + } + $j++; + } } - elsif ( $dont_ignore_dir_set{ $dir } ) { - $is_ignoring = 0; + + $previous_dir = $File::Next::dir; + $previous_dir_ignore_result = $is_ignoring; + + if ( $is_ignoring ) { + return 0; } } - if ( $is_ignoring ) { - return 0; - } } # Ignore named pipes found in directory searching. Named @@ -199,76 +480,34 @@ sub _compile_file_filter { # command line" wins. return 0 if -p $File::Next::name; - # we can't handle unreadable filenames; report them - unless ( -r _ ) { - if ( $App::Ack::report_bad_filenames ) { - App::Ack::warn( "${File::Next::name}: cannot open file for reading" ); + # We can't handle unreadable filenames; report them. + if ( not -r _ ) { + use filetest 'access'; + + if ( not -R $File::Next::name ) { + if ( $App::Ack::report_bad_filenames ) { + App::Ack::warn( "${File::Next::name}: cannot open file for reading" ); + } + return 0; } - return 0; } - my $resource = App::Ack::Resource::Basic->new($File::Next::name); - return 0 if ! $resource; - if ( $ifiles_filters->filter($resource) ) { + my $file = App::Ack::File->new($File::Next::name); + + if ( $ifiles_filters && $ifiles_filters->filter($file) ) { return 0; } - my $match_found = $direct_filters->filter($resource); + my $match_found = $direct_filters->filter($file); - # Don't bother invoking inverse filters unless we consider the current resource a match - if ( $match_found && $inverse_filters->filter( $resource ) ) { + # Don't bother invoking inverse filters unless we consider the current file a match. + if ( $match_found && $inverse_filters->filter( $file ) ) { $match_found = 0; } return $match_found; }; } -sub show_types { - my $resource = shift; - my $ors = shift; - - my @types = filetypes( $resource ); - my $types = join( ',', @types ); - my $arrow = @types ? ' => ' : ' =>'; - App::Ack::print( $resource->name, $arrow, join( ',', @types ), $ors ); - - return; -} - -# Set default colors, load Term::ANSIColor -sub load_colors { - eval 'use Term::ANSIColor 1.10 ()'; - eval 'use Win32::Console::ANSI' if $App::Ack::is_windows; - - $ENV{ACK_COLOR_MATCH} ||= 'black on_yellow'; - $ENV{ACK_COLOR_FILENAME} ||= 'bold green'; - $ENV{ACK_COLOR_LINENO} ||= 'bold yellow'; - - return; -} - -sub filetypes { - my ( $resource ) = @_; - - my @matches; - - foreach my $k (keys %App::Ack::mappings) { - my $filters = $App::Ack::mappings{$k}; - - foreach my $filter (@{$filters}) { - # clone the resource - my $clone = $resource->clone; - if ( $filter->filter($clone) ) { - push @matches, $k; - last; - } - } - } - - # http://search.cpan.org/dist/Perl-Critic/lib/Perl/Critic/Policy/Subroutines/ProhibitReturnSort.pm - @matches = sort @matches; - return @matches; -} # Returns a (fairly) unique identifier for a file. # Use this function to compare two files to see if they're @@ -280,12 +519,12 @@ sub get_file_id { return File::Next::reslash( $filename ); } else { - # XXX is this the best method? it always hits the FS - if( my ( $dev, $inode ) = (stat($filename))[0, 1] ) { + # XXX Is this the best method? It always hits the FS. + if ( my ( $dev, $inode ) = (stat($filename))[0, 1] ) { return join(':', $dev, $inode); } else { - # XXX this could be better + # XXX This could be better. return $filename; } } @@ -300,64 +539,165 @@ sub build_regex { defined $str or App::Ack::die( 'No regular expression found.' ); + if ( !$opt->{Q} ) { + # Compile the regex to see if it dies or throws warnings. + local $SIG{__WARN__} = sub { die @_ }; # Anything that warns becomes a die. + my $scratch_regex = eval { qr/$str/ }; + if ( not $scratch_regex ) { + my $err = $@; + chomp $err; + + if ( $err =~ m{^(.+?); marked by <-- HERE in m/(.+?) <-- HERE} ) { + my ($why, $where) = ($1,$2); + my $pointy = ' ' x (6+length($where)) . '^---HERE'; + App::Ack::die( "Invalid regex '$str'\nRegex: $str\n$pointy $why" ); + } + else { + App::Ack::die( "Invalid regex '$str'\n$err" ); + } + } + } + + # Check for lowercaseness before we do any modifications. + my $regex_is_lc = App::Ack::is_lowercase( $str ); + $str = quotemeta( $str ) if $opt->{Q}; + + my $scan_str = $str; + + # Whole words only. if ( $opt->{w} ) { - $str = "\\b$str" if $str =~ /^\w/; - $str = "$str\\b" if $str =~ /\w$/; - } + my $ok = 1; + + if ( $str =~ /^\\[wd]/ ) { + # Explicit \w is good. + } + else { + # Can start with \w, (, [ or dot. + if ( $str !~ /^[\w\(\[\.]/ ) { + $ok = 0; + } + } + + # Can end with \w, }, ), ], +, *, or dot. + if ( $str !~ /[\w\}\)\]\+\*\?\.]$/ ) { + $ok = 0; + } + # ... unless it's escaped. + elsif ( $str =~ /\\[\}\)\]\+\*\?\.]$/ ) { + $ok = 0; + } + + if ( !$ok ) { + App::Ack::die( '-w will not do the right thing if your regex does not begin and end with a word character.' ); + } - my $regex_is_lc = $str eq lc $str; - if ( $opt->{i} || ($opt->{smart_case} && $regex_is_lc) ) { - $str = "(?i)$str"; + if ( $str =~ /^\w+$/ ) { + # No need for fancy regex if it's a simple word. + $str = sprintf( '\b(?:%s)\b', $str ); + } + else { + $str = sprintf( '(?:^|\b|\s)\K(?:%s)(?=\s|\b|$)', $str ); + } } - my $re = eval { qr/$str/ }; - if ( !$re ) { - die "Invalid regex '$str':\n $@"; + if ( $opt->{i} || ($opt->{S} && $regex_is_lc) ) { + $_ = "(?i)$_" for ( $str, $scan_str ); } - return $re; + my $scan_regex = undef; + my $regex = eval { qr/$str/ }; + if ( $regex ) { + if ( $scan_str !~ /\$/ ) { + # No line_scan is possible if there's a $ in the regex. + $scan_regex = eval { qr/$scan_str/m }; + } + } + else { + my $err = $@; + chomp $err; + App::Ack::die( "Invalid regex '$str':\n $err" ); + } + return ($regex, $scan_regex); } +my $match_colno; + { -my @before_ctx_lines; -my @after_ctx_lines; -my $is_iterating; +# Number of context lines +my $n_before_ctx_lines; +my $n_after_ctx_lines; -my $has_printed_something; +# Array to keep track of lines that might be required for a "before" context +my @before_context_buf; +# Position to insert next line in @before_context_buf +my $before_context_pos; -BEGIN { - $has_printed_something = 0; -} +# Number of "after" context lines still pending +my $after_context_pending; + +# Number of latest line that got printed +my $printed_lineno; + +my $is_first_match; +state $has_printed_from_any_file = 0; -sub print_matches_in_resource { - my ( $resource, $opt ) = @_; - my $passthru = $opt->{passthru}; - my $max_count = $opt->{m} || -1; - my $nmatches = 0; - my $filename = $resource->name; - my $break = $opt->{break}; - my $heading = $opt->{heading}; - my $ors = $opt->{print0} ? "\0" : "\n"; - my $color = $opt->{color}; - my $print_filename = $opt->{show_filename}; +sub file_loop_normal { + my $files = shift; + + $n_before_ctx_lines = $opt_output ? 0 : ($opt_B || 0); + $n_after_ctx_lines = $opt_output ? 0 : ($opt_A || 0); + + @before_context_buf = (undef) x $n_before_ctx_lines; + $before_context_pos = 0; + + $is_tracking_context = $n_before_ctx_lines || $n_after_ctx_lines; + + $is_first_match = 1; + + my $nmatches = 0; + while ( defined( my $file = $files->next ) ) { + if ($is_tracking_context) { + $printed_lineno = 0; + $after_context_pending = 0; + if ( $opt_heading ) { + $is_first_match = 1; + } + } + my $needs_line_scan = 1; + if ( !$opt_passthru && !$opt_v ) { + $stats{prescans}++; + if ( $file->may_be_present( $scan_re ) ) { + $file->reset(); + } + else { + $needs_line_scan = 0; + } + } + if ( $needs_line_scan ) { + $stats{linescans}++; + $nmatches += print_matches_in_file( $file ); + } + last if $opt_1 && $nmatches; + } - my $has_printed_for_this_resource = 0; + return $nmatches; +} - $is_iterating = 1; - local $opt->{before_context} = $opt->{output} ? 0 : $opt->{before_context}; - local $opt->{after_context} = $opt->{output} ? 0 : $opt->{after_context}; +sub print_matches_in_file { + my $file = shift; - my $n_before_ctx_lines = $opt->{before_context} || 0; - my $n_after_ctx_lines = $opt->{after_context} || 0; + my $max_count = $opt_m || -1; # Go negative for no limit so it can never reduce to 0. + my $nmatches = 0; + my $filename = $file->name; - @after_ctx_lines = @before_ctx_lines = (); + my $has_printed_from_this_file = 0; - my $fh = $resource->open(); + my $fh = $file->open; if ( !$fh ) { if ( $App::Ack::report_bad_filenames ) { App::Ack::warn( "$filename: $!" ); @@ -366,673 +706,470 @@ sub print_matches_in_resource { } my $display_filename = $filename; - if ( $print_filename && $heading && $color ) { + if ( $opt_show_filename && $opt_heading && $opt_color ) { $display_filename = Term::ANSIColor::colored($display_filename, $ENV{ACK_COLOR_FILENAME}); } - # check for context before the main loop, so we don't - # pay for it if we don't need it - if ( $n_before_ctx_lines || $n_after_ctx_lines ) { - my $current_line = <$fh>; # prime the first line of input + # Check for context before the main loop, so we don't pay for it if we don't need it. + if ( $is_tracking_context ) { + local $_ = undef; - while ( defined $current_line ) { - while ( (@after_ctx_lines < $n_after_ctx_lines) && defined($_ = <$fh>) ) { - push @after_ctx_lines, $_; - } + $after_context_pending = 0; + + my $in_range = range_setup(); + + while ( <$fh> ) { + chomp; + $match_colno = undef; + + $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o ); - local $_ = $current_line; - my $former_dot_period = $.; - $. -= @after_ctx_lines; + my $does_match; + if ( $in_range ) { + $does_match = /$search_re/o; + if ( $does_match && $search_not_re ) { + local @-; + $does_match = !/$search_not_re/o; + } + if ( $opt_v ) { + $does_match = !$does_match; + } + else { + if ( $does_match ) { + # @- = @LAST_MATCH_START + $match_colno = $-[0] + 1; + } + } + } - if ( does_match($opt, $_) ) { - if ( !$has_printed_for_this_resource ) { - if ( $break && $has_printed_something ) { + if ( $does_match && $max_count ) { + if ( !$has_printed_from_this_file ) { + $stats{filematches}++; + if ( $opt_break && $has_printed_from_any_file ) { App::Ack::print_blank_line(); } - if ( $print_filename && $heading ) { - App::Ack::print_filename( $display_filename, $ors ); + if ( $opt_show_filename && $opt_heading ) { + App::Ack::say( $display_filename ); } } - print_line_with_context($opt, $filename, $_, $.); - $has_printed_for_this_resource = 1; + print_line_with_context( $filename, $_, $. ); + $has_printed_from_this_file = 1; + $stats{linematches}++; $nmatches++; $max_count--; } - elsif ( $passthru ) { - chomp; # XXX proper newline handling? - # XXX inline this call? - if ( $break && !$has_printed_for_this_resource && $has_printed_something ) { - App::Ack::print_blank_line(); - } - print_line_with_options($opt, $filename, $_, $., ':'); - $has_printed_for_this_resource = 1; - } - last unless $max_count != 0; - - # I tried doing this with local(), but for some reason, - # $. continued to have its new value after the exit of the - # enclosing block. I'm guessing that $. has some extra - # magic associated with it or something. If someone can - # tell me why this happened, I would love to know! - $. = $former_dot_period; # XXX this won't happen on an exception - - if ( $n_before_ctx_lines ) { - push @before_ctx_lines, $current_line; - shift @before_ctx_lines while @before_ctx_lines > $n_before_ctx_lines; - } - if ( $n_after_ctx_lines ) { - $current_line = shift @after_ctx_lines; - } else { - $current_line = <$fh>; + if ( $after_context_pending ) { + # Disable $opt_column since there are no matches in the context lines. + local $opt_column = 0; + print_line_with_options( $filename, $_, $., '-' ); + --$after_context_pending; + } + elsif ( $n_before_ctx_lines ) { + # Save line for "before" context. + $before_context_buf[$before_context_pos] = $_; + $before_context_pos = ($before_context_pos+1) % $n_before_ctx_lines; + } } + + $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o ); + + last if ($max_count == 0) && ($after_context_pending == 0); } } - else { - local $_; + elsif ( $opt_passthru ) { + local $_ = undef; + + my $in_range = range_setup(); while ( <$fh> ) { - if ( does_match($opt, $_) ) { - if ( !$has_printed_for_this_resource ) { - if ( $break && $has_printed_something ) { - App::Ack::print_blank_line(); + chomp; + + $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o ); + + $match_colno = undef; + my $does_match = /$search_re/o; + if ( $does_match && $search_not_re ) { + local @-; + $does_match = !/$search_not_re/o; + } + if ( $in_range && ($opt_v xor $does_match) ) { + if ( !$opt_v ) { + $match_colno = $-[0] + 1; + } + if ( !$has_printed_from_this_file ) { + if ( $opt_break && $has_printed_from_any_file ) { + App::Ack::print_blank_line(); } - if ( $print_filename && $heading ) { - App::Ack::print_filename( $display_filename, $ors ); + if ( $opt_show_filename && $opt_heading ) { + App::Ack::say( $display_filename ); } } - print_line_with_context($opt, $filename, $_, $.); - $has_printed_for_this_resource = 1; + print_line_with_options( $filename, $_, $., ':' ); + $has_printed_from_this_file = 1; $nmatches++; $max_count--; } - elsif ( $passthru ) { - chomp; # XXX proper newline handling? - if ( $break && !$has_printed_for_this_resource && $has_printed_something ) { + else { + if ( $opt_break && !$has_printed_from_this_file && $has_printed_from_any_file ) { App::Ack::print_blank_line(); } - print_line_with_options($opt, $filename, $_, $., ':'); - $has_printed_for_this_resource = 1; + print_line_with_options( $filename, $_, $., '-', 1 ); + $has_printed_from_this_file = 1; } - last unless $max_count != 0; - } - } - $is_iterating = 0; # XXX this won't happen on an exception - # then again, do we care? ack doesn't really - # handle exceptions anyway. + $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o ); - return $nmatches; -} - -sub print_line_with_options { - my ( $opt, $filename, $line, $line_no, $separator ) = @_; + last if $max_count == 0; + } + } + elsif ( $opt_v ) { + local $_ = undef; - $has_printed_something = 1; + $match_colno = undef; + my $in_range = range_setup(); - my $print_filename = $opt->{show_filename}; - my $print_column = $opt->{column}; - my $ors = $opt->{print0} ? "\0" : "\n"; - my $heading = $opt->{heading}; - my $output_expr = $opt->{output}; - my $color = $opt->{color}; + while ( <$fh> ) { + chomp; - my @line_parts; + $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o ); - if( $color ) { - $filename = Term::ANSIColor::colored($filename, - $ENV{ACK_COLOR_FILENAME}); - $line_no = Term::ANSIColor::colored($line_no, - $ENV{ACK_COLOR_LINENO}); - } + if ( $in_range ) { + my $does_match = /$search_re/o; + if ( $does_match && $search_not_re ) { + # local @-; No need to localize this because we don't use @-. + $does_match = !/$search_not_re/o; + } + if ( !$does_match ) { + if ( !$has_printed_from_this_file ) { + if ( $opt_break && $has_printed_from_any_file ) { + App::Ack::print_blank_line(); + } + if ( $opt_show_filename && $opt_heading ) { + App::Ack::say( $display_filename ); + } + } + print_line_with_context( $filename, $_, $. ); + $has_printed_from_this_file = 1; + $nmatches++; + $max_count--; + } + } - if($print_filename) { - if( $heading ) { - push @line_parts, $line_no; - } - else { - push @line_parts, $filename, $line_no; - } + $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o ); - if( $print_column ) { - push @line_parts, get_match_column(); + last if $max_count == 0; } } - if( $output_expr ) { - while ( $line =~ /$opt->{regex}/og ) { - my $output = eval $output_expr; - App::Ack::print( join( $separator, @line_parts, $output ), $ors ); - } - } - else { - if ( $color ) { - $line =~ /$opt->{regex}/o; # this match is redundant, but we need - # to perfom it in order to get if - # capture groups are set - - if ( @+ > 1 ) { # if we have captures - while ( $line =~ /$opt->{regex}/og ) { - my $offset = 0; # additional offset for when we add stuff - my $previous_match_end = 0; + else { # Normal search: No context, no -v, no --passthru + local $_ = undef; - for ( my $i = 1; $i < @+; $i++ ) { - my ( $match_start, $match_end ) = ( $-[$i], $+[$i] ); + my $last_match_lineno; + my $in_range = range_setup(); - next unless defined($match_start); - next if $match_start < $previous_match_end; - - my $substring = substr( $line, - $offset + $match_start, $match_end - $match_start ); - my $substitution = Term::ANSIColor::colored( $substring, - $ENV{ACK_COLOR_MATCH} ); + while ( <$fh> ) { + chomp; - substr( $line, $offset + $match_start, - $match_end - $match_start, $substitution ); + $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o ); - $previous_match_end = $match_end; # offsets do not need to be applied - $offset += length( $substitution ) - length( $substring ); + if ( $in_range ) { + $match_colno = undef; + my $is_match = /$search_re/o; + if ( $is_match && $search_not_re ) { + local @-; + $is_match = !/$search_not_re/o; + } + if ( $is_match ) { + $match_colno = $-[0] + 1; + if ( !$has_printed_from_this_file ) { + $stats{filematches}++; + if ( $opt_break && $has_printed_from_any_file ) { + App::Ack::print_blank_line(); + } + if ( $opt_show_filename && $opt_heading ) { + App::Ack::say( $display_filename ); + } } - - pos($line) = $+[0] + $offset; + if ( $opt_p ) { + if ( $last_match_lineno ) { + if ( $. > $last_match_lineno + $opt_p ) { + App::Ack::print_blank_line(); + } + } + elsif ( !$opt_break && $has_printed_from_any_file ) { + App::Ack::print_blank_line(); + } + } + s/[\r\n]+$//; + print_line_with_options( $filename, $_, $., ':' ); + $has_printed_from_this_file = 1; + $nmatches++; + $stats{linematches}++; + $max_count--; + $last_match_lineno = $.; } } - else { - my $matched = 0; # flag; if matched, need to escape afterwards - - while ( $line =~ /$opt->{regex}/og ) { - - $matched = 1; - my ( $match_start, $match_end ) = ($-[0], $+[0]); - next unless defined($match_start); - my $substring = substr( $line, $match_start, - $match_end - $match_start ); - my $substitution = Term::ANSIColor::colored( $substring, - $ENV{ACK_COLOR_MATCH} ); + $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o ); - substr( $line, $match_start, $match_end - $match_start, - $substitution ); - - pos($line) = $match_end + - (length( $substitution ) - length( $substring )); - } - # XXX why do we do this? - $line .= "\033[0m\033[K" if $matched; - } + last if $max_count == 0; } - - push @line_parts, $line; - App::Ack::print( join( $separator, @line_parts ), $ors ); } - return; + return $nmatches; } -sub iterate { - my ( $resource, $opt, $cb ) = @_; - - $is_iterating = 1; - local $opt->{before_context} = $opt->{output} ? 0 : $opt->{before_context}; - local $opt->{after_context} = $opt->{output} ? 0 : $opt->{after_context}; +sub print_line_with_options { + my ( $filename, $line, $lineno, $separator, $skip_coloring ) = @_; - my $n_before_ctx_lines = $opt->{before_context} || 0; - my $n_after_ctx_lines = $opt->{after_context} || 0; + $has_printed_from_any_file = 1; + $printed_lineno = $lineno; - @after_ctx_lines = @before_ctx_lines = (); + my @line_parts; - my $fh = $resource->open(); - if ( !$fh ) { - if ( $App::Ack::report_bad_filenames ) { - # XXX direct access to filename - App::Ack::warn( "$resource->{filename}: $!" ); + if ( $opt_show_filename && defined($filename) ) { + my $colno; + $colno = get_match_colno() if $opt_column; + if ( $opt_color ) { + $filename = Term::ANSIColor::colored( $filename, $ENV{ACK_COLOR_FILENAME} ); + $lineno = Term::ANSIColor::colored( $lineno, $ENV{ACK_COLOR_LINENO} ); + $colno = Term::ANSIColor::colored( $colno, $ENV{ACK_COLOR_COLNO} ) if $opt_column; } - return; + if ( $opt_heading ) { + push @line_parts, $lineno; + } + else { + push @line_parts, $filename, $lineno; + } + push @line_parts, $colno if $opt_column; } - # check for context before the main loop, so we don't - # pay for it if we don't need it - if ( $n_before_ctx_lines || $n_after_ctx_lines ) { - my $current_line = <$fh>; # prime the first line of input - - while ( defined $current_line ) { - while ( (@after_ctx_lines < $n_after_ctx_lines) && defined($_ = <$fh>) ) { - push @after_ctx_lines, $_; - } - - local $_ = $current_line; - my $former_dot_period = $.; - $. -= @after_ctx_lines; - - last unless $cb->(); + if ( $opt_output ) { + while ( $line =~ /$search_re/og ) { + my $output = $opt_output; + if ( @special_vars_used_by_opt_output ) { + no strict; - # I tried doing this with local(), but for some reason, - # $. continued to have its new value after the exit of the - # enclosing block. I'm guessing that $. has some extra - # magic associated with it or something. If someone can - # tell me why this happened, I would love to know! - $. = $former_dot_period; # XXX this won't happen on an exception + # Stash copies of the special variables because we can't rely + # on them not changing in the process of doing the s///. - if ( $n_before_ctx_lines ) { - push @before_ctx_lines, $current_line; - shift @before_ctx_lines while @before_ctx_lines > $n_before_ctx_lines; - } - if ( $n_after_ctx_lines ) { - $current_line = shift @after_ctx_lines; - } - else { - $current_line = <$fh>; + my %keep = map { ($_ => ${$_} // '') } @special_vars_used_by_opt_output; + $keep{_} = $line if exists $keep{_}; # Manually set it because $_ gets reset in a map. + $keep{f} = $filename if exists $keep{f}; + my $special_vars_used_by_opt_output = join( '', @special_vars_used_by_opt_output ); + $output =~ s/\$([$special_vars_used_by_opt_output])/$keep{$1}/ego; } + App::Ack::say( join( $separator, @line_parts, $output ) ); } } else { - local $_; - - while ( <$fh> ) { - last unless $cb->(); - } - } - - $is_iterating = 0; # XXX this won't happen on an exception - # then again, do we care? ack doesn't really - # handle exceptions anyway. + my $underline = ''; - return; -} - -sub get_context { - if ( not $is_iterating ) { - Carp::croak( 'get_context() called outside of iterate()' ); - } - - return ( - scalar(@before_ctx_lines) ? \@before_ctx_lines : undef, - scalar(@after_ctx_lines) ? \@after_ctx_lines : undef, - ); -} - -} - -{ - -my $is_first_match; -my $previous_file_processed; -my $previous_line_printed; - -BEGIN { - $is_first_match = 1; - $previous_line_printed = -1; -} + # We have to do underlining before any highlighting because highlighting modifies string length. + if ( $opt_underline && !$skip_coloring ) { + while ( $line =~ /$search_re/og ) { + my $match_start = $-[0] // next; + my $match_end = $+[0]; + my $match_length = $match_end - $match_start; + last if $match_length <= 0; -sub print_line_with_context { - my ( $opt, $filename, $matching_line, $line_no ) = @_; - - my $heading = $opt->{heading}; + my $spaces_needed = $match_start - length $underline; - if( !defined($previous_file_processed) || - $previous_file_processed ne $filename ) { - $previous_file_processed = $filename; - $previous_line_printed = -1; - - if( $heading ) { - $is_first_match = 1; + $underline .= (' ' x $spaces_needed); + $underline .= ('^' x $match_length); + } } - } - - my $ors = $opt->{print0} ? "\0" : "\n"; - my $match_word = $opt->{w}; - my $is_tracking_context = $opt->{after_context} || $opt->{before_context}; - my $output_expr = $opt->{output}; + if ( $opt_color && !$skip_coloring ) { + my $highlighted = 0; # If highlighted, need to escape afterwards. - $matching_line =~ s/[\r\n]+$//g; + while ( $line =~ /$search_re/og ) { + my $match_start = $-[0] // next; + my $match_end = $+[0]; + my $match_length = $match_end - $match_start; + last if $match_length <= 0; - my ( $before_context, $after_context ) = get_context(); + my $substring = substr( $line, $match_start, $match_length ); + my $substitution = Term::ANSIColor::colored( $substring, $ENV{ACK_COLOR_MATCH} ); - if ( $before_context ) { - my $first_line = $. - @{$before_context}; + # Fourth argument replaces the string specified by the first three. + substr( $line, $match_start, $match_length, $substitution ); - if ( $first_line <= $previous_line_printed ) { - splice @{$before_context}, 0, $previous_line_printed - $first_line + 1; - $first_line = $. - @{$before_context}; + # Move the offset of where /g left off forward the number of spaces of highlighting. + pos($line) = $match_end + (length( $substitution ) - length( $substring )); + $highlighted = 1; + } + # Reset formatting and delete everything to the end of the line. + $line .= "\e[0m\e[K" if $highlighted; } - if ( @{$before_context} ) { - my $offset = @{$before_context}; - if( !$is_first_match && $previous_line_printed != $first_line - 1 ) { - App::Ack::print('--', $ors); - } - foreach my $line (@{$before_context}) { - my $context_line_no = $. - $offset; - if ( $context_line_no <= $previous_line_printed ) { - next; + push @line_parts, $line; + App::Ack::say( join( $separator, @line_parts ) ); + + # Print the underline, if appropriate. + if ( $underline ne '' ) { + # Figure out how many spaces are used per line for the ANSI coloring. + state $chars_used_by_coloring; + if ( !defined($chars_used_by_coloring) ) { + $chars_used_by_coloring = 0; + if ( $opt_color ) { + my $len_fn = sub { length( Term::ANSIColor::colored( 'x', $ENV{$_[0]} ) ) - 1 }; + $chars_used_by_coloring += $len_fn->('ACK_COLOR_FILENAME') unless $opt_heading; + $chars_used_by_coloring += $len_fn->('ACK_COLOR_LINENO'); + $chars_used_by_coloring += $len_fn->('ACK_COLOR_COLNO') if $opt_column; } - - chomp $line; - local $opt->{column}; - - print_line_with_options($opt, $filename, $line, $context_line_no, '-'); - $previous_line_printed = $context_line_no; - $offset--; } - } - } - - if ( $. > $previous_line_printed ) { - if( $is_tracking_context && !$is_first_match && $previous_line_printed != $. - 1 ) { - App::Ack::print('--', $ors); - } - - print_line_with_options($opt, $filename, $matching_line, $line_no, ':'); - $previous_line_printed = $.; - } - if($after_context) { - my $offset = 1; - foreach my $line (@{$after_context}) { - # XXX improve this! - if ( $previous_line_printed >= $. + $offset ) { - $offset++; - next; - } - chomp $line; + pop @line_parts; # Leave only the stuff on the left. + if ( @line_parts ) { + my $stuff_on_the_left = join( $separator, @line_parts ); + my $spaces_needed = length($stuff_on_the_left) - $chars_used_by_coloring + 1; - if ( $opt->{regex} && does_match( $opt, $line ) ) { - print_line_with_options($opt, $filename, $line, $. + $offset, ':'); + App::Ack::print( ' ' x $spaces_needed ); } - else { - local $opt->{column}; - print_line_with_options($opt, $filename, $line, $. + $offset, '-'); - } - $previous_line_printed = $. + $offset; - $offset++; + App::Ack::say( $underline ); } } - $is_first_match = 0; - return; } -} - -{ - -my $match_column_number; - -# does_match() MUST have an $opt->{regex} set. - -sub does_match { - my ( $opt, $line ) = @_; +sub print_line_with_context { + my ( $filename, $matching_line, $lineno ) = @_; - $match_column_number = undef; + $matching_line =~ s/[\r\n]+$//; - if ( $opt->{v} ) { - return ( $line !~ /$opt->{regex}/o ); - } - else { - if ( $line =~ /$opt->{regex}/o ) { - # @- = @LAST_MATCH_START - # @+ = @LAST_MATCH_END - $match_column_number = $-[0] + 1; - return 1; + # Check if we need to print context lines first. + if ( $opt_A || $opt_B ) { + my $before_unprinted = $lineno - $printed_lineno - 1; + if ( !$is_first_match && ( !$printed_lineno || $before_unprinted > $n_before_ctx_lines ) ) { + App::Ack::say( '--' ); } - else { - return; + + # We want at most $n_before_ctx_lines of context. + if ( $before_unprinted > $n_before_ctx_lines ) { + $before_unprinted = $n_before_ctx_lines; } - } -} -sub get_match_column { - return $match_column_number; -} + while ( $before_unprinted > 0 ) { + my $line = $before_context_buf[($before_context_pos - $before_unprinted + $n_before_ctx_lines) % $n_before_ctx_lines]; -} + chomp $line; -sub resource_has_match { - my ( $resource, $opt ) = @_; + # Disable $opt->{column} since there are no matches in the context lines. + local $opt_column = 0; - my $has_match = 0; - my $fh = $resource->open(); - if ( !$fh ) { - if ( $App::Ack::report_bad_filenames ) { - # XXX direct access to filename - App::Ack::warn( "$resource->{filename}: $!" ); - } - } - else { - my $opt_v = $opt->{v}; - my $re = $opt->{regex}; - while ( <$fh> ) { - if (/$re/o xor $opt_v) { - $has_match = 1; - last; - } + print_line_with_options( $filename, $line, $lineno-$before_unprinted, '-' ); + $before_unprinted--; } - close $fh; } - return $has_match; -} + print_line_with_options( $filename, $matching_line, $lineno, ':' ); -sub count_matches_in_resource { - my ( $resource, $opt ) = @_; + # We want to get the next $n_after_ctx_lines printed. + $after_context_pending = $n_after_ctx_lines; - my $nmatches = 0; - my $fh = $resource->open(); - if ( !$fh ) { - if ( $App::Ack::report_bad_filenames ) { - # XXX direct access to filename - App::Ack::warn( "$resource->{filename}: $!" ); - } - } - else { - my $opt_v = $opt->{v}; - my $re = $opt->{regex}; - while ( <$fh> ) { - ++$nmatches if (/$re/o xor $opt_v); - } - close $fh; - } + $is_first_match = 0; - return $nmatches; + return; } -sub main { - my @arg_sources = App::Ack::ConfigLoader::retrieve_arg_sources(); +} - my $opt = App::Ack::ConfigLoader::process_args( @arg_sources ); +sub get_match_colno { + return $match_colno; +} - $App::Ack::report_bad_filenames = !$opt->{dont_report_bad_filenames}; +sub count_matches_in_file { + my $file = shift; + my $bail = shift; # True if we're just checking for existence. - if ( $opt->{flush} ) { - $| = 1; - } + my $nmatches = 0; + my $do_scan = 1; - if ( !defined($opt->{color}) && !$opt->{g} ) { - my $windows_color = 1; - if ( $App::Ack::is_windows ) { - $windows_color = eval { require Win32::Console::ANSI; } + if ( !$file->open() ) { + $do_scan = 0; + if ( $App::Ack::report_bad_filenames ) { + App::Ack::warn( $file->name . ": $!" ); } - $opt->{color} = !App::Ack::output_to_pipe() && $windows_color; - } - if ( not defined $opt->{heading} and not defined $opt->{break} ) { - $opt->{heading} = $opt->{break} = !App::Ack::output_to_pipe(); - } - - if ( defined($opt->{H}) || defined($opt->{h}) ) { - $opt->{show_filename}= $opt->{H} && !$opt->{h}; - } - - if ( my $output = $opt->{output} ) { - $output =~ s{\\}{\\\\}g; - $output =~ s{"}{\\"}g; - $opt->{output} = qq{"$output"}; - } - - my $resources; - if ( $App::Ack::is_filter_mode && !$opt->{files_from} ) { # probably -x - $resources = App::Ack::Resources->from_stdin( $opt ); - my $regex = $opt->{regex}; - $regex = shift @ARGV if not defined $regex; - $opt->{regex} = build_regex( $regex, $opt ); } else { - if ( $opt->{f} || $opt->{lines} ) { - if ( $opt->{regex} ) { - App::Ack::warn( "regex ($opt->{regex}) specified with -f or --lines" ); - App::Ack::exit_from_ack( 0 ); # XXX the 0 is misleading - } - } - else { - my $regex = $opt->{regex}; - $regex = shift @ARGV if not defined $regex; - $opt->{regex} = build_regex( $regex, $opt ); - } - my @start; - if ( not defined $opt->{files_from} ) { - @start = @ARGV; - } - if ( !exists($opt->{show_filename}) ) { - unless(@start == 1 && !(-d $start[0])) { - $opt->{show_filename} = 1; - } - } - - if ( defined $opt->{files_from} ) { - $resources = App::Ack::Resources->from_file( $opt, $opt->{files_from} ); - exit 1 unless $resources; - } - else { - @start = ('.') unless @start; - foreach my $target (@start) { - if ( !-e $target && $App::Ack::report_bad_filenames) { - App::Ack::warn( "$target: No such file or directory" ); - } + if ( !$opt_v ) { + if ( !$file->may_be_present( $scan_re ) ) { + $do_scan = 0; } - - $opt->{file_filter} = _compile_file_filter($opt, \@start); - $opt->{descend_filter} = _compile_descend_filter($opt); - - $resources = App::Ack::Resources->from_argv( $opt, \@start ); } } - App::Ack::set_up_pager( $opt->{pager} ) if defined $opt->{pager}; - my $print_filenames = $opt->{show_filename}; - my $max_count = $opt->{m}; - my $ors = $opt->{print0} ? "\0" : "\n"; - my $only_first = $opt->{1}; + if ( $do_scan ) { + $file->reset(); - my $nmatches = 0; - my $total_count = 0; -RESOURCES: - while ( my $resource = $resources->next ) { - # XXX this variable name combined with what we're trying - # to do makes no sense. - - # XXX Combine the -f and -g functions - if ( $opt->{f} ) { - # XXX printing should probably happen inside of App::Ack - if ( $opt->{show_types} ) { - show_types( $resource, $ors ); - } - else { - App::Ack::print( $resource->name, $ors ); + my $in_range = range_setup(); + + my $fh = $file->{fh}; + if ( $using_ranges ) { + while ( <$fh> ) { + chomp; + $in_range = 1 if ( !$in_range && $opt_range_start && /$opt_range_start/o ); + if ( $in_range ) { + my $is_match = /$search_re/o; + if ( $is_match && $search_not_re ) { + $is_match = !/$search_not_re/o; + } + if ( $is_match xor $opt_v ) { + ++$nmatches; + last if $bail; + } + } + $in_range = 0 if ( $in_range && $opt_range_end && /$opt_range_end/o ); } - ++$nmatches; - last RESOURCES if defined($max_count) && $nmatches >= $max_count; } - elsif ( $opt->{g} ) { - my $is_match = ( $resource->name =~ /$opt->{regex}/o ); - if ( $opt->{v} ? !$is_match : $is_match ) { - if ( $opt->{show_types} ) { - show_types( $resource, $ors ); + else { + while ( <$fh> ) { + chomp; + my $is_match = /$search_re/o; + if ( $is_match && $search_not_re ) { + $is_match = !/$search_not_re/o; } - else { - local $opt->{show_filename} = 0; - - print_line_with_options($opt, '', $resource->name, 0, $ors); + if ( $is_match xor $opt_v ) { + ++$nmatches; + last if $bail; } - ++$nmatches; - last RESOURCES if defined($max_count) && $nmatches >= $max_count; } } - elsif ( $opt->{lines} ) { - my $print_filename = $opt->{show_filename}; - my $passthru = $opt->{passthru}; - - my %line_numbers; - foreach my $line ( @{ $opt->{lines} } ) { - my @lines = split /,/, $line; - @lines = map { - /^(\d+)-(\d+)$/ - ? ( $1 .. $2 ) - : $_ - } @lines; - @line_numbers{@lines} = (1) x @lines; - } - - my $filename = $resource->name; + } + $file->close; - local $opt->{color} = 0; + return $nmatches; +} - iterate($resource, $opt, sub { - chomp; - if ( $line_numbers{$.} ) { - print_line_with_context($opt, $filename, $_, $.); - } - elsif ( $passthru ) { - print_line_with_options($opt, $filename, $_, $., ':'); - } - return 1; - }); - } - elsif ( $opt->{count} ) { - my $matches_for_this_file = count_matches_in_resource( $resource, $opt ); +sub range_setup { + return !$using_ranges || (!$opt_range_start && $opt_range_end); +} - unless ( $opt->{show_filename} ) { - $total_count += $matches_for_this_file; - next RESOURCES; - } - if ( !$opt->{l} || $matches_for_this_file > 0) { - if ( $print_filenames ) { - App::Ack::print( $resource->name, ':', $matches_for_this_file, $ors ); - } - else { - App::Ack::print( $matches_for_this_file, $ors ); - } - } - } - elsif ( $opt->{l} || $opt->{L} ) { - my $is_match = resource_has_match( $resource, $opt ); +sub _build_search_not_re { + my $opt = shift; - if ( $opt->{L} ? !$is_match : $is_match ) { - App::Ack::print( $resource->name, $ors ); - ++$nmatches; + my @not = @{$opt->{not}}; - last RESOURCES if $only_first; - last RESOURCES if defined($max_count) && $nmatches >= $max_count; - } - } - else { - $nmatches += print_matches_in_resource( $resource, $opt ); - if ( $nmatches && $only_first ) { - last RESOURCES; - } + if ( @not ) { + my @built; + for my $re ( @not ) { + my ($built,undef) = build_regex( $re, $opt ); + push( @built, $built ); } + return join( '|', @built ); } - if ( $opt->{count} && !$opt->{show_filename} ) { - App::Ack::print( $total_count, "\n" ); - } - - close $App::Ack::fh; - App::Ack::exit_from_ack( $nmatches ); + return; } +=pod + +=encoding UTF-8 =head1 NAME @@ -1045,18 +1182,18 @@ ack - grep-like text finder =head1 DESCRIPTION -Ack is designed as a replacement for 99% of the uses of F. +ack is designed as an alternative to F for programmers. -Ack searches the named input FILEs (or standard input if no files -are named, or the file name - is given) for lines containing a match -to the given PATTERN. By default, ack prints the matching lines. +ack searches the named input FILEs or DIRECTORYs for lines containing a +match to the given PATTERN. By default, ack prints the matching lines. +If no FILE or DIRECTORY is given, the current directory will be searched. PATTERN is a Perl regular expression. Perl regular expressions are commonly found in other programming languages, but for the particulars of their behavior, please consult -L. If you don't know +L. If you don't know how to use regular expression but are interested in learning, you may -consult L. If you do not +consult L. If you do not need or want ack to use regular expressions, please see the C<-Q>/C<--literal> option. @@ -1111,16 +1248,101 @@ to add/remove multiple directories from the ignore list. For a complete list of directories that do not get searched, run C. -=head1 WHEN TO USE GREP +=head1 MATCHING IN A RANGE OF LINES + +The C<--range-start> and C<--range-end> options let you specify ranges of +lines to search within each file. + +Say you had the following file, called F: + + # This function calls print on "foo". + sub foo { + print 'foo'; + } + my $print = 1; + sub bar { + print 'bar'; + } + my $task = 'print'; + +Calling C will give us five matches: + + $ ack print testfile + # This function calls print on "foo". + print 'foo'; + my $print = 1; + print 'bar'; + my $task = 'print'; + +What if we only want to search for C within the subroutines? We can +specify ranges of lines that we want ack to search. The range starts with +any line that matches the pattern C<^sub \w+>, and stops with any line that +matches C<^}>. + + $ ack --range-start='^sub \w+' --range-end='^}' print testfile + print 'foo'; + print 'bar'; + +Note that ack searched two ranges of lines. The listing below shows which +lines were in a range and which were out of the range. + + Out # This function calls print on "foo". + In sub foo { + In print 'foo'; + In } + Out my $print = 1; + In sub bar { + In print 'bar'; + In } + Out my $task = 'print'; + +You don't have to specify both C<--range-start> and C<--range-end>. IF +C<--range-start> is omitted, then the range runs from the first line in the +file until the first line that matches C<--range-end>. Similarly, if +C<--range-end> is omitted, the range runs from the first line matching +C<--range-start> to the end of the file. -I trumps I as an everyday tool 99% of the time, but don't -throw I away, because there are times you'll still need it. +For example, if you wanted to search all HTML files up until the first +instance of the C<< >>, you could do -E.g., searching through huge files looking for regexes that can be -expressed with I syntax should be quicker with I. + ack foo --html --range-end='' -If your script or parent program uses I C<--quiet> or C<--silent> -or needs exit 2 on IO error, use I. +Or to search after Perl's `__DATA__` or `__END__` markers, you would do + + ack pattern --perl --range-start='^__(END|DATA)__' + +It's possible for a range to start and stop on the same line. For example + + --range-start='' --range-end='' + +would match this line as both the start and end of the range, making a +one-line range. + + Page title + +Note that the patterns in C<--range-start> and C<--range-end> are not +affected by options like C<-i>, C<-w> and C<-Q> that modify the behavior of +the main pattern being matched. + +Again, ranges only affect where matches are looked for. Everything else in +ack works the same way. Using C<-c> option with a range will give a count +of all the matches that appear within those ranges. The C<-l> shows those +files that have a match within a range, and the C<-L> option shows files +that do not have a match within a range. + +The C<-v> option for negating a match works inside the range, too. +To see lines that don't match "google" within the "" section of +your HTML files, you could do: + + ack google -v --html --range-start=' will show all lines in the file, but only show +matches for lines within the range. =head1 OPTIONS @@ -1146,6 +1368,8 @@ when used interactively. =item B<-C [I]>, B<--context[=I]> Print I lines (default 2) of context around matching lines. +You can specify zero lines of context to override another context +specified in an ackrc. =item B<-c>, B<--count> @@ -1174,6 +1398,10 @@ Sets the color to be used for filenames. Sets the color to be used for matches. +=item B<--color-colno=I> + +Sets the color to be used for column numbers. + =item B<--color-lineno=I> Sets the color to be used for line numbers. @@ -1217,6 +1445,11 @@ The list of files to be searched is specified in I. The list of files are separated by newlines. If I is C<->, the list is loaded from standard input. +Note that the list of files is B filtered in any way. If you +add C<--type=html> in addition to C<--files-from>, the C<--type> will +be ignored. + + =item B<--[no]filter> Forces ack to act as if it were receiving input via a pipe. @@ -1230,11 +1463,28 @@ This is off by default. =item B<-g I> -Print files where the relative path + filename matches I. -This option can be combined with B<--color> to make it easier to spot -the match. +Print searchable files where the relative path + filename matches +I. -=item B<--[no]group> +Note that + + ack -g foo + +is exactly the same as + + ack -f | ack foo + +This means that just as ack will not search, for example, F<.jpg> +files, C<-g> will not list F<.jpg> files either. ack is not intended +to be a general-purpose file finder. + +Note also that if you have C<-i> in your .ackrc that the filenames +to be matched will be case-insensitive as well. + +This option can be combined with B<--color> to make it easier to +spot the match. + +=item B<--[no]group> B<--group> groups matches by file name. This is the default when used interactively. @@ -1257,17 +1507,29 @@ searched. Print a filename heading above each file's results. This is the default when used interactively. -=item B<--help>, B<-?> +=item B<--help> Print a short help statement. -=item B<--help-types>, B<--help=types> +=item B<--help-types> Print all known types. +=item B<--help-colors> + +Print a chart of various color combinations. + +=item B<--help-rgb-colors> + +Like B<--help-colors> but with more precise RGB colors. + =item B<-i>, B<--ignore-case> -Ignore case distinctions in PATTERN +Ignore case distinctions in PATTERN. Overrides B<--smart-case> and B<-I>. + +=item B<-I>, B<--no-ignore-case> + +Turns on case distinctions in PATTERN. Overrides B<--smart-case> and B<-i>. =item B<--ignore-ack-defaults> @@ -1289,22 +1551,14 @@ specify B<--ignore-dir=foo> and then no files from any foo directory are taken into account by ack unless given explicitly on the command line. -=item B<--ignore-file=I> +=item B<--ignore-file=I> -Ignore files matching I. The filters are specified +Ignore files matching I. The filters are specified identically to file type filters as seen in L. =item B<-k>, B<--known-types> -Limit selected files to those with types that ack knows about. This is -equivalent to the default behavior found in ack 1. - -=item B<--lines=I> - -Only print line I of each file. Multiple lines can be given with multiple -B<--lines> options or as a comma separated list (B<--lines=3,5,7>). B<--lines=4-7> -also works. The lines are always output in ascending order, no matter the -order given on the command line. +Limit selected files to those with types that ack knows about. =item B<-l>, B<--files-with-matches> @@ -1326,7 +1580,8 @@ same set of files. =item B<-m=I>, B<--max-count=I> -Stop reading a file after I matches. +Print only I matches out of each file. If you want to stop ack +after printing the first match of any kind, use the B<-1> options. =item B<--man> @@ -1336,17 +1591,83 @@ Print this manual page. No descending into subdirectories. +=item B<--not=PATTERN> + +Specifies a I that must NOT me true on a given line for a match to +occur. This option can be repeated. + +If you want to find all the lines with "dogs" but not if "cats" or "fish" +appear on the line, use: + + ack dogs --not cats --not fish + +Note that the options that affect "dogs" also affect "cats" and "fish", so +if you have + + ack -i -w dogs --not cats + +the the search for both "dogs" and "cats" will be case-insensitive and be +word-limited. + =item B<-o> Show only the part of each line matching PATTERN (turns off text -highlighting) +highlighting). This is exactly the same as C<--output=$&>. =item B<--output=I> Output the evaluation of I for each line (turns off text -highlighting) -If PATTERN matches more than once then a line is output for each non-overlapping match. -For more information please see the section L">. +highlighting). If PATTERN matches more than once then a line is +output for each non-overlapping match. + +I may contain the strings "\n", "\r" and "\t", which will be +expanded to their corresponding characters line feed, carriage return +and tab, respectively. + +I may also contain the following Perl special variables: + +=over 4 + +=item C<$1> through C<$9> + +The subpattern from the corresponding set of capturing parentheses. +If your pattern is C<(.+) and (.+)>, and the string is "this and +that', then C<$1> is "this" and C<$2> is "that". + +=item C<$_> + +The contents of the line in the file. + +=item C<$.> + +The number of the line in the file. + +=item C<$&>, C<$`> and C<$'> + +C<$&> is the the string matched by the pattern, C<$`> is what +precedes the match, and C<$'> is what follows it. If the pattern +is C and the string is "lexicographic", then C<$&> is +"graph", C<$`> is "lexico" and C<$'> is "ic". + +Use of these variables in your output will slow down the pattern +matching. + +=item C<$+> + +The match made by the last parentheses that matched in the pattern. +For example, if your pattern is C, +then C<$+> will contain whichever set of parentheses matched. + +=item C<$f> + +C<$f> is available, in C<--output> only, to insert the filename. +This is a stand-in for the discovered C<$filename> usage in old C<< ack2 --output >>, +which is disallowed with C improved security. + +The intended usage is to provide the grep or compile-error syntax needed for editor/IDE go-to-line integration, +e.g. C<--output=$f:$.:$_> or C<--output=$f\t$.\t$&> + +=back =item B<--pager=I>, B<--nopager> @@ -1356,7 +1677,7 @@ via the C and C environment variables. Using --pager does not suppress grouping and coloring like piping output on the command-line does. -B<--nopager> cancels any setting in ~/.ackrc, C or C. +B<--nopager> cancels any setting in F<~/.ackrc>, C or C. No output will be sent through a pager. =item B<--passthru> @@ -1365,18 +1686,55 @@ Prints all lines, whether or not they match the expression. Highlighting will still work, though, so it can be used to highlight matches while still seeing the entire file, as in: - # Watch a log file, and highlight a certain IP address + # Watch a log file, and highlight a certain IP address. $ tail -f ~/access.log | ack --passthru 123.45.67.89 =item B<--print0> -Only works in conjunction with -f, -g, -l or -c (filename output). The filenames -are output separated with a null byte instead of the usual newline. This is -helpful when dealing with filenames that contain whitespace, e.g. +Only works in conjunction with B<-f>, B<-g>, B<-l> or B<-c>, options +that only list filenames. The filenames are output separated with a +null byte instead of the usual newline. This is helpful when dealing +with filenames that contain whitespace, e.g. - # remove all files of type html + # Remove all files of type HTML. ack -f --html --print0 | xargs -0 rm -f +=item B<-p[N]>, B<--proximate[=N]> + +Groups together match lines that are within N lines of each other. +This is useful for visually picking out matches that appear close +to other matches. + +For example, if you got these results without the C<--proximate> option, + + 15: First match + 18: Second match + 19: Third match + 37: Fourth match + +they would look like this with C<--proximate=1> + + 15: First match + + 18: Second match + 19: Third match + + 37: Fourth match + +and this with C<--proximate=3>. + + 15: First match + 18: Second match + 19: Third match + + 37: Fourth match + +If N is omitted, N is set to 1. + +=item B<-P> + +Negates the effect of the B<--proximate> option. Shortcut for B<--proximate=0>. + =item B<-Q>, B<--literal> Quote all metacharacters in PATTERN, it is treated as a literal. @@ -1386,16 +1744,23 @@ Quote all metacharacters in PATTERN, it is treated as a literal. Recurse into sub-directories. This is the default and just here for compatibility with grep. You can also use it for turning B<--no-recurse> off. +=item B<--range-start=PATTERN>, B<--range-end=PATTERN> + +Specifies patterns that mark the start and end of a range. See +L for details. + =item B<-s> Suppress error messages about nonexistent or unreadable files. This is taken from fgrep. -=item B<--[no]smart-case>, B<--no-smart-case> +=item B<-S>, B<--[no]smart-case>, B<--no-smart-case> Ignore case in the search strings if PATTERN contains no uppercase -characters. This is similar to C in vim. This option is -off by default, and ignored if C<-i> is specified. +characters. This is similar to C in the vim text editor. +The options overrides B<-i> and B<-I>. + +B<-S> is a synonym for B<--smart-case>. B<-i> always overrides this option. @@ -1410,31 +1775,34 @@ Outputs the filetypes that ack associates with each file. Works with B<-f> and B<-g> options. -=item B<--type=[no]TYPE> +=item B<-t TYPE>, B<--type=TYPE>, B<--TYPE> -Specify the types of files to include or exclude from a search. +Specify the types of files to include in the search. TYPE is a filetype, like I or I. B<--type=perl> can -also be specified as B<--perl>, and B<--type=noperl> can be done -as B<--noperl>. +also be specified as B<--perl>, although this is deprecated. -If a file is of both type "foo" and "bar", specifying --foo and ---nobar will exclude the file, because an exclusion takes precedence -over an inclusion. +Type inclusions can be repeated and are ORed together. -Type specifications can be repeated and are ORed together. +See I for a list of valid types. -See I for a list of valid types. +=item B<-T TYPE>, B<--type=noTYPE>, B<--noTYPE> -=item B<--type-add I:I:I> +Specifies the type of files to exclude from the search. B<--type=noperl> +can be done as B<--noperl>, although this is deprecated. -Files with the given FILTERARGS applied to the given FILTER +If a file is of both type "foo" and "bar", specifying both B<--type=foo> +and B<--type=nobar> will exclude the file, because an exclusion takes +precedence over an inclusion. + +=item B<--type-add I:I:I> + +Files with the given ARGS applied to the given FILTER are recognized as being of (the existing) type TYPE. See also L. +=item B<--type-set I:I:I> -=item B<--type-set I:I:I> - -Files with the given FILTERARGS applied to the given FILTER are recognized as +Files with the given ARGS applied to the given FILTER are recognized as being of type TYPE. This replaces an existing definition for type TYPE. See also L. @@ -1443,9 +1811,26 @@ also L. The filters associated with TYPE are removed from Ack, and are no longer considered for searches. +=item B<--[no]underline> + +Turns on underlining of matches, where "underlining" is printing a line of +carets under the match. + + $ ack -u foo + peanuts.txt + 17: Come kick the football you fool + ^^^ ^^^ + 623: Price per square foot + ^^^ + +This is useful if you're dumping the results of an ack run into a text +file or printer that doesn't support ANSI color codes. + +The setting of underline does not affect highlighting of matches. + =item B<-v>, B<--invert-match> -Invert match: select non-matching lines +Invert match: select non-matching lines. =item B<--version> @@ -1453,14 +1838,16 @@ Display version and copyright information. =item B<-w>, B<--word-regexp> -Force PATTERN to match only whole words. The PATTERN is wrapped with -C<\b> metacharacters. +Force PATTERN to match only whole words. =item B<-x> -An abbreviation for B<--files-from=->; the list of files to search are read +An abbreviation for B<--files-from=->. The list of files to search are read from standard input, with one line per file. +Note that the list of files is B filtered in any way. If you add +C<--type=html> in addition to C<-x>, the C<--type> will be ignored. + =item B<-1> Stops after reporting first match of any kind. This is different @@ -1494,7 +1881,7 @@ might look like this: # Always sort the files --sort-files - # Always color, even if piping to a another program + # Always color, even if piping to another program --color # Use "less -r" as my pager @@ -1516,7 +1903,13 @@ an F<.ackrc> file - then you do not have to define your types over and over again. In the following examples the options will always be shown on one command line so that they can be easily copy & pasted. -I searches for foo in all perl files. I +File types can be specified both with the the I<--type=xxx> option, +or the file type as an option itself. For example, if you create +a filetype of "cobol", you can specify I<--type=cobol> or simply +I<--cobol>. File types must be at least two characters long. This +is why the C language is I<--cc> and the R language is I<--rr>. + +I searches for foo in all perl files. I tells you, that perl files are files ending in .pl, .pm, .pod or .t. So what if you would like to include .xs files as well when searching for --perl files? I @@ -1547,13 +1940,12 @@ The following does B work in the F<.ackrc> file: --type-set eiffel:ext:e,eiffel - In order to see all currently defined types, use I<--help-types>, e.g. I -In addition to filtering based on extension (like ack 1.x allowed), ack 2 -offers additional filter types. The generic syntax is -I<--type-set TYPE:FILTER:FILTERARGS>; I depends on the value +In addition to filtering based on extension, ack offers additional +filter types. The generic syntax is +I<--type-set TYPE:FILTER:ARGS>; I depends on the value of I. =over 4 @@ -1579,7 +1971,7 @@ Example: =item match:I I filters match the target filename against a regular expression. -The regular expression is made case insensitive for the search. +The regular expression is made case-insensitive for the search. Example: @@ -1597,7 +1989,65 @@ Example: =back -More filter types may be made available in the future. +=head1 ACK COLORS + +ack allows customization of the colors it uses when presenting matches +onscreen. It uses the colors available in Perl's L +module, which provides the following listed values. Note that case does not +matter when using these values. + +There are four different colors ack uses: + + Aspect Option Env. variable Default + -------- ----------------- ------------------ --------------- + filename --color-filename ACK_COLOR_FILENAME black on_yellow + match --color-match ACK_COLOR_MATCH bold green + line no. --color-lineno ACK_COLOR_LINENO bold yellow + column no. --color-colno ACK_COLOR_COLNO bold yellow + +The column number column is only used if the column number is shown because +of the --column option. + +Colors may be specified by command-line option, such as +C, or by setting an environment +variable, such as C. Options for colors +can be set in your ACKRC file (See "THE .ackrc FILE"). + +ack can understand the following colors for the foreground: + + black red green yellow blue magenta cyan white + +The optional background color is specified by prepending "on_" to one of +the foreground colors: + + on_black on_red on_green on_yellow on_blue on_magenta on_cyan on_white + +Each of the foreground colors can be modified with the following +attributes, which may or may not be supported by your terminal: + + bold faint italic underline blink reverse concealed + +Any combinations of modifiers can be added to the foreground color. If your +terminal supports it, and you enjoy visual punishment, you can specify: + + ack --color-filename="blink italic underline bold red on_yellow" + +For charts of the colors and what they look like, run C +and C. + +If the eight standard colors, in their bold, faint and unmodified states, +aren't enough for you to choose from, you can also specify colors by their +RGB values. They are specified as "rgbXYZ" where X, Y, and Z are values +between 0 and 5 giving the intensity of red, green and blue, respectively. +Therefore, "rgb500" is pure red, "rgb505" is purple, and so on. + +Background colors can be specified with the "on_" prefix prepended on an +RGB color, so that "on_rgb505" would be a purple background. + +The modifier attributes of blink, italic, underscore and so on may or may +not work on the RGB colors. + +For a chart of the 216 possible RGB colors, run C. =head1 ENVIRONMENT VARIABLES @@ -1612,43 +2062,26 @@ on the command line. Specifies the location of the user's F<.ackrc> file. If this file doesn't exist, F looks in the default location. -=item ACK_OPTIONS +=item ACK_COLOR_COLNO -This variable specifies default options to be placed in front of -any explicit options on the command line. +Color specification for the column number in ack's output. By default, the +column number is not shown. You have to enable it with the B<--column> +option. See the section "ack Colors" above. =item ACK_COLOR_FILENAME -Specifies the color of the filename when it's printed in B<--group> -mode. By default, it's "bold green". - -The recognized attributes are clear, reset, dark, bold, underline, -underscore, blink, reverse, concealed black, red, green, yellow, -blue, magenta, on_black, on_red, on_green, on_yellow, on_blue, -on_magenta, on_cyan, and on_white. Case is not significant. -Underline and underscore are equivalent, as are clear and reset. -The color alone sets the foreground color, and on_color sets the -background color. - -This option can also be set with B<--color-filename>. - -=item ACK_COLOR_MATCH - -Specifies the color of the matching text when printed in B<--color> -mode. By default, it's "black on_yellow". - -This option can also be set with B<--color-match>. - -See B for the color specifications. +Color specification for the filename in ack's output. See the section "ack +Colors" above. =item ACK_COLOR_LINENO -Specifies the color of the line number when printed in B<--color> -mode. By default, it's "bold yellow". +Color specification for the line number in ack's output. See the section +"ack Colors" above. -This option can also be set with B<--color-lineno>. +=item ACK_COLOR_MATCH -See B for the color specifications. +Color specification for the matched text in ack's output. See the section +"ack Colors" above. =item ACK_PAGER @@ -1673,7 +2106,7 @@ If you are not on Windows, you never need to use C. =head1 ACK & OTHER TOOLS -=head2 Vim integration +=head2 Simple vim integration F integrates easily with the Vim text editor. Set this in your F<.vimrc> to use F instead of F: @@ -1686,27 +2119,10 @@ with F and easily step through the results in Vim: :grep Dumper perllib -Miles Sterrett has written a Vim plugin for F which allows you to use -C<:Ack> instead of C<:grep>, as well as several other advanced features. - -L - -=head2 Emacs integration - -Phil Jackson put together an F extension that "provides a -simple compilation mode ... has the ability to guess what files you -want to search for based on the major-mode." +=head2 Editor integration -L - -=head2 TextMate integration - -Pedro Melo is a TextMate user who writes "I spend my day mostly -inside TextMate, and the built-in find-in-project sucks with large -projects. So I hacked a TextMate command that was using find + -grep to use ack. The result is the Search in Project with ack, and -you can find it here: -L" +Many users have integrated ack into their preferred text editors. +For details and links, see L. =head2 Shell and Return Code @@ -1727,13 +2143,13 @@ file is found. If no files are found, then 1 is returned. If ack gives you output you're not expecting, start with a few simple steps. -=head2 Use B<--noenv> +=head2 Try it with B<--noenv> Your environment variables and F<.ackrc> may be doing things you're not expecting, or forgotten you specified. Use B<--noenv> to ignore your environment and F<.ackrc>. -=head2 Use B<-f> to see what files have been selected +=head2 Use B<-f> to see what files have been selected for searching Ack's B<-f> was originally added as a debugging tool. If ack is not finding matches you think it should find, run F to see @@ -1743,107 +2159,150 @@ options to show the type of each file selected. =head2 Use B<--dump> This lists the ackrc files that are loaded and the options loaded -from them. -So for example you can find a list of directories that do not get searched or where filetypes are defined. +from them. You may be loading an F<.ackrc> file that you didn't know +you were loading. + +=head1 ACKRC LOCATION SEMANTICS + +Ack can load its configuration from many sources. The following list +specifies the sources Ack looks for configuration files; each one +that is found is loaded in the order specified here, and +each one overrides options set in any of the sources preceding +it. (For example, if I set --sort-files in my user ackrc, and +--nosort-files on the command line, the command line takes +precedence) + +=over 4 -=head1 TIPS +=item * -=head2 Use the F<.ackrc> file. +Defaults are loaded from App::Ack::ConfigDefaults. This can be omitted +using C<--ignore-ack-defaults>. -The F<.ackrc> is the place to put all your options you use most of -the time but don't want to remember. Put all your --type-add and ---type-set definitions in it. If you like --smart-case, set it -there, too. I also set --sort-files there. +=item * Global ackrc -=head2 Use F<-f> for working with big codesets +Options are then loaded from the global ackrc. This is located at +C on Unix-like systems. -Ack does more than search files. C will create a -list of all the Perl files in a tree, ideal for sending into F. -For example: +Under Windows XP and earlier, the global ackrc is at +C - # Change all "this" to "that" in all Perl files in a tree. - ack -f --perl | xargs perl -p -i -e's/this/that/g' +Under Windows Vista/7, the global ackrc is at +C -or if you prefer: +The C<--noenv> option prevents all ackrc files from being loaded. - perl -p -i -e's/this/that/g' $(ack -f --perl) +=item * User ackrc -=head2 Use F<-Q> when in doubt about metacharacters +Options are then loaded from the user's ackrc. This is located at +C<$HOME/.ackrc> on Unix-like systems. -If you're searching for something with a regular expression -metacharacter, most often a period in a filename or IP address, add -the -Q to avoid false positives without all the backslashing. See -the following example for more... +Under Windows XP and earlier, the user's ackrc is at +C. -=head2 Use ack to watch log files +Under Windows Vista/7, the user's ackrc is at +C. -Here's one I used the other day to find trouble spots for a website -visitor. The user had a problem loading F, so I -took the access log and scanned it with ack twice. +If you want to load a different user-level ackrc, it may be specified +with the C<$ACKRC> environment variable. - ack -Q aa.bb.cc.dd /path/to/access.log | ack -Q -B5 troublesome.gif +The C<--noenv> option prevents all ackrc files from being loaded. -The first ack finds only the lines in the Apache log for the given -IP. The second finds the match on my troublesome GIF, and shows -the previous five lines from the log in each case. +=item * Project ackrc -=head2 Examples of F<--output> +Options are then loaded from the project ackrc. The project ackrc is +the first ackrc file with the name C<.ackrc> or C<_ackrc>, first searching +in the current directory, then the parent directory, then the grandparent +directory, etc. This can be omitted using C<--noenv>. -Following variables are useful in the expansion string: +=item * --ackrc -=over 4 +The C<--ackrc> option may be included on the command line to specify an +ackrc file that can override all others. It is consulted even if C<--noenv> +is present. + +=item * Command line -=item C<$&> +Options are then loaded from the command line. -The whole string matched by PATTERN. +=back -=item C<$1>, C<$2>, ... +=head1 BUGS & ENHANCEMENTS -The contents of the 1st, 2nd ... bracketed group in PATTERN. +ack is based at GitHub at L -=item C<$`> +Please report any bugs or feature requests to the issues list at +GitHub: L. -The string before the match. +Please include the operating system that you're using; the output of +the command C; and any customizations in your F<.ackrc> +you may have. -=item C<$'> +To suggest enhancements, please submit an issue at +L. Also read the +F file in the ack code repository. -The string after the match. +Also, feel free to discuss your issues on the ack mailing +list at L. -=back +=head1 SUPPORT + +Support for and information about F can be found at: + +=over 4 + +=item * The ack homepage + +L + +=item * Source repository -For more details and other variables see -L. +L -This example shows how to add text around a particular pattern -(in this case adding _ around word with "e") +=item * The ack issues list at GitHub - ack2.pl "\w*e\w*" quick.txt --output="$`_$&_$'" - _The_ quick brown fox jumps over the lazy dog - The quick brown fox jumps _over_ the lazy dog - The quick brown fox jumps over _the_ lazy dog +L -This shows how to pick out particular parts of a match using ( ) within regular expression. +=item * The ack announcements mailing list - ack '=head(\d+)\s+(.*)' --output=' $1 : $2' - input file contains "=head1 NAME" - output "1 : NAME" +L -=head2 Share your knowledge +=item * The ack users' mailing list -Join the ack-users mailing list. Send me your tips and I may add -them here. +L + +=item * The ack development mailing list + +L + +=back + +=head1 COMMUNITY + +There are ack mailing lists and a Slack channel for ack. See +L for details. =head1 FAQ +This is the Frequently Asked Questions list for ack. + +=head2 Can I stop using grep now? + +Many people find I to be better than I as an everyday tool +99% of the time, but don't throw I away, because there are times +you'll still need it. For example, you might be looking through huge +log files and not using regular expressions. In that case, I +will probably perform better. + =head2 Why isn't ack finding a match in (some file)? -Probably because it's of a type that ack doesn't recognize. ack's -searching behavior is driven by filetype. B +First, take a look and see if ack is even looking at the file. ack is +intelligent in what files it will search and which ones it won't, but +sometimes that can be surprising. -Use the C<-f> switch to see a list of files that ack will search -for you. You can use the C<--show-types> switch to show which type -ack thinks each file is. +Use the C<-f> switch, with no regex, to see a list of files that ack +will search for you. If your file doesn't show up in the list of files +that C shows, then ack never looks in it. =head2 Wouldn't it be great if F did search & replace? @@ -1859,13 +2318,19 @@ this from the Unix shell: =head2 Can I make ack recognize F<.xyz> files? -Yes! Please see L. If you think -that F should recognize a type by default, please see -L. +Yes! Please see L in the ack manual. + +=head2 Will you make ack recognize F<.xyz> files by default? -=head2 There's already a program/package called ack. +We might, depending on how widely-used the file format is. -Yes, I know. +Submit an issue at in the GitHub issue queue at +L. Explain what the file format +is, where we can find out more about it, and what you have been using +in your F<.ackrc> to support it. + +Please do not bother creating a pull request. The code for filetypes +is trivial compared to the rest of the process we go through. =head2 Why is it called ack if it's called ack-grep? @@ -1911,266 +2376,59 @@ expression metacharacter!) =head2 Why does C<"ack '.{40000,}'"> fail? Isn't that a valid regex? -The Perl language limits the repetition quanitifier to 32K. You +The Perl language limits the repetition quantifier to 32K. You can search for C<.{32767}> but not C<.{32768}>. -=head1 ACKRC LOCATION SEMANTICS - -Ack can load its configuration from many sources. This list -specifies the sources Ack looks for configuration; each one -that is found is loaded in the order specified here, and -each one overrides options set in any of the sources preceding -it. (For example, if I set --sort-files in my user ackrc, and ---nosort-files on the command line, the command line takes -precedence) - -=over 4 - -=item * - -Defaults are loaded from App::Ack::ConfigDefaults. This can be omitted -using C<--ignore-ack-defaults>. - -=item * Global ackrc - -Options are then loaded from the global ackrc. This is located at -C on Unix-like systems, and -C on Windows. -This can be omitted using C<--noenv>. - -=item * User ackrc - -Options are then loaded from the user's ackrc. This is located at -C<$HOME/.ackrc> on Unix-like systems, and -C. If a different -ackrc is desired, it may be overridden with the C<$ACKRC> environment -variable. -This can be omitted using C<--noenv>. - -=item * Project ackrc - -Options are then loaded from the project ackrc. The project ackrc is -the first ackrc file with the name C<.ackrc> or C<_ackrc>, first searching -in the current directory, then the parent directory, then the grandparent -directory, etc. This can be omitted using C<--noenv>. - -=item * --ackrc - -The C<--ackrc> option may be included on the command line to specify an -ackrc file that can override all others. It is consulted even if C<--noenv> -is present. - -=item * ACK_OPTIONS - -Options are then loaded from the environment variable C. This can -be omitted using C<--noenv>. +=head2 Ack does "X" and shouldn't, should it? -=item * Command line - -Options are then loaded from the command line. - -=back - -=head1 DIFFERENCES BETWEEN ACK 1.X AND ACK 2.X - -A lot of changes were made for ack 2; here is a list of them. - -=head2 GENERAL CHANGES - -=over 4 - -=item * - -When no selectors are specified, ack 1.x only searches through files that -it can map to a file type. ack 2.x, by contrast, will search through -every regular, non-binary file that is not explicitly ignored via -B<--ignore-file> or B<--ignore-dir>. This is similar to the behavior of the -B<-a/--all> option in ack 1.x. - -=item * - -A more flexible filter system has been added, so that more powerful file types -may be created by the user. For details, please consult -L. - -=item * - -ack now loads multiple ackrc files; see L for -details. - -=item * - -ack's default filter definitions aren't special; you may tell ack to -completely disregard them if you don't like them. - -=back - -=head2 REMOVED OPTIONS - -=over 4 - -=item * - -Because of the change in default search behavior, the B<-a/--all> and -B<-u/--unrestricted> options have been removed. In addition, the -B<-k/--known-types> option was added to cause ack to behave with -the default search behavior of ack 1.x. - -=item * +We try to remain as close to grep's behavior as possible, so when in +doubt, see what grep does! If there's a mismatch in functionality there, +please submit an issue to GitHub, and/or bring it up on the ack-users +mailing list. -The B<-G> option has been removed. Two regular expressions on the -command line was considered too confusing; to simulate B<-G>'s functionality, -you may use the new B<-x> option to pipe filenames from one invocation of -ack into another. - -=item * - -The B<--binary> option has been removed. - -=item * - -The B<--skipped> option has been removed. - -=item * - -The B<--text> option has been removed. - -=item * - -The B<--invert-file-match> option has been removed. Instead, you may -use B<-v> with B<-g>. - -=back - -=head2 CHANGED OPTIONS - -=over 4 - -=item * - -The options that modify the regular expression's behavior (B<-i>, B<-w>, -B<-Q>, and B<-v>) may now be used with B<-g>. - -=back - -=head2 ADDED OPTIONS - -=over 4 - -=item * - -B<--files-from> was added so that a user may submit a list of filenames as -a list of files to search. +=cut -=item * +=head1 ACKNOWLEDGEMENTS -B<-x> was added to tell ack to accept a list of filenames via standard input; -this list is the list of filenames that will be used for the search. - -=item * - -B<-s> was added to tell ack to suppress error messages about non-existent or -unreadable files. - -=item * - -B<--ignore-directory> and B<--noignore-directory> were added as aliases for -B<--ignore-dir> and B<--noignore-dir> respectively. - -=item * - -B<--ignore-file> was added so that users may specify patterns of files to -ignore (ex. /.*~$/). - -=item * - -B<--dump> was added to allow users to easily find out which options are -set where. - -=item * - -B<--create-ackrc> was added so that users may create custom ackrc files based -on the default settings loaded by ack, and so that users may easily view those -defaults. - -=item * - -B<--type-del> was added to selectively remove file type definitions. - -=item * - -B<--ignore-ack-defaults> was added so that users may ignore ack's default -options in favor of their own. - -=item * - -B<--bar> was added so ack users may consult Admiral Ackbar. - -=back - -=head1 AUTHOR - -Andy Lester, C<< >> - -=head1 BUGS - -Please report any bugs or feature requests to the issues list at -Github: L - -=head1 ENHANCEMENTS - -All enhancement requests MUST first be posted to the ack-users -mailing list at L. I -will not consider a request without it first getting seen by other -ack users. This includes requests for new filetypes. - -There is a list of enhancements I want to make to F in the ack -issues list at Github: L - -Patches are always welcome, but patches with tests get the most -attention. - -=head1 SUPPORT - -Support for and information about F can be found at: - -=over 4 - -=item * The ack homepage - -L - -=item * The ack-users mailing list - -L - -=item * The ack issues list at Github - -L - -=item * AnnoCPAN: Annotated CPAN documentation - -L - -=item * CPAN Ratings - -L - -=item * Search CPAN - -L - -=item * Git source repository - -L - -=back - -=head1 ACKNOWLEDGEMENTS - -How appropriate to have Inowledgements! +How appropriate to have Inowledgements! Thanks to everyone who has contributed to ack in any way, including +Thomas Gossler, +Kieran Mace, +Volker Glave, +Axel Beckert, +Eric Pement, +Gabor Szabo, +Frieder Bluemle, +Grzegorz Kaczmarczyk, +Dan Book, +Tomasz Konojacki, +Salomon Smeke, +M. Scott Ford, +Anders Eriksson, +H.Merijn Brand, +Duke Leto, +Gerhard Poul, +Ethan Mallove, +Marek Kubica, +Ray Donnelly, +Nikolaj Schumacher, +Ed Avis, +Nick Morrott, +Austin Chamberlin, +Varadinsky, +SEbastien FeugEre, +Jakub Wilk, +Pete Houston, +Stephen Thirlwall, +Jonah Bishop, +Chris Rebert, +Denis Howe, +RaEl GundEn, +James McCoy, +Daniel Perrett, +Steven Lee, +Jonathan Perret, Fraser Tweedale, RaEl GundEn, Steffen Jaeckel, @@ -2261,17 +2519,23 @@ Ricardo Signes, Pete Krawczyk and Rob Hoelz. +=head1 AUTHOR + +Andy Lester, C<< >> + =head1 COPYRIGHT & LICENSE -Copyright 2005-2013 Andy Lester. +Copyright 2005-2023 Andy Lester. This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License v2.0. -See http://www.perlfoundation.org/artistic_license_2_0 or the LICENSE.md +See https://www.perlfoundation.org/artistic-license-20.html or the LICENSE.md file that comes with the ack distribution. =cut + +1; package App::Ack; use warnings; @@ -2281,9 +2545,11 @@ use strict; our $VERSION; our $COPYRIGHT; BEGIN { - $VERSION = '2.12'; - $COPYRIGHT = 'Copyright 2005-2013 Andy Lester.'; + $VERSION = 'v3.7.0'; # Check https://beyondgrep.com/ for updates + $COPYRIGHT = 'Copyright 2005-2023 Andy Lester.'; } +our $STANDALONE = 0; +our $ORIGINAL_PROGRAM_NAME; our $fh; @@ -2300,33 +2566,22 @@ our %ignore_dirs; our $is_filter_mode; our $output_to_pipe; -our $dir_sep_chars; -our $is_cygwin; our $is_windows; -use File::Spec 1.00015 (); +our $debug_nopens = 0; + +# Line ending, changes to "\0" if --print0. +our $ors = "\n"; BEGIN { # These have to be checked before any filehandle diddling. $output_to_pipe = not -t *STDOUT; $is_filter_mode = -p STDIN; - $is_cygwin = ($^O eq 'cygwin'); $is_windows = ($^O eq 'MSWin32'); - $dir_sep_chars = $is_windows ? quotemeta( '\\/' ) : quotemeta( File::Spec->catfile( '', '' ) ); -} - - - -sub remove_dir_sep { - my $path = shift; - $path =~ s/[$dir_sep_chars]$//; - - return $path; } - sub warn { return CORE::warn( _my_program(), ': ', @_, "\n" ); } @@ -2342,24 +2597,15 @@ sub _my_program { } - -sub filetypes_supported { - return keys %mappings; -} - -sub _get_thpppt { +sub thpppt { my $y = q{_ /|,\\'!.x',=(www)=, U }; $y =~ tr/,x!w/\nOo_/; - return $y; -} -sub _thpppt { - my $y = _get_thpppt(); App::Ack::print( "$y ack $_[0]!\n" ); exit 0; } -sub _bar { +sub ackbar { my $x; $x = <<'_BAR'; 6?!I'7!I"?%+! @@ -2402,10 +2648,10 @@ sub _bar { 77I!+!7!?!7!I"71+!7, _BAR - App::Ack::__pic($x); + return _pic_decode($x); } -sub _cathy { +sub cathy { my $x = <<'CATHY'; 0+!--+! 0|! "C!H!O!C!O!L!A!T!E!!! !|! @@ -2458,10 +2704,10 @@ sub _cathy { 0?!$! &N! )." .,! %."M! ":!M!.! 0 0N!:! %?!O! #.! ..! &,! &.!D!,! "N!I! 0 CATHY - App::Ack::__pic($x); + return _pic_decode($x); } -sub __pic { +sub _pic_decode { my($compressed) = @_; $compressed =~ s/(.)(.)/$1x(ord($2)-32)/eg; App::Ack::print( $compressed ); @@ -2470,10 +2716,6 @@ sub __pic { sub show_help { - my $help_arg = shift || 0; - - return show_help_types() if $help_arg =~ /^types?/; - App::Ack::print( <<"END_OF_HELP" ); Usage: ack [OPTION]... PATTERN [FILES OR DIRECTORIES] @@ -2483,37 +2725,47 @@ only those files and directories are checked. ack may also search STDIN, but only if no file or directory arguments are specified, or if one of them is "-". -Default switches may be specified in ACK_OPTIONS environment variable or -an .ackrc file. If you want no dependency on the environment, turn it -off with --noenv. +Default switches may be specified in an .ackrc file. If you want no dependency +on the environment, turn it off with --noenv. + +File select actions: + -f Only print the files selected, without + searching. The PATTERN must not be specified. + -g Same as -f, but only select files matching + PATTERN. -Example: ack -i select +File listing actions: + -l, --files-with-matches Print filenames with at least one match + -L, --files-without-matches Print filenames with no matches + -c, --count Print filenames and count of matching lines Searching: -i, --ignore-case Ignore case distinctions in PATTERN - --[no]smart-case Ignore case distinctions in PATTERN, + -S, --[no]smart-case Ignore case distinctions in PATTERN, only if PATTERN contains no upper case. - Ignored if -i is specified + Ignored if -i or -I are specified. + -I, --no-ignore-case Turns on case-sensitivity in PATTERN. + Negates -i and --smart-case. -v, --invert-match Invert match: select non-matching lines -w, --word-regexp Force PATTERN to match only whole words -Q, --literal Quote all metacharacters; PATTERN is literal + --range-start PATTERN Specify PATTERN as the start of a match range. + --range-end PATTERN Specify PATTERN as the end of a match range. + --match PATTERN Specify PATTERN explicitly. Typically omitted. + --not PATTERN Specifies PATTERN that must not be found on + the line for a match to occur. Repeatable. Search output: - --lines=NUM Only print line(s) NUM of each file - -l, --files-with-matches Only print filenames containing matches - -L, --files-without-matches Only print filenames with no matches --output=expr Output the evaluation of expr for each line (turns off text highlighting) -o Show only the part of a line matching PATTERN Same as --output='\$&' --passthru Print all lines, whether matching or not - --match PATTERN Specify PATTERN explicitly. -m, --max-count=NUM Stop searching in each file after NUM matches -1 Stop searching after one match of any kind -H, --with-filename Print the filename for each match (default: on unless explicitly searching a single file) -h, --no-filename Suppress the prefixing filename on output - -c, --count Show number of lines matching per file --[no]column Show the column number of the first match -A NUM, --after-context=NUM Print NUM lines of trailing context after @@ -2542,23 +2794,25 @@ File presentation: files. (default: on when used interactively) --group Same as --heading --break --nogroup Same as --noheading --nobreak - --[no]color Highlight the matching text (default: on unless + -p, --proximate=LINES Separate match output with blank lines unless + they are within LINES lines from each other. + -P, --proximate=0 Negates --proximate. + --[no]underline Print a line of carets under the matched text. + --[no]color, --[no]colour Highlight the matching text (default: on unless output is redirected, or on Windows) - --[no]colour Same as --[no]color --color-filename=COLOR --color-match=COLOR - --color-lineno=COLOR Set the color for filenames, matches, and line - numbers. + --color-colno=COLOR + --color-lineno=COLOR Set the color for filenames, matches, line and + column numbers. + --help-colors Show a list of possible color combinations. + --help-rgb-colors Show a list of advanced RGB colors. --flush Flush output immediately, even when ack is used non-interactively (when output goes to a pipe or file). File finding: - -f Only print the files selected, without - searching. The PATTERN must not be specified. - -g Same as -f, but only select files matching - PATTERN. --sort-files Sort the found files lexically. --show-types Show which types each file has. --files-from=FILE Read the list of files to search from FILE. @@ -2567,51 +2821,63 @@ File finding: File inclusion/exclusion: --[no]ignore-dir=name Add/remove directory from list of ignored dirs --[no]ignore-directory=name Synonym for ignore-dir - --ignore-file=filter Add filter for ignoring files + --ignore-file=FILTER:ARGS Add filter for ignoring files. -r, -R, --recurse Recurse into subdirectories (default: on) -n, --no-recurse No descending into subdirectories --[no]follow Follow symlinks. Default is off. - -k, --known-types Include only files of types that ack recognizes. - --type=X Include only X files, where X is a recognized - filetype. - --type=noX Exclude X files. - See "ack --help-types" for supported filetypes. +File type inclusion/exclusion: + -t X, --type=X Include only X files, where X is a filetype, + e.g. python, html, markdown, etc + -T X, --type=noX Exclude X files, where X is a filetype. + -k, --known-types Include only files of types that ack recognizes. + --help-types Display all known types, and how they're defined. File type specification: - --type-set TYPE:FILTER:FILTERARGS - Files with the given FILTERARGS applied to the - given FILTER are recognized as being of type - TYPE. This replaces an existing definition for - type TYPE. - --type-add TYPE:FILTER:FILTERARGS - Files with the given FILTERARGS applied to the - given FILTER are recognized as being type TYPE. - --type-del TYPE Removes all filters associated with TYPE. - + --type-set=TYPE:FILTER:ARGS Files with the given ARGS applied to the given + FILTER are recognized as being of type TYPE. + This replaces an existing definition for TYPE. + --type-add=TYPE:FILTER:ARGS Files with the given ARGS applied to the given + FILTER are recognized as being type TYPE. + --type-del=TYPE Removes all filters associated with TYPE. Miscellaneous: + --version Display version & copyright --[no]env Ignore environment variables and global ackrc files. --env is legal but redundant. --ackrc=filename Specify an ackrc file to use --ignore-ack-defaults Ignore default definitions included with ack. --create-ackrc Outputs a default ackrc for your customization to standard output. - --help, -? This help - --help-types Display all known types --dump Dump information on which options are loaded - from which RC files + and where they're defined. --[no]filter Force ack to treat standard input as a pipe (--filter) or tty (--nofilter) - --man Man page - --version Display version & copyright + --help This help + --man Print the manual. + --help-types Display all known types, and how they're defined. + --help-colors Show a list of possible color combinations. + --help-rgb-colors Show a list of advanced RGB colors. --thpppt Bill the Cat --bar The warning admiral --cathy Chocolate! Chocolate! Chocolate! +Filter specifications: + If FILTER is "ext", ARGS is a list of extensions checked against the + file's extension. + If FILTER is "is", ARGS must match the file's name exactly. + If FILTER is "match", ARGS is matched as a case-insensitive regex + against the filename. + If FILTER is "firstlinematch", ARGS is matched as a regex the first + line of the file's contents. + Exit status is 0 if match, 1 if no match. -This is version $VERSION of ack. +ack's home page is at https://beyondgrep.com/ + +The full ack manual is available by running "ack --man". + +This is version $App::Ack::VERSION of ack. Run "ack --version" for full version info. END_OF_HELP return; @@ -2623,16 +2889,16 @@ sub show_help_types { App::Ack::print( <<'END_OF_HELP' ); Usage: ack [OPTION]... PATTERN [FILES OR DIRECTORIES] -The following is the list of filetypes supported by ack. You can -specify a file type with the --type=TYPE format, or the --TYPE -format. For example, both --type=perl and --perl work. +The following is the list of filetypes supported by ack. You can specify a +filetype to include with -t TYPE or --type=TYPE. You can exclude a +filetype with -T TYPE or --type=noTYPE. -Note that some extensions may appear in multiple types. For example, -.pod files are both Perl and Parrot. +Note that some files may appear in multiple types. For example, a file +called Rakefile is both Ruby (--type=ruby) and Rakefile (--type=rakefile). END_OF_HELP - my @types = filetypes_supported(); + my @types = keys %App::Ack::mappings; my $maxlen = 0; for ( @types ) { $maxlen = length if $maxlen < length; @@ -2644,104 +2910,186 @@ END_OF_HELP if ( ref $ext_list ) { $ext_list = join( '; ', map { $_->to_string } @{$ext_list} ); } - App::Ack::print( sprintf( " --[no]%-*.*s %s\n", $maxlen, $maxlen, $type, $ext_list ) ); + App::Ack::print( sprintf( " %-*.*s %s\n", $maxlen, $maxlen, $type, $ext_list ) ); } return; } -sub show_man { - require Pod::Usage; - Pod::Usage::pod2usage({ - -input => $App::Ack::orig_program_name, - -verbose => 2, - -exitval => 0, - }); + +sub show_help_colors { + App::Ack::print( <<'END_OF_HELP' ); +ack allows customization of the colors it uses when presenting matches +onscreen. See the "ACK COLORS" section of the ack manual (ack --man). + +Here is a chart of how various color combinations appear: Each of the eight +foreground colors, on each of the eight background colors or no background +color, with and without the bold modifier. + +Run ack --help-rgb-colors for a chart of the RGB colors. + +END_OF_HELP + + _show_color_grid(); return; } -sub get_version_statement { - require Config; - my $copyright = get_copyright(); - my $this_perl = $Config::Config{perlpath}; - if ($^O ne 'VMS') { - my $ext = $Config::Config{_exe}; - $this_perl .= $ext unless $this_perl =~ m/$ext$/i; - } - my $ver = sprintf( '%vd', $^V ); +sub show_help_rgb { + App::Ack::print( <<'END_OF_HELP' ); +ack allows customization of the colors it uses when presenting matches +onscreen. See the "ACK COLORS" section of the ack manual (ack --man). - return <<"END_OF_VERSION"; -ack ${VERSION} -Running under Perl $ver at $this_perl +Colors may be specified as "rgbNNN" where "NNN" is a triplet of digits +from 0 to 5 specifying the intensity of red, green and blue, respectively. -$copyright +Here is a grid of the 216 possible values for NNN. -This program is free software. You may modify or distribute it -under the terms of the Artistic License v2.0. -END_OF_VERSION +END_OF_HELP + + _show_rgb_grid(); + + App::Ack::say( 'Here are the 216 possible colors with the "reverse" modifier applied.', "\n" ); + + _show_rgb_grid( 'reverse' ); + + return; } -sub print_version_statement { - App::Ack::print( get_version_statement() ); +sub _show_color_grid { + my $cell_width = 7; + + my @fg_colors = qw( black red green yellow blue magenta cyan white ); + my @bg_colors = map { "on_$_" } @fg_colors; + + App::Ack::say( + _color_cell( '' ), + map { _color_cell( $_ ) } @fg_colors + ); + + App::Ack::say( + _color_cell( '' ), + map { _color_cell( '-' x $cell_width ) } @fg_colors + ); + + for my $bg ( '', @bg_colors ) { + App::Ack::say( + _color_cell( '' ), + ( map { _color_cell( $_, "$_ $bg" ) } @fg_colors ), + $bg + ); + + App::Ack::say( + _color_cell( 'bold' ), + ( map { _color_cell( $_, "bold $_ $bg" ) } @fg_colors ), + $bg + ); + App::Ack::say(); + } return; } -sub get_copyright { - return $COPYRIGHT; +sub _color_cell { + my $text = shift; + my $color = shift; + + my $cell_width = 7; + $text = sprintf( '%-*s', $cell_width, $text ); + + return ($color ? Term::ANSIColor::colored( $text, $color ) : $text) . ' '; } -# print subs added in order to make it easy for a third party -# module (such as App::Wack) to redefine the display methods -# and show the results in a different way. -sub print { print {$fh} @_; return; } -sub print_first_filename { App::Ack::print( $_[0], "\n" ); return; } -sub print_blank_line { App::Ack::print( "\n" ); return; } -sub print_separator { App::Ack::print( "--\n" ); return; } -sub print_filename { App::Ack::print( $_[0], $_[1] ); return; } -sub print_line_no { App::Ack::print( $_[0], $_[1] ); return; } -sub print_column_no { App::Ack::print( $_[0], $_[1] ); return; } -sub print_count { - my $filename = shift; - my $nmatches = shift; - my $ors = shift; - my $count = shift; - my $show_filename = shift; +sub _show_rgb_grid { + my $modifier = shift // ''; - if ($show_filename) { - App::Ack::print( $filename ); - App::Ack::print( ':', $nmatches ) if $count; - } - else { - App::Ack::print( $nmatches ) if $count; - } - App::Ack::print( $ors ); + my $grid = <<'HERE'; +544 544 544 544 544 554 554 554 554 554 454 454 454 454 454 455 455 455 455 455 445 445 445 445 445 545 545 545 545 545 +533 533 533 543 543 553 553 553 453 453 353 353 353 354 354 355 355 355 345 345 335 335 335 435 435 535 535 535 534 534 +511 521 531 531 541 551 451 451 351 251 151 152 152 153 154 155 145 145 135 125 115 215 215 315 415 515 514 514 513 512 +500 510 520 530 540 550 450 350 250 150 050 051 052 053 054 055 045 035 025 015 005 105 205 305 405 505 504 503 502 501 +400 410 410 420 430 440 340 340 240 140 040 041 041 042 043 044 034 034 024 014 004 104 104 204 304 404 403 403 402 401 +300 300 310 320 320 330 330 230 130 130 030 030 031 032 032 033 033 023 013 013 003 003 103 203 203 303 303 302 301 301 +200 200 200 210 210 220 220 220 120 120 020 020 020 021 021 022 022 022 012 012 002 002 002 102 102 202 202 202 201 201 +100 100 100 100 100 110 110 110 110 110 010 010 010 010 010 011 011 011 011 011 001 001 001 001 001 101 101 101 101 101 + +522 522 532 542 542 552 552 452 352 352 252 252 253 254 254 255 255 245 235 235 225 225 325 425 425 525 525 524 523 523 + +411 411 421 431 431 441 441 341 241 241 141 141 142 143 143 144 144 134 124 124 114 114 214 314 314 414 414 413 412 412 + +422 422 432 432 432 442 442 442 342 342 242 242 242 243 243 244 244 244 234 234 224 224 224 324 324 424 424 424 423 423 + +311 311 311 321 321 331 331 331 231 231 131 131 131 132 132 133 133 133 123 123 113 113 113 213 213 313 313 313 312 312 + +433 433 433 433 433 443 443 443 443 443 343 343 343 343 343 344 344 344 344 344 334 334 334 334 334 434 434 434 434 434 +211 211 211 211 211 221 221 221 221 221 121 121 121 121 121 122 122 122 122 122 112 112 112 112 112 212 212 212 212 212 + +322 322 322 322 322 332 332 332 332 332 232 232 232 232 232 233 233 233 233 233 223 223 223 223 223 323 323 323 323 323 + +555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 555 +444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 444 +333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 333 +222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 222 +111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 111 +000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 +HERE + + $grid =~ s/(\d\d\d)/Term::ANSIColor::colored( "$1", "$modifier rgb$1" )/eg; + + App::Ack::say( $grid ); return; } -sub print_count0 { - my $filename = shift; - my $ors = shift; - my $show_filename = shift; - if ($show_filename) { - App::Ack::print( $filename, ':0', $ors ); - } - else { - App::Ack::print( '0', $ors ); - } +sub show_man { + require Pod::Usage; + Pod::Usage::pod2usage({ + -input => $App::Ack::ORIGINAL_PROGRAM_NAME, + -verbose => 2, + -exitval => 0, + }); return; } + + +sub get_version_statement { + require Config; + + my $copyright = $App::Ack::COPYRIGHT; + my $this_perl = $Config::Config{perlpath}; + if ($^O ne 'VMS') { + my $ext = $Config::Config{_exe}; + $this_perl .= $ext unless $this_perl =~ m/$ext$/i; + } + my $perl_ver = sprintf( 'v%vd', $^V ); + + my $build_type = $App::Ack::STANDALONE ? 'standalone version' : 'standard build'; + + return <<"END_OF_VERSION"; +ack $App::Ack::VERSION ($build_type) +Running under Perl $perl_ver at $this_perl + +$copyright + +This program is free software. You may modify or distribute it +under the terms of the Artistic License v2.0. +END_OF_VERSION +} + + +sub print { print {$fh} @_; return; } +sub say { print {$fh} @_, $ors; return; } +sub print_blank_line { print {$fh} "\n"; return; } + sub set_up_pager { my $command = shift; @@ -2770,365 +3118,186 @@ sub exit_from_ack { } +sub show_types { + my $file = shift; -1; # End of App::Ack -package App::Ack::Resource; - - -use warnings; -use strict; -use overload - '""' => 'name'; + my @types = filetypes( $file ); + my $arrow = @types ? ' => ' : ' =>'; + App::Ack::say( $file->name, $arrow, join( ',', @types ) ); -sub FAIL { - require Carp; - Carp::confess( 'Must be overloaded' ); + return; } -sub new { - return FAIL(); -} +sub filetypes { + my ( $file ) = @_; + my @matches; -sub name { - return FAIL(); -} + foreach my $k (keys %App::Ack::mappings) { + my $filters = $App::Ack::mappings{$k}; + foreach my $filter (@{$filters}) { + # Clone the file. + my $clone = $file->clone; + if ( $filter->filter($clone) ) { + push @matches, $k; + last; + } + } + } -sub is_binary { - return FAIL(); + # https://metacpan.org/pod/distribution/Perl-Critic/lib/Perl/Critic/Policy/Subroutines/ProhibitReturnSort.pm + @matches = sort @matches; + return @matches; } -sub open { - return FAIL(); -} - +sub is_lowercase { + my $pat = shift; + + # The simplest case. + return 1 if lc($pat) eq $pat; + + # If we have capitals, then go clean up any metacharacters that might have capitals. + + # Get rid of any literal backslashes first to avoid confusion. + $pat =~ s/\\\\//g; + + my $metacharacter = qr{ + |\\A # Beginning of string + |\\B # Not word boundary + |\\c[a-zA-Z] # Control characters + |\\D # Non-digit character + |\\G # End-of-match position of prior match + |\\H # Not horizontal whitespace + |\\K # Keep to the left + |\\N(\{.+?\})? # Anything but \n, OR Unicode sequence + |\\[pP]\{.+?\} # Named property and negation + |\\[pP][A-Z] # Named property and negation, single-character shorthand + |\\R # Linebreak + |\\S # Non-space character + |\\V # Not vertical whitespace + |\\W # Non-word character + |\\X # ??? + |\\x[0-9A-Fa-f]{2} # Hex sequence + |\\Z # End of string + }x; + $pat =~ s/$metacharacter//g; + + my $name = qr/[_A-Za-z][_A-Za-z0-9]*?/; + # Eliminate named captures. + $pat =~ s/\(\?'$name'//g; + $pat =~ s/\(\?<$name>//g; + + # Eliminate named backreferences. + $pat =~ s/\\k'$name'//g; + $pat =~ s/\\k<$name>//g; + $pat =~ s/\\k\{$name\}//g; + + # Now with those metacharacters and named things removed, now see if it's lowercase. + return 1 if lc($pat) eq $pat; -sub needs_line_scan { - return FAIL(); + return 0; } -sub reset { - return FAIL(); -} +1; # End of App::Ack +package App::Ack::ConfigDefault; +use warnings; +use strict; -sub close { - return FAIL(); -} -sub clone { - return FAIL(); + +sub options { + return split( /\n/, _options_block() ); } -sub firstliney { - return FAIL(); +sub options_clean { + return grep { /./ && !/^#/ } options(); } -1; -package App::Ack::Resources; +sub _options_block { + my $lines = <<'HERE'; +# This is the default ackrc for ack version ==VERSION==. +# There are four different ways to match +# +# is: Match the filename exactly +# +# ext: Match the extension of the filename exactly +# +# match: Match the filename against a Perl regular expression +# +# firstlinematch: Match the first 250 characters of the first line +# of text against a Perl regular expression. This is only for +# the --type-add option. -use warnings; -use strict; - -sub from_argv { - my $class = shift; - my $opt = shift; - my $start = shift; - - my $self = bless {}, $class; - - my $file_filter = undef; - my $descend_filter = $opt->{descend_filter}; - - if( $opt->{n} ) { - $descend_filter = sub { - return 0; - }; - } - - $self->{iter} = - File::Next::files( { - file_filter => $opt->{file_filter}, - descend_filter => $descend_filter, - error_handler => sub { my $msg = shift; App::Ack::warn( $msg ) }, - sort_files => $opt->{sort_files}, - follow_symlinks => $opt->{follow}, - }, @{$start} ); - - return $self; -} - - -sub from_file { - my $class = shift; - my $opt = shift; - my $file = shift; - - my $iter = - File::Next::from_file( { - error_handler => sub { my $msg = shift; App::Ack::warn( $msg ) }, - warning_handler => sub { my $msg = shift; App::Ack::warn( $msg ) }, - sort_files => $opt->{sort_files}, - }, $file ) or return undef; - - return bless { - iter => $iter, - }, $class; -} - -# This is for reading input lines from STDIN, not the list of files from STDIN -sub from_stdin { - my $class = shift; - my $opt = shift; - - my $self = bless {}, $class; - - my $has_been_called = 0; - - $self->{iter} = sub { - if ( !$has_been_called ) { - $has_been_called = 1; - return '-'; - } - return; - }; - - return $self; -} - -sub next { - my $self = shift; - - my $file = $self->{iter}->() or return; - - return App::Ack::Resource::Basic->new( $file ); -} - -1; -package App::Ack::Resource::Basic; - - -use warnings; -use strict; - -use Fcntl (); - -BEGIN { - our @ISA = 'App::Ack::Resource'; -} - - -sub new { - my $class = shift; - my $filename = shift; - - my $self = bless { - filename => $filename, - fh => undef, - opened => 0, - }, $class; - - if ( $self->{filename} eq '-' ) { - $self->{fh} = *STDIN; - $self->{opened} = 1; - } - - return $self; -} - - -sub name { - return $_[0]->{filename}; -} - - - -sub needs_line_scan { - my $self = shift; - my $opt = shift; - - return 1 if $opt->{v}; - - my $size = -s $self->{fh}; - if ( $size == 0 ) { - return 0; - } - elsif ( $size > 100_000 ) { - return 1; - } - - my $buffer; - my $rc = sysread( $self->{fh}, $buffer, $size ); - if ( !defined($rc) && $App::Ack::report_bad_filenames ) { - App::Ack::warn( "$self->{filename}: $!" ); - return 1; - } - return 0 unless $rc && ( $rc == $size ); - - my $regex = $opt->{regex}; - return $buffer =~ /$regex/m; -} - - -sub reset { - my $self = shift; - - # return if we haven't opened the file yet - if ( !defined($self->{fh}) ) { - return; - } - - if( !seek( $self->{fh}, 0, 0 ) && $App::Ack::report_bad_filenames ) { - App::Ack::warn( "$self->{filename}: $!" ); - } - - return; -} - - -sub close { - my $self = shift; - - # return if we haven't opened the file yet - if ( !defined($self->{fh}) ) { - return; - } - - if ( !close($self->{fh}) && $App::Ack::report_bad_filenames ) { - App::Ack::warn( $self->name() . ": $!" ); - } - - $self->{opened} = 0; - - return; -} - - -sub clone { - my ( $self ) = @_; - - return __PACKAGE__->new($self->name); -} - -sub firstliney { - my ( $self ) = @_; - - my $fh = $self->open(); - - unless(exists $self->{firstliney}) { - my $buffer = ''; - my $rc = sysread( $fh, $buffer, 250 ); - unless($rc) { # XXX handle this better? - $buffer = ''; - } - $buffer =~ s/[\r\n].*//s; - $self->{firstliney} = $buffer; - $self->reset; - } - - $self->close; - - return $self->{firstliney}; -} - -sub open { - my ( $self ) = @_; - - return $self->{fh} if $self->{opened}; - - unless ( open $self->{fh}, '<', $self->{filename} ) { - return; - } - - $self->{opened} = 1; - - return $self->{fh}; -} - -1; -package App::Ack::ConfigDefault; - -use warnings; -use strict; - -sub options { - my @options = split( /\n/, _options_block() ); - @options = grep { /./ && !/^#/ } @options; - - return @options; -} - -sub _options_block { - return <<'HERE'; -# This is the default ackrc for ack 2.0 - -# There are four different ways to match -# -# is: Match the filename exactly -# -# ext: Match the extension of the filename exactly -# -# match: Match the filename against a Perl regular expression -# -# firstlinematch: Match the first 250 characters of the first line -# of text against a Perl regular expression. This is only for -# the --type-add option. - - -### Directories to ignore +### Directories to ignore # Bazaar +# https://bazaar.canonical.com/ --ignore-directory=is:.bzr # Codeville +# http://freshmeat.sourceforge.net/projects/codeville --ignore-directory=is:.cdv -# Interface Builder +# Interface Builder (Xcode) +# https://en.wikipedia.org/wiki/Interface_Builder --ignore-directory=is:~.dep --ignore-directory=is:~.dot --ignore-directory=is:~.nib --ignore-directory=is:~.plst # Git +# https://git-scm.com/ --ignore-directory=is:.git +# When submodules are used, .git is a file. +--ignore-file=is:.git # Mercurial +# https://www.mercurial-scm.org/ --ignore-directory=is:.hg -# quilt +# Quilt +# https://directory.fsf.org/wiki/Quilt --ignore-directory=is:.pc # Subversion +# https://subversion.apache.org/ --ignore-directory=is:.svn # Monotone +# https://www.monotone.ca/ --ignore-directory=is:_MTN # CVS +# https://savannah.nongnu.org/projects/cvs --ignore-directory=is:CVS # RCS +# https://www.gnu.org/software/rcs/ --ignore-directory=is:RCS # SCCS +# https://en.wikipedia.org/wiki/Source_Code_Control_System --ignore-directory=is:SCCS # darcs +# http://darcs.net/ --ignore-directory=is:_darcs # Vault/Fortress --ignore-directory=is:_sgbak # autoconf +# https://www.gnu.org/software/autoconf/ --ignore-directory=is:autom4te.cache # Perl module building @@ -3136,17 +3305,33 @@ sub _options_block { --ignore-directory=is:_build # Perl Devel::Cover module's output directory +# https://metacpan.org/release/Devel-Cover --ignore-directory=is:cover_db # Node modules created by npm --ignore-directory=is:node_modules # CMake cache +# https://www.cmake.org/ --ignore-directory=is:CMakeFiles # Eclipse workspace folder +# https://eclipse.org/ --ignore-directory=is:.metadata +# Cabal (Haskell) sandboxes +# https://www.haskell.org/cabal/users-guide/installing-packages.html +--ignore-directory=is:.cabal-sandbox + +# Python caches +# https://docs.python.org/3/tutorial/modules.html +--ignore-directory=is:__pycache__ +--ignore-directory=is:.pytest_cache + +# macOS Finder remnants +--ignore-directory=is:__MACOSX +--ignore-file=is:.DS_Store + ### Files to ignore # Backup files @@ -3156,13 +3341,13 @@ sub _options_block { # Emacs swap files --ignore-file=match:/^#.+#$/ -# vi/vim swap files ---ignore-file=match:/[._].*\.swp$/ +# vi/vim swap files https://www.vim.org/ +--ignore-file=match:/[._].*[.]swp$/ # core dumps ---ignore-file=match:/core\.\d+$/ +--ignore-file=match:/core[.]\d+$/ -# minified Javascript +# minified JavaScript --ignore-file=match:/[.-]min[.]js$/ --ignore-file=match:/[.]js[.]min$/ @@ -3170,59 +3355,97 @@ sub _options_block { --ignore-file=match:/[.]min[.]css$/ --ignore-file=match:/[.]css[.]min$/ +# JS and CSS source maps +--ignore-file=match:/[.]js[.]map$/ +--ignore-file=match:/[.]css[.]map$/ + # PDFs, because they pass Perl's -T detection --ignore-file=ext:pdf # Common graphics, just as an optimization --ignore-file=ext:gif,jpg,jpeg,png +# Common archives, as an optimization +--ignore-file=ext:gz,tar,tgz,zip -### Filetypes defined +# Python compiled modules +--ignore-file=ext:pyc,pyd,pyo -# Perl http://perl.org/ ---type-add=perl:ext:pl,pm,pod,t,psgi ---type-add=perl:firstlinematch:/^#!.*\bperl/ +# Python's pickle serialization format +# https://docs.python.org/2/library/pickle.html#example +# https://docs.python.org/3.7/library/pickle.html#examples +--ignore-file=ext:pkl,pickle -# Perl tests ---type-add=perltest:ext:t +# C extensions +--ignore-file=ext:so + +# Compiled gettext files +--ignore-file=ext:mo + +# Visual Studio user and workspace settings +# https://code.visualstudio.com/docs/getstarted/settings +--ignore-dir=is:.vscode -# Makefiles http://www.gnu.org/s/make/ +### Filetypes defined + +# Makefiles +# https://www.gnu.org/s/make/ --type-add=make:ext:mk --type-add=make:ext:mak --type-add=make:is:makefile --type-add=make:is:Makefile +--type-add=make:is:Makefile.Debug +--type-add=make:is:Makefile.Release --type-add=make:is:GNUmakefile -# Rakefiles http://rake.rubyforge.org/ +# Rakefiles +# https://rake.rubyforge.org/ --type-add=rake:is:Rakefile -# CMake http://www.cmake.org/ +# CMake +# https://cmake.org/ --type-add=cmake:is:CMakeLists.txt --type-add=cmake:ext:cmake +# Bazel build tool +# https://docs.bazel.build/versions/master/skylark/bzl-style.html +--type-add=bazel:ext:bzl +# https://docs.bazel.build/versions/master/guide.html#bazelrc-the-bazel-configuration-file +--type-add=bazel:ext:bazelrc +# https://docs.bazel.build/versions/master/build-ref.html#BUILD_files +--type-add=bazel:is:BUILD +# https://docs.bazel.build/versions/master/build-ref.html#workspace +--type-add=bazel:is:WORKSPACE + + # Actionscript --type-add=actionscript:ext:as,mxml -# Ada http://www.adaic.org/ +# Ada +# https://www.adaic.org/ --type-add=ada:ext:ada,adb,ads -# ASP http://msdn.microsoft.com/en-us/library/aa286483.aspx +# ASP +# https://docs.microsoft.com/en-us/previous-versions/office/developer/server-technologies/aa286483(v=msdn.10) --type-add=asp:ext:asp -# ASP.Net http://www.asp.net/ +# ASP.Net +# https://dotnet.microsoft.com/apps/aspnet --type-add=aspx:ext:master,ascx,asmx,aspx,svc # Assembly --type-add=asm:ext:asm,s -# Batch +# DOS/Windows batch --type-add=batch:ext:bat,cmd -# ColdFusion http://en.wikipedia.org/wiki/ColdFusion +# ColdFusion +# https://en.wikipedia.org/wiki/ColdFusion --type-add=cfmx:ext:cfc,cfm,cfml -# Clojure http://clojure.org/ ---type-add=clojure:ext:clj +# Clojure +# https://clojure.org/ +--type-add=clojure:ext:clj,cljs,edn,cljc # C # .xs are Perl C files @@ -3231,134 +3454,244 @@ sub _options_block { # C header files --type-add=hh:ext:h -# CoffeeScript http://coffeescript.org/ +# CoffeeScript +# https://coffeescript.org/ --type-add=coffeescript:ext:coffee # C++ --type-add=cpp:ext:cpp,cc,cxx,m,hpp,hh,h,hxx +# C++ header files +--type-add=hpp:ext:hpp,hh,h,hxx + # C# --type-add=csharp:ext:cs -# CSS http://www.w3.org/Style/CSS/ +# Crystal-lang +# https://crystal-lang.org/ +--type-add=crystal:ext:cr,ecr + +# CSS +# https://www.w3.org/Style/CSS/ --type-add=css:ext:css -# Dart http://www.dartlang.org/ +# Dart +# https://dart.dev/ --type-add=dart:ext:dart -# Delphi http://en.wikipedia.org/wiki/Embarcadero_Delphi +# Delphi +# https://en.wikipedia.org/wiki/Embarcadero_Delphi --type-add=delphi:ext:pas,int,dfm,nfm,dof,dpk,dproj,groupproj,bdsgroup,bdsproj -# Elixir http://elixir-lang.org/ +# Elixir +# https://elixir-lang.org/ --type-add=elixir:ext:ex,exs -# Emacs Lisp http://www.gnu.org/software/emacs +# Elm +# https://elm-lang.org +--type-add=elm:ext:elm + +# Emacs Lisp +# https://www.gnu.org/software/emacs --type-add=elisp:ext:el -# Erlang http://www.erlang.org/ +# Erlang +# https://www.erlang.org/ --type-add=erlang:ext:erl,hrl -# Fortran http://en.wikipedia.org/wiki/Fortran +# Fortran +# https://en.wikipedia.org/wiki/Fortran --type-add=fortran:ext:f,f77,f90,f95,f03,for,ftn,fpp -# Google Go http://golang.org/ +# Go +# https://golang.org/ --type-add=go:ext:go -# Groovy http://groovy.codehaus.org/ +# Groovy +# https://www.groovy-lang.org/ --type-add=groovy:ext:groovy,gtmpl,gpp,grunit,gradle -# Haskell http://www.haskell.org/ +# GSP +# https://gsp.grails.org/ +--type-add=gsp:ext:gsp + +# Haskell +# https://www.haskell.org/ --type-add=haskell:ext:hs,lhs # HTML ---type-add=html:ext:htm,html +--type-add=html:ext:htm,html,xhtml -# Java http://www.oracle.com/technetwork/java/index.html +# Jade +# http://jade-lang.com/ +--type-add=jade:ext:jade + +# Java +# https://www.oracle.com/technetwork/java/index.html --type-add=java:ext:java,properties # JavaScript --type-add=js:ext:js -# JSP http://www.oracle.com/technetwork/java/javaee/jsp/index.html ---type-add=jsp:ext:jsp,jspx,jhtm,jhtml +# JSP +# https://www.oracle.com/technetwork/java/javaee/jsp/index.html +--type-add=jsp:ext:jsp,jspx,jspf,jhtm,jhtml -# JSON http://www.json.org/ +# JSON +# https://json.org/ --type-add=json:ext:json -# Less http://www.lesscss.org/ +# Kotlin +# https://kotlinlang.org/ +--type-add=kotlin:ext:kt,kts + +# Less +# http://www.lesscss.org/ --type-add=less:ext:less -# Common Lisp http://common-lisp.net/ +# Common Lisp +# https://common-lisp.net/ --type-add=lisp:ext:lisp,lsp -# Lua http://www.lua.org/ +# Lua +# https://www.lua.org/ --type-add=lua:ext:lua --type-add=lua:firstlinematch:/^#!.*\blua(jit)?/ +# Markdown +# https://en.wikipedia.org/wiki/Markdown +--type-add=markdown:ext:md,markdown +# We understand that there are many ad hoc extensions for markdown +# that people use. .md and .markdown are the two that ack recognizes. +# You are free to add your own in your ackrc file. + +# Matlab +# https://en.wikipedia.org/wiki/MATLAB +--type-add=matlab:ext:m + # Objective-C --type-add=objc:ext:m,h # Objective-C++ --type-add=objcpp:ext:mm,h -# OCaml http://caml.inria.fr/ ---type-add=ocaml:ext:ml,mli +# OCaml +# https://ocaml.org/ +--type-add=ocaml:ext:ml,mli,mll,mly -# Matlab http://en.wikipedia.org/wiki/MATLAB ---type-add=matlab:ext:m +# Perl +# https://perl.org/ +--type-add=perl:ext:pl,pm,pod,t,psgi +--type-add=perl:firstlinematch:/^#!.*\bperl/ -# Parrot http://www.parrot.org/ ---type-add=parrot:ext:pir,pasm,pmc,ops,pod,pg,tg +# Perl tests +--type-add=perltest:ext:t -# PHP http://www.php.net/ +# Perl's Plain Old Documentation format, POD +--type-add=pod:ext:pod + +# PHP +# https://www.php.net/ --type-add=php:ext:php,phpt,php3,php4,php5,phtml --type-add=php:firstlinematch:/^#!.*\bphp/ -# Plone http://plone.org/ +# Plone +# https://plone.org/ --type-add=plone:ext:pt,cpt,metadata,cpy,py -# Python http://www.python.org/ +# PowerShell +# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scripts +# https://learn.microsoft.com/en-us/powershell/scripting/developer/module/understanding-a-windows-powershell-module +--type-add=powershell:ext:ps1,psm1 + +# PureScript +# https://www.purescript.org +--type-add=purescript:ext:purs + +# Python +# https://www.python.org/ --type-add=python:ext:py --type-add=python:firstlinematch:/^#!.*\bpython/ -# R http://www.r-project.org/ ---type-add=rr:ext:R +# R +# https://www.r-project.org/ +# https://r4ds.had.co.nz/r-markdown.html +--type-add=rr:ext:R,Rmd + +# reStructured Text +# https://docutils.sourceforge.io/rst.html +--type-add=rst:ext:rst -# Ruby http://www.ruby-lang.org/ +# Ruby +# https://www.ruby-lang.org/ --type-add=ruby:ext:rb,rhtml,rjs,rxml,erb,rake,spec --type-add=ruby:is:Rakefile --type-add=ruby:firstlinematch:/^#!.*\bruby/ -# Rust http://www.rust-lang.org/ +# Rust +# https://www.rust-lang.org/ --type-add=rust:ext:rs -# Sass http://sass-lang.com +# Sass +# https://sass-lang.com --type-add=sass:ext:sass,scss -# Scala http://www.scala-lang.org/ ---type-add=scala:ext:scala +# Scala +# https://www.scala-lang.org/ +--type-add=scala:ext:scala,sbt -# Scheme http://groups.csail.mit.edu/mac/projects/scheme/ +# Scheme +# https://groups.csail.mit.edu/mac/projects/scheme/ --type-add=scheme:ext:scm,ss # Shell --type-add=shell:ext:sh,bash,csh,tcsh,ksh,zsh,fish --type-add=shell:firstlinematch:/^#!.*\b(?:ba|t?c|k|z|fi)?sh\b/ -# Smalltalk http://www.smalltalk.org/ +# Smalltalk +# http://www.smalltalk.org/ --type-add=smalltalk:ext:st -# SQL http://www.iso.org/iso/catalogue_detail.htm?csnumber=45498 +# Smarty +# https://www.smarty.net/ +--type-add=smarty:ext:tpl + +# SQL +# https://www.iso.org/standard/45498.html --type-add=sql:ext:sql,ctl -# Tcl http://www.tcl.tk/ +# Stylus +# http://stylus-lang.com/ +--type-add=stylus:ext:styl + +# SVG +# https://en.wikipedia.org/wiki/Scalable_Vector_Graphics +--type-add=svg:ext:svg + +# Swift +# https://developer.apple.com/swift/ +--type-add=swift:ext:swift +--type-add=swift:firstlinematch:/^#!.*\bswift/ + +# Tcl +# https://www.tcl.tk/ --type-add=tcl:ext:tcl,itcl,itk -# LaTeX http://www.latex-project.org/ +# TeX & LaTeX +# https://www.latex-project.org/ --type-add=tex:ext:tex,cls,sty -# Template Toolkit http://template-toolkit.org/ ---type-add=tt:ext:tt,tt2,ttml +# Template Toolkit (Perl) +# http//template-toolkit.org/ +--type-add=ttml:ext:tt,tt2,ttml + +# TOML +# https://toml.io/ +--type-add=toml:ext:toml + +# TypeScript +# https://www.typescriptlang.org/ +--type-add=ts:ext:ts,tsx # Visual Basic --type-add=vb:ext:bas,cls,frm,ctl,vb,resx @@ -3366,19 +3699,26 @@ sub _options_block { # Verilog --type-add=verilog:ext:v,vh,sv -# VHDL http://www.eda.org/twiki/bin/view.cgi/P1076/WebHome +# VHDL +# http://www.eda.org/twiki/bin/view.cgi/P1076/WebHome --type-add=vhdl:ext:vhd,vhdl -# Vim http://www.vim.org/ +# Vim +# https://www.vim.org/ --type-add=vim:ext:vim -# XML http://www.w3.org/TR/REC-xml/ ---type-add=xml:ext:xml,dtd,xsl,xslt,ent +# XML +# https://www.w3.org/TR/REC-xml/ +--type-add=xml:ext:xml,dtd,xsd,xsl,xslt,ent,wsdl --type-add=xml:firstlinematch:/<[?]xml/ -# YAML http://yaml.org/ +# YAML +# https://yaml.org/ --type-add=yaml:ext:yaml,yml HERE + $lines =~ s/==VERSION==/$App::Ack::VERSION/sm; + + return $lines; } 1; @@ -3389,7 +3729,7 @@ use strict; use warnings; use Cwd 3.00 (); -use File::Spec 3.00; +use File::Spec 3.00 (); use if ($^O eq 'MSWin32'), 'Win32'; @@ -3400,44 +3740,26 @@ sub new { return bless {}, $class; } -sub _remove_redundancies { - my ( @configs ) = @_; - - if ( $App::Ack::is_windows ) { - # inode stat always returns 0 on windows, so just check filenames. - my (%seen, @uniq); - - foreach my $path (map { $_->{path} } @configs) { - push @uniq, $path unless $seen{$path}; - $seen{$path} = 1; - } - - return @uniq; - } - - else { - - my %dev_and_inode_seen; - foreach my $config ( @configs ) { - my $path = $config->{path}; - my ( $dev, $inode ) = (stat $path)[0, 1]; - - if( defined($dev) ) { - if( $dev_and_inode_seen{"$dev:$inode"} ) { - undef $config; - } - else { - $dev_and_inode_seen{"$dev:$inode"} = 1; - } - } +sub _remove_redundancies { + my @configs = @_; + + my %seen; + my @uniq; + foreach my $config (@configs) { + my $path = $config->{path}; + my $key = -e $path ? Cwd::realpath( $path ) : $path; + if ( not $App::Ack::is_windows ) { + # On Unix, uniquify on inode. + my ($dev, $inode) = (stat $key)[0, 1]; + $key = "$dev:$inode" if defined $dev; } - - return grep { defined() } @configs; - + push( @uniq, $config ) unless $seen{$key}++; } + return @uniq; } + sub _check_for_ackrc { return unless defined $_[0]; @@ -3445,14 +3767,14 @@ sub _check_for_ackrc { map { File::Spec->catfile(@_, $_) } qw(.ackrc _ackrc); - die File::Spec->catdir(@_) . " contains both .ackrc and _ackrc.\n" . - "Please remove one of those files.\n" - if @files > 1; + App::Ack::die( File::Spec->catdir(@_) . ' contains both .ackrc and _ackrc. Please remove one of those files.' ) + if @files > 1; return wantarray ? @files : $files[0]; } # end _check_for_ackrc + sub find_config_files { my @config_files; @@ -3474,84 +3796,111 @@ sub find_config_files { push @config_files, map { +{ path => $_ } } _check_for_ackrc($ENV{'HOME'}); } - my @dirs = File::Spec->splitdir(Cwd::getcwd()); - while(@dirs) { + my $cwd = Cwd::getcwd(); + return () unless defined $cwd; + + # XXX This should go through some untainted cwd-fetching function, and not get untainted brute-force like this. + $cwd =~ /(.+)/; + $cwd = $1; + my @dirs = File::Spec->splitdir( $cwd ); + while ( @dirs ) { my $ackrc = _check_for_ackrc(@dirs); - if(defined $ackrc) { + if ( defined $ackrc ) { push @config_files, { project => 1, path => $ackrc }; last; } pop @dirs; } - # XXX we only test for existence here, so if the file is - # deleted out from under us, this will fail later. =( + # We only test for existence here, so if the file is deleted out from under us, this will fail later. return _remove_redundancies( @config_files ); } +1; +package App::Ack::ConfigLoader; -sub read_rcfile { - my $file = shift; - - return unless defined $file && -e $file; - - my @lines; +use strict; +use warnings; +use 5.010; - open( my $fh, '<', $file ) or App::Ack::die( "Unable to read $file: $!" ); - while ( my $line = <$fh> ) { - chomp $line; - $line =~ s/^\s+//; - $line =~ s/\s+$//; +use File::Spec 3.00 (); +use Getopt::Long 2.38 (); +use Text::ParseWords 3.1 (); - next if $line eq ''; - next if $line =~ /^#/; +sub configure_parser { + my @opts = @_; - push( @lines, $line ); - } - close $fh; + my @standard = qw( + default + bundling + no_auto_help + no_auto_version + no_ignore_case + ); + Getopt::Long::Configure( @standard, @opts ); - return @lines; + return; } -1; -package App::Ack::ConfigLoader; -use strict; -use warnings; +sub _generate_ignore_dir { + my ( $option_name, $opt ) = @_; -use Carp 1.04 (); -use Getopt::Long 2.35 (); -use Text::ParseWords 3.1 (); + my $is_inverted = $option_name =~ /^--no/; + + return sub { + my ( undef, $dir ) = @_; + $dir = _remove_directory_separator( $dir ); + if ( $dir !~ /:/ ) { + $dir = 'is:' . $dir; + } -my @INVALID_COMBINATIONS; + my ( $filter_type, $args ) = split /:/, $dir, 2; -BEGIN { - my @context = qw( -A -B -C --after-context --before-context --context ); - my @pretty = qw( --heading --group --break ); - my @filename = qw( -h -H --with-filename --no-filename ); - - @INVALID_COMBINATIONS = ( - # XXX normalize - [qw(-l)] => [@context, @pretty, @filename, qw(-L -o --passthru --output --max-count --column -f -g --show-types)], - [qw(-L)] => [@context, @pretty, @filename, qw(-l -o --passthru --output --max-count --column -f -g --show-types -c --count)], - [qw(--line)] => [@context, @pretty, @filename, qw(-l --files-with-matches --files-without-matches -L -o --passthru --match -m --max-count -1 -c --count --column --print0 -f -g --show-types)], - [qw(-o)] => [@context, qw(--output -c --count --column --column -f --show-types)], - [qw(--passthru)] => [@context, qw(--output --column -m --max-count -1 -c --count -f -g)], - [qw(--output)] => [@context, qw(-c --count -f -g)], - [qw(--match)] => [qw(-f -g)], - [qw(-m --max-count)] => [qw(-1 -f -g -c --count)], - [qw(-h --no-filename)] => [qw(-H --with-filename -f -g --group --heading)], - [qw(-H --with-filename)] => [qw(-h --no-filename -f -g)], - [qw(-c --count)] => [@context, @pretty, qw(--column -f -g)], - [qw(--column)] => [qw(-f -g)], - [@context] => [qw(-f -g)], - [qw(-f)] => [qw(-g), @pretty], - [qw(-g)] => [qw(-f), @pretty], - ); + if ( $filter_type eq 'firstlinematch' ) { + App::Ack::die( qq{Invalid filter specification "$filter_type" for option '$option_name'} ); + } + + my $filter = App::Ack::Filter->create_filter($filter_type, split(/,/, $args)); + my $collection; + + my $previous_inversion_matches = $opt->{idirs} && !($is_inverted xor $opt->{idirs}[-1]->is_inverted()); + + if ( $previous_inversion_matches ) { + $collection = $opt->{idirs}[-1]; + + if ( $is_inverted ) { + # This relies on invert of an inverted filter to return the original. + $collection = $collection->invert(); + } + } + else { + $collection = App::Ack::Filter::Collection->new(); + push @{ $opt->{idirs} }, $is_inverted ? $collection->invert() : $collection; + } + + $collection->add($filter); + + if ( $filter_type eq 'is' ) { + $collection->add(App::Ack::Filter::IsPath->new($args)); + } + }; +} + + +sub _remove_directory_separator { + my $path = shift; + + state $dir_sep_chars = $App::Ack::is_windows ? quotemeta( '\\/' ) : quotemeta( File::Spec->catfile( '', '' ) ); + + $path =~ s/[$dir_sep_chars]$//; + + return $path; } -sub process_filter_spec { + +sub _process_filter_spec { my ( $spec ) = @_; if ( $spec =~ /^(\w+):(\w+):(.*)/ ) { @@ -3571,11 +3920,12 @@ sub process_filter_spec { return ( $type_name, App::Ack::Filter->create_filter('ext', @extensions) ); } else { - Carp::croak "invalid filter specification '$spec'"; + App::Ack::die( "Invalid filter specification '$spec'" ); } } -sub uninvert_filter { + +sub _uninvert_filter { my ( $opt, @filters ) = @_; return unless defined $opt->{filters} && @filters; @@ -3592,23 +3942,20 @@ sub uninvert_filter { $i--; } } + + return; } -sub process_filetypes { + +sub _process_filetypes { my ( $opt, $arg_sources ) = @_; - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); # start with default options, minus some annoying ones - Getopt::Long::Configure( - 'no_ignore_case', - 'no_auto_abbrev', - 'pass_through', - ); my %additional_specs; my $add_spec = sub { my ( undef, $spec ) = @_; - my ( $name, $filter ) = process_filter_spec($spec); + my ( $name, $filter ) = _process_filter_spec($spec); push @{ $App::Ack::mappings{$name} }, $filter; @@ -3620,7 +3967,7 @@ sub process_filetypes { @filters = map { $_->invert() } @filters; } else { - uninvert_filter( $opt, @filters ); + _uninvert_filter( $opt, @filters ); } push @{ $opt->{'filters'} }, @filters; @@ -3630,7 +3977,7 @@ sub process_filetypes { my $set_spec = sub { my ( undef, $spec ) = @_; - my ( $name, $filter ) = process_filter_spec($spec); + my ( $name, $filter ) = _process_filter_spec($spec); $App::Ack::mappings{$name} = [ $filter ]; @@ -3659,24 +4006,21 @@ sub process_filetypes { 'type-del=s' => $delete_spec, ); + configure_parser( 'no_auto_abbrev', 'pass_through' ); foreach my $source (@{$arg_sources}) { - my ( $source_name, $args ) = @{$source}{qw/name contents/}; + my $args = $source->{contents}; if ( ref($args) ) { # $args are modified in place, so no need to munge $arg_sources - local @ARGV = @{$args}; - Getopt::Long::GetOptions(%type_arg_specs); - @{$args} = @ARGV; + Getopt::Long::GetOptionsFromArray( $args, %type_arg_specs ); } else { ( undef, $source->{contents} ) = - Getopt::Long::GetOptionsFromString($args, %type_arg_specs); + Getopt::Long::GetOptionsFromString( $args, %type_arg_specs ); } } $additional_specs{'k|known-types'} = sub { - my ( undef, $value ) = @_; - my @filters = map { @{$_} } values(%App::Ack::mappings); push @{ $opt->{'filters'} }, @filters; @@ -3685,43 +4029,52 @@ sub process_filetypes { return \%additional_specs; } -sub removed_option { - my ( $option, $explanation ) = @_; - - $explanation ||= ''; - return sub { - warn "Option '$option' is not valid in ack 2\n$explanation"; - exit 1; - }; -} sub get_arg_spec { my ( $opt, $extra_specs ) = @_; - my $dash_a_explanation = <{ $value . '!' }; + } + + if ( $callback ) { + $callback->( $getopt, $cb_value ); + } + else { + App::Ack::die( "Unknown type '$value'" ); + } + + return; + } + + $opt->{not} = []; return { 1 => sub { $opt->{1} = $opt->{m} = 1 }, - 'A|after-context=i' => \$opt->{after_context}, - 'B|before-context=i' - => \$opt->{before_context}, - 'C|context:i' => sub { shift; my $val = shift; $opt->{before_context} = $opt->{after_context} = ($val || 2) }, - 'a' => removed_option('-a', $dash_a_explanation), - 'all' => removed_option('--all', $dash_a_explanation), + 'A|after-context:-1' => sub { shift; $opt->{A} = _context_value(shift) }, + 'B|before-context:-1' => sub { shift; $opt->{B} = _context_value(shift) }, + 'C|context:-1' => sub { shift; $opt->{B} = $opt->{A} = _context_value(shift) }, 'break!' => \$opt->{break}, - c => \$opt->{count}, + 'c|count' => \$opt->{c}, 'color|colour!' => \$opt->{color}, 'color-match=s' => \$ENV{ACK_COLOR_MATCH}, 'color-filename=s' => \$ENV{ACK_COLOR_FILENAME}, + 'color-colno=s' => \$ENV{ACK_COLOR_COLNO}, 'color-lineno=s' => \$ENV{ACK_COLOR_LINENO}, 'column!' => \$opt->{column}, - count => \$opt->{count}, - 'create-ackrc' => sub { print "$_\n" for ( '--ignore-ack-defaults', App::Ack::ConfigDefault::options() ); exit; }, + 'create-ackrc' => sub { say for ( '--ignore-ack-defaults', App::Ack::ConfigDefault::options() ); exit; }, + 'debug' => \$opt->{debug}, 'env!' => sub { my ( undef, $value ) = @_; @@ -3732,30 +4085,28 @@ EOT f => \$opt->{f}, 'files-from=s' => \$opt->{files_from}, 'filter!' => \$App::Ack::is_filter_mode, - flush => \$opt->{flush}, + flush => sub { $| = 1 }, 'follow!' => \$opt->{follow}, g => \$opt->{g}, - G => removed_option('-G'), 'group!' => sub { shift; $opt->{heading} = $opt->{break} = shift }, 'heading!' => \$opt->{heading}, 'h|no-filename' => \$opt->{h}, 'H|with-filename' => \$opt->{H}, - 'i|ignore-case' => \$opt->{i}, - 'ignore-directory|ignore-dir=s' # XXX Combine this version with the negated version below - => sub { - my ( undef, $dir ) = @_; - - $dir = App::Ack::remove_dir_sep( $dir ); - if ( $dir !~ /^(?:is|match):/ ) { - $dir = 'is:' . $dir; - } - push @{ $opt->{idirs} }, $dir; - }, - 'ignore-file=s' => sub { - my ( undef, $file ) = @_; - push @{ $opt->{ifiles} }, $file; - }, - 'lines=s' => sub { shift; my $val = shift; push @{$opt->{lines}}, $val }, + 'i|ignore-case' => sub { $opt->{i} = 1; $opt->{S} = 0; }, + 'I|no-ignore-case' => sub { $opt->{i} = 0; $opt->{S} = 0; }, + 'ignore-directory|ignore-dir=s' => _generate_ignore_dir('--ignore-dir', $opt), + 'ignore-file=s' => sub { + my ( undef, $file ) = @_; + + my ( $filter_type, $args ) = split /:/, $file, 2; + + my $filter = App::Ack::Filter->create_filter($filter_type, split(/,/, $args//'')); + + if ( !$opt->{ifiles} ) { + $opt->{ifiles} = App::Ack::Filter::Collection->new(); + } + $opt->{ifiles}->add($filter); + }, 'l|files-with-matches' => \$opt->{l}, 'L|files-without-matches' @@ -3770,127 +4121,101 @@ EOT $opt->{pager} = $value || $ENV{PAGER}; }, - 'noignore-directory|noignore-dir=s' - => sub { - my ( undef, $dir ) = @_; - - # XXX can you do --noignore-dir=match,...? - $dir = App::Ack::remove_dir_sep( $dir ); - if ( $dir !~ /^(?:is|match):/ ) { - $dir = 'is:' . $dir; - } - if ( $dir !~ /^(?:is|match):/ ) { - Carp::croak("invalid noignore-directory argument: '$dir'"); - } - - @{ $opt->{idirs} } = grep { - $_ ne $dir - } @{ $opt->{idirs} }; - - push @{ $opt->{no_ignore_dirs} }, $dir; - }, + 'noignore-directory|noignore-dir=s' => _generate_ignore_dir('--noignore-dir', $opt), 'nopager' => sub { $opt->{pager} = undef }, + 'not=s' => $opt->{not}, 'passthru' => \$opt->{passthru}, 'print0' => \$opt->{print0}, + 'p|proximate:1' => \$opt->{p}, + 'P' => sub { $opt->{p} = 0 }, 'Q|literal' => \$opt->{Q}, 'r|R|recurse' => sub { $opt->{n} = 0 }, - 's' => \$opt->{dont_report_bad_filenames}, + 'range-start=s' => \$opt->{range_start}, + 'range-end=s' => \$opt->{range_end}, + 'range-invert!' => \$opt->{range_invert}, + 's' => \$opt->{s}, 'show-types' => \$opt->{show_types}, - 'smart-case!' => \$opt->{smart_case}, + 'S|smart-case!' => sub { my (undef,$value) = @_; $opt->{S} = $value; $opt->{i} = 0 if $value; }, 'sort-files' => \$opt->{sort_files}, - 'type=s' => sub { - my ( $getopt, $value ) = @_; - - my $cb_value = 1; - if ( $value =~ s/^no// ) { - $cb_value = 0; - } - - my $callback = $extra_specs->{ $value . '!' }; - - if ( $callback ) { - $callback->( $getopt, $cb_value ); - } - else { - Carp::croak( "Unknown type '$value'" ); - } - }, - 'u' => removed_option('-u'), - 'unrestricted' => removed_option('--unrestricted'), + 't|type=s' => \&_type_handler, + 'T=s' => sub { my ($getopt,$value) = @_; $value="no$value"; _type_handler($getopt,$value); }, + 'underline!' => \$opt->{underline}, 'v|invert-match' => \$opt->{v}, 'w|word-regexp' => \$opt->{w}, 'x' => sub { $opt->{files_from} = '-' }, - 'version' => sub { App::Ack::print_version_statement(); exit; }, - 'help|?:s' => sub { shift; App::Ack::show_help(@_); exit; }, + 'help' => sub { App::Ack::show_help(); exit; }, 'help-types' => sub { App::Ack::show_help_types(); exit; }, - 'man' => sub { App::Ack::show_man(); exit; }, + 'help-colors' => sub { App::Ack::show_help_colors(); exit; }, + 'help-rgb-colors' => sub { App::Ack::show_help_rgb(); exit; }, $extra_specs ? %{$extra_specs} : (), }; # arg_specs } -sub process_other { - my ( $opt, $extra_specs, $arg_sources ) = @_; - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); # start with default options, minus some annoying ones - Getopt::Long::Configure( - 'bundling', - 'no_ignore_case', - ); +sub _context_value { + my $val = shift; + + # Contexts default to 2. + return (!defined($val) || ($val < 0)) ? 2 : $val; +} + + +sub _process_other { + my ( $opt, $extra_specs, $arg_sources ) = @_; my $argv_source; my $is_help_types_active; foreach my $source (@{$arg_sources}) { - my ( $source_name, $args ) = @{$source}{qw/name contents/}; - - if ( $source_name eq 'ARGV' ) { - $argv_source = $args; + if ( $source->{name} eq 'ARGV' ) { + $argv_source = $source->{contents}; last; } } if ( $argv_source ) { # This *should* always be true, but you never know... - my @copy = @{$argv_source}; - local @ARGV = @copy; - - Getopt::Long::Configure('pass_through'); - - Getopt::Long::GetOptions( + configure_parser( 'pass_through' ); + Getopt::Long::GetOptionsFromArray( [ @{$argv_source} ], 'help-types' => \$is_help_types_active, ); - - Getopt::Long::Configure('no_pass_through'); } - my $arg_specs = get_arg_spec($opt, $extra_specs); + my $arg_specs = get_arg_spec( $opt, $extra_specs ); + configure_parser(); foreach my $source (@{$arg_sources}) { my ( $source_name, $args ) = @{$source}{qw/name contents/}; - my $args_for_source = $arg_specs; + my $args_for_source = { %{$arg_specs} }; - if ( $source->{project} ) { + if ( $source->{is_ackrc} ) { my $illegal = sub { - my ( $option ) = @_; + my $name = shift; + App::Ack::die( "Option --$name is forbidden in .ackrc files." ); + }; - return sub { - die "Option $option is illegal in project ackrcs"; - }; + $args_for_source = { + %{$args_for_source}, + 'output=s' => $illegal, + 'match=s' => $illegal, + }; + } + if ( $source->{project} ) { + my $illegal = sub { + my $name = shift; + App::Ack::die( "Option --$name is forbidden in project .ackrc files." ); }; - $args_for_source = { %$args_for_source, - 'output=s'=> $illegal->('--output'), - 'pager:s' => $illegal->('--pager'), - 'match=s' => $illegal->('--match'), + $args_for_source = { + %{$args_for_source}, + 'pager:s' => $illegal, }; } my $ret; if ( ref($args) ) { - local @ARGV = @{$args}; - $ret = Getopt::Long::GetOptions( %{$args_for_source} ); - @{$args} = @ARGV; + $ret = Getopt::Long::GetOptionsFromArray( $args, %{$args_for_source} ); } else { ( $ret, $source->{contents} ) = @@ -3912,47 +4237,25 @@ sub process_other { return; } -sub should_dump_options { - my ( $sources ) = @_; - - - foreach my $source (@{$sources}) { - my ( $name, $options ) = @{$source}{qw/name contents/}; - - if($name eq 'ARGV') { - my $dump; - local @ARGV = @{$options}; - Getopt::Long::Configure('default', 'pass_through', 'no_auto_help', 'no_auto_version'); - Getopt::Long::GetOptions( - 'dump' => \$dump, - ); - @{$options} = @ARGV; - return $dump; - } - } - return; -} -sub explode_sources { +sub _explode_sources { my ( $sources ) = @_; my @new_sources; - Getopt::Long::Configure('default', 'pass_through', 'no_auto_help', 'no_auto_version'); - my %opt; - my $arg_spec = get_arg_spec(\%opt); + my $arg_spec = get_arg_spec( \%opt, {} ); + my $dummy_sub = sub {}; my $add_type = sub { my ( undef, $arg ) = @_; - # XXX refactor? if ( $arg =~ /(\w+)=/) { - $arg_spec->{$1} = sub {}; + $arg_spec->{$1} = $dummy_sub; } else { ( $arg ) = split /:/, $arg; - $arg_spec->{$arg} = sub {}; + $arg_spec->{$arg} = $dummy_sub; } }; @@ -3962,26 +4265,27 @@ sub explode_sources { delete $arg_spec->{$arg}; }; + configure_parser( 'pass_through' ); foreach my $source (@{$sources}) { my ( $name, $options ) = @{$source}{qw/name contents/}; if ( ref($options) ne 'ARRAY' ) { $source->{contents} = $options = [ Text::ParseWords::shellwords($options) ]; } - for ( my $j = 0; $j < @{$options}; $j++ ) { + + for my $j ( 0 .. @{$options}-1 ) { next unless $options->[$j] =~ /^-/; my @chunk = ( $options->[$j] ); push @chunk, $options->[$j] while ++$j < @{$options} && $options->[$j] !~ /^-/; $j--; my @copy = @chunk; - local @ARGV = @chunk; - Getopt::Long::GetOptions( + Getopt::Long::GetOptionsFromArray( [@chunk], 'type-add=s' => $add_type, 'type-set=s' => $add_type, 'type-del=s' => $del_type, + %{$arg_spec} ); - Getopt::Long::GetOptions( %{$arg_spec} ); push @new_sources, { name => $name, @@ -3993,7 +4297,8 @@ sub explode_sources { return \@new_sources; } -sub compare_opts { + +sub _compare_opts { my ( $a, $b ) = @_; my $first_a = $a->[0]; @@ -4005,40 +4310,42 @@ sub compare_opts { return $first_a cmp $first_b; } -sub dump_options { + +sub _dump_options { my ( $sources ) = @_; - $sources = explode_sources($sources); + $sources = _explode_sources($sources); my %opts_by_source; my @source_names; foreach my $source (@{$sources}) { - my ( $name, $contents ) = @{$source}{qw/name contents/}; + my $name = $source->{name}; if ( not $opts_by_source{$name} ) { $opts_by_source{$name} = []; push @source_names, $name; } - push @{$opts_by_source{$name}}, $contents; + push @{$opts_by_source{$name}}, $source->{contents}; } foreach my $name (@source_names) { my $contents = $opts_by_source{$name}; - print $name, "\n"; - print '=' x length($name), "\n"; - print ' ', join(' ', @{$_}), "\n" foreach sort { compare_opts($a, $b) } @{$contents}; + say $name; + say '=' x length($name); + say ' ', join(' ', @{$_}) for sort { _compare_opts($a, $b) } @{$contents}; } return; } -sub remove_default_options_if_needed { + +sub _remove_default_options_if_needed { my ( $sources ) = @_; my $default_index; - foreach my $index ( 0 .. $#$sources ) { + foreach my $index ( 0 .. $#{$sources} ) { if ( $sources->[$index]{'name'} eq 'Defaults' ) { $default_index = $index; last; @@ -4049,34 +4356,23 @@ sub remove_default_options_if_needed { my $should_remove = 0; - # Start with default options, minus some annoying ones. - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); - Getopt::Long::Configure( - 'no_ignore_case', - 'no_auto_abbrev', - 'pass_through', - ); + configure_parser( 'no_auto_abbrev', 'pass_through' ); - foreach my $index ( $default_index + 1 .. $#$sources ) { - my ( $name, $args ) = @{$sources->[$index]}{qw/name contents/}; + foreach my $index ( $default_index + 1 .. $#{$sources} ) { + my $args = $sources->[$index]->{contents}; if (ref($args)) { - local @ARGV = @{$args}; - Getopt::Long::GetOptions( + Getopt::Long::GetOptionsFromArray( $args, 'ignore-ack-defaults' => \$should_remove, ); - @{$args} = @ARGV; } else { - ( undef, $sources->[$index]{contents} ) = Getopt::Long::GetOptionsFromString($args, + ( undef, $sources->[$index]{contents} ) = Getopt::Long::GetOptionsFromString( $args, 'ignore-ack-defaults' => \$should_remove, ); } } - Getopt::Long::Configure('default'); - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); - return $sources unless $should_remove; my @copy = @{$sources}; @@ -4084,55 +4380,6 @@ sub remove_default_options_if_needed { return \@copy; } -sub check_for_mutually_exclusive_options { - my ( $arg_sources ) = @_; - - my %mutually_exclusive_with; - my @copy = @{$arg_sources}; - - for(my $i = 0; $i < @INVALID_COMBINATIONS; $i += 2) { - my ( $lhs, $rhs ) = @INVALID_COMBINATIONS[ $i, $i + 1 ]; - - foreach my $l_opt ( @{$lhs} ) { - foreach my $r_opt ( @{$rhs} ) { - push @{ $mutually_exclusive_with{ $l_opt } }, $r_opt; - push @{ $mutually_exclusive_with{ $r_opt } }, $l_opt; - } - } - } - - while( @copy ) { - my %set_opts; - - my $source = shift @copy; - my ( $source_name, $args ) = @{$source}{qw/name contents/}; - $args = ref($args) ? [ @{$args} ] : [ Text::ParseWords::shellwords($args) ]; - - foreach my $opt ( @{$args} ) { - next unless $opt =~ /^[-+]/; - last if $opt eq '--'; - - if( $opt =~ /^(.*)=/ ) { - $opt = $1; - } - elsif ( $opt =~ /^(-[^-]).+/ ) { - $opt = $1; - } - - $set_opts{ $opt } = 1; - - my $mutex_opts = $mutually_exclusive_with{ $opt }; - - next unless $mutex_opts; - - foreach my $mutex_opt ( @{$mutex_opts} ) { - if($set_opts{ $mutex_opt }) { - die "Options '$mutex_opt' and '$opt' are mutually exclusive\n"; - } - } - } - } -} sub process_args { my $arg_sources = \@_; @@ -4141,32 +4388,44 @@ sub process_args { pager => $ENV{ACK_PAGER_COLOR} || $ENV{ACK_PAGER}, ); - check_for_mutually_exclusive_options($arg_sources); + $arg_sources = _remove_default_options_if_needed($arg_sources); - $arg_sources = remove_default_options_if_needed($arg_sources); - - if ( should_dump_options($arg_sources) ) { - dump_options($arg_sources); - exit(0); + # Check for --dump early. + foreach my $source (@{$arg_sources}) { + if ( $source->{name} eq 'ARGV' ) { + my $dump; + configure_parser( 'pass_through' ); + Getopt::Long::GetOptionsFromArray( $source->{contents}, + 'dump' => \$dump, + ); + if ( $dump ) { + _dump_options($arg_sources); + exit(0); + } + } } - my $type_specs = process_filetypes(\%opt, $arg_sources); - process_other(\%opt, $type_specs, $arg_sources); + my $type_specs = _process_filetypes(\%opt, $arg_sources); + + _check_for_mutex_options( $type_specs ); + + _process_other(\%opt, $type_specs, $arg_sources); while ( @{$arg_sources} ) { my $source = shift @{$arg_sources}; - my ( $source_name, $args ) = @{$source}{qw/name contents/}; + my $args = $source->{contents}; # All of our sources should be transformed into an array ref if ( ref($args) ) { + my $source_name = $source->{name}; if ( $source_name eq 'ARGV' ) { @ARGV = @{$args}; } elsif (@{$args}) { - Carp::croak "source '$source_name' has extra arguments!"; + App::Ack::die( "Source '$source_name' has extra arguments!" ); } } else { - Carp::croak 'The impossible has occurred!'; + App::Ack::die( 'The impossible has occurred!' ); } } my $filters = ($opt{filters} ||= []); @@ -4185,17 +4444,12 @@ sub retrieve_arg_sources { my $noenv; my $ackrc; - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); - Getopt::Long::Configure('pass_through'); - Getopt::Long::Configure('no_auto_abbrev'); - + configure_parser( 'no_auto_abbrev', 'pass_through' ); Getopt::Long::GetOptions( 'noenv' => \$noenv, 'ackrc=s' => \$ackrc, ); - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); - my @files; if ( !$noenv ) { @@ -4205,39 +4459,32 @@ sub retrieve_arg_sources { if ( $ackrc ) { # We explicitly use open so we get a nice error message. # XXX This is a potential race condition!. - if(open my $fh, '<', $ackrc) { + if ( open my $fh, '<', $ackrc ) { close $fh; } else { - die "Unable to load ackrc '$ackrc': $!" + App::Ack::die( "Unable to load ackrc '$ackrc': $!" ); } push( @files, { path => $ackrc } ); } push @arg_sources, { name => 'Defaults', - contents => [ App::Ack::ConfigDefault::options() ], + contents => [ App::Ack::ConfigDefault::options_clean() ], }; foreach my $file ( @files) { - my @lines = App::Ack::ConfigFinder::read_rcfile($file->{path}); - - if(@lines) { + my @lines = read_rcfile($file->{path}); + if ( @lines ) { push @arg_sources, { name => $file->{path}, contents => \@lines, project => $file->{project}, + is_ackrc => 1, }; } } - if ( $ENV{ACK_OPTIONS} && !$noenv ) { - push @arg_sources, { - name => 'ACK_OPTIONS', - contents => $ENV{ACK_OPTIONS}, - }; - } - push @arg_sources, { name => 'ARGV', contents => [ @ARGV ], @@ -4246,65 +4493,938 @@ sub retrieve_arg_sources { return @arg_sources; } -1; # End of App::Ack::ConfigLoader -package App::Ack::Filter; -use strict; -use warnings; -use overload - '""' => 'to_string'; +sub read_rcfile { + my $file = shift; -use Carp 1.04 (); + return unless defined $file && -e $file; -my %filter_types; + my @lines; + open( my $fh, '<', $file ) or App::Ack::die( "Unable to read $file: $!" ); + while ( defined( my $line = <$fh> ) ) { + chomp $line; + $line =~ s/^\s+//; + $line =~ s/\s+$//; -sub create_filter { - my ( undef, $type, @args ) = @_; + next if $line eq ''; + next if $line =~ /^\s*#/; - if ( my $package = $filter_types{$type} ) { - return $package->new(@args); + push( @lines, $line ); } - Carp::croak "Unknown filter type '$type'"; + close $fh or App::Ack::die( "Unable to close $file: $!" ); + + return @lines; } -sub register_filter { - my ( undef, $type, $package ) = @_; +# Verifies no mutex options were passed. Dies if they were. +sub _check_for_mutex_options { + my $type_specs = shift; - $filter_types{$type} = $package; + my $mutex = mutex_options(); + + my ($raw,$used) = _options_used( $type_specs ); + + my @used = sort { lc $a cmp lc $b } keys %{$used}; + + for my $i ( @used ) { + for my $j ( @used ) { + next if $i eq $j; + if ( $mutex->{$i}{$j} ) { + my $x = $raw->[ $used->{$i} ]; + my $y = $raw->[ $used->{$j} ]; + App::Ack::die( "Options '$x' and '$y' can't be used together." ); + } + } + } return; } -sub invert { - my ( $self ) = @_; +# Processes the command line option and returns a hash of the options that were +# used on the command line, using their full name. "--prox" shows up in the hash as "--proximate". +sub _options_used { + my $type_specs = shift; + + my %dummy_opt; + my $real_spec = get_arg_spec( \%dummy_opt, $type_specs ); + + # The real argument parsing doesn't check for --type-add, --type-del or --type-set because + # they get removed by the argument processing. We have to account for them here. + my $sub_dummy = sub {}; + $real_spec = { + %{$real_spec}, + 'type-add=s' => $sub_dummy, + 'type-del=s' => $sub_dummy, + 'type-set=s' => $sub_dummy, + 'ignore-ack-defaults' => $sub_dummy, + }; + + my %parsed; + my @raw; + my %spec_capture_parsed; + my %spec_capture_raw; + + + # Capture the %parsed hash. + CAPTURE_PARSED: { + my $parsed_pos = 0; + my $sub_count = sub { + my $arg = shift; + $arg = "$arg"; + $parsed{$arg} = $parsed_pos++; + }; + %spec_capture_parsed = ( + '<>' => sub { $parsed_pos++ }, # Bump forward one pos for non-options. + map { $_ => $sub_count } keys %{$real_spec} + ); + } + + # Capture the @raw array. + CAPTURE_RAW: { + my $raw_pos = 0; + %spec_capture_raw = ( + '<>' => sub { $raw_pos++ }, # Bump forward one pos for non-options. + ); + + my $sub_count = sub { + my $arg = shift; + + $arg = "$arg"; + $raw[$raw_pos] = length($arg) == 1 ? "-$arg" : "--$arg"; + $raw_pos++; + }; + + for my $opt_spec ( keys %{$real_spec} ) { + my $negatable; + my $type; + my $default; + + $negatable = ($opt_spec =~ s/!$//); + + if ( $opt_spec =~ s/(=[si])$// ) { + $type = $1; + } + if ( $opt_spec =~ s/(:.+)$// ) { + $default = $1; + } + + my @aliases = split( /\|/, $opt_spec ); + for my $alias ( @aliases ) { + $alias .= $type if defined $type; + $alias .= $default if defined $default; + $alias .= '!' if $negatable; + + $spec_capture_raw{$alias} = $sub_count; + } + } + } + + # Parse @ARGV twice, once with each capture spec. + configure_parser( 'pass_through' ); # Ignore invalid options. + Getopt::Long::GetOptionsFromArray( [@ARGV], %spec_capture_raw ); + Getopt::Long::GetOptionsFromArray( [@ARGV], %spec_capture_parsed ); + + return (\@raw,\%parsed); +} + + +sub mutex_options { + # This list is machine-generated by dev/crank-mutex. Do not modify it by hand. + + return { + 1 => { + m => 1, + passthru => 1, + }, + A => { + L => 1, + c => 1, + f => 1, + g => 1, + l => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + }, + B => { + L => 1, + c => 1, + f => 1, + g => 1, + l => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + }, + C => { + L => 1, + c => 1, + f => 1, + g => 1, + l => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + }, + H => { + L => 1, + f => 1, + g => 1, + l => 1, + }, + I => { + f => 1, + }, + L => { + A => 1, + B => 1, + C => 1, + H => 1, + L => 1, + break => 1, + c => 1, + column => 1, + f => 1, + g => 1, + group => 1, + h => 1, + heading => 1, + l => 1, + 'no-filename' => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + 'show-types' => 1, + v => 1, + 'with-filename' => 1, + }, + break => { + L => 1, + c => 1, + f => 1, + g => 1, + l => 1, + }, + c => { + A => 1, + B => 1, + C => 1, + L => 1, + break => 1, + column => 1, + f => 1, + g => 1, + group => 1, + heading => 1, + m => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + }, + column => { + L => 1, + c => 1, + f => 1, + g => 1, + l => 1, + o => 1, + output => 1, + passthru => 1, + v => 1, + }, + f => { + A => 1, + B => 1, + C => 1, + H => 1, + I => 1, + L => 1, + break => 1, + c => 1, + column => 1, + f => 1, + 'files-from' => 1, + g => 1, + group => 1, + h => 1, + heading => 1, + i => 1, + l => 1, + m => 1, + match => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + 'smart-case' => 1, + u => 1, + v => 1, + x => 1, + }, + 'files-from' => { + f => 1, + g => 1, + x => 1, + }, + g => { + A => 1, + B => 1, + C => 1, + H => 1, + L => 1, + break => 1, + c => 1, + column => 1, + f => 1, + 'files-from' => 1, + g => 1, + group => 1, + h => 1, + heading => 1, + l => 1, + m => 1, + match => 1, + not => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + u => 1, + x => 1, + }, + group => { + L => 1, + c => 1, + f => 1, + g => 1, + l => 1, + }, + h => { + L => 1, + f => 1, + g => 1, + l => 1, + }, + heading => { + L => 1, + c => 1, + f => 1, + g => 1, + l => 1, + }, + i => { + f => 1, + }, + l => { + A => 1, + B => 1, + C => 1, + H => 1, + L => 1, + break => 1, + column => 1, + f => 1, + g => 1, + group => 1, + h => 1, + heading => 1, + l => 1, + 'no-filename' => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + 'show-types' => 1, + 'with-filename' => 1, + }, + m => { + 1 => 1, + c => 1, + f => 1, + g => 1, + passthru => 1, + }, + match => { + f => 1, + g => 1, + }, + 'no-filename' => { + L => 1, + l => 1, + }, + not => { + g => 1, + }, + o => { + A => 1, + B => 1, + C => 1, + L => 1, + c => 1, + column => 1, + f => 1, + g => 1, + l => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + 'show-types' => 1, + v => 1, + }, + output => { + A => 1, + B => 1, + C => 1, + L => 1, + c => 1, + column => 1, + f => 1, + g => 1, + l => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + 'show-types' => 1, + u => 1, + v => 1, + }, + p => { + A => 1, + B => 1, + C => 1, + L => 1, + c => 1, + f => 1, + g => 1, + l => 1, + o => 1, + output => 1, + p => 1, + passthru => 1, + }, + passthru => { + 1 => 1, + A => 1, + B => 1, + C => 1, + L => 1, + c => 1, + column => 1, + f => 1, + g => 1, + l => 1, + m => 1, + o => 1, + output => 1, + p => 1, + v => 1, + }, + 'show-types' => { + L => 1, + l => 1, + o => 1, + output => 1, + }, + 'smart-case' => { + f => 1, + }, + u => { + f => 1, + g => 1, + output => 1, + }, + v => { + L => 1, + column => 1, + f => 1, + o => 1, + output => 1, + passthru => 1, + }, + 'with-filename' => { + L => 1, + l => 1, + }, + x => { + f => 1, + 'files-from' => 1, + g => 1, + }, + }; + +} # End of mutex_options() + + +1; # End of App::Ack::ConfigLoader +package App::Ack::File; + +use warnings; +use strict; + +use File::Spec (); + + +sub new { + my $class = shift; + my $filename = shift; + + my $self = bless { + filename => $filename, + fh => undef, + }, $class; + + if ( $self->{filename} eq '-' ) { + $self->{fh} = *STDIN; + } + + return $self; +} + + + +sub name { + return $_[0]->{filename}; +} + + + +sub basename { + my ( $self ) = @_; + + return $self->{basename} //= (File::Spec->splitpath($self->name))[2]; +} + + + +sub open { + my ( $self ) = @_; + + if ( !$self->{fh} ) { + if ( open $self->{fh}, '<', $self->{filename} ) { + # Do nothing. + } + else { + $self->{fh} = undef; + } + } + + return $self->{fh}; +} + + +sub may_be_present { + my $self = shift; + my $regex = shift; + + # Tells if the file needs a line-by-line scan. This is a big + # optimization because if you can tell from the outset that the pattern + # is not found in the file at all, then there's no need to do the + # line-by-line iteration. + + # Slurp up an entire file up to 10M, see if there are any matches + # in it, and if so, let us know so we can iterate over it directly. + + # The $regex may be undef if it had a "$" in it, and is therefore unsuitable for this heuristic. + + my $may_be_present = 1; + if ( $regex && $self->open() && -f $self->{fh} ) { + my $buffer; + my $size = 10_000_000; + my $rc = sysread( $self->{fh}, $buffer, $size ); + if ( !defined($rc) ) { + if ( $App::Ack::report_bad_filenames ) { + App::Ack::warn( $self->name . ": $!" ); + } + $may_be_present = 0; + } + else { + # If we read all 10M, then we need to scan the rest. + # If there are any carriage returns, our results are flaky, so scan the rest. + if ( ($rc == $size) || (index($buffer,"\r") >= 0) ) { + $may_be_present = 1; + } + else { + if ( $buffer !~ /$regex/o ) { + $may_be_present = 0; + } + } + } + } + + return $may_be_present; +} + + + +sub reset { + my $self = shift; + + if ( defined($self->{fh}) ) { + return unless -f $self->{fh}; + + if ( !seek( $self->{fh}, 0, 0 ) && $App::Ack::report_bad_filenames ) { + App::Ack::warn( "$self->{filename}: $!" ); + } + } + + return; +} + + + +sub close { + my $self = shift; + + if ( $self->{fh} ) { + if ( !close($self->{fh}) && $App::Ack::report_bad_filenames ) { + App::Ack::warn( $self->name() . ": $!" ); + } + $self->{fh} = undef; + } + + return; +} + + + +sub clone { + my ( $self ) = @_; + + return __PACKAGE__->new($self->name); +} + + + +sub firstliney { + my ( $self ) = @_; + + if ( !exists $self->{firstliney} ) { + my $fh = $self->open(); + if ( !$fh ) { + if ( $App::Ack::report_bad_filenames ) { + App::Ack::warn( $self->name . ': ' . $! ); + } + $self->{firstliney} = ''; + } + else { + my $buffer; + my $rc = sysread( $fh, $buffer, 250 ); + if ( $rc ) { + $buffer =~ s/[\r\n].*//s; + } + else { + if ( !defined($rc) ) { + App::Ack::warn( $self->name . ': ' . $! ); + } + $buffer = ''; + } + $self->{firstliney} = $buffer; + $self->reset; + } + } + + return $self->{firstliney}; +} + +1; +package App::Ack::Files; + + + +use warnings; +use strict; +use 5.010; + + +sub from_argv { + my $class = shift; + my $opt = shift; + my $start = shift; + + my $self = bless {}, $class; + + my $descend_filter = $opt->{descend_filter}; + + if ( $opt->{n} ) { + $descend_filter = sub { + return 0; + }; + } + + $self->{iter} = + File::Next::files( { + file_filter => $opt->{file_filter}, + descend_filter => $descend_filter, + error_handler => _generate_error_handler(), + warning_handler => sub {}, + sort_files => $opt->{sort_files}, + follow_symlinks => $opt->{follow}, + }, @{$start} ); + + return $self; +} + + +sub from_file { + my $class = shift; + my $opt = shift; + my $file = shift; + + my $error_handler = _generate_error_handler(); + my $iter = + File::Next::from_file( { + error_handler => $error_handler, + warning_handler => $error_handler, + sort_files => $opt->{sort_files}, + }, $file ) or return undef; + + return bless { + iter => $iter, + }, $class; +} + + + + +sub from_stdin { + my $class = shift; + + my $self = bless {}, $class; + + $self->{iter} = sub { + state $has_been_called = 0; + + if ( !$has_been_called ) { + $has_been_called = 1; + return '-'; + } + return; + }; + + return $self; +} + + +sub next { + my $self = shift; + + my $file = $self->{iter}->(); + + return unless defined($file); + + return App::Ack::File->new( $file ); +} + + +sub _generate_error_handler { + if ( $App::Ack::report_bad_filenames ) { + return sub { + my $msg = shift; + App::Ack::warn( $msg ); + }; + } + else { + return sub {}; + } +} + +1; +package App::Ack::Filter; + +use strict; +use warnings; + + +my %filter_types; + + +sub create_filter { + my ( undef, $type, @args ) = @_; + + if ( my $package = $filter_types{$type} ) { + return $package->new(@args); + } + my $allowed_types = join( ', ', sort keys %filter_types ); + App::Ack::die( "Unknown filter type '$type'. Type must be one of: $allowed_types." ); +} + + +sub register_filter { + my ( undef, $type, $package ) = @_; + + $filter_types{$type} = $package; + + return; +} + + +sub invert { + my ( $self ) = @_; + + return App::Ack::Filter::Inverse->new( $self ); +} + + +sub is_inverted { + return 0; +} + + +sub to_string { + return '(unimplemented to_string)'; +} + + +sub inspect { + my ( $self ) = @_; + + return ref($self); +} + +1; +package App::Ack::Filter::Collection; + + +use strict; +use warnings; +BEGIN { + our @ISA = 'App::Ack::Filter'; +} + +sub new { + my ( $class ) = @_; + + return bless { + groups => {}, + ungrouped => [], + }, $class; +} + +sub filter { + my ( $self, $file ) = @_; + + for my $group (values %{$self->{groups}}) { + return 1 if $group->filter($file); + } + + for my $filter (@{$self->{ungrouped}}) { + return 1 if $filter->filter($file); + } + + return 0; +} + +sub add { + my ( $self, $filter ) = @_; + + if (exists $filter->{'groupname'}) { + my $group = ($self->{groups}->{$filter->{groupname}} ||= $filter->create_group()); + $group->add($filter); + } + else { + push @{$self->{'ungrouped'}}, $filter; + } + + return; +} + +sub inspect { + my ( $self ) = @_; + + return ref($self) . " - $self"; +} + +sub to_string { + my ( $self ) = @_; + + return join(', ', map { "($_)" } @{$self->{ungrouped}}); +} + +1; +package App::Ack::Filter::Default; + + +use strict; +use warnings; +BEGIN { + our @ISA = 'App::Ack::Filter'; +} + +sub new { + my ( $class ) = @_; + + return bless {}, $class; +} + +sub filter { + my ( undef, $file ) = @_; + + return -T $file->name; +} + +1; +package App::Ack::Filter::Extension; + + +use strict; +use warnings; +BEGIN { + our @ISA = 'App::Ack::Filter'; +} + + +sub new { + my ( $class, @extensions ) = @_; + + my $exts = join('|', map { "\Q$_\E"} @extensions); + my $re = qr/[.](?:$exts)$/i; + + return bless { + extensions => \@extensions, + regex => $re, + groupname => 'ExtensionGroup', + }, $class; +} + +sub create_group { + return App::Ack::Filter::ExtensionGroup->new(); +} + +sub filter { + my ( $self, $file ) = @_; - return App::Ack::Filter::Inverse->new( $self ); + return $file->name =~ /$self->{regex}/; } +sub inspect { + my ( $self ) = @_; -sub is_inverted { - return 0; + return ref($self) . ' - ' . $self->{regex}; } - sub to_string { my ( $self ) = @_; - return '(unimplemented to_string)'; + return join( ' ', map { ".$_" } @{$self->{extensions}} ); } - -sub inspect { - my ( $self ) = @_; - - return ref($self); +BEGIN { + App::Ack::Filter->register_filter(ext => __PACKAGE__); } 1; -package App::Ack::Filter::Extension; +package App::Ack::Filter::ExtensionGroup; + use strict; use warnings; @@ -4312,55 +5432,51 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } - sub new { - my ( $class, @extensions ) = @_; - - my $exts = join('|', map { "\Q$_\E"} @extensions); - my $re = qr/[.](?:$exts)$/i; + my ( $class ) = @_; return bless { - extensions => \@extensions, - regex => $re, - groupname => 'ExtensionGroup', + data => {}, }, $class; } -sub create_group { - return App::Ack::Filter::ExtensionGroup->new(); +sub add { + my ( $self, $filter ) = @_; + + foreach my $ext (@{$filter->{extensions}}) { + $self->{data}->{lc $ext} = 1; + } + + return; } sub filter { - my ( $self, $resource ) = @_; + my ( $self, $file ) = @_; - my $re = $self->{'regex'}; + if ($file->name =~ /[.]([^.]*)$/) { + return exists $self->{'data'}->{lc $1}; + } - return $resource->name =~ /$re/; + return 0; } sub inspect { my ( $self ) = @_; - my $re = $self->{'regex'}; - - return ref($self) . " - $re"; + return ref($self) . " - $self"; } sub to_string { my ( $self ) = @_; - my $exts = $self->{'extensions'}; - - return join(' ', map { ".$_" } @{$exts}); -} - -BEGIN { - App::Ack::Filter->register_filter(ext => __PACKAGE__); + return join(' ', map { ".$_" } sort keys %{$self->{data}}); } 1; package App::Ack::Filter::FirstLineMatch; + + use strict; use warnings; BEGIN { @@ -4383,21 +5499,16 @@ sub new { # .min.js file (which might be only one "line" long) into memory. sub filter { - my ( $self, $resource ) = @_; - - my $re = $self->{'regex'}; - - my $line = $resource->firstliney; + my ( $self, $file ) = @_; - return $line =~ /$re/; + return $file->firstliney =~ /$self->{regex}/; } sub inspect { my ( $self ) = @_; - my $re = $self->{'regex'}; - return ref($self) . " - $re"; + return ref($self) . ' - ' . $self->{regex}; } sub to_string { @@ -4405,7 +5516,7 @@ sub to_string { (my $re = $self->{regex}) =~ s{\([^:]*:(.*)\)$}{$1}; - return "first line matches /$re/"; + return "First line matches /$re/"; } BEGIN { @@ -4413,7 +5524,9 @@ BEGIN { } 1; -package App::Ack::Filter::Is; +package App::Ack::Filter::Inverse; + + use strict; use warnings; @@ -4421,52 +5534,41 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } -use File::Spec 3.00 (); - sub new { - my ( $class, $filename ) = @_; + my ( $class, $filter ) = @_; return bless { - filename => $filename, - groupname => 'IsGroup', + filter => $filter, }, $class; } -sub create_group { - return App::Ack::Filter::IsGroup->new(); -} - sub filter { - my ( $self, $resource ) = @_; - - my $filename = $self->{'filename'}; - my $base = (File::Spec->splitpath($resource->name))[2]; + my ( $self, $file ) = @_; - return $base eq $filename; + return !$self->{filter}->filter( $file ); } -sub inspect { - my ( $self ) = @_; +sub invert { + my $self = shift; - my $filename = $self->{'filename'}; + return $self->{'filter'}; +} - return ref($self) . " - $filename"; +sub is_inverted { + return 1; } -sub to_string { +sub inspect { my ( $self ) = @_; - my $filename = $self->{'filename'}; - - return $filename; -} + my $filter = $self->{'filter'}; -BEGIN { - App::Ack::Filter->register_filter(is => __PACKAGE__); + return "!$filter"; } 1; -package App::Ack::Filter::Match; +package App::Ack::Filter::Is; + use strict; use warnings; @@ -4474,50 +5576,46 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } -use File::Spec 3.00; +use File::Spec 3.00 (); sub new { - my ( $class, $re ) = @_; - - $re =~ s{^/|/$}{}g; # XXX validate? - $re = qr/$re/i; + my ( $class, $filename ) = @_; return bless { - regex => $re, + filename => $filename, + groupname => 'IsGroup', }, $class; } -sub filter { - my ( $self, $resource ) = @_; +sub create_group { + return App::Ack::Filter::IsGroup->new(); +} - my $re = $self->{'regex'}; - my $base = (File::Spec->splitpath($resource->name))[2]; +sub filter { + my ( $self, $file ) = @_; - return $base =~ /$re/; + return (File::Spec->splitpath($file->name))[2] eq $self->{filename}; } sub inspect { my ( $self ) = @_; - my $re = $self->{'regex'}; - - print ref($self) . " - $re"; + return ref($self) . ' - ' . $self->{filename}; } sub to_string { my ( $self ) = @_; - my $re = $self->{'regex'}; - - return "filename matches $re"; + return $self->{filename}; } BEGIN { - App::Ack::Filter->register_filter(match => __PACKAGE__); + App::Ack::Filter->register_filter(is => __PACKAGE__); } 1; -package App::Ack::Filter::Default; +package App::Ack::Filter::IsGroup; + use strict; use warnings; @@ -4528,17 +5626,40 @@ BEGIN { sub new { my ( $class ) = @_; - return bless {}, $class; + return bless { + data => {}, + }, $class; +} + +sub add { + my ( $self, $filter ) = @_; + + $self->{data}->{ $filter->{filename} } = 1; + + return; } sub filter { - my ( $self, $resource ) = @_; + my ( $self, $file ) = @_; + + return exists $self->{data}->{ $file->basename }; +} + +sub inspect { + my ( $self ) = @_; + + return ref($self) . " - $self"; +} - return -T $resource->name; +sub to_string { + my ( $self ) = @_; + + return join(' ', keys %{$self->{data}}); } 1; -package App::Ack::Filter::Inverse; +package App::Ack::Filter::IsPath; + use strict; use warnings; @@ -4546,41 +5667,41 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } + sub new { - my ( $class, $filter ) = @_; + my ( $class, $filename ) = @_; return bless { - filter => $filter, + filename => $filename, + groupname => 'IsPathGroup', }, $class; } -sub filter { - my ( $self, $resource ) = @_; - - my $filter = $self->{'filter'}; - return !$filter->filter( $resource ); +sub create_group { + return App::Ack::Filter::IsPathGroup->new(); } -sub invert { - my $self = shift; - - return $self->{'filter'}; -} +sub filter { + my ( $self, $file ) = @_; -sub is_inverted { - return 1; + return $file->name eq $self->{filename}; } sub inspect { my ( $self ) = @_; - my $filter = $self->{'filter'}; + return ref($self) . ' - ' . $self->{filename}; +} - return "!$filter"; +sub to_string { + my ( $self ) = @_; + + return $self->{filename}; } 1; -package App::Ack::Filter::Collection; +package App::Ack::Filter::IsPathGroup; + use strict; use warnings; @@ -4588,57 +5709,26 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } -use File::Spec 3.00 (); - sub new { my ( $class ) = @_; return bless { - groups => {}, - ungrouped => [], + data => {}, }, $class; } -sub filter { - my ( $self, $resource ) = @_; - - for my $group (values %{$self->{'groups'}}) { - if ($group->filter($resource)) { - return 1; - } - } - - for my $filter (@{$self->{'ungrouped'}}) { - if ($filter->filter($resource)) { - return 1; - } - } - - return 0; -} - sub add { my ( $self, $filter ) = @_; - if (exists $filter->{'groupname'}) { - my $groups = $self->{'groups'}; - my $group_name = $filter->{'groupname'}; + $self->{data}->{ $filter->{filename} } = 1; - my $group; - if (exists $groups->{$group_name}) { - $group = $groups->{$group_name}; - } - else { - $group = $groups->{$group_name} = $filter->create_group(); - } + return; +} - $group->add($filter); - } - else { - push @{$self->{'ungrouped'}}, $filter; - } +sub filter { + my ( $self, $file ) = @_; - return; + return exists $self->{data}->{$file->name}; } sub inspect { @@ -4650,13 +5740,11 @@ sub inspect { sub to_string { my ( $self ) = @_; - my $ungrouped = $self->{'ungrouped'}; - - return join(', ', map { "($_)" } @{$ungrouped}); + return join(' ', keys %{$self->{data}}); } 1; -package App::Ack::Filter::IsGroup; +package App::Ack::Filter::Match; use strict; use warnings; @@ -4664,45 +5752,49 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } -use File::Spec 3.00 (); + sub new { - my ( $class ) = @_; + my ( $class, $re ) = @_; + + $re =~ s{^/|/$}{}g; # XXX validate? + $re = qr/$re/i; return bless { - data => {}, + regex => $re, + groupname => 'MatchGroup', }, $class; } -sub add { - my ( $self, $filter ) = @_; - - $self->{data}->{ $filter->{filename} } = 1; +sub create_group { + return App::Ack::Filter::MatchGroup->new; } sub filter { - my ( $self, $resource ) = @_; - - my $data = $self->{'data'}; - my $base = (File::Spec->splitpath($resource->name))[2]; + my ( $self, $file ) = @_; - return exists $data->{$base}; + return $file->basename =~ /$self->{regex}/; } sub inspect { my ( $self ) = @_; - return ref($self) . " - $self"; + return ref($self) . ' - ' . $self->{regex}; } sub to_string { my ( $self ) = @_; - return join(' ', keys %{$self->{data}}); + return "Filename matches $self->{regex}"; +} + +BEGIN { + App::Ack::Filter->register_filter(match => __PACKAGE__); } 1; -package App::Ack::Filter::ExtensionGroup; +package App::Ack::Filter::MatchGroup; + use strict; use warnings; @@ -4710,50 +5802,34 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } -use File::Spec 3.00 (); - sub new { my ( $class ) = @_; return bless { - data => {}, + matches => [], + big_re => undef, }, $class; } sub add { my ( $self, $filter ) = @_; - my $data = $self->{'data'}; - my $extensions = $filter->{'extensions'}; - - foreach my $ext (@{$extensions}) { - $data->{lc $ext} = 1; - } -} - -sub filter { - my ( $self, $resource ) = @_; + push @{ $self->{matches} }, $filter->{regex}; - if ($resource->name =~ /[.]([^.]*)$/) { - return exists $self->{'data'}->{lc $1}; - } + my $re = join('|', map { "(?:$_)" } @{ $self->{matches} }); + $self->{big_re} = qr/$re/; - return 0; + return; } -sub inspect { - my ( $self ) = @_; +sub filter { + my ( $self, $file ) = @_; - return ref($self) . " - $self"; + return $file->basename =~ /$self->{big_re}/; } -sub to_string { - my ( $self ) = @_; - - my $data = $self->{'data'}; - - return join(' ', map { ".$_" } (keys %$data)); -} +# This class has no inspect() or to_string() method. +# It will just use the default one unless someone writes something useful. 1; package File::Next; @@ -4762,7 +5838,7 @@ use strict; use warnings; -our $VERSION = '1.12'; +our $VERSION = '1.18'; @@ -4778,7 +5854,7 @@ BEGIN { %files_defaults = ( file_filter => undef, descend_filter => undef, - error_handler => sub { CORE::die @_ }, + error_handler => sub { CORE::die $_[0] }, warning_handler => sub { CORE::warn @_ }, sort_files => undef, follow_symlinks => 1, @@ -4792,12 +5868,12 @@ sub files { die _bad_invocation() if @_ && defined($_[0]) && ($_[0] eq __PACKAGE__); my ($parms,@queue) = _setup( \%files_defaults, @_ ); - my $filter = $parms->{file_filter}; + my $filter = $parms->{file_filter}; return sub { - while (@queue) { - my ($dirname,$file,$fullpath) = splice( @queue, 0, 3 ); - if ( -f $fullpath || -p $fullpath || $fullpath =~ m{^/dev/fd} ) { + while ( my $entry = shift @queue ) { + my ( $dirname, $file, $fullpath, $is_dir, $is_file, $is_fifo ) = @{$entry}; + if ( $is_file || $is_fifo ) { if ( $filter ) { local $_ = $file; local $File::Next::dir = $dirname; @@ -4806,7 +5882,7 @@ sub files { } return wantarray ? ($dirname,$file,$fullpath) : $fullpath; } - elsif ( -d _ ) { + if ( $is_dir ) { unshift( @queue, _candidate_files( $parms, $fullpath ) ); } } # while @@ -4820,14 +5896,15 @@ sub files { + sub from_file { die _bad_invocation() if @_ && defined($_[0]) && ($_[0] eq __PACKAGE__); my ($parms,@queue) = _setup( \%files_defaults, @_ ); my $err = $parms->{error_handler}; - my $warn = $parms->{error_handler}; + my $warn = $parms->{warning_handler}; - my $filename = $queue[1]; + my $filename = $queue[0]->[1]; if ( !defined($filename) ) { $err->( 'Must pass a filename to from_file()' ); @@ -4840,12 +5917,12 @@ sub from_file { } else { if ( !open( $fh, '<', $filename ) ) { - $err->( "Unable to open $filename: $!" ); + $err->( "Unable to open $filename: $!", $! + 0 ); return undef; } } - my $filter = $parms->{file_filter}; + my $filter = $parms->{file_filter}; return sub { local $/ = $parms->{nul_separated} ? "\x00" : $/; while ( my $fullpath = <$fh> ) { @@ -4908,7 +5985,7 @@ sub _setup { } # Any leftover keys are bogus - for my $badkey ( keys %passed_parms ) { + for my $badkey ( sort keys %passed_parms ) { my $sub = (caller(1))[3]; $parms->{error_handler}->( "Invalid option passed to $sub(): $badkey" ); } @@ -4921,12 +5998,13 @@ sub _setup { for ( @_ ) { my $start = reslash( $_ ); - if (-d $start) { - push @queue, ($start,undef,$start); - } - else { - push @queue, (undef,$start,$start); - } + my $is_dir = -d $start; + my $is_file = -f _; + my $is_fifo = (-p _) || ($start =~ m{^/dev/fd}); + push @queue, + $is_dir + ? [ $start, undef, $start, $is_dir, $is_file, $is_fifo ] + : [ undef, $start, $start, $is_dir, $is_file, $is_fifo ]; } return ($parms,@queue); @@ -4939,47 +6017,46 @@ sub _candidate_files { my $dh; if ( !opendir $dh, $dirname ) { - $parms->{error_handler}->( "$dirname: $!" ); + $parms->{error_handler}->( "$dirname: $!", $! + 0 ); return; } my @newfiles; my $descend_filter = $parms->{descend_filter}; my $follow_symlinks = $parms->{follow_symlinks}; - my $sort_sub = $parms->{sort_files}; for my $file ( grep { !exists $skip_dirs{$_} } readdir $dh ) { - my $has_stat; - - # Only do directory checking if we have a descend_filter my $fullpath = File::Spec->catdir( $dirname, $file ); if ( !$follow_symlinks ) { next if -l $fullpath; - $has_stat = 1; } + else { + stat($fullpath); + } + my $is_dir = -d _; + my $is_file = -f _; + my $is_fifo = (-p _) || ($fullpath =~ m{^/dev/fd}); + # Only do directory checking if we have a descend_filter if ( $descend_filter ) { - if ( $has_stat ? (-d _) : (-d $fullpath) ) { + if ( $is_dir ) { local $File::Next::dir = $fullpath; local $_ = $file; next if not $descend_filter->(); } } - if ( $sort_sub ) { - push( @newfiles, [ $dirname, $file, $fullpath ] ); - } - else { - push( @newfiles, $dirname, $file, $fullpath ); - } + push @newfiles, [ $dirname, $file, $fullpath, $is_dir, $is_file, $is_fifo ]; } closedir $dh; + my $sort_sub = $parms->{sort_files}; if ( $sort_sub ) { - return map { @{$_} } sort $sort_sub @newfiles; + @newfiles = sort $sort_sub @newfiles; } return @newfiles; } + 1; # End of File::Next