#
$App::Ack::STANDALONE = 1;
+{
package main;
use strict;
use warnings;
-our $VERSION = 'v3.4.0'; # Check https://beyondgrep.com/ for updates
+our $VERSION = 'v3.8.1'; # Check https://beyondgrep.com/ for updates
use 5.010001;
use File::Spec ();
+use Getopt::Long ();
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;
# Flag if we need any context tracking.
our $is_tracking_context;
-# The regex that we search for in each file.
-our $search_re;
+# The regex that we use to match each line in the file.
+our $re_match;
+
+# Regex for matching for highlighting in matched lines.
+our $re_hilite;
-# Special /m version of our $search_re.
-our $scan_re;
+# The regex that matches for things we want to exclude via the --not option.
+our $re_not;
+
+# Version of the match regex for checking to see if the file should be scanned line-by-line.
+our $re_scan;
our @special_vars_used_by_opt_output;
$ENV{ACK_COLOR_COLNO} ||= 'bold yellow';
}
- my $p = App::Ack::ConfigLoader::opt_parser( 'no_auto_abbrev', 'pass_through' );
- $p->getoptions(
+ App::Ack::ConfigLoader::configure_parser( 'no_auto_abbrev', 'pass_through' );
+ Getopt::Long::GetOptions(
help => sub { App::Ack::show_help(); exit; },
version => sub { App::Ack::print( App::Ack::get_version_statement() ); exit; },
man => sub { App::Ack::show_man(); },
$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};
}
if ( $opt_range_start ) {
- ($opt_range_start, undef) = build_regex( $opt_range_start, {} );
+ ($opt_range_start, undef) = App::Ack::build_regex( $opt_range_start, {} );
}
if ( $opt_range_end ) {
- ($opt_range_end, undef) = build_regex( $opt_range_end, {} );
+ ($opt_range_end, undef) = App::Ack::build_regex( $opt_range_end, {} );
}
$using_ranges = $opt_range_start || $opt_range_end;
}
}
+
# 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;
+ defined $opt_regex or App::Ack::die( 'No regular expression found.' );
+ ($re_match, $re_not, $re_hilite, $re_scan) = App::Ack::build_all_regexes( $opt_regex, $opt );
+ $stats{re_match} = $re_match;
+ $stats{re_not} = $re_not;
+ $stats{re_hilite} = $re_hilite;
+ $stats{re_scan} = $re_scan;
}
else {
if ( $opt_f ) {
}
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 );
+ defined $opt_regex or App::Ack::die( 'No regular expression found.' );
+ ($re_match, $re_not, $re_hilite, $re_scan) = App::Ack::build_all_regexes( $opt_regex, $opt );
+ $stats{re_match} = $re_match;
+ $stats{re_not} = $re_not;
+ $stats{re_hilite} = $re_hilite;
+ $stats{re_scan} = $re_scan;
}
my @start;
if ( not defined $opt->{files_from} ) {
if ( $opt_debug ) {
require List::Util;
- my @stats = qw( search_re scan_re prescans linescans filematches linematches );
+ my @stats = qw( re_match re_scan re_not prescans linescans filematches linematches );
my $width = List::Util::max( map { length } @stats );
for my $stat ( @stats ) {
close $App::Ack::fh;
App::Ack::exit_from_ack( $nmatches );
-}
+} # End of MAIN
+
+
+exit 0;
-# End of MAIN
sub file_loop_fg {
my $files = shift;
};
}
+
sub _compile_file_filter {
my ( $opt, $start ) = @_;
return sub {
if ( $opt_g ) {
- if ( $File::Next::name =~ /$search_re/o ) {
+ if ( $File::Next::name =~ /$re_match/o ) {
return 0 if $opt_v;
}
else {
else {
my @dirs = File::Spec->splitdir($File::Next::dir);
- my $is_ignoring = 0;
+ my $is_ignoring;
for ( my $i = 0; $i < @dirs; $i++) {
my $dir_rsrc = App::Ack::File->new(File::Spec->catfile(@dirs[0 .. $i]));
$match_found = 0;
}
return $match_found;
- };
+ }; # End of compiled sub
}
}
}
-# Returns a regex object based on a string and command-line options.
-# Dies when the regex $str is undefined (i.e. not given on command line).
-
-sub build_regex {
- my $str = shift;
- my $opt = shift;
-
- 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 $ok = 1;
-
- if ( $str =~ /^\\[wd]/ ) {
- # Explicit \w is good.
- }
- else {
- # Can start with \w, (, [ or dot.
- if ( $str !~ /^[\w\(\[\.]/ ) {
- $ok = 0;
- }
- }
-
- # Can end with \w, }, ), ], +, *, or dot.
- if ( $str !~ /[\w\}\)\]\+\*\?\.]$/ ) {
- $ok = 0;
- }
- # ... unless it's escaped.
- elsif ( $str =~ /\\[\}\)\]\+\*\?\.]$/ ) {
- $ok = 0;
- }
-
- if ( !$ok ) {
- App::Ack::die( '-w will not do the right thing if your regex does not begin and end with a word character.' );
- }
-
- 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 );
- }
- }
-
- if ( $opt->{i} || ($opt->{S} && $regex_is_lc) ) {
- $_ = "(?i)$_" for ( $str, $scan_str );
- }
-
- my $scan_regex = undef;
- my $regex = eval { qr/$str/ };
- if ( $regex ) {
- if ( $scan_str !~ /\$/ ) {
- # No line_scan is possible if there's a $ in the regex.
- $scan_regex = eval { qr/$scan_str/m };
- }
- }
- else {
- my $err = $@;
- chomp $err;
- App::Ack::die( "Invalid regex '$str':\n $err" );
- }
-
- return ($regex, $scan_regex);
-}
my $match_colno;
my $printed_lineno;
my $is_first_match;
-state $has_printed_from_any_file = 0;
+state $has_printed_from_any_file;
sub file_loop_normal {
my $needs_line_scan = 1;
if ( !$opt_passthru && !$opt_v ) {
$stats{prescans}++;
- if ( $file->may_be_present( $scan_re ) ) {
+ if ( $file->may_be_present( $re_scan ) ) {
$file->reset();
}
else {
sub print_matches_in_file {
my $file = shift;
- 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;
- my $has_printed_from_this_file = 0;
-
my $fh = $file->open;
if ( !$fh ) {
if ( $App::Ack::report_bad_filenames ) {
}
# Check for context before the main loop, so we don't pay for it if we don't need it.
+ my $nmatches;
+ my $max_count = $opt_m || -1; # Go negative for no limit so it can never reduce to 0.
if ( $is_tracking_context ) {
- local $_ = undef;
+ $nmatches = pmif_context( $fh, $filename, $display_filename, $max_count );
+ }
+ elsif ( $opt_passthru ) {
+ $nmatches = pmif_passthru( $fh, $filename, $display_filename, $max_count );
+ }
+ elsif ( $opt_v ) {
+ $nmatches = pmif_opt_v( $fh, $filename, $display_filename, $max_count );
+ }
+ else {
+ $nmatches = pmif_normal( $fh, $filename, $display_filename, $max_count );
+ }
+
+ return $nmatches;
+}
- $after_context_pending = 0;
- my $in_range = range_setup();
+sub pmif_context {
+ my $fh = shift;
+ my $filename = shift;
+ my $display_filename = shift;
+ my $max_count = shift;
- while ( <$fh> ) {
- chomp;
- $match_colno = undef;
+ my $in_range = range_setup();
+ my $has_printed_from_this_file;
+ my $nmatches = 0;
- $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o );
+ $after_context_pending = 0;
+ local $_ = undef;
- 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;
- }
+ while ( <$fh> ) {
+ chomp;
+ $match_colno = undef;
+
+ $in_range = 1 if ( $using_ranges && !$in_range && defined($opt_range_start) && /$opt_range_start/o );
+
+ my $does_match;
+ if ( $in_range ) {
+ $does_match = /$re_match/o;
+ if ( $does_match && defined($re_not) ) {
+ local @-;
+ $does_match = !/$re_not/o;
+ }
+ if ( $opt_v ) {
+ $does_match = !$does_match;
+ }
+ else {
+ if ( $does_match ) {
+ # @- = @LAST_MATCH_START
+ $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 );
- }
+ 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_from_this_file = 1;
- $stats{linematches}++;
- $nmatches++;
- $max_count--;
}
- else {
- 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;
+ print_line_with_context( $filename, $_, $. );
+ $has_printed_from_this_file = 1;
+ $stats{linematches}++;
+ $nmatches++;
+ $max_count--;
+ }
+ else {
+ 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 && defined($opt_range_end) && /$opt_range_end/o );
+
+ last if ($max_count == 0) && ($after_context_pending == 0);
+ }
+
+ return $nmatches;
+}
+
+
+sub pmif_passthru {
+ my $fh = shift;
+ my $filename = shift;
+ my $display_filename = shift;
+ my $max_count = shift;
+
+ my $in_range = range_setup();
+ my $has_printed_from_this_file;
+ my $nmatches = 0;
+
+ local $_ = undef;
+
+ while ( <$fh> ) {
+ chomp;
+
+ $in_range = 1 if ( $using_ranges && !$in_range && defined($opt_range_start) && /$opt_range_start/o );
+
+ $match_colno = undef;
+ my $does_match = /$re_match/o;
+ if ( $does_match && defined($re_not) ) {
+ local @-;
+ $does_match = !/$re_not/o;
+ }
+ if ( $in_range && $does_match ) {
+ $match_colno = $-[0] + 1;
+ if ( !$has_printed_from_this_file ) {
+ if ( $opt_break && $has_printed_from_any_file ) {
+ App::Ack::print_blank_line();
}
- 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;
+ 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();
+ }
+ 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 );
+ $in_range = 0 if ( $using_ranges && $in_range && defined($opt_range_end) && /$opt_range_end/o );
- last if ($max_count == 0) && ($after_context_pending == 0);
- }
+ last if $max_count == 0;
}
- elsif ( $opt_passthru ) {
- local $_ = undef;
- my $in_range = range_setup();
+ return $nmatches;
+}
- while ( <$fh> ) {
- chomp;
- $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o );
+sub pmif_opt_v {
+ my $fh = shift;
+ my $filename = shift;
+ my $display_filename = shift;
+ my $max_count = shift;
- $match_colno = undef;
- if ( $in_range && ($opt_v xor /$search_re/o) ) {
- if ( !$opt_v ) {
- $match_colno = $-[0] + 1;
- }
+ my $in_range = range_setup();
+ my $has_printed_from_this_file;
+ my $nmatches = 0;
+
+ $match_colno = undef;
+ local $_ = undef;
+
+ while ( <$fh> ) {
+ chomp;
+
+ $in_range = 1 if ( $using_ranges && !$in_range && defined($opt_range_start) && /$opt_range_start/o );
+
+ if ( $in_range ) {
+ my $does_match = /$re_match/o;
+ if ( $does_match && defined($re_not) ) {
+ # local @-; No need to localize this because we don't use @-.
+ $does_match = !/$re_not/o;
+ }
+ if ( !$does_match ) {
if ( !$has_printed_from_this_file ) {
if ( $opt_break && $has_printed_from_any_file ) {
App::Ack::print_blank_line();
App::Ack::say( $display_filename );
}
}
- print_line_with_options( $filename, $_, $., ':' );
+ print_line_with_context( $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();
- }
- 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 $_ = undef;
- $match_colno = undef;
- my $in_range = range_setup();
+ $in_range = 0 if ( $using_ranges && $in_range && defined($opt_range_end) && /$opt_range_end/o );
- while ( <$fh> ) {
- chomp;
+ last if $max_count == 0;
+ }
- $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o );
+ return $nmatches;
+}
- 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::say( $display_filename );
- }
- }
- print_line_with_context( $filename, $_, $. );
- $has_printed_from_this_file = 1;
- $nmatches++;
- $max_count--;
- }
- }
- $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o );
+sub pmif_normal {
+ my $fh = shift;
+ my $filename = shift;
+ my $display_filename = shift;
+ my $max_count = shift;
- last if $max_count == 0;
- }
- }
- else { # Normal search: No context, no -v, no --passthru
- local $_ = undef;
+ my $in_range = range_setup();
+ my $has_printed_from_this_file;
+ my $nmatches = 0;
- my $last_match_lineno;
- my $in_range = range_setup();
+ my $last_match_lineno;
+ local $_ = undef;
- while ( <$fh> ) {
- chomp;
+ while ( <$fh> ) {
+ chomp;
- $in_range = 1 if ( $using_ranges && !$in_range && $opt_range_start && /$opt_range_start/o );
+ $in_range = 1 if ( $using_ranges && !$in_range && defined($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::say( $display_filename );
- }
+ if ( $in_range ) {
+ $match_colno = undef;
+ my $is_match = /$re_match/o;
+ if ( $is_match && defined($re_not) ) {
+ local @-;
+ $is_match = !/$re_not/o;
+ }
+ if ( $is_match ) {
+ $match_colno = $-[0] + 1;
+ if ( !$has_printed_from_this_file ) {
+ $stats{filematches}++;
+ if ( $opt_break && $has_printed_from_any_file ) {
+ App::Ack::print_blank_line();
}
- if ( $opt_p ) {
- if ( $last_match_lineno ) {
- if ( $. > $last_match_lineno + $opt_p ) {
- App::Ack::print_blank_line();
- }
- }
- elsif ( !$opt_break && $has_printed_from_any_file ) {
+ if ( $opt_show_filename && $opt_heading ) {
+ App::Ack::say( $display_filename );
+ }
+ }
+ if ( $opt_p ) {
+ if ( $last_match_lineno ) {
+ if ( $. > $last_match_lineno + $opt_p ) {
App::Ack::print_blank_line();
}
}
- s/[\r\n]+$//;
- print_line_with_options( $filename, $_, $., ':' );
- $has_printed_from_this_file = 1;
- $nmatches++;
- $stats{linematches}++;
- $max_count--;
- $last_match_lineno = $.;
+ elsif ( !$opt_break && $has_printed_from_any_file ) {
+ App::Ack::print_blank_line();
+ }
}
+ s/[\r\n]+$//;
+ print_line_with_options( $filename, $_, $., ':' );
+ $has_printed_from_this_file = 1;
+ $nmatches++;
+ $stats{linematches}++;
+ $max_count--;
+ $last_match_lineno = $.;
}
+ }
- $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o );
+ $in_range = 0 if ( $using_ranges && $in_range && defined($opt_range_end) && /$opt_range_end/o );
- last if $max_count == 0;
- }
+ last if $max_count == 0;
}
return $nmatches;
}
if ( $opt_output ) {
- while ( $line =~ /$search_re/og ) {
+ while ( $line =~ /$re_match/og ) {
my $output = $opt_output;
if ( @special_vars_used_by_opt_output ) {
no strict;
# We have to do underlining before any highlighting because highlighting modifies string length.
if ( $opt_underline && !$skip_coloring ) {
- while ( $line =~ /$search_re/og ) {
+ while ( $line =~ /$re_hilite/og ) {
my $match_start = $-[0] // next;
my $match_end = $+[0];
my $match_length = $match_end - $match_start;
if ( $opt_color && !$skip_coloring ) {
my $highlighted = 0; # If highlighted, need to escape afterwards.
- while ( $line =~ /$search_re/og ) {
+ while ( $line =~ /$re_hilite/og ) {
my $match_start = $-[0] // next;
my $match_end = $+[0];
my $match_length = $match_end - $match_start;
}
else {
if ( !$opt_v ) {
- if ( !$file->may_be_present( $scan_re ) ) {
+ if ( !$file->may_be_present( $re_scan ) ) {
$do_scan = 0;
}
}
if ( $using_ranges ) {
while ( <$fh> ) {
chomp;
- $in_range = 1 if ( !$in_range && $opt_range_start && /$opt_range_start/o );
+ $in_range = 1 if ( !$in_range && defined($opt_range_start) && /$opt_range_start/o );
if ( $in_range ) {
- if ( /$search_re/o xor $opt_v ) {
+ my $is_match = /$re_match/o;
+ if ( $is_match && defined($re_not) ) {
+ $is_match = !/$re_not/o;
+ }
+ if ( $is_match xor $opt_v ) {
++$nmatches;
last if $bail;
}
}
- $in_range = 0 if ( $in_range && $opt_range_end && /$opt_range_end/o );
+ $in_range = 0 if ( $in_range && defined($opt_range_end) && /$opt_range_end/o );
}
}
else {
while ( <$fh> ) {
chomp;
- if ( /$search_re/o xor $opt_v ) {
+ my $is_match = /$re_match/o;
+ if ( $is_match && defined($re_not) ) {
+ $is_match = !/$re_not/o;
+ }
+ if ( $is_match xor $opt_v ) {
++$nmatches;
last if $bail;
}
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
+file until the first line that matches C<--range-end>. Similarly, if
C<--range-end> is omitted, the range runs from the first line matching
C<--range-start> to the end of the file.
For example, if you wanted to search all HTML files up until the first
instance of the C<< <body> >>, you could do
- ack foo --range-end='<body>'
+ ack foo --html --range-end='<body>'
Or to search after Perl's `__DATA__` or `__END__` markers, you would do
- ack pattern --range-end='^__(END|DATA)__'
+ ack pattern --perl --range-start='^__(END|DATA)__'
It's possible for a range to start and stop on the same line. For example
Specifies an ackrc file to load after all others; see L</"ACKRC LOCATION SEMANTICS">.
+=item B<--and=PATTERN>
+
+Specifies a I<PATTERN> that MUST ALSO be found on a given line for a match to
+occur. This option can be repeated.
+
+If you want to find all the lines with both "dogs" or "cats", use:
+
+ ack dogs --and cats
+
+Note that the options that affect "dogs" also affect "cats", so if you have
+
+ ack -i -w dogs --and cats
+
+then the search for both "dogs" and "cats" will be case-insensitive and be
+word-limited.
+
+See also the other two boolean options C<--or> and C<--not>, neither of
+which can be used with C<--and>.
+
=item B<-A I<NUM>>, B<--after-context=I<NUM>>
Print I<NUM> lines of trailing context after matching lines.
No descending into subdirectories.
+=item B<--not=PATTERN>
+
+Specifies a I<PATTERN> that must NOT be true on a given line for a match to
+occur. This option can be repeated.
+
+If you want to find all the lines with "dogs" but not if "cats" or "fish"
+appear on the line, use:
+
+ ack dogs --not cats --not fish
+
+Note that the options that affect "dogs" also affect "cats" and "fish", so
+if you have
+
+ ack -i -w dogs --not cats
+
+then the search for both "dogs" and "cats" will be case-insensitive and be
+word-limited.
+
+See also the other two boolean options C<--and> and C<--or>, neither of
+which can be used with C<--not>.
+
=item B<-o>
Show only the part of each line matching PATTERN (turns off text
highlighting). This is exactly the same as C<--output=$&>.
+=item B<--or=PATTERN>
+
+Specifies a I<PATTERN> that MAY be found on a given line for a match to
+occur. This option can be repeated.
+
+If you want to find all the lines with "dogs" or "cats", use:
+
+ ack dogs --or cats
+
+Note that the options that affect "dogs" also affect "cats", so if you have
+
+ ack -i -w dogs --or cats
+
+then the search for both "dogs" and "cats" will be case-insensitive and be
+word-limited.
+
+See also the other two boolean options C<--and> and C<--not>, neither of
+which can be used with C<--or>.
+
=item B<--output=I<expr>>
Output the evaluation of I<expr> for each line (turns off text
=item C<$&>, C<$`> and C<$'>
-C<$&> is the the string matched by the pattern, C<$`> is what
+C<$&> is the string matched by the pattern, C<$`> is what
precedes the match, and C<$'> is what follows it. If the pattern
is C<gra(ph|nd)> and the string is "lexicographic", then C<$&> is
"graph", C<$`> is "lexico" and C<$'> is "ic".
over again. In the following examples the options will always be shown
on one command line so that they can be easily copy & pasted.
-File types can be specified both with the the I<--type=xxx> option,
+File types can be specified both with the I<--type=xxx> option,
or the file type as an option itself. For example, if you create
a filetype of "cobol", you can specify I<--type=cobol> or simply
I<--cobol>. File types must be at least two characters long. This
-------- ----------------- ------------------ ---------------
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
+ 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.
ack is based at GitHub at L<https://github.com/beyondgrep/ack3>
Please report any bugs or feature requests to the issues list at
-Github: L<https://github.com/beyondgrep/ack3/issues>.
+GitHub: L<https://github.com/beyondgrep/ack3/issues>.
Please include the operating system that you're using; the output of
the command C<ack --version>; and any customizations in your F<.ackrc>
L<https://github.com/beyondgrep/ack3>
-=item * The ack issues list at Github
+=item * The ack issues list at GitHub
L<https://github.com/beyondgrep/ack3/issues>
How appropriate to have I<ack>nowledgements!
Thanks to everyone who has contributed to ack in any way, including
+Geraint Edwards,
+Loren Howard,
+Yaroslav Halchenko,
+Thiago Perrotta,
+Thomas Gossler,
+Kieran Mace,
+Volker Glave,
+Axel Beckert,
+Eric Pement,
+Gabor Szabo,
+Frieder Bluemle,
+Grzegorz Kaczmarczyk,
Dan Book,
Tomasz Konojacki,
Salomon Smeke,
=head1 COPYRIGHT & LICENSE
-Copyright 2005-2020 Andy Lester.
+Copyright 2005-2024 Andy Lester.
This program is free software; you can redistribute it and/or modify
it under the terms of the Artistic License v2.0.
=cut
1;
+}
+{
package App::Ack;
use warnings;
our $VERSION;
our $COPYRIGHT;
BEGIN {
- $VERSION = 'v3.4.0'; # Check https://beyondgrep.com/ for updates
- $COPYRIGHT = 'Copyright 2005-2020 Andy Lester.';
+ $VERSION = 'v3.8.1'; # Check https://beyondgrep.com/ for updates
+ $COPYRIGHT = 'Copyright 2005-2024 Andy Lester.';
}
our $STANDALONE = 0;
our $ORIGINAL_PROGRAM_NAME;
--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.
+ --and PATTERN Specifies PATTERN that MUST also be found on
+ the line for a match to occur. Repeatable.
+ --or PATTERN Specifies PATTERN that MAY also be found on
+ the line for a match to occur. Repeatable.
+ --not PATTERN Specifies PATTERN that must NOT be found on
+ the line for a match to occur. Repeatable.
Search output:
--output=expr Output the evaluation of expr for each line
ack allows customization of the colors it uses when presenting matches
onscreen. See the "ACK COLORS" section of the ack manual (ack --man).
-Colors may be specified as "rggNNN" where "NNN" is a triplet of digits
+Colors may be specified as "rgbNNN" where "NNN" is a triplet of digits
from 0 to 5 specifying the intensity of red, green and blue, respectively.
Here is a grid of the 216 possible values for NNN.
# Get rid of any literal backslashes first to avoid confusion.
$pat =~ s/\\\\//g;
- my $metacharacter = qr/
+ my $metacharacter = qr{
|\\A # Beginning of string
|\\B # Not word boundary
|\\c[a-zA-Z] # Control characters
|\\X # ???
|\\x[0-9A-Fa-f]{2} # Hex sequence
|\\Z # End of string
- /x;
+ }x;
$pat =~ s/$metacharacter//g;
my $name = qr/[_A-Za-z][_A-Za-z0-9]*?/;
}
+# Returns a regex object based on a string and command-line options.
+# Dies when the regex $str is undefined (i.e. not given on command line).
+
+sub build_regex {
+ my $str = shift;
+ my $opt = shift;
+
+ # Check for lowercaseness before we do any modifications.
+ my $regex_is_lc = App::Ack::is_lowercase( $str );
+
+ if ( $opt->{Q} ) {
+ $str = quotemeta( $str );
+ }
+ else {
+ # Compile the regex to see if it dies or throws warnings.
+ local $SIG{__WARN__} = sub { CORE::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" );
+ }
+ }
+ }
+
+ my $scan_str = $str;
+
+ # Whole words only.
+ if ( $opt->{w} ) {
+ my $ok = 1;
+
+ if ( $str =~ /^\\[wd]/ ) {
+ # Explicit \w is good.
+ }
+ else {
+ # Can start with \w, (, [ or dot.
+ if ( $str !~ /^[\w\(\[\.]/ ) {
+ $ok = 0;
+ }
+ }
+
+ # Can end with \w, }, ), ], +, *, or dot.
+ if ( $str !~ /[\w\}\)\]\+\*\?\.]$/ ) {
+ $ok = 0;
+ }
+ # ... unless it's escaped.
+ elsif ( $str =~ /\\[\}\)\]\+\*\?\.]$/ ) {
+ $ok = 0;
+ }
+
+ if ( !$ok ) {
+ App::Ack::die( '-w will not do the right thing if your regex does not begin and end with a word character.' );
+ }
+
+ 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 );
+ }
+ }
+
+ if ( $opt->{i} || ($opt->{S} && $regex_is_lc) ) {
+ $_ = "(?i)$_" for ( $str, $scan_str );
+ }
+
+ 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);
+}
+
+
+sub build_all_regexes {
+ my $opt_regex = shift;
+ my $opt = shift;
+
+ my $re_match;
+ my $re_not;
+ my $re_hilite;
+ my $re_scan;
+
+ my @parts;
+
+ # AND: alpha and beta
+ if ( @parts = @{$opt->{and}} ) {
+ my @match_parts;
+ my @hilite_parts;
+
+ for my $part ( @parts ) {
+ my ($match, undef) = build_regex( $part, $opt );
+ push @match_parts, "(?=.*$match)";
+ push @hilite_parts, $match;
+ }
+
+ my ($match, $scan) = build_regex( $opt_regex, $opt );
+ push @match_parts, ".*$match";
+ push @hilite_parts, $match;
+
+ $re_match = join( '', @match_parts );
+ $re_hilite = join( '|', @hilite_parts );
+ $re_scan = $scan;
+ }
+ # OR: alpha OR beta
+ elsif ( @parts = @{$opt->{or}} ) {
+ my @match_parts;
+ my @scan_parts;
+
+ for my $part ( $opt_regex, @parts ) {
+ my ($match, $scan) = build_regex( $part, $opt );
+ push @match_parts, $match;
+ push @scan_parts, $scan;
+ }
+
+ $re_match = join( '|', @match_parts );
+ $re_hilite = $re_match;
+ $re_scan = join( '|', @scan_parts );
+ }
+ # NOT: alpha NOT beta
+ elsif ( @parts = @{$opt->{not}} ) {
+ ($re_match, $re_scan) = build_regex( $opt_regex, $opt );
+ $re_hilite = $re_match;
+
+ my @not_parts;
+ for my $part ( @parts ) {
+ (my $re, undef) = build_regex( $part, $opt );
+ push @not_parts, $re;
+ }
+ $re_not = join( '|', @not_parts );
+ }
+ # No booleans.
+ else {
+ ($re_match, $re_scan) = build_regex( $opt_regex, $opt );
+ $re_hilite = $re_match;
+ }
+
+ return ($re_match, $re_not, $re_hilite, $re_scan);
+}
+
+
1; # End of App::Ack
+}
+{
package App::Ack::ConfigDefault;
use warnings;
# core dumps
--ignore-file=match:/core[.]\d+$/
-# minified Javascript
+# minified JavaScript
--ignore-file=match:/[.-]min[.]js$/
--ignore-file=match:/[.]js[.]min$/
# Common archives, as an optimization
--ignore-file=ext:gz,tar,tgz,zip
-# Python compiles modules
+# Python compiled modules
--ignore-file=ext:pyc,pyd,pyo
+# Python's pickle serialization format
+# https://docs.python.org/2/library/pickle.html#example
+# https://docs.python.org/3.7/library/pickle.html#examples
+--ignore-file=ext:pkl,pickle
+
# C extensions
--ignore-file=ext:so
# Compiled gettext files
--ignore-file=ext:mo
+# Visual Studio user and workspace settings
+# https://code.visualstudio.com/docs/getstarted/settings
+--ignore-dir=is:.vscode
+
### Filetypes defined
# Makefiles
--type-add=cmake:is:CMakeLists.txt
--type-add=cmake:ext:cmake
+# Bazel build tool
+# https://docs.bazel.build/versions/master/skylark/bzl-style.html
+--type-add=bazel:ext:bzl
+# https://docs.bazel.build/versions/master/guide.html#bazelrc-the-bazel-configuration-file
+--type-add=bazel:ext:bazelrc
+# https://docs.bazel.build/versions/master/build-ref.html#BUILD_files
+--type-add=bazel:is:BUILD
+# https://docs.bazel.build/versions/master/build-ref.html#workspace
+--type-add=bazel:is:WORKSPACE
+
+
# Actionscript
--type-add=actionscript:ext:as,mxml
# C#
--type-add=csharp:ext:cs
+# Crystal-lang
+# https://crystal-lang.org/
+--type-add=crystal:ext:cr,ecr
+
# CSS
# https://www.w3.org/Style/CSS/
--type-add=css:ext:css
# https://elixir-lang.org/
--type-add=elixir:ext:ex,exs
+# Elm
+# https://elm-lang.org
+--type-add=elm:ext:elm
+
# Emacs Lisp
# https://www.gnu.org/software/emacs
--type-add=elisp:ext:el
# https://plone.org/
--type-add=plone:ext:pt,cpt,metadata,cpy,py
+# PowerShell
+# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scripts
+# https://learn.microsoft.com/en-us/powershell/scripting/developer/module/understanding-a-windows-powershell-module
+--type-add=powershell:ext:ps1,psm1
+
+# PureScript
+# https://www.purescript.org
+--type-add=purescript:ext:purs
+
# Python
# https://www.python.org/
--type-add=python:ext:py
--type-add=python:firstlinematch:/^#!.*\bpython/
+# Pytest
+# https://pytest.org/
+# Pytest files are *.py files that start with test_ or end with _test.py
+# https://docs.pytest.org/en/stable/explanation/goodpractices.html#test-discovery
+--type-add=pytest:match:_test\.py$
+--type-add=pytest:match:^test_.*\.py$
+
# R
# https://www.r-project.org/
---type-add=rr:ext:R
+# https://r4ds.had.co.nz/r-markdown.html
+--type-add=rr:ext:R,Rmd
# reStructured Text
# https://docutils.sourceforge.io/rst.html
# Scala
# https://www.scala-lang.org/
---type-add=scala:ext:scala
+--type-add=scala:ext:scala,sbt
# Scheme
# https://groups.csail.mit.edu/mac/projects/scheme/
# https://www.tcl.tk/
--type-add=tcl:ext:tcl,itcl,itk
+# Terraform
+# https://github.com/hashicorp/terraform
+--type-add=terraform=.tf,.tfvars
+
# TeX & LaTeX
# https://www.latex-project.org/
--type-add=tex:ext:tex,cls,sty
# https://toml.io/
--type-add=toml:ext:toml
-# Typescript
+# TypeScript
# https://www.typescriptlang.org/
--type-add=ts:ext:ts,tsx
}
1;
+}
+{
package App::Ack::ConfigFinder;
}
1;
+}
+{
package App::Ack::ConfigLoader;
use strict;
use 5.010;
use File::Spec 3.00 ();
-use Getopt::Long 2.39 ();
+use Getopt::Long 2.38 ();
use Text::ParseWords 3.1 ();
-sub opt_parser {
+sub configure_parser {
my @opts = @_;
my @standard = qw(
no_auto_version
no_ignore_case
);
- return Getopt::Long::Parser->new( config => [ @standard, @opts ] );
+ Getopt::Long::Configure( @standard, @opts );
+
+ return;
}
+
sub _generate_ignore_dir {
my ( $option_name, $opt ) = @_;
'type-del=s' => $delete_spec,
);
- my $p = opt_parser( 'no_auto_abbrev', 'pass_through' );
+ configure_parser( 'no_auto_abbrev', 'pass_through' );
foreach my $source (@{$arg_sources}) {
my $args = $source->{contents};
if ( ref($args) ) {
# $args are modified in place, so no need to munge $arg_sources
- $p->getoptionsfromarray( $args, %type_arg_specs );
+ Getopt::Long::GetOptionsFromArray( $args, %type_arg_specs );
}
else {
( undef, $source->{contents} ) =
- $p->getoptionsfromstring( $args, %type_arg_specs );
+ Getopt::Long::GetOptionsFromString( $args, %type_arg_specs );
}
}
return;
}
+ $opt->{and} = [];
+ $opt->{or} = [];
+ $opt->{not} = [];
+
return {
1 => sub { $opt->{1} = $opt->{m} = 1 },
+ 'and=s' => $opt->{and},
'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) },
},
'noignore-directory|noignore-dir=s' => _generate_ignore_dir('--noignore-dir', $opt),
'nopager' => sub { $opt->{pager} = undef },
+ 'not=s' => $opt->{not},
+ 'or=s' => $opt->{or},
'passthru' => \$opt->{passthru},
'print0' => \$opt->{print0},
'p|proximate:1' => \$opt->{p},
}
if ( $argv_source ) { # This *should* always be true, but you never know...
- my $p = opt_parser( 'pass_through' );
- $p->getoptionsfromarray( [ @{$argv_source} ],
+ configure_parser( 'pass_through' );
+ Getopt::Long::GetOptionsFromArray( [ @{$argv_source} ],
'help-types' => \$is_help_types_active,
);
}
my $arg_specs = get_arg_spec( $opt, $extra_specs );
- my $p = opt_parser();
+ configure_parser();
foreach my $source (@{$arg_sources}) {
my ( $source_name, $args ) = @{$source}{qw/name contents/};
my $ret;
if ( ref($args) ) {
- $ret = $p->getoptionsfromarray( $args, %{$args_for_source} );
+ $ret = Getopt::Long::GetOptionsFromArray( $args, %{$args_for_source} );
}
else {
( $ret, $source->{contents} ) =
- $p->getoptionsfromstring( $args, %{$args_for_source} );
+ Getopt::Long::GetOptionsFromString( $args, %{$args_for_source} );
}
if ( !$ret ) {
if ( !$is_help_types_active ) {
delete $arg_spec->{$arg};
};
- my $p = opt_parser( 'pass_through' );
+ configure_parser( 'pass_through' );
foreach my $source (@{$sources}) {
my ( $name, $options ) = @{$source}{qw/name contents/};
if ( ref($options) ne 'ARRAY' ) {
$j--;
my @copy = @chunk;
- $p->getoptionsfromarray( [@chunk],
+ Getopt::Long::GetOptionsFromArray( [@chunk],
'type-add=s' => $add_type,
'type-set=s' => $add_type,
'type-del=s' => $del_type,
my $should_remove = 0;
- my $p = opt_parser( 'no_auto_abbrev', 'pass_through' );
+ configure_parser( 'no_auto_abbrev', 'pass_through' );
foreach my $index ( $default_index + 1 .. $#{$sources} ) {
my $args = $sources->[$index]->{contents};
if (ref($args)) {
- $p->getoptionsfromarray( $args,
+ Getopt::Long::GetOptionsFromArray( $args,
'ignore-ack-defaults' => \$should_remove,
);
}
else {
- ( undef, $sources->[$index]{contents} ) = $p->getoptionsfromstring( $args,
+ ( undef, $sources->[$index]{contents} ) = Getopt::Long::GetOptionsFromString( $args,
'ignore-ack-defaults' => \$should_remove,
);
}
foreach my $source (@{$arg_sources}) {
if ( $source->{name} eq 'ARGV' ) {
my $dump;
- my $p = opt_parser( 'pass_through' );
- $p->getoptionsfromarray( $source->{contents},
+ configure_parser( 'pass_through' );
+ Getopt::Long::GetOptionsFromArray( $source->{contents},
'dump' => \$dump,
);
if ( $dump ) {
my $noenv;
my $ackrc;
- my $p = opt_parser( 'no_auto_abbrev', 'pass_through' );
- $p->getoptions(
+ configure_parser( 'no_auto_abbrev', 'pass_through' );
+ Getopt::Long::GetOptions(
'noenv' => \$noenv,
'ackrc=s' => \$ackrc,
);
}
# 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 );
+ configure_parser( 'pass_through' ); # Ignore invalid options.
+ Getopt::Long::GetOptionsFromArray( [@ARGV], %spec_capture_raw );
+ Getopt::Long::GetOptionsFromArray( [@ARGV], %spec_capture_parsed );
return (\@raw,\%parsed);
}
g => 1,
l => 1,
},
+ I => {
+ f => 1,
+ },
L => {
A => 1,
B => 1,
v => 1,
'with-filename' => 1,
},
+ and => {
+ g => 1,
+ not => 1,
+ or => 1,
+ },
break => {
L => 1,
c => 1,
B => 1,
C => 1,
H => 1,
+ I => 1,
L => 1,
break => 1,
c => 1,
group => 1,
h => 1,
heading => 1,
+ i => 1,
l => 1,
m => 1,
match => 1,
output => 1,
p => 1,
passthru => 1,
+ 'smart-case' => 1,
u => 1,
v => 1,
x => 1,
C => 1,
H => 1,
L => 1,
+ and => 1,
break => 1,
c => 1,
column => 1,
l => 1,
m => 1,
match => 1,
+ not => 1,
o => 1,
+ or => 1,
output => 1,
p => 1,
passthru => 1,
g => 1,
l => 1,
},
+ i => {
+ f => 1,
+ },
l => {
A => 1,
B => 1,
L => 1,
l => 1,
},
+ not => {
+ and => 1,
+ g => 1,
+ },
o => {
A => 1,
B => 1,
'show-types' => 1,
v => 1,
},
+ or => {
+ and => 1,
+ g => 1,
+ },
output => {
A => 1,
B => 1,
o => 1,
output => 1,
},
+ 'smart-case' => {
+ f => 1,
+ },
u => {
f => 1,
g => 1,
1; # End of App::Ack::ConfigLoader
+}
+{
package App::Ack::File;
use warnings;
}
1;
+}
+{
package App::Ack::Files;
}
1;
+}
+{
package App::Ack::Filter;
use strict;
}
1;
+}
+{
package App::Ack::Filter::Collection;
}
1;
+}
+{
package App::Ack::Filter::Default;
}
1;
+}
+{
package App::Ack::Filter::Extension;
}
1;
+}
+{
package App::Ack::Filter::ExtensionGroup;
}
1;
+}
+{
package App::Ack::Filter::FirstLineMatch;
}
1;
+}
+{
package App::Ack::Filter::Inverse;
}
1;
+}
+{
package App::Ack::Filter::Is;
}
1;
+}
+{
package App::Ack::Filter::IsGroup;
}
1;
+}
+{
package App::Ack::Filter::IsPath;
}
1;
+}
+{
package App::Ack::Filter::IsPathGroup;
}
1;
+}
+{
package App::Ack::Filter::Match;
use strict;
}
1;
+}
+{
package App::Ack::Filter::MatchGroup;
# It will just use the default one unless someone writes something useful.
1;
+}
+{
package File::Next;
use strict;
1; # End of File::Next
+}