From 511d41892ec26808fb8568a2164ad6f3d1b7a72d Mon Sep 17 00:00:00 2001 From: Tony Duckles Date: Sat, 9 May 2020 15:39:55 -0500 Subject: [PATCH] bin/ack: ack v3.3.1 --- bin/ack | 4875 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 2721 insertions(+), 2154 deletions(-) diff --git a/bin/ack b/bin/ack index 80329a4..4a5c174 100755 --- a/bin/ack +++ b/bin/ack @@ -4,55 +4,72 @@ # Please DO NOT EDIT or send patches for it. # # Please take a look at the source from -# https://github.com/beyondgrep/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.22'; # Check https://beyondgrep.com/ for updates -use 5.008008; -use Getopt::Long 2.38 (); -use Carp 1.04 (); +our $VERSION = 'v3.3.1'; # Check https://beyondgrep.com/ for updates + +use 5.010001; use File::Spec (); -# XXX Don't make this so brute force -# See also: https://github.com/beyondgrep/ack2/issues/89 -our $opt_after_context; -our $opt_before_context; -our $opt_output; -our $opt_print0; -our $opt_color; -our $opt_heading; -our $opt_show_filename; -our $opt_regex; +# Global command-line options +our $opt_1; +our $opt_A; +our $opt_B; our $opt_break; -our $opt_count; -our $opt_v; -our $opt_m; -our $opt_g; +our $opt_color; +our $opt_column; +our $opt_debug; +our $opt_c; our $opt_f; -our $opt_lines; +our $opt_g; +our $opt_heading; our $opt_L; our $opt_l; +our $opt_m; +our $opt_output; our $opt_passthru; -our $opt_column; +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; -# These are all our globals. +# The regex that we search for in each file. +our $search_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" ); } @@ -62,7 +79,7 @@ MAIN: { for my $arg ( @ARGV ) { last if ( $arg eq '--' ); - # Get the --thpppt, --bar, --cathy checking out of the way. + # 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(); @@ -72,29 +89,275 @@ MAIN: { $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'); - 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; }, + + # 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'; + } + + my $p = App::Ack::ConfigLoader::opt_parser( 'no_auto_abbrev', 'pass_through' ); + $p->getoptions( + 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 ); + $stats{search_re} = $search_re; + $stats{scan_re} = $scan_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 ); + $stats{search_re} = $search_re; + $stats{scan_re} = $scan_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 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 ); +} + +# End of MAIN + +sub file_loop_fg { + my $files = shift; + + 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 $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; + } + + if ( !$opt_l || $matches_for_this_file > 0 ) { + if ( $opt_show_filename ) { + App::Ack::say( $file->name, ':', $matches_for_this_file ); + } + else { + App::Ack::say( $matches_for_this_file ); + } + } + } + + 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 $nmatches; } + sub _compile_descend_filter { my ( $opt ) = @_; @@ -119,8 +382,8 @@ sub _compile_descend_filter { $idirs = $opt->{idirs}; return sub { - my $resource = App::Ack::Resource->new($File::Next::dir); - return !grep { $_->filter($resource) } @{$idirs}; + my $file = App::Ack::File->new($File::Next::dir); + return !grep { $_->filter($file) } @{$idirs}; }; } @@ -155,11 +418,11 @@ sub _compile_file_filter { return sub { if ( $opt_g ) { - if ( $File::Next::name =~ /$opt_regex/ && $opt_v ) { - return 0; + if ( $File::Next::name =~ /$search_re/o ) { + return 0 if $opt_v; } - if ( $File::Next::name !~ /$opt_regex/ && !$opt_v ) { - return 0; + else { + return 0 if !$opt_v; } } # ack always selects files that are specified on the command @@ -179,7 +442,7 @@ sub _compile_file_filter { my $is_ignoring = 0; for ( my $i = 0; $i < @dirs; $i++) { - my $dir_rsrc = App::Ack::Resource->new(File::Spec->catfile(@dirs[0 .. $i])); + my $dir_rsrc = App::Ack::File->new(File::Spec->catfile(@dirs[0 .. $i])); my $j = 0; for my $filter (@ignore_dir_filter) { @@ -217,68 +480,22 @@ sub _compile_file_filter { } } - my $resource = App::Ack::Resource->new($File::Next::name); + my $file = App::Ack::File->new($File::Next::name); - if ( $ifiles_filters && $ifiles_filters->filter($resource) ) { + 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 @@ -291,7 +508,7 @@ sub get_file_id { } else { # XXX Is this the best method? It always hits the FS. - if( my ( $dev, $inode ) = (stat($filename))[0, 1] ) { + if ( my ( $dev, $inode ) = (stat($filename))[0, 1] ) { return join(':', $dev, $inode); } else { @@ -310,30 +527,90 @@ 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} ) { - my $pristine_str = $str; + my $ok = 1; - $str = "(?:$str)"; - $str = "\\b$str" if $pristine_str =~ /^\w/; - $str = "$str\\b" if $pristine_str =~ /\w$/; - } + 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/m }; - 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_column_number; +my $match_colno; { @@ -350,21 +627,17 @@ my $before_context_pos; my $after_context_pending; # Number of latest line that got printed -my $printed_line_no; - -my $is_iterating; +my $printed_lineno; my $is_first_match; -my $has_printed_something; +state $has_printed_from_any_file = 0; -BEGIN { - $has_printed_something = 0; -} -# Set up context tracking variables. -sub set_up_line_context { - $n_before_ctx_lines = $opt_output ? 0 : ($opt_before_context || 0); - $n_after_ctx_lines = $opt_output ? 0 : ($opt_after_context || 0); +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; @@ -373,49 +646,46 @@ sub set_up_line_context { $is_first_match = 1; - return; -} - -# Adjust context tracking variables when entering a new file. -sub set_up_line_context_for_file { - $printed_line_no = 0; - $after_context_pending = 0; - if ( $opt_heading && !$opt_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; } - return; + return $nmatches; } -=begin Developers -This subroutine jumps through a number of optimization hoops to -try to be fast in the more common use cases of ack. For one thing, -in non-context tracking searches (not using -A, -B, or -C), -conditions that normally would be checked inside the loop happen -outside, resulting in three nearly identical loops for -v, --passthru, -and normal searching. Any changes that happen to one should propagate -to the others if they make sense. The non-context branches also inline -does_match for performance reasons; any relevant changes that happen here -must also happen there. - -=end Developers - -=cut - -sub print_matches_in_resource { - my ( $resource ) = @_; - - my $max_count = $opt_m || -1; - my $nmatches = 0; - my $filename = $resource->name; - my $ors = $opt_print0 ? "\0" : "\n"; +sub print_matches_in_file { + my $file = shift; - my $has_printed_for_this_resource = 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; - $is_iterating = 1; + 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: $!" ); @@ -430,276 +700,319 @@ sub print_matches_in_resource { # 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; + $after_context_pending = 0; + + my $in_range = range_setup(); + while ( <$fh> ) { - if ( does_match( $_ ) && $max_count ) { - if ( !$has_printed_for_this_resource ) { - if ( $opt_break && $has_printed_something ) { - App::Ack::print_blank_line(); - } - if ( $opt_show_filename && $opt_heading ) { - App::Ack::print_filename( $display_filename, $ors ); + chomp; + $match_colno = undef; + + $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o ); + + my $does_match; + if ( $in_range ) { + if ( $opt_v ) { + $does_match = !/$search_re/o; + } + else { + if ( $does_match = /$search_re/o ) { + # @- = @LAST_MATCH_START + # @+ = @LAST_MATCH_END + $match_colno = $-[0] + 1; + } + } + } + + 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 ( $opt_show_filename && $opt_heading ) { + App::Ack::say( $display_filename ); } } print_line_with_context( $filename, $_, $. ); - $has_printed_for_this_resource = 1; + $has_printed_from_this_file = 1; + $stats{linematches}++; $nmatches++; $max_count--; } - elsif ( $opt_passthru ) { - chomp; # XXX Proper newline handling? - # XXX Inline this call? - if ( $opt_break && !$has_printed_for_this_resource && $has_printed_something ) { - App::Ack::print_blank_line(); - } - print_line_with_options( $filename, $_, $., ':' ); - $has_printed_for_this_resource = 1; - } else { - chomp; # XXX Proper newline handling? - print_line_if_context( $filename, $_, $., '-' ); + 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 { - if ( $opt_passthru ) { - local $_; + elsif ( $opt_passthru ) { + local $_ = undef; - while ( <$fh> ) { - $match_column_number = undef; - if ( $opt_v ? !/$opt_regex/o : /$opt_regex/o ) { - if ( !$opt_v ) { - $match_column_number = $-[0] + 1; - } - if ( !$has_printed_for_this_resource ) { - if ( $opt_break && $has_printed_something ) { - App::Ack::print_blank_line(); - } - if ( $opt_show_filename && $opt_heading ) { - App::Ack::print_filename( $display_filename, $ors ); - } - } - print_line_with_context( $filename, $_, $. ); - $has_printed_for_this_resource = 1; - $nmatches++; - $max_count--; + my $in_range = range_setup(); + + while ( <$fh> ) { + chomp; + + $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o ); + + $match_colno = undef; + if ( $in_range && ($opt_v xor /$search_re/o) ) { + if ( !$opt_v ) { + $match_colno = $-[0] + 1; } - else { - chomp; # XXX proper newline handling? - if ( $opt_break && !$has_printed_for_this_resource && $has_printed_something ) { + if ( !$has_printed_from_this_file ) { + if ( $opt_break && $has_printed_from_any_file ) { App::Ack::print_blank_line(); } - print_line_with_options( $filename, $_, $., ':' ); - $has_printed_for_this_resource = 1; + if ( $opt_show_filename && $opt_heading ) { + App::Ack::say( $display_filename ); + } + } + print_line_with_options( $filename, $_, $., ':' ); + $has_printed_from_this_file = 1; + $nmatches++; + $max_count--; + } + else { + if ( $opt_break && !$has_printed_from_this_file && $has_printed_from_any_file ) { + App::Ack::print_blank_line(); } - last unless $max_count != 0; + print_line_with_options( $filename, $_, $., '-', 1 ); + $has_printed_from_this_file = 1; } + + $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o ); + + last if $max_count == 0; } - elsif ( $opt_v ) { - local $_; + } + elsif ( $opt_v ) { + local $_ = undef; - $match_column_number = undef; - while ( <$fh> ) { - if ( !/$opt_regex/o ) { - if ( !$has_printed_for_this_resource ) { - if ( $opt_break && $has_printed_something ) { + $match_colno = undef; + my $in_range = range_setup(); + + while ( <$fh> ) { + chomp; + + $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o ); + + if ( $in_range ) { + if ( !/$search_re/o ) { + 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::print_filename( $display_filename, $ors ); + App::Ack::say( $display_filename ); } } print_line_with_context( $filename, $_, $. ); - $has_printed_for_this_resource = 1; + $has_printed_from_this_file = 1; $nmatches++; $max_count--; } - last unless $max_count != 0; } + + $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o ); + + last if $max_count == 0; } - else { - local $_; + } + else { # Normal search: No context, no -v, no --passthru + local $_ = undef; - while ( <$fh> ) { - $match_column_number = undef; - if ( /$opt_regex/o ) { - $match_column_number = $-[0] + 1; - if ( !$has_printed_for_this_resource ) { - if ( $opt_break && $has_printed_something ) { + my $last_match_lineno; + my $in_range = range_setup(); + + while ( <$fh> ) { + chomp; + + $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o ); + + if ( $in_range ) { + $match_colno = undef; + if ( /$search_re/o ) { + $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::print_filename( $display_filename, $ors ); + App::Ack::say( $display_filename ); + } + } + 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]+$//g; + s/[\r\n]+$//; print_line_with_options( $filename, $_, $., ':' ); - $has_printed_for_this_resource = 1; + $has_printed_from_this_file = 1; $nmatches++; + $stats{linematches}++; $max_count--; + $last_match_lineno = $.; } - last unless $max_count != 0; } - } - } + $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o ); - $is_iterating = 0; + last if $max_count == 0; + } + } return $nmatches; } -sub print_line_with_options { - my ( $filename, $line, $line_no, $separator ) = @_; - $has_printed_something = 1; - $printed_line_no = $line_no; +sub print_line_with_options { + my ( $filename, $line, $lineno, $separator, $skip_coloring ) = @_; - my $ors = $opt_print0 ? "\0" : "\n"; + $has_printed_from_any_file = 1; + $printed_lineno = $lineno; my @line_parts; - if( $opt_color ) { - $filename = Term::ANSIColor::colored($filename, - $ENV{ACK_COLOR_FILENAME}); - $line_no = Term::ANSIColor::colored($line_no, - $ENV{ACK_COLOR_LINENO}); - } - - if($opt_show_filename) { - if( $opt_heading ) { - push @line_parts, $line_no; - } - else { - push @line_parts, $filename, $line_no; + 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; } - - if( $opt_column ) { - push @line_parts, get_match_column(); + if ( $opt_heading ) { + push @line_parts, $lineno; } - } - if( $opt_output ) { - while ( $line =~ /$opt_regex/og ) { - # XXX We need to stop using eval() for --output. See https://github.com/beyondgrep/ack2/issues/421 - my $output = eval $opt_output; - App::Ack::print( join( $separator, @line_parts, $output ), $ors ); + else { + push @line_parts, $filename, $lineno; } + push @line_parts, $colno if $opt_column; } - else { - if ( $opt_color ) { - # This match is redundant, but we need to perfom it in order to get if capture groups are set. - $line =~ /$opt_regex/o; - - 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; - - last if $-[0] == $+[0]; - - for ( my $i = 1; $i < @+; $i++ ) { - my ( $match_start, $match_end ) = ( $-[$i], $+[$i] ); - next unless defined($match_start); - next if $match_start < $previous_match_end; + if ( $opt_output ) { + while ( $line =~ /$search_re/og ) { + my $output = $opt_output; + if ( @special_vars_used_by_opt_output ) { + no strict; - my $substring = substr( $line, - $offset + $match_start, $match_end - $match_start ); - my $substitution = Term::ANSIColor::colored( $substring, - $ENV{ACK_COLOR_MATCH} ); + # Stash copies of the special variables because we can't rely + # on them not changing in the process of doing the s///. - substr( $line, $offset + $match_start, - $match_end - $match_start, $substitution ); - - $previous_match_end = $match_end; # Offsets do not need to be applied. - $offset += length( $substitution ) - length( $substring ); - } - - pos($line) = $+[0] + $offset; - } + 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; } - else { - my $matched = 0; # 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); - last if $match_start == $match_end; + App::Ack::say( join( $separator, @line_parts, $output ) ); + } + } + else { + my $underline = ''; - my $substring = substr( $line, $match_start, - $match_end - $match_start ); - my $substitution = Term::ANSIColor::colored( $substring, - $ENV{ACK_COLOR_MATCH} ); + # 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; - substr( $line, $match_start, $match_end - $match_start, - $substitution ); + my $spaces_needed = $match_start - length $underline; - pos($line) = $match_end + - (length( $substitution ) - length( $substring )); - } - # XXX Why do we do this? - $line .= "\033[0m\033[K" if $matched; + $underline .= (' ' x $spaces_needed); + $underline .= ('^' x $match_length); } } + if ( $opt_color && !$skip_coloring ) { + my $highlighted = 0; # If highlighted, need to escape afterwards. - push @line_parts, $line; - App::Ack::print( join( $separator, @line_parts ), $ors ); - } - - return; -} + 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 iterate { - my ( $resource, $cb ) = @_; + my $substring = substr( $line, $match_start, $match_length ); + my $substitution = Term::ANSIColor::colored( $substring, $ENV{ACK_COLOR_MATCH} ); - $is_iterating = 1; + # Fourth argument replaces the string specified by the first three. + substr( $line, $match_start, $match_length, $substitution ); - my $fh = $resource->open(); - if ( !$fh ) { - if ( $App::Ack::report_bad_filenames ) { - App::Ack::warn( $resource->name . ': ' . $! ); + # 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; } - return; - } - # Check for context before the main loop, so we don't pay for it if we don't need it. - if ( $is_tracking_context ) { - $after_context_pending = 0; + 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; + } + } - while ( <$fh> ) { - last unless $cb->(); - } - } - else { - local $_; + 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; - while ( <$fh> ) { - last unless $cb->(); + App::Ack::print( ' ' x $spaces_needed ); + } + App::Ack::say( $underline ); } } - $is_iterating = 0; - return; } sub print_line_with_context { - my ( $filename, $matching_line, $line_no ) = @_; + my ( $filename, $matching_line, $lineno ) = @_; - my $ors = $opt_print0 ? "\0" : "\n"; - my $is_tracking_context = $opt_after_context || $opt_before_context; - - $matching_line =~ s/[\r\n]+$//g; + $matching_line =~ s/[\r\n]+$//; # Check if we need to print context lines first. - if ( $is_tracking_context ) { - my $before_unprinted = $line_no - $printed_line_no - 1; - if ( !$is_first_match && ( !$printed_line_no || $before_unprinted > $n_before_ctx_lines ) ) { - App::Ack::print('--', $ors); + 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( '--' ); } # We want at most $n_before_ctx_lines of context. @@ -715,12 +1028,12 @@ sub print_line_with_context { # Disable $opt->{column} since there are no matches in the context lines. local $opt_column = 0; - print_line_with_options( $filename, $line, $line_no-$before_unprinted, '-' ); + print_line_with_options( $filename, $line, $lineno-$before_unprinted, '-' ); $before_unprinted--; } } - print_line_with_options( $filename, $matching_line, $line_no, ':' ); + print_line_with_options( $filename, $matching_line, $lineno, ':' ); # We want to get the next $n_after_ctx_lines printed. $after_context_pending = $n_after_ctx_lines; @@ -730,359 +1043,112 @@ sub print_line_with_context { return; } -# Print the line only if it's part of a context we need to display. -sub print_line_if_context { - my ( $filename, $line, $line_no, $separator ) = @_; - - 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, $line, $line_no, $separator ); - --$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; - } - - return; -} - } -# does_match() MUST have an $opt_regex set. - -=begin Developers - -This subroutine is inlined a few places in print_matches_in_resource -for performance reasons, so any changes here must be copied there as -well. - -=end Developers - -=cut - -sub does_match { - my ( $line ) = @_; - - $match_column_number = undef; - - 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; - } - else { - return; - } - } +sub get_match_colno { + return $match_colno; } -sub get_match_column { - return $match_column_number; -} +sub count_matches_in_file { + my $file = shift; + my $bail = shift; # True if we're just checking for existence. -sub resource_has_match { - my ( $resource ) = @_; + my $nmatches = 0; + my $do_scan = 1; - my $has_match = 0; - my $fh = $resource->open(); - if ( !$fh ) { + if ( !$file->open() ) { + $do_scan = 0; if ( $App::Ack::report_bad_filenames ) { - App::Ack::warn( $resource->name . ': ' . $! ); + App::Ack::warn( $file->name . ": $!" ); } } else { - while ( <$fh> ) { - if (/$opt_regex/o xor $opt_v) { - $has_match = 1; - last; + if ( !$opt_v ) { + if ( !$file->may_be_present( $scan_re ) ) { + $do_scan = 0; } } - close $fh; } - return $has_match; -} + if ( $do_scan ) { + $file->reset(); -sub count_matches_in_resource { - my ( $resource ) = @_; + my $in_range = range_setup(); - my $nmatches = 0; - my $fh = $resource->open(); - if ( !$fh ) { - if ( $App::Ack::report_bad_filenames ) { - App::Ack::warn( $resource->name . ': ' . $! ); + 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 ) { + if ( /$search_re/o xor $opt_v ) { + ++$nmatches; + last if $bail; + } + } + $in_range = 0 if ( $in_range && $opt_range_end && /$opt_range_end/o ); + } } - } - else { - while ( <$fh> ) { - ++$nmatches if (/$opt_regex/o xor $opt_v); + else { + while ( <$fh> ) { + chomp; + if ( /$search_re/o xor $opt_v ) { + ++$nmatches; + last if $bail; + } + } } - close $fh; } + $file->close; return $nmatches; } -sub main { - my @arg_sources = App::Ack::ConfigLoader::retrieve_arg_sources(); - my $opt = App::Ack::ConfigLoader::process_args( @arg_sources ); +sub range_setup { + return !$using_ranges || (!$opt_range_start && $opt_range_end); +} - $opt_after_context = $opt->{after_context}; - $opt_before_context = $opt->{before_context}; - $opt_output = $opt->{output}; - $opt_print0 = $opt->{print0}; - $opt_color = $opt->{color}; - $opt_heading = $opt->{heading}; - $opt_show_filename = $opt->{show_filename}; - $opt_regex = $opt->{regex}; - $opt_break = $opt->{break}; - $opt_count = $opt->{count}; - $opt_v = $opt->{v}; - $opt_m = $opt->{m}; - $opt_g = $opt->{g}; - $opt_f = $opt->{f}; - $opt_lines = $opt->{lines}; - $opt_L = $opt->{L}; - $opt_l = $opt->{l}; - $opt_passthru = $opt->{passthru}; - $opt_column = $opt->{column}; - $App::Ack::report_bad_filenames = !$opt->{dont_report_bad_filenames}; +=pod - if ( $opt->{flush} ) { - $| = 1; - } +=encoding UTF-8 - 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; - } - if ( not defined $opt_heading and not defined $opt_break ) { - $opt_heading = $opt_break = $opt->{break} = !App::Ack::output_to_pipe(); - } +=head1 NAME - if ( defined($opt->{H}) || defined($opt->{h}) ) { - $opt_show_filename = $opt->{show_filename} = $opt->{H} && !$opt->{h}; - } +ack - grep-like text finder - if ( my $output = $opt_output ) { - $output =~ s{\\}{\\\\}g; - $output =~ s{"}{\\"}g; - $opt_output = qq{"$output"}; - } +=head1 SYNOPSIS - my $resources; - if ( $App::Ack::is_filter_mode && !$opt->{files_from} ) { # probably -x - $resources = App::Ack::Resources->from_stdin( $opt ); - $opt_regex = shift @ARGV if not defined $opt_regex; - $opt_regex = $opt->{regex} = build_regex( $opt_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 { - $opt_regex = shift @ARGV if not defined $opt_regex; - $opt_regex = $opt->{regex} = build_regex( $opt_regex, $opt ); - } - if ( $opt_regex && $opt_regex =~ /\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; - } - } + ack [options] PATTERN [FILE...] + ack -f [options] [DIRECTORY...] - 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" ); - } - } +=head1 DESCRIPTION - $opt->{file_filter} = _compile_file_filter($opt, \@start); - $opt->{descend_filter} = _compile_descend_filter($opt); +ack is designed as an alternative to F for programmers. - $resources = App::Ack::Resources->from_argv( $opt, \@start ); - } - } - App::Ack::set_up_pager( $opt->{pager} ) if defined $opt->{pager}; +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. - my $ors = $opt_print0 ? "\0" : "\n"; - my $only_first = $opt->{1}; +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 +how to use regular expression but are interested in learning, you may +consult L. If you do not +need or want ack to use regular expressions, please see the +C<-Q>/C<--literal> option. - my $nmatches = 0; - my $total_count = 0; +Ack can also list files that would be searched, without actually +searching them, to let you take advantage of ack's file-type filtering +capabilities. - set_up_line_context(); +=head1 FILE SELECTION -RESOURCES: - while ( my $resource = $resources->next ) { - if ($is_tracking_context) { - set_up_line_context_for_file(); - } - - if ( $opt_f ) { - if ( $opt->{show_types} ) { - show_types( $resource, $ors ); - } - else { - App::Ack::print( $resource->name, $ors ); - } - ++$nmatches; - last RESOURCES if defined($opt_m) && $nmatches >= $opt_m; - } - elsif ( $opt_g ) { - if ( $opt->{show_types} ) { - show_types( $resource, $ors ); - } - else { - local $opt_show_filename = 0; # XXX Why is this local? - - print_line_with_options( '', $resource->name, 0, $ors ); - } - ++$nmatches; - last RESOURCES if defined($opt_m) && $nmatches >= $opt_m; - } - elsif ( $opt_lines ) { - 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; - - local $opt_color = 0; - - iterate( $resource, sub { - chomp; - - if ( $line_numbers{$.} ) { - print_line_with_context( $filename, $_, $. ); - } - elsif ( $opt_passthru ) { - print_line_with_options( $filename, $_, $., ':' ); - } - elsif ( $is_tracking_context ) { - print_line_if_context( $filename, $_, $., '-' ); - } - return 1; - }); - } - elsif ( $opt_count ) { - my $matches_for_this_file = count_matches_in_resource( $resource ); - - if ( not $opt_show_filename ) { - $total_count += $matches_for_this_file; - next RESOURCES; - } - - if ( !$opt_l || $matches_for_this_file > 0) { - if ( $opt_show_filename ) { - 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 ); - - if ( $opt_L ? !$is_match : $is_match ) { - App::Ack::print( $resource->name, $ors ); - ++$nmatches; - - last RESOURCES if $only_first; - last RESOURCES if defined($opt_m) && $nmatches >= $opt_m; - } - } - else { - $nmatches += print_matches_in_resource( $resource, $opt ); - if ( $nmatches && $only_first ) { - last RESOURCES; - } - } - } - - if ( $opt_count && !$opt_show_filename ) { - App::Ack::print( $total_count, "\n" ); - } - - close $App::Ack::fh; - App::Ack::exit_from_ack( $nmatches ); -} - -=pod - -=encoding UTF-8 - -=head1 NAME - -ack - grep-like text finder - -=head1 SYNOPSIS - - ack [options] PATTERN [FILE...] - ack -f [options] [DIRECTORY...] - -=head1 DESCRIPTION - -ack is designed as an alternative to F for programmers. - -ack searches the named input files or directories 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 -how to use regular expression but are interested in learning, you may -consult L. If you do not -need or want ack to use regular expressions, please see the -C<-Q>/C<--literal> option. - -Ack can also list files that would be searched, without actually -searching them, to let you take advantage of ack's file-type filtering -capabilities. - -=head1 FILE SELECTION - -If files are not specified for searching, either on the command -line or piped in with the C<-x> option, I delves into -subdirectories selecting files for searching. +If files are not specified for searching, either on the command +line or piped in with the C<-x> option, I delves into +subdirectories selecting files for searching. I is intelligent about the files it searches. It knows about certain file types, based on both the extension on the file and, @@ -1125,16 +1191,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 unitl 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. + +For example, if you wanted to search all HTML files up until the first +instance of the C<< >>, you could do + + ack foo --range-end='' + +Or to search after Perl's `__DATA__` or `__END__` markers, you would do + + ack pattern --range-end='^__(END|DATA)__' -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. +It's possible for a range to start and stop on the same line. For example -E.g., searching through huge files looking for regexes that can be -expressed with I syntax should be quicker with I. + --range-start='' --range-end='' -If your script or parent program uses I C<--quiet> or C<--silent> -or needs exit 2 on IO error, use I. +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 @@ -1190,6 +1341,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. @@ -1233,6 +1388,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. @@ -1290,17 +1450,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> @@ -1322,22 +1494,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> @@ -1359,7 +1523,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> @@ -1372,14 +1537,62 @@ No descending into subdirectories. =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> @@ -1389,7 +1602,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> @@ -1398,18 +1611,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. @@ -1419,16 +1669,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. @@ -1443,31 +1700,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. @@ -1476,9 +1736,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> @@ -1486,29 +1763,16 @@ Display version and copyright information. =item B<-w>, B<--word-regexp> -=item B<-w>, B<--word-regexp> - -Turn on "words mode". This sometimes matches a whole word, but the -semantics is quite subtle. If the passed regexp begins with a word -character, then a word boundary is required before the match. If the -passed regexp ends with a word character, or with a word character -followed by newline, then a word boundary is required after the match. - -Thus, for example, B<-w> with the regular expression C will not -match the strings C or C. However, if the regular -expression is C<(ox|ass)> then it will match those strings. Because -the regular expression's first character is C<(>, the B<-w> flag has -no effect at the start, and because the last character is C<)>, it has -no effect at the end. - -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 @@ -1570,7 +1834,7 @@ 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 +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 @@ -1604,9 +1868,9 @@ The following does B work in the F<.ackrc> file: 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 @@ -1632,7 +1896,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: @@ -1650,7 +1914,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 @@ -1665,43 +1987,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 @@ -1724,27 +2029,6 @@ If you are not on Windows, you never need to use C. =back -=head1 AVAILABLE COLORS - -F 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. - -=head2 Foreground colors - - black red green yellow blue magenta cyan white - - bright_black bright_red bright_green bright_yellow - bright_blue bright_magenta bright_cyan bright_white - -=head2 Background colors - - on_black on_red on_green on_yellow - on_blue on_magenta on_cyan on_white - - on_bright_black on_bright_red on_bright_green on_bright_yellow - on_bright_blue on_bright_magenta on_bright_cyan on_bright_white - =head1 ACK & OTHER TOOLS =head2 Simple vim integration @@ -1784,13 +2068,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 @@ -1800,114 +2084,151 @@ 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 TIPS +=head1 ACKRC LOCATION SEMANTICS -=head2 Use the F<.ackrc> file. +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) -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. +=over 4 -=head2 Use F<-f> for working with big codesets +=item * -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: +Defaults are loaded from App::Ack::ConfigDefaults. This can be omitted +using C<--ignore-ack-defaults>. - # Change all "this" to "that" in all Perl files in a tree. - ack -f --perl | xargs perl -p -i -e's/this/that/g' +=item * Global ackrc -or if you prefer: +Options are then loaded from the global ackrc. This is located at +C on Unix-like systems. - perl -p -i -e's/this/that/g' $(ack -f --perl) +Under Windows XP and earlier, the global ackrc is at +C -=head2 Use F<-Q> when in doubt about metacharacters +Under Windows Vista/7, the global ackrc is at +C -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... +The C<--noenv> option prevents all ackrc files from being loaded. -=head2 Use ack to watch log files +=item * User ackrc -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. +Options are then loaded from the user's ackrc. This is located at +C<$HOME/.ackrc> on Unix-like systems. - ack -Q aa.bb.cc.dd /path/to/access.log | ack -Q -B5 troublesome.gif +Under Windows XP and earlier, the user's ackrc is at +C. -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. +Under Windows Vista/7, the user's ackrc is at +C. -=head2 Examples of F<--output> +If you want to load a different user-level ackrc, it may be specified +with the C<$ACKRC> environment variable. -Following variables are useful in the expansion string: +The C<--noenv> option prevents all ackrc files from being loaded. -=over 4 +=item * Project ackrc -=item C<$&> +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>. -The whole string matched by PATTERN. +=item * --ackrc -=item C<$1>, C<$2>, ... +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. -The contents of the 1st, 2nd ... bracketed group in PATTERN. +=item * Command line -=item C<$`> +Options are then loaded from the command line. -The string before the match. +=back -=item C<$'> +=head1 BUGS & ENHANCEMENTS -The string after the match. +ack is based at GitHub at L -=back +Please report any bugs or feature requests to the issues list at +Github: L. -For more details and other variables see -L. +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. -This example shows how to add text around a particular pattern -(in this case adding _ around word with "e") +To suggest enhancements, please submit an issue at +L. Also read the +F file in the ack code repository. - 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 +Also, feel free to discuss your issues on the ack mailing +list at L. -This shows how to pick out particular parts of a match using ( ) within regular expression. +=head1 SUPPORT - ack '=head(\d+)\s+(.*)' --output=' $1 : $2' - input file contains "=head1 NAME" - output "1 : NAME" +Support for and information about F can be found at: -=head1 COMMUNITY +=over 4 -There are ack mailing lists and a Slack channel for ack. See -L for details. +=item * The ack homepage -=head1 FAQ +L -=head2 Why isn't ack finding a match in (some file)? +=item * Source repository -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 +L + +=item * The ack issues list at Github + +L + +=item * The ack announcements mailing list + +L + +=item * The ack users' mailing list + +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)? + +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, 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. -NOTE: If you're using an old ack before 2.0, it's probably because it's of -a type that ack doesn't recognize. In ack 1.x, the searching behavior is -driven by filetype. B You can use the C<--show-types> switch to show -which type ack thinks each file is. - =head2 Wouldn't it be great if F did search & replace? No, ack will always be read-only. Perl has a perfectly good way @@ -1922,13 +2243,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? @@ -1979,285 +2306,23 @@ can search for C<.{32767}> but not C<.{32768}>. =head2 Ack does "X" and shouldn't, should it? -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 -bring it up on the ack-users mailing list. - -=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 - -=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. - -Under Windows XP and earlier, the global ackrc is at -C - -Under Windows Vista/7, the global ackrc is at -C - -The C<--noenv> option prevents all ackrc files from being loaded. - -=item * User ackrc - -Options are then loaded from the user's ackrc. This is located at -C<$HOME/.ackrc> on Unix-like systems. - -Under Windows XP and earlier, the user's ackrc is at -C. - -Under Windows Vista/7, the user's ackrc is at -C. - -If you want to load a different user-level ackrc, it may be specified -with the C<$ACKRC> environment variable. - -The C<--noenv> option prevents all ackrc files from being loaded. - -=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>. - -=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 * - -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. - -=item * - -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. +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. -=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 * MetaCPAN - -L - -=item * Git source repository - -L - -=back +=cut =head1 ACKNOWLEDGEMENTS How appropriate to have Inowledgements! Thanks to everyone who has contributed to ack in any way, including -Michele Campeotto, +Dan Book, +Tomasz Konojacki, +Salomon Smeke, +M. Scott Ford, +Anders Eriksson, H.Merijn Brand, Duke Leto, Gerhard Poul, @@ -2371,17 +2436,23 @@ Ricardo Signes, Pete Krawczyk and Rob Hoelz. +=head1 AUTHOR + +Andy Lester, C<< >> + =head1 COPYRIGHT & LICENSE -Copyright 2005-2017 Andy Lester. +Copyright 2005-2020 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; @@ -2391,9 +2462,11 @@ use strict; our $VERSION; our $COPYRIGHT; BEGIN { - $VERSION = '2.22'; - $COPYRIGHT = 'Copyright 2005-2017 Andy Lester.'; + $VERSION = 'v3.3.1'; # Check https://beyondgrep.com/ for updates + $COPYRIGHT = 'Copyright 2005-2020 Andy Lester.'; } +our $STANDALONE = 0; +our $ORIGINAL_PROGRAM_NAME; our $fh; @@ -2410,33 +2483,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' || $^O eq 'msys'); $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" ); } @@ -2452,11 +2514,6 @@ sub _my_program { } - -sub filetypes_supported { - return keys %mappings; -} - sub thpppt { my $y = q{_ /|,\\'!.x',=(www)=, U }; $y =~ tr/,x!w/\nOo_/; @@ -2576,10 +2633,6 @@ sub _pic_decode { 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] @@ -2589,37 +2642,45 @@ 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. -Example: ack -i select +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. + +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. 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 @@ -2648,23 +2709,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. @@ -2673,55 +2736,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. ack's home page is at https://beyondgrep.com/ The full ack manual is available by running "ack --man". -This is version $VERSION of ack. Run "ack --version" for full version info. +This is version $App::Ack::VERSION of ack. Run "ack --version" for full version info. END_OF_HELP return; @@ -2733,16 +2804,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; @@ -2754,364 +2825,299 @@ 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, - }); - - 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 ); - return <<"END_OF_VERSION"; -ack ${VERSION} -Running under Perl $ver at $this_perl +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). -$copyright +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. -This program is free software. You may modify or distribute it -under the terms of the Artistic License v2.0. -END_OF_VERSION -} +Run ack --help-rgb-colors for a chart of the RGB colors. +END_OF_HELP -sub print_version_statement { - App::Ack::print( get_version_statement() ); + _show_color_grid(); return; } -sub get_copyright { - return $COPYRIGHT; -} - - -sub print { print {$fh} @_; return; } -sub print_blank_line { App::Ack::print( "\n" ); return; } -sub print_filename { App::Ack::print( $_[0], $_[1] ); return; } - -sub set_up_pager { - my $command = shift; - return if App::Ack::output_to_pipe(); +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). - my $pager; - if ( not open( $pager, '|-', $command ) ) { - App::Ack::die( qq{Unable to pipe to pager "$command": $!} ); - } - $fh = $pager; +Colors may be specified as "rggNNN" where "NNN" is a triplet of digits +from 0 to 5 specifying the intensity of red, green and blue, respectively. - return; -} +Here is a grid of the 216 possible values for NNN. +END_OF_HELP -sub output_to_pipe { - return $output_to_pipe; -} + _show_rgb_grid(); + App::Ack::say( 'Here are the 216 possible colors with the "reverse" modifier applied.', "\n" ); -sub exit_from_ack { - my $nmatches = shift; + _show_rgb_grid( 'reverse' ); - my $rc = $nmatches ? 0 : 1; - exit $rc; + return; } +sub _show_color_grid { + my $cell_width = 7; -1; # End of App::Ack -package App::Ack::Resource; - - -use warnings; -use strict; -use overload - '""' => 'name'; + 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 + ); -sub new { - my $class = shift; - my $filename = shift; + App::Ack::say( + _color_cell( '' ), + map { _color_cell( '-' x $cell_width ) } @fg_colors + ); - my $self = bless { - filename => $filename, - fh => undef, - opened => 0, - }, $class; + for my $bg ( '', @bg_colors ) { + App::Ack::say( + _color_cell( '' ), + ( map { _color_cell( $_, "$_ $bg" ) } @fg_colors ), + $bg + ); - if ( $self->{filename} eq '-' ) { - $self->{fh} = *STDIN; - $self->{opened} = 1; + App::Ack::say( + _color_cell( 'bold' ), + ( map { _color_cell( $_, "bold $_ $bg" ) } @fg_colors ), + $bg + ); + App::Ack::say(); } - return $self; -} - - - -sub name { - return $_[0]->{filename}; + return; } +sub _color_cell { + my $text = shift; + my $color = shift; -sub basename { - my ( $self ) = @_; - - # XXX Definedness? Pre-populate the slot with an undef? - unless ( exists $self->{basename} ) { - $self->{basename} = (File::Spec->splitpath($self->name))[2]; - } + my $cell_width = 7; + $text = sprintf( '%-*s', $cell_width, $text ); - return $self->{basename}; + return ($color ? Term::ANSIColor::colored( $text, $color ) : $text) . ' '; } +sub _show_rgb_grid { + my $modifier = shift // ''; -sub open { - my ( $self ) = @_; - - if ( !$self->{opened} ) { - if ( open $self->{fh}, '<', $self->{filename} ) { - $self->{opened} = 1; - } - else { - $self->{fh} = undef; - } - } - - return $self->{fh}; -} + 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 -sub needs_line_scan { - my $self = shift; - my $opt = shift; +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 - return 1 if $opt->{v}; +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 - my $size = -s $self->{fh}; - if ( $size == 0 ) { - return 0; - } - elsif ( $size > 100_000 ) { - return 1; - } +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 - 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 ); +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 - return $buffer =~ /$opt->{regex}/m; -} +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 ); -sub reset { - my $self = shift; + return; +} - # 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}: $!" ); - } +sub show_man { + require Pod::Usage; + Pod::Usage::pod2usage({ + -input => $App::Ack::ORIGINAL_PROGRAM_NAME, + -verbose => 2, + -exitval => 0, + }); return; } -sub close { - my $self = shift; - - # Return if we haven't opened the file yet. - if ( !defined($self->{fh}) ) { - return; - } +sub get_version_statement { + require Config; - if ( !close($self->{fh}) && $App::Ack::report_bad_filenames ) { - App::Ack::warn( $self->name() . ": $!" ); + 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 ); - $self->{opened} = 0; - - return; -} - + 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 -sub clone { - my ( $self ) = @_; +$copyright - return __PACKAGE__->new($self->name); +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 firstliney { - my ( $self ) = @_; - - if ( !exists $self->{firstliney} ) { - my $fh = $self->open(); - if ( !$fh ) { - if ( $App::Ack::report_bad_filenames ) { - App::Ack::warn( $self->name . ': ' . $! ); - } - return ''; - } +sub set_up_pager { + my $command = shift; - 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; + return if App::Ack::output_to_pipe(); - $self->close; + my $pager; + if ( not open( $pager, '|-', $command ) ) { + App::Ack::die( qq{Unable to pipe to pager "$command": $!} ); } + $fh = $pager; - return $self->{firstliney}; + return; } -1; -package App::Ack::Resources; - - - -use Errno qw(EACCES); -use warnings; -use strict; - -sub _generate_error_handler { - my $opt = shift; - - if ( $opt->{dont_report_bad_filenames} ) { - return sub { - my $msg = shift; - if ( $! == EACCES ) { - return; - } - App::Ack::warn( $msg ); - }; - } - else { - return sub { - my $msg = shift; - App::Ack::warn( $msg ); - }; - } +sub output_to_pipe { + return $output_to_pipe; } -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 => _generate_error_handler($opt), - warning_handler => sub {}, - sort_files => $opt->{sort_files}, - follow_symlinks => $opt->{follow}, - }, @{$start} ); +sub exit_from_ack { + my $nmatches = shift; - return $self; + my $rc = $nmatches ? 0 : 1; + exit $rc; } -sub from_file { - my $class = shift; - my $opt = shift; - my $file = shift; +sub show_types { + my $file = shift; - my $iter = - File::Next::from_file( { - error_handler => _generate_error_handler($opt), - warning_handler => _generate_error_handler($opt), - sort_files => $opt->{sort_files}, - }, $file ) or return undef; + my @types = filetypes( $file ); + my $arrow = @types ? ' => ' : ' =>'; + App::Ack::say( $file->name, $arrow, join( ',', @types ) ); - return bless { - iter => $iter, - }, $class; + return; } -# 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; +sub filetypes { + my ( $file ) = @_; - my $has_been_called = 0; + my @matches; - $self->{iter} = sub { - if ( !$has_been_called ) { - $has_been_called = 1; - return '-'; + 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; + } } - return; - }; + } - return $self; + # https://metacpan.org/pod/distribution/Perl-Critic/lib/Perl/Critic/Policy/Subroutines/ProhibitReturnSort.pm + @matches = sort @matches; + return @matches; } -sub next { - my $self = shift; - my $file = $self->{iter}->() or return; +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; - return App::Ack::Resource->new( $file ); + return 0; } -1; + +1; # End of App::Ack package App::Ack::ConfigDefault; use warnings; @@ -3150,52 +3156,52 @@ sub _options_block { ### Directories to ignore # Bazaar -# http://bazaar.canonical.com/ +# https://bazaar.canonical.com/ --ignore-directory=is:.bzr # Codeville -# http://freecode.com/projects/codeville +# http://freshmeat.sourceforge.net/projects/codeville --ignore-directory=is:.cdv # Interface Builder (Xcode) -# http://en.wikipedia.org/wiki/Interface_Builder +# https://en.wikipedia.org/wiki/Interface_Builder --ignore-directory=is:~.dep --ignore-directory=is:~.dot --ignore-directory=is:~.nib --ignore-directory=is:~.plst # Git -# http://git-scm.com/ +# https://git-scm.com/ --ignore-directory=is:.git -# When using submodules, .git is a file. +# When submodules are used, .git is a file. --ignore-file=is:.git # Mercurial -# http://mercurial.selenic.com/ +# https://www.mercurial-scm.org/ --ignore-directory=is:.hg -# quilt -# http://directory.fsf.org/wiki/Quilt +# Quilt +# https://directory.fsf.org/wiki/Quilt --ignore-directory=is:.pc # Subversion -# http://subversion.tigris.org/ +# https://subversion.apache.org/ --ignore-directory=is:.svn # Monotone -# http://www.monotone.ca/ +# https://www.monotone.ca/ --ignore-directory=is:_MTN # CVS -# http://savannah.nongnu.org/projects/cvs +# https://savannah.nongnu.org/projects/cvs --ignore-directory=is:CVS # RCS -# http://www.gnu.org/software/rcs/ +# https://www.gnu.org/software/rcs/ --ignore-directory=is:RCS # SCCS -# http://en.wikipedia.org/wiki/Source_Code_Control_System +# https://en.wikipedia.org/wiki/Source_Code_Control_System --ignore-directory=is:SCCS # darcs @@ -3206,7 +3212,7 @@ sub _options_block { --ignore-directory=is:_sgbak # autoconf -# http://www.gnu.org/software/autoconf/ +# https://www.gnu.org/software/autoconf/ --ignore-directory=is:autom4te.cache # Perl module building @@ -3221,17 +3227,26 @@ sub _options_block { --ignore-directory=is:node_modules # CMake cache -# http://www.cmake.org/ +# https://www.cmake.org/ --ignore-directory=is:CMakeFiles # Eclipse workspace folder -# http://eclipse.org/ +# https://eclipse.org/ --ignore-directory=is:.metadata # Cabal (Haskell) sandboxes -# http://www.haskell.org/cabal/users-guide/installing-packages.html +# 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 @@ -3241,11 +3256,11 @@ sub _options_block { # Emacs swap files --ignore-file=match:/^#.+#$/ -# vi/vim swap files http://vim.org/ ---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 --ignore-file=match:/[.-]min[.]js$/ @@ -3265,32 +3280,36 @@ sub _options_block { # 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 compiles 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/ +# C extensions +--ignore-file=ext:so -# Perl tests ---type-add=perltest:ext:t +# Compiled gettext files +--ignore-file=ext:mo + +### Filetypes defined # Makefiles -# http://www.gnu.org/s/make/ +# 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/ +# https://rake.rubyforge.org/ --type-add=rake:is:Rakefile # CMake -# http://www.cmake.org/ +# https://cmake.org/ --type-add=cmake:is:CMakeLists.txt --type-add=cmake:ext:cmake @@ -3298,29 +3317,29 @@ sub _options_block { --type-add=actionscript:ext:as,mxml # Ada -# http://www.adaic.org/ +# https://www.adaic.org/ --type-add=ada:ext:ada,adb,ads # ASP -# http://msdn.microsoft.com/en-us/library/aa286483.aspx +# 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/ +# 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 +# https://en.wikipedia.org/wiki/ColdFusion --type-add=cfmx:ext:cfc,cfm,cfml # Clojure -# http://clojure.org/ +# https://clojure.org/ --type-add=clojure:ext:clj,cljs,edn,cljc # C @@ -3331,7 +3350,7 @@ sub _options_block { --type-add=hh:ext:h # CoffeeScript -# http://coffeescript.org/ +# https://coffeescript.org/ --type-add=coffeescript:ext:coffee # C++ @@ -3344,47 +3363,47 @@ sub _options_block { --type-add=csharp:ext:cs # CSS -# http://www.w3.org/Style/CSS/ +# https://www.w3.org/Style/CSS/ --type-add=css:ext:css # Dart -# http://www.dartlang.org/ +# https://dart.dev/ --type-add=dart:ext:dart # Delphi -# http://en.wikipedia.org/wiki/Embarcadero_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/ +# https://elixir-lang.org/ --type-add=elixir:ext:ex,exs # Emacs Lisp -# http://www.gnu.org/software/emacs +# https://www.gnu.org/software/emacs --type-add=elisp:ext:el # Erlang -# http://www.erlang.org/ +# https://www.erlang.org/ --type-add=erlang:ext:erl,hrl # Fortran -# http://en.wikipedia.org/wiki/Fortran +# https://en.wikipedia.org/wiki/Fortran --type-add=fortran:ext:f,f77,f90,f95,f03,for,ftn,fpp # Go -# http://golang.org/ +# https://golang.org/ --type-add=go:ext:go # Groovy -# http://groovy.codehaus.org/ +# https://www.groovy-lang.org/ --type-add=groovy:ext:groovy,gtmpl,gpp,grunit,gradle # GSP -# http://groovy.codehaus.org/GSP +# https://gsp.grails.org/ --type-add=gsp:ext:gsp # Haskell -# http://www.haskell.org/ +# https://www.haskell.org/ --type-add=haskell:ext:hs,lhs # HTML @@ -3395,18 +3414,18 @@ sub _options_block { --type-add=jade:ext:jade # Java -# http://www.oracle.com/technetwork/java/index.html +# 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 +# https://www.oracle.com/technetwork/java/javaee/jsp/index.html --type-add=jsp:ext:jsp,jspx,jspf,jhtm,jhtml # JSON -# http://www.json.org/ +# https://json.org/ --type-add=json:ext:json # Kotlin @@ -3418,14 +3437,25 @@ sub _options_block { --type-add=less:ext:less # Common Lisp -# http://common-lisp.net/ +# https://common-lisp.net/ --type-add=lisp:ext:lisp,lsp # Lua -# http://www.lua.org/ +# 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 @@ -3433,59 +3463,62 @@ sub _options_block { --type-add=objcpp:ext:mm,h # OCaml -# http://caml.inria.fr/ +# 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/ + +# Perl tests +--type-add=perltest:ext:t -# Parrot -# http://www.parrot.org/ ---type-add=parrot:ext:pir,pasm,pmc,ops,pod,pg,tg +# Perl's Plain Old Documentation format, POD +--type-add=pod:ext:pod # PHP -# http://www.php.net/ +# https://www.php.net/ --type-add=php:ext:php,phpt,php3,php4,php5,phtml --type-add=php:firstlinematch:/^#!.*\bphp/ # Plone -# http://plone.org/ +# https://plone.org/ --type-add=plone:ext:pt,cpt,metadata,cpy,py # Python -# http://www.python.org/ +# https://www.python.org/ --type-add=python:ext:py --type-add=python:firstlinematch:/^#!.*\bpython/ # R -# http://www.r-project.org/ +# https://www.r-project.org/ --type-add=rr:ext:R # reStructured Text -# http://docutils.sourceforge.net/rst.html +# https://docutils.sourceforge.io/rst.html --type-add=rst:ext:rst # Ruby -# http://www.ruby-lang.org/ +# 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/ +# https://www.rust-lang.org/ --type-add=rust:ext:rs # Sass -# http://sass-lang.com +# https://sass-lang.com --type-add=sass:ext:sass,scss # Scala -# http://www.scala-lang.org/ +# https://www.scala-lang.org/ --type-add=scala:ext:scala # Scheme -# http://groups.csail.mit.edu/mac/projects/scheme/ +# https://groups.csail.mit.edu/mac/projects/scheme/ --type-add=scheme:ext:scm,ss # Shell @@ -3497,33 +3530,41 @@ sub _options_block { --type-add=smalltalk:ext:st # Smarty -# http://www.smarty.net/ +# https://www.smarty.net/ --type-add=smarty:ext:tpl # SQL -# http://www.iso.org/iso/catalogue_detail.htm?csnumber=45498 +# https://www.iso.org/standard/45498.html --type-add=sql:ext:sql,ctl # Stylus -# http://learnboost.github.io/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 -# http://www.tcl.tk/ +# 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 (Perl) -# http://template-toolkit.org/ ---type-add=tt:ext:tt,tt2,ttml +# http//template-toolkit.org/ +--type-add=ttml:ext:tt,tt2,ttml + +# Typescript +# https://www.typescriptlang.org/ +--type-add=ts:ext:ts,tsx # Visual Basic --type-add=vb:ext:bas,cls,frm,ctl,vb,resx @@ -3536,16 +3577,16 @@ sub _options_block { --type-add=vhdl:ext:vhd,vhdl # Vim -# http://www.vim.org/ +# https://www.vim.org/ --type-add=vim:ext:vim # XML -# http://www.w3.org/TR/REC-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/ +# https://yaml.org/ --type-add=yaml:ext:yaml,yml HERE $lines =~ s/==VERSION==/$App::Ack::VERSION/sm; @@ -3561,7 +3602,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'; @@ -3577,16 +3618,18 @@ sub _remove_redundancies { my @configs = @_; my %seen; + my @uniq; foreach my $config (@configs) { - my $key = $config->{path}; + 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; } - undef $config if $seen{$key}++; + push( @uniq, $config ) unless $seen{$key}++; } - return grep { defined } @configs; + return @uniq; } @@ -3597,9 +3640,8 @@ 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 @@ -3634,9 +3676,9 @@ sub find_config_files { $cwd =~ /(.+)/; $cwd = $1; my @dirs = File::Spec->splitdir( $cwd ); - while(@dirs) { + while ( @dirs ) { my $ackrc = _check_for_ackrc(@dirs); - if(defined $ackrc) { + if ( defined $ackrc ) { push @config_files, { project => 1, path => $ackrc }; last; } @@ -3647,67 +3689,28 @@ sub find_config_files { return _remove_redundancies( @config_files ); } - - -sub read_rcfile { - my $file = shift; - - return unless defined $file && -e $file; - - my @lines; - - open( my $fh, '<', $file ) or App::Ack::die( "Unable to read $file: $!" ); - while ( my $line = <$fh> ) { - chomp $line; - $line =~ s/^\s+//; - $line =~ s/\s+$//; - - next if $line eq ''; - next if $line =~ /^\s*#/; - - push( @lines, $line ); - } - close $fh or App::Ack::die( "Unable to close $file: $!" ); - - return @lines; -} - 1; package App::Ack::ConfigLoader; use strict; use warnings; +use 5.010; -use Carp 1.04 (); -use Getopt::Long 2.38 (); +use File::Spec 3.00 (); +use Getopt::Long 2.39 (); use Text::ParseWords 3.1 (); +sub opt_parser { + my @opts = @_; -my @INVALID_COMBINATIONS; - -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], + my @standard = qw( + default + bundling + no_auto_help + no_auto_version + no_ignore_case ); + return Getopt::Long::Parser->new( config => [ @standard, @opts ] ); } sub _generate_ignore_dir { @@ -3718,7 +3721,7 @@ sub _generate_ignore_dir { return sub { my ( undef, $dir ) = @_; - $dir = App::Ack::remove_dir_sep( $dir ); + $dir = _remove_directory_separator( $dir ); if ( $dir !~ /:/ ) { $dir = 'is:' . $dir; } @@ -3726,7 +3729,7 @@ sub _generate_ignore_dir { my ( $filter_type, $args ) = split /:/, $dir, 2; if ( $filter_type eq 'firstlinematch' ) { - Carp::croak( qq{Invalid filter specification "$filter_type" for option '$option_name'} ); + App::Ack::die( qq{Invalid filter specification "$filter_type" for option '$option_name'} ); } my $filter = App::Ack::Filter->create_filter($filter_type, split(/,/, $args)); @@ -3738,20 +3741,13 @@ sub _generate_ignore_dir { $collection = $opt->{idirs}[-1]; if ( $is_inverted ) { - # XXX this relies on invert of an inverted filter - # to return the original - $collection = $collection->invert() + # This relies on invert of an inverted filter to return the original. + $collection = $collection->invert(); } } else { $collection = App::Ack::Filter::Collection->new(); - - if ( $is_inverted ) { - push @{ $opt->{idirs} }, $collection->invert(); - } - else { - push @{ $opt->{idirs} }, $collection; - } + push @{ $opt->{idirs} }, $is_inverted ? $collection->invert() : $collection; } $collection->add($filter); @@ -3762,7 +3758,19 @@ sub _generate_ignore_dir { }; } -sub process_filter_spec { + +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 { my ( $spec ) = @_; if ( $spec =~ /^(\w+):(\w+):(.*)/ ) { @@ -3782,12 +3790,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; @@ -3809,21 +3817,15 @@ sub uninvert_filter { } -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; @@ -3835,7 +3837,7 @@ sub process_filetypes { @filters = map { $_->invert() } @filters; } else { - uninvert_filter( $opt, @filters ); + _uninvert_filter( $opt, @filters ); } push @{ $opt->{'filters'} }, @filters; @@ -3845,7 +3847,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 ]; @@ -3874,24 +3876,21 @@ sub process_filetypes { 'type-del=s' => $delete_spec, ); + my $p = opt_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; + $p->getoptionsfromarray( $args, %type_arg_specs ); } else { ( undef, $source->{contents} ) = - Getopt::Long::GetOptionsFromString($args, %type_arg_specs); + $p->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; @@ -3901,54 +3900,49 @@ sub process_filetypes { } -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 = <<'EOT'; -You don't need -a, ack 1.x users. This is because ack 2.x has --k/--known-types which makes it only select files of known types, rather -than any text file (which is the behavior of ack 1.x). -If you're surprised to see this message because you didn't put -a on the -command line, you may have options in an .ackrc, or in the ACKRC_OPTIONS -environment variable. Try using the --dump flag to help find it. -EOT + sub _type_handler { + my ( $getopt, $value ) = @_; + + my $cb_value = 1; + if ( $value =~ s/^no// ) { + $cb_value = 0; + } + my $callback; + { + no warnings; + $callback = $extra_specs->{ $value . '!' }; + } - sub _context_value { - my $val = shift; + if ( $callback ) { + $callback->( $getopt, $cb_value ); + } + else { + App::Ack::die( "Unknown type '$value'" ); + } - # Contexts default to 2. - return (!defined($val) || ($val < 0)) ? 2 : $val; + return; } return { 1 => sub { $opt->{1} = $opt->{m} = 1 }, - 'A|after-context:-1' => sub { shift; $opt->{after_context} = _context_value(shift) }, - 'B|before-context:-1' => sub { shift; $opt->{before_context} = _context_value(shift) }, - 'C|context:-1' => sub { shift; $opt->{before_context} = $opt->{after_context} = _context_value(shift) }, - '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 ) = @_; @@ -3959,29 +3953,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}, + '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 ( undef, $file ) = @_; - my ( $filter_type, $args ) = split /:/, $file, 2; + my ( $filter_type, $args ) = split /:/, $file, 2; - my $filter = App::Ack::Filter->create_filter($filter_type, split(/,/, $args)); + 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); - }, - 'lines=s' => sub { shift; my $val = shift; push @{$opt->{lines}}, $val }, + if ( !$opt->{ifiles} ) { + $opt->{ifiles} = App::Ack::Filter::Collection->new(); + } + $opt->{ifiles}->add($filter); + }, 'l|files-with-matches' => \$opt->{l}, 'L|files-without-matches' @@ -4000,108 +3993,100 @@ EOT 'nopager' => sub { $opt->{pager} = undef }, '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 ) = @_; +sub _context_value { + my $val = shift; - # Start with default options, minus some annoying ones. - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); - Getopt::Long::Configure( - 'bundling', - 'no_ignore_case', - ); + # 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( + my $p = opt_parser( 'pass_through' ); + $p->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 ); + my $p = opt_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 { - die "Options --output, --pager and --match are forbidden in project .ackrc files.\n"; + my $name = shift; + App::Ack::die( "Option --$name is forbidden in .ackrc files." ); }; $args_for_source = { %{$args_for_source}, 'output=s' => $illegal, - 'pager: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}, + 'pager:s' => $illegal, + }; + } my $ret; if ( ref($args) ) { - local @ARGV = @{$args}; - $ret = Getopt::Long::GetOptions( %{$args_for_source} ); - @{$args} = @ARGV; + $ret = $p->getoptionsfromarray( $args, %{$args_for_source} ); } else { ( $ret, $source->{contents} ) = - Getopt::Long::GetOptionsFromString( $args, %{$args_for_source} ); + $p->getoptionsfromstring( $args, %{$args_for_source} ); } if ( !$ret ) { if ( !$is_help_types_active ) { @@ -4120,47 +4105,24 @@ sub process_other { } -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; } }; @@ -4170,6 +4132,7 @@ sub explode_sources { delete $arg_spec->{$arg}; }; + my $p = opt_parser( 'pass_through' ); foreach my $source (@{$sources}) { my ( $name, $options ) = @{$source}{qw/name contents/}; if ( ref($options) ne 'ARRAY' ) { @@ -4184,13 +4147,12 @@ sub explode_sources { $j--; my @copy = @chunk; - local @ARGV = @chunk; - Getopt::Long::GetOptions( + $p->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, @@ -4203,7 +4165,7 @@ sub explode_sources { } -sub compare_opts { +sub _compare_opts { my ( $a, $b ) = @_; my $first_a = $a->[0]; @@ -4216,36 +4178,36 @@ sub compare_opts { } -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; @@ -4261,34 +4223,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', - ); + my $p = opt_parser( 'no_auto_abbrev', 'pass_through' ); foreach my $index ( $default_index + 1 .. $#{$sources} ) { - my ( $name, $args ) = @{$sources->[$index]}{qw/name contents/}; + my $args = $sources->[$index]->{contents}; if (ref($args)) { - local @ARGV = @{$args}; - Getopt::Long::GetOptions( + $p->getoptionsfromarray( $args, 'ignore-ack-defaults' => \$should_remove, ); - @{$args} = @ARGV; } else { - ( undef, $sources->[$index]{contents} ) = Getopt::Long::GetOptionsFromString($args, + ( undef, $sources->[$index]{contents} ) = $p->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}; @@ -4297,59 +4248,6 @@ sub remove_default_options_if_needed { } -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"; - } - } - } - } - - return; -} - - sub process_args { my $arg_sources = \@_; @@ -4357,32 +4255,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; + my $p = opt_parser( 'pass_through' ); + $p->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} ||= []); @@ -4401,17 +4311,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'); - - Getopt::Long::GetOptions( + my $p = opt_parser( 'no_auto_abbrev', 'pass_through' ); + $p->getoptions( 'noenv' => \$noenv, 'ackrc=s' => \$ackrc, ); - Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); - my @files; if ( !$noenv ) { @@ -4421,11 +4326,11 @@ 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 } ); } @@ -4436,24 +4341,17 @@ sub retrieve_arg_sources { }; 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 ], @@ -4462,60 +4360,867 @@ sub retrieve_arg_sources { return @arg_sources; } -1; # End of App::Ack::ConfigLoader -package App::Ack::Filter; - -use strict; -use warnings; +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; - return App::Ack::Filter::Inverse->new( $self ); -} + 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, + }; -sub is_inverted { - return 0; -} + my %parsed; + my @raw; + my %spec_capture_parsed; + my %spec_capture_raw; -sub to_string { - my ( $self ) = @_; + # 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} + ); + } - return '(unimplemented to_string)'; + # 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. + my $p = opt_parser( 'pass_through' ); # Ignore invalid options. + $p->getoptionsfromarray( [@ARGV], %spec_capture_raw ); + $p->getoptionsfromarray( [@ARGV], %spec_capture_parsed ); + + return (\@raw,\%parsed); } -sub inspect { - my ( $self ) = @_; +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, + }, + 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, + 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, + o => 1, + output => 1, + p => 1, + passthru => 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, + 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, + }, + 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, + }, + 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, + }, + 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; +} - return ref($self); +sub filter { + my ( undef, $file ) = @_; + + return -T $file->name; } 1; @@ -4547,27 +5252,21 @@ sub create_group { } sub filter { - my ( $self, $resource ) = @_; - - my $re = $self->{'regex'}; + my ( $self, $file ) = @_; - return $resource->name =~ /$re/; + return $file->name =~ /$self->{regex}/; } sub inspect { my ( $self ) = @_; - my $re = $self->{'regex'}; - - return ref($self) . " - $re"; + return ref($self) . ' - ' . $self->{regex}; } sub to_string { my ( $self ) = @_; - my $exts = $self->{'extensions'}; - - return join(' ', map { ".$_" } @{$exts}); + return join( ' ', map { ".$_" } @{$self->{extensions}} ); } BEGIN { @@ -4575,8 +5274,7 @@ BEGIN { } 1; -package App::Ack::Filter::FirstLineMatch; - +package App::Ack::Filter::ExtensionGroup; use strict; @@ -4586,106 +5284,49 @@ BEGIN { } sub new { - my ( $class, $re ) = @_; - - $re =~ s{^/|/$}{}g; # XXX validate? - $re = qr{$re}i; + my ( $class ) = @_; return bless { - regex => $re, + data => {}, }, $class; } -# This test reads the first 250 characters of a file, then just uses the -# first line found in that. This prevents reading something like an entire -# .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; - - return $line =~ /$re/; -} - -sub inspect { - my ( $self ) = @_; - - my $re = $self->{'regex'}; - - return ref($self) . " - $re"; -} - -sub to_string { - my ( $self ) = @_; - - (my $re = $self->{regex}) =~ s{\([^:]*:(.*)\)$}{$1}; - - return "first line matches /$re/"; -} - -BEGIN { - App::Ack::Filter->register_filter(firstlinematch => __PACKAGE__); -} - -1; -package App::Ack::Filter::Is; - - -use strict; -use warnings; -BEGIN { - our @ISA = 'App::Ack::Filter'; -} - -use File::Spec 3.00 (); - -sub new { - my ( $class, $filename ) = @_; +sub add { + my ( $self, $filter ) = @_; - return bless { - filename => $filename, - groupname => 'IsGroup', - }, $class; -} + foreach my $ext (@{$filter->{extensions}}) { + $self->{data}->{lc $ext} = 1; + } -sub create_group { - return App::Ack::Filter::IsGroup->new(); + return; } sub filter { - my ( $self, $resource ) = @_; + my ( $self, $file ) = @_; - my $filename = $self->{'filename'}; - my $base = (File::Spec->splitpath($resource->name))[2]; + if ($file->name =~ /[.]([^.]*)$/) { + return exists $self->{'data'}->{lc $1}; + } - return $base eq $filename; + return 0; } sub inspect { my ( $self ) = @_; - my $filename = $self->{'filename'}; - - return ref($self) . " - $filename"; + return ref($self) . " - $self"; } sub to_string { my ( $self ) = @_; - my $filename = $self->{'filename'}; - - return $filename; -} - -BEGIN { - App::Ack::Filter->register_filter(is => __PACKAGE__); + return join(' ', map { ".$_" } sort keys %{$self->{data}}); } 1; -package App::Ack::Filter::Match; +package App::Ack::Filter::FirstLineMatch; + + use strict; use warnings; @@ -4693,75 +5334,44 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } -use File::Spec 3.00; - - sub new { my ( $class, $re ) = @_; $re =~ s{^/|/$}{}g; # XXX validate? - $re = qr/$re/i; + $re = qr{$re}i; return bless { regex => $re, - groupname => 'MatchGroup', }, $class; } -sub create_group { - return App::Ack::Filter::MatchGroup->new; -} +# This test reads the first 250 characters of a file, then just uses the +# first line found in that. This prevents reading something like an entire +# .min.js file (which might be only one "line" long) into memory. sub filter { - my ( $self, $resource ) = @_; + my ( $self, $file ) = @_; - my $re = $self->{'regex'}; - - return $resource->basename =~ /$re/; + return $file->firstliney =~ /$self->{regex}/; } sub inspect { my ( $self ) = @_; - my $re = $self->{'regex'}; - - print ref($self) . " - $re"; - return; + return ref($self) . ' - ' . $self->{regex}; } sub to_string { my ( $self ) = @_; - my $re = $self->{'regex'}; - - return "filename matches $re"; -} + (my $re = $self->{regex}) =~ s{\([^:]*:(.*)\)$}{$1}; -BEGIN { - App::Ack::Filter->register_filter(match => __PACKAGE__); + return "First line matches /$re/"; } -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 ( $self, $resource ) = @_; - - return -T $resource->name; + App::Ack::Filter->register_filter(firstlinematch => __PACKAGE__); } 1; @@ -4784,10 +5394,9 @@ sub new { } sub filter { - my ( $self, $resource ) = @_; + my ( $self, $file ) = @_; - my $filter = $self->{'filter'}; - return !$filter->filter( $resource ); + return !$self->{filter}->filter( $file ); } sub invert { @@ -4809,7 +5418,7 @@ sub inspect { } 1; -package App::Ack::Filter::Collection; +package App::Ack::Filter::Is; use strict; @@ -4818,59 +5427,41 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } +use File::Spec 3.00 (); + sub new { - my ( $class ) = @_; + my ( $class, $filename ) = @_; return bless { - groups => {}, - ungrouped => [], + filename => $filename, + groupname => 'IsGroup', }, $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 create_group { + return App::Ack::Filter::IsGroup->new(); } -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; - } +sub filter { + my ( $self, $file ) = @_; - return; + return (File::Spec->splitpath($file->name))[2] eq $self->{filename}; } sub inspect { my ( $self ) = @_; - return ref($self) . " - $self"; + return ref($self) . ' - ' . $self->{filename}; } sub to_string { my ( $self ) = @_; - my $ungrouped = $self->{'ungrouped'}; + return $self->{filename}; +} - return join(', ', map { "($_)" } @{$ungrouped}); +BEGIN { + App::Ack::Filter->register_filter(is => __PACKAGE__); } 1; @@ -4883,8 +5474,6 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } -use File::Spec 3.00 (); - sub new { my ( $class ) = @_; @@ -4902,12 +5491,9 @@ sub add { } sub filter { - my ( $self, $resource ) = @_; + my ( $self, $file ) = @_; - my $data = $self->{'data'}; - my $base = $resource->basename; - - return exists $data->{$base}; + return exists $self->{data}->{ $file->basename }; } sub inspect { @@ -4923,7 +5509,7 @@ sub to_string { } 1; -package App::Ack::Filter::ExtensionGroup; +package App::Ack::Filter::IsPath; use strict; @@ -4932,48 +5518,40 @@ BEGIN { our @ISA = 'App::Ack::Filter'; } + sub new { - my ( $class ) = @_; + my ( $class, $filename ) = @_; return bless { - data => {}, + filename => $filename, + groupname => 'IsPathGroup', }, $class; } -sub add { - my ( $self, $filter ) = @_; - - foreach my $ext (@{$filter->{extensions}}) { - $self->{data}->{lc $ext} = 1; - } - - return; +sub create_group { + return App::Ack::Filter::IsPathGroup->new(); } sub filter { - my ( $self, $resource ) = @_; - - if ($resource->name =~ /[.]([^.]*)$/) { - return exists $self->{'data'}->{lc $1}; - } + my ( $self, $file ) = @_; - return 0; + return $file->name eq $self->{filename}; } sub inspect { my ( $self ) = @_; - return ref($self) . " - $self"; + return ref($self) . ' - ' . $self->{filename}; } sub to_string { my ( $self ) = @_; - return join(' ', map { ".$_" } sort keys %{$self->{data}}); + return $self->{filename}; } 1; -package App::Ack::Filter::MatchGroup; +package App::Ack::Filter::IsPathGroup; use strict; @@ -4986,45 +5564,38 @@ sub new { my ( $class ) = @_; return bless { - matches => [], - big_re => undef, + data => {}, }, $class; } sub add { my ( $self, $filter ) = @_; - push @{ $self->{matches} }, $filter->{regex}; - - my $re = join('|', map { "(?:$_)" } @{ $self->{matches} }); - $self->{big_re} = qr/$re/; + $self->{data}->{ $filter->{filename} } = 1; return; } sub filter { - my ( $self, $resource ) = @_; - - my $re = $self->{big_re}; + my ( $self, $file ) = @_; - return $resource->basename =~ /$re/; + return exists $self->{data}->{$file->name}; } sub inspect { my ( $self ) = @_; - # XXX Needs an explicit return. + return ref($self) . " - $self"; } sub to_string { my ( $self ) = @_; - # XXX Needs an explicit return. + return join(' ', keys %{$self->{data}}); } 1; -package App::Ack::Filter::IsPath; - +package App::Ack::Filter::Match; use strict; use warnings; @@ -5033,45 +5604,47 @@ BEGIN { } + sub new { - my ( $class, $filename ) = @_; + my ( $class, $re ) = @_; + + $re =~ s{^/|/$}{}g; # XXX validate? + $re = qr/$re/i; return bless { - filename => $filename, - groupname => 'IsPathGroup', + regex => $re, + groupname => 'MatchGroup', }, $class; } sub create_group { - return App::Ack::Filter::IsPathGroup->new(); + return App::Ack::Filter::MatchGroup->new; } sub filter { - my ( $self, $resource ) = @_; + my ( $self, $file ) = @_; - return $resource->name eq $self->{'filename'}; + return $file->basename =~ /$self->{regex}/; } sub inspect { my ( $self ) = @_; - my $filename = $self->{'filename'}; - - return ref($self) . " - $filename"; + return ref($self) . ' - ' . $self->{regex}; } sub to_string { my ( $self ) = @_; - my $filename = $self->{'filename'}; + return "Filename matches $self->{regex}"; +} - return $filename; +BEGIN { + App::Ack::Filter->register_filter(match => __PACKAGE__); } 1; -package App::Ack::Filter::IsPathGroup; - - +package App::Ack::Filter::MatchGroup; use strict; @@ -5084,37 +5657,30 @@ sub new { my ( $class ) = @_; return bless { - data => {}, + matches => [], + big_re => undef, }, $class; } sub add { my ( $self, $filter ) = @_; - $self->{data}->{ $filter->{filename} } = 1; + push @{ $self->{matches} }, $filter->{regex}; + + my $re = join('|', map { "(?:$_)" } @{ $self->{matches} }); + $self->{big_re} = qr/$re/; return; } sub filter { - my ( $self, $resource ) = @_; - - my $data = $self->{'data'}; + my ( $self, $file ) = @_; - return exists $data->{$resource->name}; + return $file->basename =~ /$self->{big_re}/; } -sub inspect { - my ( $self ) = @_; - - return ref($self) . " - $self"; -} - -sub to_string { - my ( $self ) = @_; - - return join(' ', keys %{$self->{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; @@ -5123,7 +5689,7 @@ use strict; use warnings; -our $VERSION = '1.16'; +our $VERSION = '1.18'; @@ -5154,11 +5720,11 @@ sub files { my ($parms,@queue) = _setup( \%files_defaults, @_ ); + my $filter = $parms->{file_filter}; return sub { - my $filter = $parms->{file_filter}; - while (@queue) { - my ($dirname,$file,$fullpath) = splice( @queue, 0, 3 ); - if ( -f $fullpath || -p _ || $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; @@ -5167,7 +5733,7 @@ sub files { } return wantarray ? ($dirname,$file,$fullpath) : $fullpath; } - if ( -d _ ) { + if ( $is_dir ) { unshift( @queue, _candidate_files( $parms, $fullpath ) ); } } # while @@ -5181,6 +5747,7 @@ sub files { + sub from_file { die _bad_invocation() if @_ && defined($_[0]) && ($_[0] eq __PACKAGE__); @@ -5188,7 +5755,7 @@ sub from_file { my $err = $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()' ); @@ -5206,8 +5773,8 @@ sub from_file { } } + my $filter = $parms->{file_filter}; return sub { - my $filter = $parms->{file_filter}; local $/ = $parms->{nul_separated} ? "\x00" : $/; while ( my $fullpath = <$fh> ) { chomp $fullpath; @@ -5269,7 +5836,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" ); } @@ -5282,12 +5849,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); @@ -5307,40 +5875,39 @@ sub _candidate_files { 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; - 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 -- 2.43.0