.vimrc: Change hotkey for QuickfixToggle()
[dotfiles.git] / bin / ack
diff --git a/bin/ack b/bin/ack
index 2aa5941b27102a6cd9b3aa39e929b5b9b113f9ea..0af126e179e1e56fb7de9d745b5ab913aa430adf 100755 (executable)
--- a/bin/ack
+++ b/bin/ack
 # Please DO NOT EDIT or send patches for it.
 #
 # Please take a look at the source from
-# http://github.com/petdance/ack2
+# https://github.com/beyondgrep/ack3
 # and submit patches against the individual files
 # that build ack.
 #
 
+$App::Ack::STANDALONE = 1;
+package main;
+
 use strict;
 use warnings;
 
-use 5.008008;
-
+our $VERSION = 'v3.4.0'; # Check https://beyondgrep.com/ for updates
 
-# XXX Don't make this so brute force
-# See also: https://github.com/petdance/ack2/issues/89
+use 5.010001;
 
-use Getopt::Long 2.35 ();
+use File::Spec ();
 
-use Carp 1.04 ();
 
-our $VERSION = '2.04';
-# Check http://beyondgrep.com/ for updates
 
-# These are all our globals.
+# Global command-line options
+our $opt_1;
+our $opt_A;
+our $opt_B;
+our $opt_break;
+our $opt_color;
+our $opt_column;
+our $opt_debug;
+our $opt_c;
+our $opt_f;
+our $opt_g;
+our $opt_heading;
+our $opt_L;
+our $opt_l;
+our $opt_m;
+our $opt_output;
+our $opt_passthru;
+our $opt_p;
+our $opt_range_start;
+our $opt_range_end;
+our $opt_range_invert;
+our $opt_regex;
+our $opt_show_filename;
+our $opt_show_types;
+our $opt_underline;
+our $opt_v;
+
+# Flag if we need any context tracking.
+our $is_tracking_context;
+
+# The regex that we search for in each file.
+our $search_re;
+
+# 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" );
     }
 
     # Do preliminary arg checking;
     my $env_is_usable = 1;
-    for ( @ARGV ) {
-        last if ( $_ eq '--' );
+    for my $arg ( @ARGV ) {
+        last if ( $arg eq '--' );
 
-        # Get the --thpppt and --bar checking out of the way.
-        /^--th[pt]+t+$/ && App::Ack::_thpppt($_);
-        /^--bar$/ && App::Ack::_bar();
+        # Get the --thpppt, --bar, --cathy and --man checking out of the way.
+        $arg =~ /^--th[pt]+t+$/ and App::Ack::thpppt($arg);
+        $arg eq '--bar'         and App::Ack::ackbar();
+        $arg eq '--cathy'       and App::Ack::cathy();
 
         # See if we want to ignore the environment. (Don't tell Al Gore.)
-        if ( /^--(no)?env$/ ) {
-            $env_is_usable = defined $1 ? 0 : 1;
+        $arg eq '--env'         and $env_is_usable = 1;
+        $arg eq '--noenv'       and $env_is_usable = 0;
+    }
+
+    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.' );
         }
     }
-    if ( !$env_is_usable ) {
+    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 );
 }
 
-sub _compile_descend_filter {
-    my ( $opt ) = @_;
+# End of MAIN
 
-    my $idirs            = $opt->{idirs};
-    my $dont_ignore_dirs = $opt->{no_ignore_dirs};
+sub file_loop_fg {
+    my $files = shift;
 
-    # if we have one or more --noignore-dir directives, we can't ignore
-    # entire subdirectory hierarchies, so we return an "accept all"
-    # filter and scrutinize the files more in _compile_file_filter
-    return if $dont_ignore_dirs;
-    return unless $idirs && @{$idirs};
+    my $nmatches = 0;
+    while ( defined( my $file = $files->next ) ) {
+        if ( $opt_show_types ) {
+            App::Ack::show_types( $file );
+        }
+        elsif ( $opt_g ) {
+            print_line_with_options( undef, $file->name, 0, $App::Ack::ors );
+        }
+        else {
+            App::Ack::say( $file->name );
+        }
+        ++$nmatches;
+        last if defined($opt_m) && ($nmatches >= $opt_m);
+    }
+
+    return $nmatches;
+}
+
+
+sub file_loop_c {
+    my $files = shift;
+
+    my $total_count = 0;
+    while ( defined( my $file = $files->next ) ) {
+        my $matches_for_this_file = count_matches_in_file( $file );
 
-    my %ignore_dirs;
+        if ( not $opt_show_filename ) {
+            $total_count += $matches_for_this_file;
+            next;
+        }
 
-    foreach my $idir (@{$idirs}) {
-        if ( $idir =~ /^(\w+):(.*)/ ) {
-            if ( $1 eq 'is') {
-                $ignore_dirs{$2} = 1;
+        if ( !$opt_l || $matches_for_this_file > 0 ) {
+            if ( $opt_show_filename ) {
+                my $display_filename = $file->name;
+                if ( $opt_color ) {
+                    $display_filename = Term::ANSIColor::colored($display_filename, $ENV{ACK_COLOR_FILENAME});
+                }
+                App::Ack::say( $display_filename, ':', $matches_for_this_file );
             }
             else {
-                Carp::croak( 'Non-is filters are not yet supported for --ignore-dir' );
+                App::Ack::say( $matches_for_this_file );
             }
         }
+    }
+
+    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 ) = @_;
+
+    my $idirs = 0;
+    my $dont_ignore_dirs = 0;
+
+    for my $filter (@{$opt->{idirs} || []}) {
+        if ($filter->is_inverted()) {
+            $dont_ignore_dirs++;
+        }
         else {
-            Carp::croak( qq{Invalid filter specification "$idir"} );
+            $idirs++;
         }
     }
 
+    # If we have one or more --noignore-dir directives, we can't ignore
+    # entire subdirectory hierarchies, so we return an "accept all"
+    # filter and scrutinize the files more in _compile_file_filter.
+    return if $dont_ignore_dirs;
+    return unless $idirs;
+
+    $idirs = $opt->{idirs};
+
     return sub {
-        return !exists $ignore_dirs{$_} && !exists $ignore_dirs{$File::Next::dir};
+        my $file = App::Ack::File->new($File::Next::dir);
+        return !grep { $_->filter($file) } @{$idirs};
     };
 }
 
 sub _compile_file_filter {
     my ( $opt, $start ) = @_;
 
-    my $ifiles = $opt->{ifiles};
-    $ifiles  ||= [];
+    my $ifiles_filters = $opt->{ifiles};
 
-    my @ifiles_filters = map {
-        my $filter;
+    my $filters         = $opt->{'filters'} || [];
+    my $direct_filters = App::Ack::Filter::Collection->new();
+    my $inverse_filters = App::Ack::Filter::Collection->new();
 
-        if ( /^(\w+):(.+)/ ) {
-            my ($how,$what) = ($1,$2);
-            $filter = App::Ack::Filter->create_filter($how, split(/,/, $what));
+    foreach my $filter (@{$filters}) {
+        if ($filter->is_inverted()) {
+            # We want to check if files match the uninverted filters
+            $inverse_filters->add($filter->invert());
         }
         else {
-            Carp::croak( qq{Invalid filter specification "$_"} );
+            $direct_filters->add($filter);
         }
-        $filter
-    } @{$ifiles};
-
-    my $filters         = $opt->{'filters'} || [];
-    my $inverse_filters = [ grep {  $_->is_inverted() } @{$filters} ];
-    @{$filters}         =   grep { !$_->is_inverted() } @{$filters};
+    }
 
     my %is_member_of_starting_set = map { (get_file_id($_) => 1) } @{$start};
 
-    my $ignore_dir_list      = $opt->{idirs};
-    my $dont_ignore_dir_list = $opt->{no_ignore_dirs};
+    my @ignore_dir_filter = @{$opt->{idirs} || []};
+    my @is_inverted       = map { $_->is_inverted() } @ignore_dir_filter;
+    # This depends on InverseFilter->invert returning the original filter (for optimization).
+    @ignore_dir_filter         = map { $_->is_inverted() ? $_->invert() : $_ } @ignore_dir_filter;
+    my $dont_ignore_dir_filter = grep { $_ } @is_inverted;
+    my $previous_dir = '';
+    my $previous_dir_ignore_result;
 
-    my %ignore_dir_set;
-    my %dont_ignore_dir_set;
-
-    foreach my $filter (@{ $ignore_dir_list }) {
-        if ( $filter =~ /^(\w+):(.*)/ ) {
-            if ( $1 eq 'is' ) {
-                $ignore_dir_set{ $2 } = 1;
-            } else {
-                Carp::croak( 'Non-is filters are not yet supported for --ignore-dir' );
+    return sub {
+        if ( $opt_g ) {
+            if ( $File::Next::name =~ /$search_re/o ) {
+                return 0 if $opt_v;
             }
-        } else {
-            Carp::croak( qq{Invalid filter specification "$filter"} );
-        }
-    }
-    foreach my $filter (@{ $dont_ignore_dir_list }) {
-        if ( $filter =~ /^(\w+):(.*)/ ) {
-            if ( $1 eq 'is' ) {
-                $dont_ignore_dir_set{ $2 } = 1;
-            } else {
-                Carp::croak( 'Non-is filters are not yet supported for --ignore-dir' );
+            else {
+                return 0 if !$opt_v;
             }
-        } else {
-            Carp::croak( qq{Invalid filter specification "$filter"} );
         }
-    }
-
-    return sub {
         # ack always selects files that are specified on the command
         # line, regardless of filetype.  If you want to ack a JPEG,
         # and say "ack foo whatever.jpg" it will do it for you.
         return 1 if $is_member_of_starting_set{ get_file_id($File::Next::name) };
 
-        if ( $dont_ignore_dir_list ) {
-            my ( undef, $dirname ) = File::Spec->splitpath($File::Next::name);
-            my @dirs               = File::Spec->splitdir($dirname);
+        if ( $dont_ignore_dir_filter ) {
+            if ( $previous_dir eq $File::Next::dir ) {
+                if ( $previous_dir_ignore_result ) {
+                    return 0;
+                }
+            }
+            else {
+                my @dirs = File::Spec->splitdir($File::Next::dir);
+
+                my $is_ignoring = 0;
 
-            my $is_ignoring = 0;
+                for ( my $i = 0; $i < @dirs; $i++) {
+                    my $dir_rsrc = App::Ack::File->new(File::Spec->catfile(@dirs[0 .. $i]));
 
-            foreach my $dir ( @dirs ) {
-                if ( $ignore_dir_set{ $dir } ) {
-                    $is_ignoring = 1;
+                    my $j = 0;
+                    for my $filter (@ignore_dir_filter) {
+                        if ( $filter->filter($dir_rsrc) ) {
+                            $is_ignoring = !$is_inverted[$j];
+                        }
+                        $j++;
+                    }
                 }
-                elsif ( $dont_ignore_dir_set{ $dir } ) {
-                    $is_ignoring = 0;
+
+                $previous_dir               = $File::Next::dir;
+                $previous_dir_ignore_result = $is_ignoring;
+
+                if ( $is_ignoring ) {
+                    return 0;
                 }
             }
-            if ( $is_ignoring ) {
-                return 0;
-            }
         }
 
         # Ignore named pipes found in directory searching.  Named
@@ -189,91 +472,38 @@ sub _compile_file_filter {
         # command line" wins.
         return 0 if -p $File::Next::name;
 
-        # we can't handle unreadable filenames; report them
-        unless ( -r _ ) {
-            if ( $App::Ack::report_bad_filenames ) {
-                App::Ack::warn( "${File::Next::name}: cannot open file for reading" );
-            }
-            return 0;
-        }
-
-        my $resource = App::Ack::Resource::Basic->new($File::Next::name);
-        return 0 if ! $resource;
-        foreach my $filter (@ifiles_filters) {
-            return 0 if $filter->filter($resource);
-        }
-        my $match_found = 1;
-        if ( @{$filters} ) {
-            $match_found = 0;
+        # We can't handle unreadable filenames; report them.
+        if ( not -r _ ) {
+            use filetest 'access';
 
-            foreach my $filter (@{$filters}) {
-                if ($filter->filter($resource)) {
-                    $match_found = 1;
-                    last;
-                }
-            }
-        }
-        # Don't bother invoking inverse filters unless we consider the current resource a match
-        if ( $match_found && @{$inverse_filters} ) {
-            foreach my $filter ( @{$inverse_filters} ) {
-                if ( not $filter->filter( $resource ) ) {
-                    $match_found = 0;
-                    last;
+            if ( not -R $File::Next::name ) {
+                if ( $App::Ack::report_bad_filenames ) {
+                    App::Ack::warn( "${File::Next::name}: cannot open file for reading" );
                 }
+                return 0;
             }
         }
-        return $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 ()';
-
-    $ENV{ACK_COLOR_MATCH}    ||= 'black on_yellow';
-    $ENV{ACK_COLOR_FILENAME} ||= 'bold green';
-    $ENV{ACK_COLOR_LINENO}   ||= 'bold yellow';
 
-    return;
-}
-
-# inefficient, but functional
-sub filetypes {
-    my ( $resource ) = @_;
+        my $file = App::Ack::File->new($File::Next::name);
 
-    my @matches;
+        if ( $ifiles_filters && $ifiles_filters->filter($file) ) {
+            return 0;
+        }
 
-    foreach my $k (keys %App::Ack::mappings) {
-        my $filters = $App::Ack::mappings{$k};
+        my $match_found = $direct_filters->filter($file);
 
-        foreach my $filter (@{$filters}) {
-            # clone the resource
-            my $clone = $resource->clone;
-            if ( $filter->filter($clone) ) {
-                push @matches, $k;
-                last;
-            }
+        # 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 sort @matches;
+        return $match_found;
+    };
 }
 
-# returns a (fairly) unique identifier for a file
-# use this function to compare two files to see if they're
-# equal (ie. the same file, but with a different path/links/etc)
+
+# Returns a (fairly) unique identifier for a file.
+# Use this function to compare two files to see if they're
+# equal (ie. the same file, but with a different path/links/etc).
 sub get_file_id {
     my ( $filename ) = @_;
 
@@ -281,19 +511,19 @@ sub get_file_id {
         return File::Next::reslash( $filename );
     }
     else {
-        # XXX is this the best method? it always hits the FS
-        if( my ( $dev, $inode ) = (stat($filename))[0, 1] ) {
+        # XXX Is this the best method? It always hits the FS.
+        if ( my ( $dev, $inode ) = (stat($filename))[0, 1] ) {
             return join(':', $dev, $inode);
         }
         else {
-            # XXX this could be better
+            # XXX This could be better.
             return $filename;
         }
     }
 }
 
 # Returns a regex object based on a string and command-line options.
-# Dies when the regex $str is undefinied (i.e. not given on command line).
+# Dies when the regex $str is undefined (i.e. not given on command line).
 
 sub build_regex {
     my $str = shift;
@@ -301,64 +531,165 @@ sub build_regex {
 
     defined $str or App::Ack::die( 'No regular expression found.' );
 
+    if ( !$opt->{Q} ) {
+        # Compile the regex to see if it dies or throws warnings.
+        local $SIG{__WARN__} = sub { die @_ };  # Anything that warns becomes a die.
+        my $scratch_regex = eval { qr/$str/ };
+        if ( not $scratch_regex ) {
+            my $err = $@;
+            chomp $err;
+
+            if ( $err =~ m{^(.+?); marked by <-- HERE in m/(.+?) <-- HERE} ) {
+                my ($why, $where) = ($1,$2);
+                my $pointy = ' ' x (6+length($where)) . '^---HERE';
+                App::Ack::die( "Invalid regex '$str'\nRegex: $str\n$pointy $why" );
+            }
+            else {
+                App::Ack::die( "Invalid regex '$str'\n$err" );
+            }
+        }
+    }
+
+    # Check for lowercaseness before we do any modifications.
+    my $regex_is_lc = App::Ack::is_lowercase( $str );
+
     $str = quotemeta( $str ) if $opt->{Q};
+
+    my $scan_str = $str;
+
+    # Whole words only.
     if ( $opt->{w} ) {
-        $str = "\\b$str" if $str =~ /^\w/;
-        $str = "$str\\b" if $str =~ /\w$/;
-    }
+        my $ok = 1;
+
+        if ( $str =~ /^\\[wd]/ ) {
+            # Explicit \w is good.
+        }
+        else {
+            # Can start with \w, (, [ or dot.
+            if ( $str !~ /^[\w\(\[\.]/ ) {
+                $ok = 0;
+            }
+        }
+
+        # Can end with \w, }, ), ], +, *, or dot.
+        if ( $str !~ /[\w\}\)\]\+\*\?\.]$/ ) {
+            $ok = 0;
+        }
+        # ... unless it's escaped.
+        elsif ( $str =~ /\\[\}\)\]\+\*\?\.]$/ ) {
+            $ok = 0;
+        }
+
+        if ( !$ok ) {
+            App::Ack::die( '-w will not do the right thing if your regex does not begin and end with a word character.' );
+        }
 
-    my $regex_is_lc = $str eq lc $str;
-    if ( $opt->{i} || ($opt->{smart_case} && $regex_is_lc) ) {
-        $str = "(?i)$str";
+        if ( $str =~ /^\w+$/ ) {
+            # No need for fancy regex if it's a simple word.
+            $str = sprintf( '\b(?:%s)\b', $str );
+        }
+        else {
+            $str = sprintf( '(?:^|\b|\s)\K(?:%s)(?=\s|\b|$)', $str );
+        }
     }
 
-    my $re = eval { qr/$str/ };
-    if ( !$re ) {
-        die "Invalid regex '$str':\n  $@";
+    if ( $opt->{i} || ($opt->{S} && $regex_is_lc) ) {
+        $_ = "(?i)$_" for ( $str, $scan_str );
     }
 
-    return $re;
+    my $scan_regex = undef;
+    my $regex = eval { qr/$str/ };
+    if ( $regex ) {
+        if ( $scan_str !~ /\$/ ) {
+            # No line_scan is possible if there's a $ in the regex.
+            $scan_regex = eval { qr/$scan_str/m };
+        }
+    }
+    else {
+        my $err = $@;
+        chomp $err;
+        App::Ack::die( "Invalid regex '$str':\n  $err" );
+    }
 
+    return ($regex, $scan_regex);
 }
 
+my $match_colno;
+
 {
 
-my @before_ctx_lines;
-my @after_ctx_lines;
-my $is_iterating;
+# Number of context lines
+my $n_before_ctx_lines;
+my $n_after_ctx_lines;
 
-my $has_printed_something;
+# Array to keep track of lines that might be required for a "before" context
+my @before_context_buf;
+# Position to insert next line in @before_context_buf
+my $before_context_pos;
 
-BEGIN {
-    $has_printed_something = 0;
-}
+# Number of "after" context lines still pending
+my $after_context_pending;
+
+# Number of latest line that got printed
+my $printed_lineno;
+
+my $is_first_match;
+state $has_printed_from_any_file = 0;
+
+
+sub 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);
 
-sub print_matches_in_resource {
-    my ( $resource, $opt ) = @_;
+    @before_context_buf = (undef) x $n_before_ctx_lines;
+    $before_context_pos = 0;
 
-    my $passthru       = $opt->{passthru};
-    my $max_count      = $opt->{m} || -1;
-    my $nmatches       = 0;
-    my $filename       = $resource->name;
-    my $break          = $opt->{break};
-    my $heading        = $opt->{heading};
-    my $ors            = $opt->{print0} ? "\0" : "\n";
-    my $color          = $opt->{color};
-    my $print_filename = $opt->{show_filename};
+    $is_tracking_context = $n_before_ctx_lines || $n_after_ctx_lines;
 
-    my $has_printed_for_this_resource = 0;
+    $is_first_match = 1;
 
-    $is_iterating = 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 $nmatches;
+}
 
-    local $opt->{before_context} = $opt->{output} ? 0 : $opt->{before_context};
-    local $opt->{after_context}  = $opt->{output} ? 0 : $opt->{after_context};
 
-    my $n_before_ctx_lines = $opt->{before_context} || 0;
-    my $n_after_ctx_lines  = $opt->{after_context}  || 0;
+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;
 
-    @after_ctx_lines = @before_ctx_lines = ();
+    my $has_printed_from_this_file = 0;
 
-    my $fh = $resource->open();
+    my $fh = $file->open;
     if ( !$fh ) {
         if ( $App::Ack::report_bad_filenames ) {
             App::Ack::warn( "$filename: $!" );
@@ -367,657 +698,425 @@ sub print_matches_in_resource {
     }
 
     my $display_filename = $filename;
-    if ( $print_filename && $heading && $color ) {
+    if ( $opt_show_filename && $opt_heading && $opt_color ) {
         $display_filename = Term::ANSIColor::colored($display_filename, $ENV{ACK_COLOR_FILENAME});
     }
 
-    # check for context before the main loop, so we don't
-    # pay for it if we don't need it
-    if ( $n_before_ctx_lines || $n_after_ctx_lines ) {
-        my $current_line = <$fh>; # prime the first line of input
+    # Check for context before the main loop, so we don't pay for it if we don't need it.
+    if ( $is_tracking_context ) {
+        local $_ = undef;
 
-        while ( defined $current_line ) {
-            while ( (@after_ctx_lines < $n_after_ctx_lines) && defined($_ = <$fh>) ) {
-                push @after_ctx_lines, $_;
-            }
+        $after_context_pending = 0;
+
+        my $in_range = range_setup();
 
-            local $_ = $current_line;
-            my $former_dot_period = $.;
-            $. -= @after_ctx_lines;
+        while ( <$fh> ) {
+            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($opt, $_) ) {
-                if ( !$has_printed_for_this_resource ) {
-                    if ( $break && $has_printed_something ) {
+            if ( $does_match && $max_count ) {
+                if ( !$has_printed_from_this_file ) {
+                    $stats{filematches}++;
+                    if ( $opt_break && $has_printed_from_any_file ) {
                         App::Ack::print_blank_line();
                     }
-                    if ( $print_filename && $heading ) {
-                        App::Ack::print_filename( $display_filename, $ors );
+                    if ( $opt_show_filename && $opt_heading ) {
+                        App::Ack::say( $display_filename );
                     }
                 }
-                print_line_with_context($opt, $filename, $_, $.);
-                $has_printed_for_this_resource = 1;
+                print_line_with_context( $filename, $_, $. );
+                $has_printed_from_this_file = 1;
+                $stats{linematches}++;
                 $nmatches++;
                 $max_count--;
             }
-            elsif ( $passthru ) {
-                chomp; # XXX proper newline handling?
-                # XXX inline this call?
-                if ( $break && !$has_printed_for_this_resource && $has_printed_something ) {
-                    App::Ack::print_blank_line();
-                }
-                print_line_with_options($opt, $filename, $_, $., ':');
-                $has_printed_for_this_resource = 1;
-            }
-            last unless $max_count != 0;
-
-            # I tried doing this with local(), but for some reason,
-            # $. continued to have its new value after the exit of the
-            # enclosing block.  I'm guessing that $. has some extra
-            # magic associated with it or something.  If someone can
-            # tell me why this happened, I would love to know!
-            $. = $former_dot_period; # XXX this won't happen on an exception
-
-            if ( $n_before_ctx_lines ) {
-                push @before_ctx_lines, $current_line;
-                shift @before_ctx_lines while @before_ctx_lines > $n_before_ctx_lines;
-            }
-            if ( $n_after_ctx_lines ) {
-                $current_line = shift @after_ctx_lines;
-            }
             else {
-                $current_line = <$fh>;
+                if ( $after_context_pending ) {
+                    # Disable $opt_column since there are no matches in the context lines.
+                    local $opt_column = 0;
+                    print_line_with_options( $filename, $_, $., '-' );
+                    --$after_context_pending;
+                }
+                elsif ( $n_before_ctx_lines ) {
+                    # Save line for "before" context.
+                    $before_context_buf[$before_context_pos] = $_;
+                    $before_context_pos = ($before_context_pos+1) % $n_before_ctx_lines;
+                }
             }
-        }
+
+            $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o );
+
+            last if ($max_count == 0) && ($after_context_pending == 0);
+        }
     }
-    else {
-        local $_;
+    elsif ( $opt_passthru ) {
+        local $_ = undef;
+
+        my $in_range = range_setup();
 
         while ( <$fh> ) {
-            if ( does_match($opt, $_) ) {
-                if ( !$has_printed_for_this_resource ) {
-                    if ( $break && $has_printed_something ) {
+            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;
+                }
+                if ( !$has_printed_from_this_file ) {
+                    if ( $opt_break && $has_printed_from_any_file ) {
                         App::Ack::print_blank_line();
                     }
-                    if ( $print_filename && $heading ) {
-                        App::Ack::print_filename( $display_filename, $ors );
+                    if ( $opt_show_filename && $opt_heading ) {
+                        App::Ack::say( $display_filename );
                     }
                 }
-                print_line_with_context($opt, $filename, $_, $.);
-                $has_printed_for_this_resource = 1;
+                print_line_with_options( $filename, $_, $., ':' );
+                $has_printed_from_this_file = 1;
                 $nmatches++;
                 $max_count--;
             }
-            elsif ( $passthru ) {
-                chomp; # XXX proper newline handling?
-                if ( $break && !$has_printed_for_this_resource && $has_printed_something ) {
+            else {
+                if ( $opt_break && !$has_printed_from_this_file && $has_printed_from_any_file ) {
                     App::Ack::print_blank_line();
                 }
-                print_line_with_options($opt, $filename, $_, $., ':');
-                $has_printed_for_this_resource = 1;
+                print_line_with_options( $filename, $_, $., '-', 1 );
+                $has_printed_from_this_file = 1;
             }
-            last unless $max_count != 0;
-        }
-    }
-
-    $is_iterating = 0; # XXX this won't happen on an exception
-                       #     then again, do we care? ack doesn't really
-                       #     handle exceptions anyway.
-
-    return $nmatches;
-}
-
-sub print_line_with_options {
-    my ( $opt, $filename, $line, $line_no, $separator ) = @_;
-
-    $has_printed_something = 1;
-
-    my $print_filename = $opt->{show_filename};
-    my $print_column   = $opt->{column};
-    my $ors            = $opt->{print0} ? "\0" : "\n";
-    my $heading        = $opt->{heading};
-    my $output_expr    = $opt->{output};
-    my $color          = $opt->{color};
-
-    my @line_parts;
-
-    if( $color ) {
-        $filename = Term::ANSIColor::colored($filename,
-            $ENV{ACK_COLOR_FILENAME});
-        $line_no  = Term::ANSIColor::colored($line_no,
-            $ENV{ACK_COLOR_LINENO});
-    }
 
-    if($print_filename) {
-        if( $heading ) {
-            push @line_parts, $line_no;
-        }
-        else {
-            push @line_parts, $filename, $line_no;
-        }
+            $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o );
 
-        if( $print_column ) {
-            push @line_parts, get_match_column();
-        }
-    }
-    if( $output_expr ) {
-        while ( $line =~ /$opt->{regex}/og ) {
-            my $output = eval $output_expr;
-            App::Ack::print( join( $separator, @line_parts, $output ), $ors );
+            last if $max_count == 0;
         }
     }
-    else {
-        if ( $color ) {
-            my @capture_indices = get_capture_indices();
-            if( @capture_indices ) {
-                my $offset = 0; # additional offset for when we add stuff
-
-                foreach my $index_pair ( @capture_indices ) {
-                    my ( $match_start, $match_end ) = @{$index_pair};
+    elsif ( $opt_v ) {
+        local $_ = undef;
 
-                    my $substring = substr( $line,
-                        $offset + $match_start, $match_end - $match_start );
-                    my $substitution = Term::ANSIColor::colored( $substring,
-                        $ENV{ACK_COLOR_MATCH} );
+        $match_colno = undef;
+        my $in_range = range_setup();
 
-                    substr( $line, $offset + $match_start,
-                        $match_end - $match_start, $substitution );
-
-                    $offset += length( $substitution ) - length( $substring );
+        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::say( $display_filename );
+                        }
+                    }
+                    print_line_with_context( $filename, $_, $. );
+                    $has_printed_from_this_file = 1;
+                    $nmatches++;
+                    $max_count--;
                 }
             }
-            else {
-                my $matched = 0; # flag; if matched, need to escape afterwards
 
-                while ( $line =~ /$opt->{regex}/og ) {
+            $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o );
 
-                    $matched = 1;
-                    my ( $match_start, $match_end ) = ($-[0], $+[0]);
-
-                    my $substring = substr( $line, $match_start,
-                        $match_end - $match_start );
-                    my $substitution = Term::ANSIColor::colored( $substring,
-                        $ENV{ACK_COLOR_MATCH} );
+            last if $max_count == 0;
+        }
+    }
+    else {  # Normal search: No context, no -v, no --passthru
+        local $_ = undef;
 
-                    substr( $line, $match_start, $match_end - $match_start,
-                        $substitution );
+        my $last_match_lineno;
+        my $in_range = range_setup();
 
-                    pos($line) = $match_end +
-                    (length( $substitution ) - length( $substring ));
+        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::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]+$//;
+                    print_line_with_options( $filename, $_, $., ':' );
+                    $has_printed_from_this_file = 1;
+                    $nmatches++;
+                    $stats{linematches}++;
+                    $max_count--;
+                    $last_match_lineno = $.;
                 }
-                $line .= "\033[0m\033[K" if $matched;
             }
-        }
 
-        push @line_parts, $line;
-        App::Ack::print( join( $separator, @line_parts ), $ors );
+            $in_range = 0 if ( $using_ranges && $in_range && $opt_range_end && /$opt_range_end/o );
+
+            last if $max_count == 0;
+        }
     }
 
-    return;
+    return $nmatches;
 }
 
-sub iterate {
-    my ( $resource, $opt, $cb ) = @_;
 
-    $is_iterating = 1;
-
-    local $opt->{before_context} = $opt->{output} ? 0 : $opt->{before_context};
-    local $opt->{after_context}  = $opt->{output} ? 0 : $opt->{after_context};
+sub print_line_with_options {
+    my ( $filename, $line, $lineno, $separator, $skip_coloring ) = @_;
 
-    my $n_before_ctx_lines = $opt->{before_context} || 0;
-    my $n_after_ctx_lines  = $opt->{after_context}  || 0;
+    $has_printed_from_any_file = 1;
+    $printed_lineno = $lineno;
 
-    @after_ctx_lines = @before_ctx_lines = ();
+    my @line_parts;
 
-    my $fh = $resource->open();
-    if ( !$fh ) {
-        if ( $App::Ack::report_bad_filenames ) {
-            # XXX direct access to filename
-            App::Ack::warn( "$resource->{filename}: $!" );
+    if ( $opt_show_filename && defined($filename) ) {
+        my $colno;
+        $colno = get_match_colno() if $opt_column;
+        if ( $opt_color ) {
+            $filename = Term::ANSIColor::colored( $filename, $ENV{ACK_COLOR_FILENAME} );
+            $lineno   = Term::ANSIColor::colored( $lineno,   $ENV{ACK_COLOR_LINENO} );
+            $colno    = Term::ANSIColor::colored( $colno,    $ENV{ACK_COLOR_COLNO} ) if $opt_column;
         }
-        return;
+        if ( $opt_heading ) {
+            push @line_parts, $lineno;
+        }
+        else {
+            push @line_parts, $filename, $lineno;
+        }
+        push @line_parts, $colno if $opt_column;
     }
 
-    # check for context before the main loop, so we don't
-    # pay for it if we don't need it
-    if ( $n_before_ctx_lines || $n_after_ctx_lines ) {
-        my $current_line = <$fh>; # prime the first line of input
-
-        while ( defined $current_line ) {
-            while ( (@after_ctx_lines < $n_after_ctx_lines) && defined($_ = <$fh>) ) {
-                push @after_ctx_lines, $_;
-            }
-
-            local $_ = $current_line;
-            my $former_dot_period = $.;
-            $. -= @after_ctx_lines;
+    if ( $opt_output ) {
+        while ( $line =~ /$search_re/og ) {
+            my $output = $opt_output;
+            if ( @special_vars_used_by_opt_output ) {
+                no strict;
 
-            last unless $cb->();
+                # Stash copies of the special variables because we can't rely
+                # on them not changing in the process of doing the s///.
 
-            # I tried doing this with local(), but for some reason,
-            # $. continued to have its new value after the exit of the
-            # enclosing block.  I'm guessing that $. has some extra
-            # magic associated with it or something.  If someone can
-            # tell me why this happened, I would love to know!
-            $. = $former_dot_period; # XXX this won't happen on an exception
-
-            if ( $n_before_ctx_lines ) {
-                push @before_ctx_lines, $current_line;
-                shift @before_ctx_lines while @before_ctx_lines > $n_before_ctx_lines;
-            }
-            if ( $n_after_ctx_lines ) {
-                $current_line = shift @after_ctx_lines;
-            }
-            else {
-                $current_line = <$fh>;
+                my %keep = map { ($_ => ${$_} // '') } @special_vars_used_by_opt_output;
+                $keep{_} = $line if exists $keep{_}; # Manually set it because $_ gets reset in a map.
+                $keep{f} = $filename if exists $keep{f};
+                my $special_vars_used_by_opt_output = join( '', @special_vars_used_by_opt_output );
+                $output =~ s/\$([$special_vars_used_by_opt_output])/$keep{$1}/ego;
             }
+            App::Ack::say( join( $separator, @line_parts, $output ) );
         }
     }
     else {
-        local $_;
-
-        while ( <$fh> ) {
-            last unless $cb->();
-        }
-    }
-
-    $is_iterating = 0; # XXX this won't happen on an exception
-                       #     then again, do we care? ack doesn't really
-                       #     handle exceptions anyway.
-
-    return;
-}
-
-sub get_context {
-    if ( not $is_iterating ) {
-        Carp::croak( 'get_context() called outside of iterate()' );
-    }
-
-    return (
-        scalar(@before_ctx_lines) ? \@before_ctx_lines : undef,
-        scalar(@after_ctx_lines)  ? \@after_ctx_lines  : undef,
-    );
-}
-
-}
-
-{
-
-my $is_first_match;
-my $previous_file_processed;
-my $previous_line_printed;
-
-BEGIN {
-    $is_first_match        = 1;
-    $previous_line_printed = -1;
-}
+        my $underline = '';
 
-sub print_line_with_context {
-    my ( $opt, $filename, $matching_line, $line_no ) = @_;
-
-    my $heading = $opt->{heading};
+        # 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;
 
-    if( !defined($previous_file_processed) ||
-      $previous_file_processed ne $filename ) {
-        $previous_file_processed = $filename;
-        $previous_line_printed   = -1;
+                my $spaces_needed = $match_start - length $underline;
 
-        if( $heading ) {
-            $is_first_match = 1;
+                $underline .= (' ' x $spaces_needed);
+                $underline .= ('^' x $match_length);
+            }
         }
-    }
+        if ( $opt_color && !$skip_coloring ) {
+            my $highlighted = 0; # If highlighted, need to escape afterwards.
 
-    my $ors                 = $opt->{print0} ? "\0" : "\n";
-    my $match_word          = $opt->{w};
-    my $is_tracking_context = $opt->{after_context} || $opt->{before_context};
-    my $output_expr         = $opt->{output};
+            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;
 
-    $matching_line =~ s/[\r\n]+$//g;
+                my $substring    = substr( $line, $match_start, $match_length );
+                my $substitution = Term::ANSIColor::colored( $substring, $ENV{ACK_COLOR_MATCH} );
 
-    my ( $before_context, $after_context ) = get_context();
+                # Fourth argument replaces the string specified by the first three.
+                substr( $line, $match_start, $match_length, $substitution );
 
-    if ( $before_context ) {
-        my $first_line = $. - @{$before_context};
-
-        if ( $first_line <= $previous_line_printed ) {
-            splice @{$before_context}, 0, $previous_line_printed - $first_line + 1;
-            $first_line = $. - @{$before_context};
+                # Move the offset of where /g left off forward the number of spaces of highlighting.
+                pos($line) = $match_end + (length( $substitution ) - length( $substring ));
+                $highlighted = 1;
+            }
+            # Reset formatting and delete everything to the end of the line.
+            $line .= "\e[0m\e[K" if $highlighted;
         }
-        if ( @{$before_context} ) {
-            my $offset = @{$before_context};
 
-            if( !$is_first_match && $previous_line_printed != $first_line - 1 ) {
-                App::Ack::print('--', $ors);
-            }
-            foreach my $line (@{$before_context}) {
-                my $context_line_no = $. - $offset;
-                if ( $context_line_no <= $previous_line_printed ) {
-                    next;
+        push @line_parts, $line;
+        App::Ack::say( join( $separator, @line_parts ) );
+
+        # Print the underline, if appropriate.
+        if ( $underline ne '' ) {
+            # Figure out how many spaces are used per line for the ANSI coloring.
+            state $chars_used_by_coloring;
+            if ( !defined($chars_used_by_coloring) ) {
+                $chars_used_by_coloring = 0;
+                if ( $opt_color ) {
+                    my $len_fn = sub { length( Term::ANSIColor::colored( 'x', $ENV{$_[0]} ) ) - 1 };
+                    $chars_used_by_coloring += $len_fn->('ACK_COLOR_FILENAME') unless $opt_heading;
+                    $chars_used_by_coloring += $len_fn->('ACK_COLOR_LINENO');
+                    $chars_used_by_coloring += $len_fn->('ACK_COLOR_COLNO') if $opt_column;
                 }
-
-                chomp $line;
-                print_line_with_options($opt, $filename, $line, $context_line_no, '-');
-                $previous_line_printed = $context_line_no;
-                $offset--;
             }
-        }
-    }
-
-    if ( $. > $previous_line_printed ) {
-        if( $is_tracking_context && !$is_first_match && $previous_line_printed != $. - 1 ) {
-            App::Ack::print('--', $ors);
-        }
 
-        print_line_with_options($opt, $filename, $matching_line, $line_no, ':');
-        $previous_line_printed = $.;
-    }
+            pop @line_parts; # Leave only the stuff on the left.
+            if ( @line_parts ) {
+                my $stuff_on_the_left = join( $separator, @line_parts );
+                my $spaces_needed = length($stuff_on_the_left) - $chars_used_by_coloring + 1;
 
-    if($after_context) {
-        my $offset = 1;
-        foreach my $line (@{$after_context}) {
-            # XXX improve this!
-            if ( $previous_line_printed >= $. + $offset ) {
-                $offset++;
-                next;
+                App::Ack::print( ' ' x $spaces_needed );
             }
-            chomp $line;
-            my $separator = ($opt->{regex} && does_match( $opt, $line )) ? ':' : '-';
-            print_line_with_options($opt, $filename, $line, $. + $offset, $separator);
-            $previous_line_printed = $. + $offset;
-            $offset++;
+            App::Ack::say( $underline );
         }
     }
 
-    $is_first_match = 0;
-
     return;
 }
 
-}
-
-{
-
-my @capture_indices;
-my $match_column_number;
-
-# does_match() MUST have an $opt->{regex} set.
-
-sub does_match {
-    my ( $opt, $line ) = @_;
+sub print_line_with_context {
+    my ( $filename, $matching_line, $lineno ) = @_;
 
-    $match_column_number = undef;
-    @capture_indices     = ();
+    $matching_line =~ s/[\r\n]+$//;
 
-    if ( $opt->{v} ) {
-        return ( $line !~ /$opt->{regex}/o );
-    }
-    else {
-        if ( $line =~ /$opt->{regex}/o ) {
-            # @- = @LAST_MATCH_START
-            # @+ = @LAST_MATCH_END
-            $match_column_number = $-[0] + 1;
-
-            if ( @- > 1 ) {
-                @capture_indices = map {
-                    [ $-[$_], $+[$_] ]
-                } ( 1 .. $#- );
-            }
-            return 1;
+    # Check if we need to print context lines first.
+    if ( $opt_A || $opt_B ) {
+        my $before_unprinted = $lineno - $printed_lineno - 1;
+        if ( !$is_first_match && ( !$printed_lineno || $before_unprinted > $n_before_ctx_lines ) ) {
+            App::Ack::say( '--' );
         }
-        else {
-            return;
-        }
-    }
-}
 
-sub get_capture_indices {
-    return @capture_indices;
-}
+        # We want at most $n_before_ctx_lines of context.
+        if ( $before_unprinted > $n_before_ctx_lines ) {
+            $before_unprinted = $n_before_ctx_lines;
+        }
 
-sub get_match_column {
-    return $match_column_number;
-}
+        while ( $before_unprinted > 0 ) {
+            my $line = $before_context_buf[($before_context_pos - $before_unprinted + $n_before_ctx_lines) % $n_before_ctx_lines];
 
-}
+            chomp $line;
 
-sub resource_has_match {
-    my ( $resource, $opt ) = @_;
+            # Disable $opt->{column} since there are no matches in the context lines.
+            local $opt_column = 0;
 
-    my $has_match = 0;
-    my $fh = $resource->open();
-    if ( !$fh ) {
-        if ( $App::Ack::report_bad_filenames ) {
-            # XXX direct access to filename
-            App::Ack::warn( "$resource->{filename}: $!" );
-        }
-    }
-    else {
-        my $opt_v = $opt->{v};
-        my $re    = $opt->{regex};
-        while ( <$fh> ) {
-            if (/$re/o xor $opt_v) {
-                $has_match = 1;
-                last;
-            }
+            print_line_with_options( $filename, $line, $lineno-$before_unprinted, '-' );
+            $before_unprinted--;
         }
-        close $fh;
     }
 
-    return $has_match;
-}
+    print_line_with_options( $filename, $matching_line, $lineno, ':' );
 
-sub count_matches_in_resource {
-    my ( $resource, $opt ) = @_;
+    # We want to get the next $n_after_ctx_lines printed.
+    $after_context_pending = $n_after_ctx_lines;
 
-    my $nmatches = 0;
-    my $fh = $resource->open();
-    if ( !$fh ) {
-        if ( $App::Ack::report_bad_filenames ) {
-            # XXX direct access to filename
-            App::Ack::warn( "$resource->{filename}: $!" );
-        }
-    }
-    else {
-        my $opt_v = $opt->{v};
-        my $re    = $opt->{regex};
-        while ( <$fh> ) {
-            ++$nmatches if (/$re/o xor $opt_v);
-        }
-        close $fh;
-    }
+    $is_first_match = 0;
 
-    return $nmatches;
+    return;
 }
 
-sub main {
-    my @arg_sources = App::Ack::ConfigLoader::retrieve_arg_sources();
-
-    my $opt = App::Ack::ConfigLoader::process_args( @arg_sources );
-
-    $App::Ack::report_bad_filenames = !$opt->{dont_report_bad_filenames};
-
-    if ( $opt->{flush} ) {
-        $| = 1;
-    }
+}
 
-    if ( not defined $opt->{color} ) {
-        $opt->{color} = !App::Ack::output_to_pipe() && !$App::Ack::is_windows;
-    }
-    if ( not defined $opt->{heading} and not defined $opt->{break}  ) {
-        $opt->{heading} = $opt->{break} = !App::Ack::output_to_pipe();
-    }
+sub get_match_colno {
+    return $match_colno;
+}
 
-    if ( defined($opt->{H}) || defined($opt->{h}) ) {
-        $opt->{show_filename}= $opt->{H} && !$opt->{h};
-    }
+sub count_matches_in_file {
+    my $file = shift;
+    my $bail = shift;   # True if we're just checking for existence.
 
-    if ( my $output = $opt->{output} ) {
-        $output        =~ s{\\}{\\\\}g;
-        $output        =~ s{"}{\\"}g;
-        $opt->{output} = qq{"$output"};
-    }
+    my $nmatches = 0;
+    my $do_scan = 1;
 
-    my $resources;
-    if ( $App::Ack::is_filter_mode && !$opt->{files_from} ) { # probably -x
-        $resources    = App::Ack::Resources->from_stdin( $opt );
-        my $regex = $opt->{regex};
-        $regex = shift @ARGV if not defined $regex;
-        $opt->{regex} = build_regex( $regex, $opt );
+    if ( !$file->open() ) {
+        $do_scan = 0;
+        if ( $App::Ack::report_bad_filenames ) {
+            App::Ack::warn( $file->name . ": $!" );
+        }
     }
     else {
-        if ( $opt->{f} || $opt->{lines} ) {
-            if ( $opt->{regex} ) {
-                App::Ack::warn( "regex ($opt->{regex}) specified with -f or --lines" );
-                App::Ack::exit_from_ack( 0 ); # XXX the 0 is misleading
-            }
-        }
-        else {
-            my $regex = $opt->{regex};
-            $regex = shift @ARGV if not defined $regex;
-            $opt->{regex} = build_regex( $regex, $opt );
-        }
-        my @start;
-        if ( not defined $opt->{files_from} ) {
-            @start = @ARGV;
-        }
-        if ( !exists($opt->{show_filename}) ) {
-            unless(@start == 1 && !(-d $start[0])) {
-                $opt->{show_filename} = 1;
-            }
-        }
-
-        if ( defined $opt->{files_from} ) {
-            $resources = App::Ack::Resources->from_file( $opt, $opt->{files_from} );
-            exit 1 unless $resources;
-        }
-        else {
-            @start = ('.') unless @start;
-            foreach my $target (@start) {
-                if ( !-e $target && $App::Ack::report_bad_filenames) {
-                    App::Ack::warn( "$target: No such file or directory" );
-                }
+        if ( !$opt_v ) {
+            if ( !$file->may_be_present( $scan_re ) ) {
+                $do_scan = 0;
             }
-
-            $opt->{file_filter}    = _compile_file_filter($opt, \@start);
-            $opt->{descend_filter} = _compile_descend_filter($opt);
-
-            $resources = App::Ack::Resources->from_argv( $opt, \@start );
         }
     }
-    App::Ack::set_up_pager( $opt->{pager} ) if defined $opt->{pager};
-
-    my $print_filenames = $opt->{show_filename};
-    my $max_count       = $opt->{m};
-    my $ors             = $opt->{print0} ? "\0" : "\n";
-    my $only_first      = $opt->{1};
-
-    my $nmatches    = 0;
-    my $total_count = 0;
-RESOURCES:
-    while ( my $resource = $resources->next ) {
-        # XXX this variable name combined with what we're trying
-        # to do makes no sense.
-
-        # XXX Combine the -f and -g functions
-        if ( $opt->{f} ) {
-            # XXX printing should probably happen inside of App::Ack
-            if ( $opt->{show_types} ) {
-                show_types( $resource, $ors );
-            }
-            else {
-                App::Ack::print( $resource->name, $ors );
-            }
-            ++$nmatches;
-            last RESOURCES if defined($max_count) && $nmatches >= $max_count;
-        }
-        elsif ( $opt->{g} ) {
-            my $is_match = ( $resource->name =~ /$opt->{regex}/o );
-            if ( $opt->{v} ? !$is_match : $is_match ) {
-                if ( $opt->{show_types} ) {
-                    show_types( $resource, $ors );
-                }
-                else {
-                    App::Ack::print( $resource->name, $ors );
-                }
-                ++$nmatches;
-                last RESOURCES if defined($max_count) && $nmatches >= $max_count;
-            }
-        }
-        elsif ( $opt->{lines} ) {
-            my $print_filename = $opt->{show_filename};
-            my $passthru       = $opt->{passthru};
-
-            my %line_numbers;
-            foreach my $line ( @{ $opt->{lines} } ) {
-                my @lines             = split /,/, $line;
-                @lines                = map {
-                    /^(\d+)-(\d+)$/
-                        ? ( $1 .. $2 )
-                        : $_
-                } @lines;
-                @line_numbers{@lines} = (1) x @lines;
-            }
 
-            my $filename = $resource->name;
+    if ( $do_scan ) {
+        $file->reset();
 
-            local $opt->{color} = 0;
+        my $in_range = range_setup();
 
-            iterate($resource, $opt, sub {
+        my $fh = $file->{fh};
+        if ( $using_ranges ) {
+            while ( <$fh> ) {
                 chomp;
-
-                if ( $line_numbers{$.} ) {
-                    print_line_with_context($opt, $filename, $_, $.);
-                }
-                elsif ( $passthru ) {
-                    print_line_with_options($opt, $filename, $_, $., ':');
-                }
-                return 1;
-            });
-        }
-        elsif ( $opt->{count} ) {
-            my $matches_for_this_file = count_matches_in_resource( $resource, $opt );
-
-            unless ( $opt->{show_filename} ) {
-                $total_count += $matches_for_this_file;
-                next RESOURCES;
-            }
-
-            if ( !$opt->{l} || $matches_for_this_file > 0) {
-                if ( $print_filenames ) {
-                    App::Ack::print( $resource->name, ':', $matches_for_this_file, $ors );
-                }
-                else {
-                    App::Ack::print( $matches_for_this_file, $ors );
+                $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;
+                    }
                 }
-            }
-        }
-        elsif ( $opt->{l} || $opt->{L} ) {
-            my $is_match = resource_has_match( $resource, $opt );
-
-            if ( $opt->{L} ? !$is_match : $is_match ) {
-                App::Ack::print( $resource->name, $ors );
-                ++$nmatches;
-
-                last RESOURCES if $only_first;
-                last RESOURCES if defined($max_count) && $nmatches >= $max_count;
+                $in_range = 0 if ( $in_range && $opt_range_end && /$opt_range_end/o );
             }
         }
         else {
-            $nmatches += print_matches_in_resource( $resource, $opt );
-            if ( $nmatches && $only_first ) {
-                last RESOURCES;
+            while ( <$fh> ) {
+                chomp;
+                if ( /$search_re/o xor $opt_v ) {
+                    ++$nmatches;
+                    last if $bail;
+                }
             }
         }
     }
+    $file->close;
 
-    if ( $opt->{count} && !$opt->{show_filename} ) {
-        App::Ack::print( $total_count, "\n" );
-    }
+    return $nmatches;
+}
 
-    close $App::Ack::fh;
-    App::Ack::exit_from_ack( $nmatches );
+
+sub range_setup {
+    return !$using_ranges || (!$opt_range_start && $opt_range_end);
 }
 
 
+=pod
+
+=encoding UTF-8
 
 =head1 NAME
 
@@ -1030,18 +1129,18 @@ ack - grep-like text finder
 
 =head1 DESCRIPTION
 
-Ack is designed as a replacement for 99% of the uses of F<grep>.
+ack is designed as an alternative to F<grep> for programmers.
 
-Ack searches the named input FILEs (or standard input if no files
-are named, or the file name - is given) for lines containing a match
-to the given PATTERN.  By default, ack prints the matching lines.
+ack searches the named input FILEs or DIRECTORYs for lines containing a
+match to the given PATTERN.  By default, ack prints the matching lines.
+If no FILE or DIRECTORY is given, the current directory will be searched.
 
 PATTERN is a Perl regular expression.  Perl regular expressions
 are commonly found in other programming languages, but for the particulars
 of their behavior, please consult
-L<http://perldoc.perl.org/perlreref.html|perlreref>.  If you don't know
+L<perlreref|https://perldoc.perl.org/perlreref.html>.  If you don't know
 how to use regular expression but are interested in learning, you may
-consult L<http://perldoc.perl.org/perlretut.html|perlretut>.  If you do not
+consult L<perlretut|https://perldoc.perl.org/perlretut.html>.  If you do not
 need or want ack to use regular expressions, please see the
 C<-Q>/C<--literal> option.
 
@@ -1096,21 +1195,110 @@ to add/remove multiple directories from the ignore list.
 For a complete list of directories that do not get searched, run
 C<ack --dump>.
 
-=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<testfile>:
+
+    # This function calls print on "foo".
+    sub foo {
+        print 'foo';
+    }
+    my $print = 1;
+    sub bar {
+        print 'bar';
+    }
+    my $task = 'print';
+
+Calling C<ack print> 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<print> 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<< <body> >>, you could do
+
+    ack foo --range-end='<body>'
+
+Or to search after Perl's `__DATA__` or `__END__` markers, you would do
 
-I<ack> trumps I<grep> as an everyday tool 99% of the time, but don't
-throw I<grep> away, because there are times you'll still need it.
+    ack pattern --range-end='^__(END|DATA)__'
 
-E.g., searching through huge files looking for regexes that can be
-expressed with I<grep> syntax should be quicker with I<grep>.
+It's possible for a range to start and stop on the same line.  For example
 
-If your script or parent program uses I<grep> C<--quiet> or C<--silent>
-or needs exit 2 on IO error, use I<grep>.
+    --range-start='<title>' --range-end='</title>'
+
+would match this line as both the start and end of the range, making a
+one-line range.
+
+    <title>Page title</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 "<head>" section of
+your HTML files, you could do:
+
+    ack google -v --html --range-start='<head' --range-end='</head>'
+
+Specifying a range to search does not affect how matches are displayed.
+The context for a match will still be the same, and
+
+Using the context options work the same way, and will show context
+lines for matches even if the context lines fall outside the range.
+Similarly, C<--passthru> will show all lines in the file, but only show
+matches for lines within the range.
 
 =head1 OPTIONS
 
 =over 4
 
+=item B<--ackrc>
+
+Specifies an ackrc file to load after all others; see L</"ACKRC LOCATION SEMANTICS">.
+
 =item B<-A I<NUM>>, B<--after-context=I<NUM>>
 
 Print I<NUM> lines of trailing context after matching lines.
@@ -1127,6 +1315,8 @@ when used interactively.
 =item B<-C [I<NUM>]>, B<--context[=I<NUM>]>
 
 Print I<NUM> lines (default 2) of context around matching lines.
+You can specify zero lines of context to override another context
+specified in an ackrc.
 
 =item B<-c>, B<--count>
 
@@ -1140,7 +1330,7 @@ count.
 
 =item B<--[no]color>, B<--[no]colour>
 
-B<--color> highlights the matching text.  B<--nocolor> supresses
+B<--color> highlights the matching text.  B<--nocolor> suppresses
 the color.  This is on by default unless the output is redirected.
 
 On Windows, this option is off by default unless the
@@ -1155,6 +1345,10 @@ Sets the color to be used for filenames.
 
 Sets the color to be used for matches.
 
+=item B<--color-colno=I<color>>
+
+Sets the color to be used for column numbers.
+
 =item B<--color-lineno=I<color>>
 
 Sets the color to be used for line numbers.
@@ -1195,12 +1389,17 @@ as a path to search.
 =item B<--files-from=I<FILE>>
 
 The list of files to be searched is specified in I<FILE>.  The list of
-files are seperated by newlines.  If I<FILE> is C<->, the list is loaded
+files are separated by newlines.  If I<FILE> is C<->, the list is loaded
 from standard input.
 
+Note that the list of files is B<not> 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 recieving input via a pipe.
+Forces ack to act as if it were receiving input via a pipe.
 
 =item B<--[no]follow>
 
@@ -1211,11 +1410,30 @@ This is off by default.
 
 =item B<-g I<PATTERN>>
 
-Print files where the relative path + filename matches I<PATTERN>.
+Print searchable files where the relative path + filename matches
+I<PATTERN>.
 
-=item B<--[no]group>
+Note that
 
-B<--group> groups matches by file name.  This is the default
+    ack -g foo
+
+is exactly the same as
+
+    ack -f | ack foo
+
+This means that just as ack will not search, for example, F<.jpg>
+files, C<-g> will not list F<.jpg> files either.  ack is not intended
+to be a general-purpose file finder.
+
+Note also that if you have C<-i> in your .ackrc that the filenames
+to be matched will be case-insensitive as well.
+
+This option can be combined with B<--color> to make it easier to
+spot the match.
+
+=item B<--[no]group>
+
+B<--group> groups matches by file name.  This is the default
 when used interactively.
 
 B<--nogroup> prints one result per line, like grep.  This is the
@@ -1236,17 +1454,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>
 
@@ -1268,22 +1498,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<FILTERTYPE:FILTERARGS>>
+=item B<--ignore-file=I<FILTER:ARGS>>
 
-Ignore files matching I<FILTERTYPE:FILTERARGS>.  The filters are specified
+Ignore files matching I<FILTER:ARGS>.  The filters are specified
 identically to file type filters as seen in L</"Defining your own types">.
 
 =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<NUM>>
-
-Only print line I<NUM> 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>
 
@@ -1305,7 +1527,8 @@ same set of files.
 
 =item B<-m=I<NUM>>, B<--max-count=I<NUM>>
 
-Stop reading a file after I<NUM> matches.
+Print only I<NUM> 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>
 
@@ -1318,14 +1541,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<expr>>
 
 Output the evaluation of I<expr> 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</"Examples of F<--output>">.
+highlighting). If PATTERN matches more than once then a line is
+output for each non-overlapping match.
+
+I<expr> may contain the strings "\n", "\r" and "\t", which will be
+expanded to their corresponding characters line feed, carriage return
+and tab, respectively.
+
+I<expr> 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<gra(ph|nd)> 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<Version: (.+)|Revision: (.+)>,
+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<ack3> 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<program>>, B<--nopager>
 
@@ -1335,7 +1606,7 @@ via the C<ACK_PAGER> and C<ACK_PAGER_COLOR> 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<ACK_PAGER> or C<ACK_PAGER_COLOR>.
+B<--nopager> cancels any setting in F<~/.ackrc>, C<ACK_PAGER> or C<ACK_PAGER_COLOR>.
 No output will be sent through a pager.
 
 =item B<--passthru>
@@ -1344,18 +1615,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.
@@ -1365,16 +1673,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<MATCHING IN A RANGE OF LINES> 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<smartcase> in vim. This option is
-off by default, and ignored if C<-i> is specified.
+characters. This is similar to C<smartcase> 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.
 
@@ -1389,31 +1704,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<perl> or I<xml>.  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.
+
+Type inclusions can be repeated and are ORed together.
+
+See I<ack --help-types> for a list of valid types.
 
-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.
+=item B<-T TYPE>, B<--type=noTYPE>, B<--noTYPE>
 
-Type specifications can be repeated and are ORed together.
+Specifies the type of files to exclude from the search.  B<--type=noperl>
+can be done as B<--noperl>, although this is deprecated.
 
-See I<ack --help=types> for a list of valid types.
+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<TYPE>:I<FILTER>:I<FILTERARGS>>
+=item B<--type-add I<TYPE>:I<FILTER>:I<ARGS>>
 
-Files with the given FILTERARGS applied to the given FILTER
+Files with the given ARGS applied to the given FILTER
 are recognized as being of (the existing) type TYPE.
 See also L</"Defining your own types">.
 
+=item B<--type-set I<TYPE>:I<FILTER>:I<ARGS>>
 
-=item B<--type-set I<TYPE>:I<FILTER>:I<FILTERARGS>>
-
-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</"Defining your own types">.
 
@@ -1422,9 +1740,26 @@ also L</"Defining your own types">.
 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>
 
@@ -1432,14 +1767,16 @@ Display version and copyright information.
 
 =item B<-w>, B<--word-regexp>
 
-Force PATTERN to match only whole words.  The PATTERN is wrapped with
-C<\b> metacharacters.
+Force PATTERN to match only whole words.
 
 =item B<-x>
 
-An abbreviation for B<--files-from=->; the list of files to search are read
+An abbreviation for B<--files-from=->. The list of files to search are read
 from standard input, with one line per file.
 
+Note that the list of files is B<not> 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
@@ -1457,6 +1794,10 @@ a regular expression.
 
 Check with the admiral for traps.
 
+=item B<--cathy>
+
+Chocolate, Chocolate, Chocolate!
+
 =back
 
 =head1 THE .ackrc FILE
@@ -1469,7 +1810,7 @@ might look like this:
     # Always sort the files
     --sort-files
 
-    # Always color, even if piping to a another program
+    # Always color, even if piping to another program
     --color
 
     # Use "less -r" as my pager
@@ -1491,7 +1832,13 @@ an F<.ackrc> file - then you do not have to define your types over and
 over again. In the following examples the options will always be shown
 on one command line so that they can be easily copy & pasted.
 
-I<ack --perl foo> searches for foo in all perl files. I<ack --help=types>
+File types can be specified both with the the I<--type=xxx> option,
+or the file type as an option itself.  For example, if you create
+a filetype of "cobol", you can specify I<--type=cobol> or simply
+I<--cobol>.  File types must be at least two characters long.  This
+is why the C language is I<--cc> and the R language is I<--rr>.
+
+I<ack --perl foo> searches for foo in all perl files. I<ack --help-types>
 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<ack --type-add perl:ext:xs --perl foo>
@@ -1522,13 +1869,12 @@ The following does B<NOT> work in the F<.ackrc> file:
 
   --type-set eiffel:ext:e,eiffel
 
-
 In order to see all currently defined types, use I<--help-types>, e.g.
 I<ack --type-set backup:ext:bak --type-add perl:ext:perl --help-types>
 
-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<FILTERARGS> 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<ARGS> depends on the value
 of I<FILTER>.
 
 =over 4
@@ -1554,7 +1900,7 @@ Example:
 =item match:I<PATTERN>
 
 I<match> 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:
 
@@ -1572,7 +1918,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<Term::ANSIColor>
+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<ack --color-filename='red on_white'>, or by setting an environment
+variable, such as C<ACK_COLOR_FILENAME='red on_white'>.  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<ack --help-colors>
+and C<ack --help-rgb-colors>.
+
+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<ack --help-rgb-colors>.
 
 =head1 ENVIRONMENT VARIABLES
 
@@ -1587,43 +1991,26 @@ on the command line.
 Specifies the location of the user's F<.ackrc> file.  If this file doesn't
 exist, F<ack> 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<ACK_COLOR_FILENAME> 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<ACK_COLOR_FILENAME> for the color specifications.
+Color specification for the matched text in ack's output.  See the section
+"ack Colors" above.
 
 =item ACK_PAGER
 
@@ -1648,7 +2035,7 @@ If you are not on Windows, you never need to use C<ACK_PAGER_COLOR>.
 
 =head1 ACK & OTHER TOOLS
 
-=head2 Vim integration
+=head2 Simple vim integration
 
 F<ack> integrates easily with the Vim text editor. Set this in your
 F<.vimrc> to use F<ack> instead of F<grep>:
@@ -1661,27 +2048,10 @@ with F<ack> and easily step through the results in Vim:
 
   :grep Dumper perllib
 
-Miles Sterrett has written a Vim plugin for F<ack> which allows you to use
-C<:Ack> instead of C<:grep>, as well as several other advanced features.
-
-L<https://github.com/mileszs/ack.vim>
-
-=head2 Emacs integration
-
-Phil Jackson put together an F<ack.el> extension that "provides a
-simple compilation mode ... has the ability to guess what files you
-want to search for based on the major-mode."
+=head2 Editor integration
 
-L<http://www.shellarchive.co.uk/content/emacs.html>
-
-=head2 TextMate integration
-
-Pedro Melo is a TextMate user who writes "I spend my day mostly
-inside TextMate, and the built-in find-in-project sucks with large
-projects.  So I hacked a TextMate command that was using find +
-grep to use ack.  The result is the Search in Project with ack, and
-you can find it here:
-L<http://www.simplicidade.org/notes/archives/2008/03/search_in_proje.html>"
+Many users have integrated ack into their preferred text editors.
+For details and links, see L<https://beyondgrep.com/more-tools/>.
 
 =head2 Shell and Return Code
 
@@ -1702,13 +2072,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<ack -f> to see
@@ -1718,124 +2088,150 @@ options to show the type of each file selected.
 =head2 Use B<--dump>
 
 This lists the ackrc files that are loaded and the options loaded
-from them.
-So for example you can find a list of directories that do not get searched or where filetypes are defined.
+from them.  You may be loading an F<.ackrc> file that you didn't know
+you were loading.
 
-=head1 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<ack -f --perl> will create a
-list of all the Perl files in a tree, ideal for sending into F<xargs>.
-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</etc/ackrc> 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<C:\Documents and Settings\All Users\Application Data\ackrc>
 
-=head2 Use F<-Q> when in doubt about metacharacters
+Under Windows Vista/7, the global ackrc is at
+C<C:\ProgramData\ackrc>
 
-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<troublesome.gif>, 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<C:\Documents and Settings\$USER\Application Data\ackrc>.
 
-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<C:\Users\$USER\AppData\Roaming\ackrc>.
 
-=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<https://github.com/beyondgrep/ack3>
 
-=back
+Please report any bugs or feature requests to the issues list at
+Github: L<https://github.com/beyondgrep/ack3/issues>.
 
-For more details and other variables see
-L<http://perldoc.perl.org/perlvar.html#Variables-related-to-regular-expressions|perlvar>.
+Please include the operating system that you're using; the output of
+the command C<ack --version>; 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<https://github.com/beyondgrep/ack3/issues>.  Also read the
+F<DEVELOPERS.md> 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<https://groups.google.com/group/ack-users>.
 
-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<ack> can be found at:
 
-=head2 Share your knowledge
+=over 4
 
-Join the ack-users mailing list.  Send me your tips and I may add
-them here.
+=item * The ack homepage
 
-=head1 FAQ
+L<https://beyondgrep.com/>
 
-=head2 Why isn't ack finding a match in (some file)?
+=item * Source repository
+
+L<https://github.com/beyondgrep/ack3>
+
+=item * The ack issues list at Github
+
+L<https://github.com/beyondgrep/ack3/issues>
+
+=item * The ack announcements mailing list
+
+L<https://groups.google.com/group/ack-announcement>
+
+=item * The ack users' mailing list
 
-Probably because it's of a type that ack doesn't recognize.  ack's
-searching behavior is driven by filetype.  B<If ack doesn't know
-what kind of file it is, ack ignores the file.>
+L<https://groups.google.com/group/ack-users>
+
+=item * The ack development mailing list
+
+L<https://groups.google.com/group/ack-users>
+
+=back
+
+=head1 COMMUNITY
+
+There are ack mailing lists and a Slack channel for ack.  See
+L<https://beyondgrep.com/community/> for details.
+
+=head1 FAQ
 
-Use the C<-f> switch to see a list of files that ack will search
-for you.
+This is the Frequently Asked Questions list for ack.
 
-If you want ack to search files that it doesn't recognize, use the
-C<-a> switch.
+=head2 Can I stop using grep now?
 
-If you want ack to search every file, even ones that it always
-ignores like coredumps and backup files, use the C<-u> switch.
+Many people find I<ack> to be better than I<grep> as an everyday tool
+99% of the time, but don't throw I<grep> 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<grep>
+will probably perform better.
 
-=head2 Why does ack ignore unknown files by default?
+=head2 Why isn't ack finding a match in (some file)?
 
-ack is designed by a programmer, for programmers, for searching
-large trees of code.  Most codebases have a lot files in them which
-aren't source files (like compiled object files, source control
-metadata, etc), and grep wastes a lot of time searching through all
-of those as well and returning matches from those files.
+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.
 
-That's why ack's behavior of not searching things it doesn't recognize
-is one of its greatest strengths: the speed you get from only
-searching the things that you want to be looking at.
+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<ack -f> shows, then ack never looks in it.
 
 =head2 Wouldn't it be great if F<ack> did search & replace?
 
@@ -1849,15 +2245,21 @@ this from the Unix shell:
 
     $ perl -i -p -e's/foo/bar/g' $(ack -f --php)
 
-=head2 Can you make ack recognize F<.xyz> files?
+=head2 Can I make ack recognize F<.xyz> files?
+
+Yes!  Please see L</"Defining your own types"> in the ack manual.
 
-Yes!  Please see L</"Defining your own types">.  If you think
-that F<ack> should recognize a type by default, please see
-L</"ENHANCEMENTS">.
+=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<https://github.com/beyondgrep/ack3/issues>.  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?
 
@@ -1901,1714 +2303,2069 @@ would like to search for these, you may prefix your search term with C<--> or
 use the C<--match> option.  (However, don't forget that C<+> is a regular
 expression metacharacter!)
 
-=head1 ACKRC LOCATION SEMANTICS
+=head2 Why does C<"ack '.{40000,}'"> fail?  Isn't that a valid regex?
 
-Ack can load its configuration from many sources.  This list
-specifies the sources Ack looks for configuration; each one
-that is found is loaded in the order specified here, and
-each one overrides options set in any of the sources preceding
-it.  (For example, if I set --sort-files in my user ackrc, and
---nosort-files on the command line, the command line takes
-precedence)
+The Perl language limits the repetition quantifier to 32K.  You
+can search for C<.{32767}> but not C<.{32768}>.
 
-=over 4
+=head2 Ack does "X" and shouldn't, should it?
 
-=item *
+We try to remain as close to grep's behavior as possible, so when in
+doubt, see what grep does!  If there's a mismatch in functionality there,
+please submit an issue to GitHub, and/or bring it up on the ack-users
+mailing list.
 
-Defaults are loaded from App::Ack::ConfigDefaults.  This can be omitted
-using C<--ignore-ack-defaults>.
+=cut
 
-=item * Global ackrc
+=head1 ACKNOWLEDGEMENTS
 
-Options are then loaded from the global ackrc.  This is located at
-C</etc/ackrc> on Unix-like systems, and
-C<C:\Documents and Settings\All Users\Application Data> on Windows.
-This can be omitted using C<--noenv>.
+How appropriate to have I<ack>nowledgements!
 
-=item * User ackrc
+Thanks to everyone who has contributed to ack in any way, including
+Dan Book,
+Tomasz Konojacki,
+Salomon Smeke,
+M. Scott Ford,
+Anders Eriksson,
+H.Merijn Brand,
+Duke Leto,
+Gerhard Poul,
+Ethan Mallove,
+Marek Kubica,
+Ray Donnelly,
+Nikolaj Schumacher,
+Ed Avis,
+Nick Morrott,
+Austin Chamberlin,
+Varadinsky,
+SE<eacute>bastien FeugE<egrave>re,
+Jakub Wilk,
+Pete Houston,
+Stephen Thirlwall,
+Jonah Bishop,
+Chris Rebert,
+Denis Howe,
+RaE<uacute>l GundE<iacute>n,
+James McCoy,
+Daniel Perrett,
+Steven Lee,
+Jonathan Perret,
+Fraser Tweedale,
+RaE<aacute>l GundE<aacute>n,
+Steffen Jaeckel,
+Stephan Hohe,
+Michael Beijen,
+Alexandr Ciornii,
+Christian Walde,
+Charles Lee,
+Joe McMahon,
+John Warwick,
+David Steinbrunner,
+Kara Martens,
+Volodymyr Medvid,
+Ron Savage,
+Konrad Borowski,
+Dale Sedivic,
+Michael McClimon,
+Andrew Black,
+Ralph Bodenner,
+Shaun Patterson,
+Ryan Olson,
+Shlomi Fish,
+Karen Etheridge,
+Olivier Mengue,
+Matthew Wild,
+Scott Kyle,
+Nick Hooey,
+Bo Borgerson,
+Mark Szymanski,
+Marq Schneider,
+Packy Anderson,
+JR Boyens,
+Dan Sully,
+Ryan Niebur,
+Kent Fredric,
+Mike Morearty,
+Ingmar Vanhassel,
+Eric Van Dewoestine,
+Sitaram Chamarty,
+Adam James,
+Richard Carlsson,
+Pedro Melo,
+AJ Schuster,
+Phil Jackson,
+Michael Schwern,
+Jan Dubois,
+Christopher J. Madsen,
+Matthew Wickline,
+David Dyck,
+Jason Porritt,
+Jjgod Jiang,
+Thomas Klausner,
+Uri Guttman,
+Peter Lewis,
+Kevin Riggle,
+Ori Avtalion,
+Torsten Blix,
+Nigel Metheringham,
+GE<aacute>bor SzabE<oacute>,
+Tod Hagan,
+Michael Hendricks,
+E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason,
+Piers Cawley,
+Stephen Steneker,
+Elias Lutfallah,
+Mark Leighton Fisher,
+Matt Diephouse,
+Christian Jaeger,
+Bill Sully,
+Bill Ricker,
+David Golden,
+Nilson Santos F. Jr,
+Elliot Shank,
+Merijn Broeren,
+Uwe Voelker,
+Rick Scott,
+Ask BjE<oslash>rn Hansen,
+Jerry Gay,
+Will Coleda,
+Mike O'Regan,
+Slaven ReziE<0x107>,
+Mark Stosberg,
+David Alan Pisoni,
+Adriano Ferreira,
+James Keenan,
+Leland Johnson,
+Ricardo Signes,
+Pete Krawczyk and
+Rob Hoelz.
 
-Options are then loaded from the user's ackrc.  This is located at
-C<$HOME/.ackrc> on Unix-like systems, and
-C<C:\Documents and Settings\$USER\Application Data>.  If a different
-ackrc is desired, it may be overriden with the C<$ACKRC> environment
-variable.
-This can be omitted using C<--noenv>.
+=head1 AUTHOR
 
-=item * Project ackrc
+Andy Lester, C<< <andy at petdance.com> >>
 
-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>.
+=head1 COPYRIGHT & LICENSE
 
-=item * ACK_OPTIONS
+Copyright 2005-2020 Andy Lester.
 
-Options are then loaded from the enviroment variable C<ACK_OPTIONS>.  This can
-be omitted using C<--noenv>.
+This program is free software; you can redistribute it and/or modify
+it under the terms of the Artistic License v2.0.
 
-=item * Command line
+See https://www.perlfoundation.org/artistic-license-20.html or the LICENSE.md
+file that comes with the ack distribution.
 
-Options are then loaded from the command line.
+=cut
 
-=back
+1;
+package App::Ack;
 
-=head1 DIFFERENCES BETWEEN ACK 1.X AND ACK 2.X
+use warnings;
+use strict;
 
-A lot of changes were made for ack 2; here is a list of them.
 
-=head2 GENERAL CHANGES
+our $VERSION;
+our $COPYRIGHT;
+BEGIN {
+    $VERSION = 'v3.4.0'; # Check https://beyondgrep.com/ for updates
+    $COPYRIGHT = 'Copyright 2005-2020 Andy Lester.';
+}
+our $STANDALONE = 0;
+our $ORIGINAL_PROGRAM_NAME;
 
-=over 4
+our $fh;
 
-=item *
+BEGIN {
+    $fh = *STDOUT;
+}
 
-When no selectors are specified, ack 1.x only searches through files that
-it can map to a file type.  ack 2.x, by constrast, 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 *
+our %types;
+our %type_wanted;
+our %mappings;
+our %ignore_dirs;
 
-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</"Defining your own types">.
+our $is_filter_mode;
+our $output_to_pipe;
 
-=item *
+our $is_windows;
 
-ack now loads multiple ackrc files; see L</"ACKRC LOCATION SEMANTICS"> for
-details.
+our $debug_nopens = 0;
 
-=item *
+# Line ending, changes to "\0" if --print0.
+our $ors = "\n";
 
-ack's default filter definitions aren't special; you may tell ack to
-completely disregard them if you don't like them.
+BEGIN {
+    # These have to be checked before any filehandle diddling.
+    $output_to_pipe  = not -t *STDOUT;
+    $is_filter_mode = -p STDIN;
 
-=back
+    $is_windows      = ($^O eq 'MSWin32');
+}
 
-=head2 REMOVED OPTIONS
 
-=over 4
+sub warn {
+    return CORE::warn( _my_program(), ': ', @_, "\n" );
+}
 
-=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.
+sub die {
+    return CORE::die( _my_program(), ': ', @_, "\n" );
+}
 
-=item *
+sub _my_program {
+    require File::Basename;
+    return File::Basename::basename( $0 );
+}
 
-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 *
+sub thpppt {
+    my $y = q{_   /|,\\'!.x',=(www)=,   U   };
+    $y =~ tr/,x!w/\nOo_/;
 
-The B<--binary> option has been removed.
+    App::Ack::print( "$y ack $_[0]!\n" );
+    exit 0;
+}
 
-=item *
+sub ackbar {
+    my $x;
+    $x = <<'_BAR';
+ 6?!I'7!I"?%+!
+ 3~!I#7#I"7#I!?!+!="+"="+!:!
+ 2?#I!7!I!?#I!7!I"+"=%+"=#
+ 1?"+!?*+!=#~"=!+#?"="+!
+ 0?"+!?"I"?&+!="~!=!~"=!+%="+"
+ /I!+!?)+!?!+!=$~!=!~!="+!="+"?!="?!
+ .?%I"?%+%='?!=#~$="
+ ,,!?%I"?(+$=$~!=#:"~$:!~!
+ ,I!?!I!?"I"?!+#?"+!?!+#="~$:!~!:!~!:!,!:!,":#~!
+ +I!?&+!="+!?#+$=!~":!~!:!~!:!,!:#,!:!,%:"
+ *+!I!?!+$=!+!=!+!?$+#=!~":!~":#,$:",#:!,!:!
+ *I!?"+!?!+!=$+!?#+#=#~":$,!:",!:!,&:"
+ )I!?$=!~!=#+"?!+!=!+!=!~!="~!:!~":!,'.!,%:!~!
+ (=!?"+!?!=!~$?"+!?!+!=#~"=",!="~$,$.",#.!:!=!
+ (I"+"="~"=!+&=!~"=!~!,!~!+!=!?!+!?!=!I!?!+"=!.",!.!,":!
+ %I$?!+!?!=%+!~!+#~!=!~#:#=!~!+!~!=#:!,%.!,!.!:"
+ $I!?!=!?!I!+!?"+!=!~!=!~!?!I!?!=!+!=!~#:",!~"=!~!:"~!=!:",&:" '-/
+ $?!+!I!?"+"=!+"~!,!:"+#~#:#,"=!~"=!,!~!,!.",!:".!:! */! !I!t!'!s! !a! !g!r!e!p!!! !/!
+ $+"=!+!?!+"~!=!:!~!:"I!+!,!~!=!:!~!,!:!,$:!~".&:"~!,# (-/
+ %~!=!~!=!:!.!+"~!:!,!.!,!~!=!:$.!,":!,!.!:!~!,!:!=!.#="~!,!:" ./!
+ %=!~!?!+"?"+!=!~",!.!:!?!~!.!:!,!:!,#.!,!:","~!:!=!~!=!:",!~! ./!
+ %+"~":!~!=#~!:!~!,!.!~!:",!~!=!~!.!:!,!.",!:!,":!=":!.!,!:!7! -/!
+ %~",!:".#:!=!:!,!:"+!:!~!:!.!,!~!,!.#,!.!,$:"~!,":"~!=! */!
+ &=!~!=#+!=!~",!.!:",#:#,!.",+:!,!.",!=!+!?!
+ &~!=!~!=!~!:"~#:",!.!,#~!:!.!+!,!.",$.",$.#,!+!I!?!
+ &~!="~!:!~":!~",!~!=!~":!,!:!~!,!:!,&.$,#."+!?!I!?!I!
+ &~!=!~!=!+!,!:!~!:!=!,!:!~&:$,!.!,".!,".!,#."~!+!?$I!
+ &~!=!~!="~!=!:!~":!,!~%:#,!:",!.!,#.",#I!7"I!?!+!?"I"
+ &+!I!7!:#~"=!~!:!,!:"~$.!=!.!,!~!,$.#,!~!7!I#?!+!?"I"7!
+ %7#?!+!~!:!=!~!=!~":!,!:"~":#.!,)7#I"?"I!7&
+ %7#I!=":!=!~!:"~$:"~!:#,!:!,!:!~!:#,!7#I!?#7)
+ $7$+!,!~!=#~!:!~!:!~$:#,!.!~!:!=!,":!7#I"?#7+=!?!
+ $7#I!~!,!~#=!~!:"~!:!,!:!,#:!=!~",":!7$I!?#I!7*+!=!+"
+ "I!7$I!,":!,!.!=":$,!:!,$:$7$I!+!?"I!7+?"I!7!I!7!,!
+ !,!7%I!:",!."~":!,&.!,!:!~!I!7$I!+!?"I!7,?!I!7',!
+ !7(,!.#~":!,%.!,!7%I!7!?#I"7,+!?!7*
+7+:!,!~#,"=!7'I!?#I"7/+!7+
+77I!+!7!?!7!I"71+!7,
+_BAR
 
-The B<--skipped> option has been removed.
+    return _pic_decode($x);
+}
+
+sub cathy {
+    my $x = <<'CATHY';
+ 0+!--+!
+ 0|! "C!H!O!C!O!L!A!T!E!!! !|!
+ 0|! "C!H!O!C!O!L!A!T!E!!! !|!
+ 0|! "C!H!O!C!O!L!A!T!E!!! !|!
+ 0|! $A"C!K!!! $|!
+ 0+!--+!
+ 6\! 1:!,!.! !
+ 7\! /.!M!~!Z!M!~!
+ 8\! /~!D! "M! !
+ 4.! $\! /M!~!.!8! +.!M# 4
+ 0,!.! (\! .~!M!N! ,+!I!.!M!.! 3
+ /?!O!.!M!:! '\! .O!.! +~!Z!=!N!.! 4
+ ..! !D!Z!.!Z!.! '\! 9=!M".! 6
+ /.! !.!~!M".! '\! 8~! 9
+ 4M!.! /.!7!N!M!.! F
+ 4.! &:!M! !N"M# !M"N!M! #D!M&=! =
+ :M!7!M#:! !~!M!7!,!$!M!:! #.! !O!N!.!M!:!M# ;
+ 8Z!M"~!N!$!D!.!N!?! !I!N!.! (?!M! !M!,!D!M".! 9
+ (?!Z!M!N!:! )=!M!O!8!.!M!+!M! !M!,! !O!M! +,!M!.!M!~!Z!N!M!:! &:!~! 0
+ &8!7!.!~!M"D!M!,! &M!?!=!8! !M!,!O! !M!+! !+!O!.!M! $M#~! !.!8!M!Z!.!M! !O!M"Z! %:!~!M!Z!M!Z!.! +
+ &:!M!7!,! *M!.!Z!M! !8"M!.!M!~! !.!M!.!=! #~!8!.!M! !7!M! "N!Z#I! !D!M!,!M!.! $."M!,! !M!.! *
+ 2$!O! "N! !.!M!I! !7" "M! "+!O! !~!M! !d!O!.!7!I!M!.! !.!O!=!M!.! !M",!M!.! %.!$!O!D! +
+ 1~!O! "M!+! !8!$! "M! "?!O! %Z!8!D!M!?!8!I!O!7!M! #M!.!M! "M",!M! 4
+ 07!~! ".!8! !.!M! "I!+! !.!M! &Z!D!.!7!=!M! !:!.!M! #:!8"+! !.!+!8! !8! 3
+ /~!M! #N! !~!M!$! !.!M! !.!M" &~!M! "~!M!O! "D! $M! !8! "M!,!M!+!D!.! 1
+ #.! #?!M!N!.! #~!O! $M!.!7!$! "?" !?!~!M! '7!8!?!M!.!+!M"O! $?"$!D! !.!O! !$!7!I!.! 0
+ $,!M!:!O!?! ".! !?!=! $=!:!O! !M! "M! !M! !+!$! (.! +.!M! !M!.! !8! !+"Z!~! $:!M!$! !.! '
+ #.!8!.!I!$! $7!I! %M" !=!M! !~!M!D! "7!I! .I!O! %?!=!,!D! !,!M! !D!~!8!~! %D!M! (
+ #.!M"?! $=!O! %=!N! "8!.! !Z!M! #M!~! (M!:! #.!M" &O! !M!.! !?!,! !8!.!N!~! $8!N!M!,!.! %
+ *$!O! &M!,! "O! !.!M!.! #M! (~!M( &O!.! !7! "M! !.!M!.!M!,! #.!M! !M! &
+ )=!8!.! $.!M!O!.! "$!.!I!N! !I!M# (7!M(I! %D"Z!M! "=!I! "M! !M!:! #~!D! '
+ )D! &8!N!:! ".!O! !M!="M! "M! (7!M) %." !M!D!."M!.! !$!=! !M!,! +
+ (M! &+!.!M! #Z!7!O!M!.!~!8! +,!M#D!?!M#D! #.!Z!M#,!Z!?! !~!N! "N!.! !M! +
+ 'D!:! %$!D! !?! #M!Z! !8!.! !M"?!7!?!7! '+!I!D! !?!O!:!M!:! ":!M!:! !M!7".!M! "8!+! !:!D! !.!M! *
+ %.!O!:! $.!O!+! !D!.! #M! "M!.!+!N!I!Z! "7!M!N!M!N!?!I!7!Z!=!M'D"~! #M!.!8!$! !:! !.!M! "N!?! !,!O! )
+ !.!?!M!:!M!I! %8!,! "M!.! #M! "N! !M!.! !M!.! !+!~! !.!M!.! ':!M! $M! $M!Z!$! !M!.! "D! "M! "?!M! (
+ !7!8! !+!I! ".! "$!=! ":!$! "+! !M!.! !O! !M!I!M".! !=!~! ",!O! '=!M! $$!,! #N!:! ":!8!.! !D!~! !,!M!.! !:!M!.! &
+ !:!,!.! &Z" #D! !.!8!."M!.! !8!?!Z!M!.!M! #Z!~! !?!M!Z!.! %~!O!.!8!$!N!8!O!I!:!~! !+! #M!.! !.!M!.! !+!M! ".!~!M!+! $
+ !.! 'D!I! #?!M!.!M!,! !.!Z! !.!8! #M&O!I!?! (~!I!M"." !M!Z!.! !M!N!.! "+!$!.! "M!.! !M!?!.! "8!M! $
+ (O!8! $M! !M!.! ".!:! !+!=! #M! #.!M! !+" *$!M":!.! !M!~! "M!7! #M! #7!Z! "M"$!M!.! !.! #
+ '$!Z! #.!7!+!M! $.!,! !+!:! #N! #.!M!.!+!M! +D!M! #=!N! ":!O! #=!M! #Z!D! $M!I! %
+ $,! ".! $.!M" %$!.! !?!~! "+!7!." !.!M!,! !M! *,!N!M!.$M!?! "D!,! #M!.! #N! +
+ ,M!Z! &M! "I!,! "M! %I!M! !?!=!.! (Z!8!M! $:!M!.! !,!M! $D! #.!M!.! )
+ +8!O! &.!8! "I!,! !~!M! &N!M! !M!D! '?!N!O!." $?!7! "?!~! #M!.! #I!D!.! (
+ 3M!,! "N!.! !D" &.!+!M!.! !M":!.":!M!7!M!D! 'M!.! "M!.! "M!,! $I! )
+ 3I! #M! "M!,! !:! &.!M" ".!,! !.!$!M!I! #.! !:! !.!M!?! "N!+! ".! /
+ 1M!,! #.!M!8!M!=!.! +~!N"O!Z"~! *+!M!.! "M! 2
+ 0.!M! &M!.! 8:! %.!M!Z! "M!=! *O!,! %
+ 0?!$! &N! )." .,! %."M! ":!M!.! 0
+ 0N!:! %?!O! #.! ..! &,! &.!D!,! "N!I! 0
+CATHY
+    return _pic_decode($x);
+}
+
+sub _pic_decode {
+    my($compressed) = @_;
+    $compressed =~ s/(.)(.)/$1x(ord($2)-32)/eg;
+    App::Ack::print( $compressed );
+    exit 0;
+}
 
-=item *
 
-The B<--text> option has been removed.
+sub show_help {
+    App::Ack::print( <<"END_OF_HELP" );
+Usage: ack [OPTION]... PATTERN [FILES OR DIRECTORIES]
 
-=item *
+Search for PATTERN in each source file in the tree from the current
+directory on down.  If any files or directories are specified, then
+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 "-".
 
-The B<--invert-file-match> option has been removed.  Instead, you may
-use B<-v> with B<-g>.
+Default switches may be specified in an .ackrc file. If you want no dependency
+on the environment, turn it off with --noenv.
 
-=back
+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.
 
-=head2 CHANGED OPTIONS
+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
 
-=over 4
+Searching:
+  -i, --ignore-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 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.
 
-=item *
+Search output:
+  --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
+  -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
+  --[no]column                  Show the column number of the first match
 
-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>.
+  -A NUM, --after-context=NUM   Print NUM lines of trailing context after
+                                matching lines.
+  -B NUM, --before-context=NUM  Print NUM lines of leading context before
+                                matching lines.
+  -C [NUM], --context[=NUM]     Print NUM lines (default 2) of output context.
 
-=back
+  --print0                      Print null byte as separator between filenames,
+                                only works with -f, -g, -l, -L or -c.
 
-=head2 ADDED OPTIONS
+  -s                            Suppress error messages about nonexistent or
+                                unreadable files.
 
-=over 4
 
-=item *
+File presentation:
+  --pager=COMMAND               Pipes all ack output through COMMAND.  For
+                                example, --pager="less -R".  Ignored if output
+                                is redirected.
+  --nopager                     Do not send output through a pager.  Cancels
+                                any setting in ~/.ackrc, ACK_PAGER or
+                                ACK_PAGER_COLOR.
+  --[no]heading                 Print a filename heading above each file's
+                                results.  (default: on when used interactively)
+  --[no]break                   Print a break between results from different
+                                files.  (default: on when used interactively)
+  --group                       Same as --heading --break
+  --nogroup                     Same as --noheading --nobreak
+  -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)
+  --color-filename=COLOR
+  --color-match=COLOR
+  --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).
 
-B<--files-from> was added so that a user may submit a list of filenames as
-a list of files to search.
 
-=item *
+File finding:
+  --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.
+  -x                            Read the list of files to search from STDIN.
 
-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.
+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: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.
 
-=item *
+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.
 
-B<-s> was added to tell ack to suppress error messages about non-existent or
-unreadable files.
+File type specification:
+  --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.
 
-=item *
+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.
+  --dump                        Dump information on which options are loaded
+                                and where they're defined.
+  --[no]filter                  Force ack to treat standard input as a pipe
+                                (--filter) or tty (--nofilter)
+  --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!
 
-B<--ignore-directory> and B<--noignore-directory> were added as aliases for
-B<--ignore-dir> and B<--noignore-dir> respectively.
+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.
 
-=item *
+Exit status is 0 if match, 1 if no match.
 
-B<--ignore-file> was added so that users may specify patterns of files to
-ignore (ex. /.*~$/).
+ack's home page is at https://beyondgrep.com/
 
-=item *
+The full ack manual is available by running "ack --man".
 
-B<--dump> was added to allow users to easily find out which options are
-set where.
+This is version $App::Ack::VERSION of ack.  Run "ack --version" for full version info.
+END_OF_HELP
 
-=item *
+    return;
+ }
 
-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.
+sub show_help_types {
+    App::Ack::print( <<'END_OF_HELP' );
+Usage: ack [OPTION]... PATTERN [FILES OR DIRECTORIES]
 
-=item *
+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.
 
-B<--ignore-ack-defaults> was added so that users may ignore ack's default
-options in favor of their own.
+Note that some files may appear in multiple types.  For example, a file
+called Rakefile is both Ruby (--type=ruby) and Rakefile (--type=rakefile).
 
-=item *
+END_OF_HELP
 
-B<--bar> was added so ack users may consult Admiral Ackbar.
+    my @types = keys %App::Ack::mappings;
+    my $maxlen = 0;
+    for ( @types ) {
+        $maxlen = length if $maxlen < length;
+    }
+    for my $type ( sort @types ) {
+        next if $type =~ /^-/; # Stuff to not show
+        my $ext_list = $mappings{$type};
 
-=back
+        if ( ref $ext_list ) {
+            $ext_list = join( '; ', map { $_->to_string } @{$ext_list} );
+        }
+        App::Ack::print( sprintf( "    %-*.*s %s\n", $maxlen, $maxlen, $type, $ext_list ) );
+    }
 
-=head1 AUTHOR
+    return;
+}
 
-Andy Lester, C<< <andy at petdance.com> >>
 
-=head1 BUGS
 
-Please report any bugs or feature requests to the issues list at
-Github: L<https://github.com/petdance/ack2/issues>
+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).
 
-=head1 ENHANCEMENTS
+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.
 
-All enhancement requests MUST first be posted to the ack-users
-mailing list at L<http://groups.google.com/group/ack-users>.  I
-will not consider a request without it first getting seen by other
-ack users.  This includes requests for new filetypes.
+Run ack --help-rgb-colors for a chart of the RGB colors.
 
-There is a list of enhancements I want to make to F<ack> in the ack
-issues list at Github: L<https://github.com/petdance/ack2/issues>
+END_OF_HELP
 
-Patches are always welcome, but patches with tests get the most
-attention.
+    _show_color_grid();
 
-=head1 SUPPORT
+    return;
+}
 
-Support for and information about F<ack> can be found at:
 
-=over 4
 
-=item * The ack homepage
+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).
 
-L<http://beyondgrep.com/>
+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.
 
-=item * The ack-users mailing list
+Here is a grid of the 216 possible values for NNN.
 
-L<http://groups.google.com/group/ack-users>
+END_OF_HELP
 
-=item * The ack issues list at Github
+    _show_rgb_grid();
 
-L<https://github.com/petdance/ack2/issues>
+    App::Ack::say( 'Here are the 216 possible colors with the "reverse" modifier applied.', "\n" );
 
-=item * AnnoCPAN: Annotated CPAN documentation
+    _show_rgb_grid( 'reverse' );
 
-L<http://annocpan.org/dist/ack>
+    return;
+}
 
-=item * CPAN Ratings
 
-L<http://cpanratings.perl.org/d/ack>
+sub _show_color_grid {
+    my $cell_width = 7;
 
-=item * Search CPAN
+    my @fg_colors = qw( black red green yellow blue magenta cyan white );
+    my @bg_colors = map { "on_$_" } @fg_colors;
 
-L<http://search.cpan.org/dist/ack>
+    App::Ack::say(
+        _color_cell( '' ),
+        map { _color_cell( $_ ) } @fg_colors
+    );
 
-=item * Git source repository
+    App::Ack::say(
+        _color_cell( '' ),
+        map { _color_cell( '-' x $cell_width ) } @fg_colors
+    );
 
-L<https://github.com/petdance/ack2>
+    for my $bg ( '', @bg_colors ) {
+        App::Ack::say(
+            _color_cell( '' ),
+            ( map { _color_cell( $_, "$_ $bg" ) } @fg_colors ),
+            $bg
+        );
 
-=back
+        App::Ack::say(
+            _color_cell( 'bold' ),
+            ( map { _color_cell( $_, "bold $_ $bg" ) } @fg_colors ),
+            $bg
+        );
+        App::Ack::say();
+    }
 
-=head1 ACKNOWLEDGEMENTS
+    return;
+}
 
-How appropriate to have I<ack>nowledgements!
 
-Thanks to everyone who has contributed to ack in any way, including
-Dale Sedivic,
-Michael McClimon,
-Andrew Black,
-Ralph Bodenner,
-Shaun Patterson,
-Ryan Olson,
-Shlomi Fish,
-Karen Etheridge,
-Olivier Mengue,
-Matthew Wild,
-Scott Kyle,
-Nick Hooey,
-Bo Borgerson,
-Mark Szymanski,
-Marq Schneider,
-Packy Anderson,
-JR Boyens,
-Dan Sully,
-Ryan Niebur,
-Kent Fredric,
-Mike Morearty,
-Ingmar Vanhassel,
-Eric Van Dewoestine,
-Sitaram Chamarty,
-Adam James,
-Richard Carlsson,
-Pedro Melo,
-AJ Schuster,
-Phil Jackson,
-Michael Schwern,
-Jan Dubois,
-Christopher J. Madsen,
-Matthew Wickline,
-David Dyck,
-Jason Porritt,
-Jjgod Jiang,
-Thomas Klausner,
-Uri Guttman,
-Peter Lewis,
-Kevin Riggle,
-Ori Avtalion,
-Torsten Blix,
-Nigel Metheringham,
-GE<aacute>bor SzabE<oacute>,
-Tod Hagan,
-Michael Hendricks,
-E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason,
-Piers Cawley,
-Stephen Steneker,
-Elias Lutfallah,
-Mark Leighton Fisher,
-Matt Diephouse,
-Christian Jaeger,
-Bill Sully,
-Bill Ricker,
-David Golden,
-Nilson Santos F. Jr,
-Elliot Shank,
-Merijn Broeren,
-Uwe Voelker,
-Rick Scott,
-Ask BjE<oslash>rn Hansen,
-Jerry Gay,
-Will Coleda,
-Mike O'Regan,
-Slaven ReziE<0x107>,
-Mark Stosberg,
-David Alan Pisoni,
-Adriano Ferreira,
-James Keenan,
-Leland Johnson,
-Ricardo Signes,
-Pete Krawczyk and
-Rob Hoelz.
-
-=head1 COPYRIGHT & LICENSE
-
-Copyright 2005-2013 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
-file that comes with the ack distribution.
+sub _color_cell {
+    my $text  = shift;
+    my $color = shift;
 
-=cut
-package File::Next;
+    my $cell_width = 7;
+    $text = sprintf( '%-*s', $cell_width, $text );
 
-use strict;
-use warnings;
+    return ($color ? Term::ANSIColor::colored( $text, $color ) : $text) . ' ';
+}
 
 
-our $VERSION = '1.12';
+sub _show_rgb_grid {
+    my $modifier = shift // '';
 
+    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
 
-use File::Spec ();
+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
 
-our $name; # name of the current file
-our $dir;  # dir of the current file
+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
 
-our %files_defaults;
-our %skip_dirs;
+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
 
-BEGIN {
-    %files_defaults = (
-        file_filter     => undef,
-        descend_filter  => undef,
-        error_handler   => sub { CORE::die @_ },
-        warning_handler => sub { CORE::warn @_ },
-        sort_files      => undef,
-        follow_symlinks => 1,
-        nul_separated   => 0,
-    );
-    %skip_dirs = map {($_,1)} (File::Spec->curdir, File::Spec->updir);
-}
+433 433 433 433 433 443 443 443 443 443 343 343 343 343 343 344 344 344 344 344 334 334 334 334 334 434 434 434 434 434
+211 211 211 211 211 221 221 221 221 221 121 121 121 121 121 122 122 122 122 122 112 112 112 112 112 212 212 212 212 212
 
+322 322 322 322 322 332 332 332 332 332 232 232 232 232 232 233 233 233 233 233 223 223 223 223 223 323 323 323 323 323
 
-sub files {
-    die _bad_invocation() if @_ && defined($_[0]) && ($_[0] eq __PACKAGE__);
+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
 
-    my ($parms,@queue) = _setup( \%files_defaults, @_ );
-    my $filter = $parms->{file_filter};
+    $grid =~ s/(\d\d\d)/Term::ANSIColor::colored( "$1", "$modifier rgb$1" )/eg;
 
-    return sub {
-        while (@queue) {
-            my ($dirname,$file,$fullpath) = splice( @queue, 0, 3 );
-            if ( -f $fullpath || -p $fullpath || $fullpath =~ m{^/dev/fd} ) {
-                if ( $filter ) {
-                    local $_ = $file;
-                    local $File::Next::dir = $dirname;
-                    local $File::Next::name = $fullpath;
-                    next if not $filter->();
-                }
-                return wantarray ? ($dirname,$file,$fullpath) : $fullpath;
-            }
-            elsif ( -d _ ) {
-                unshift( @queue, _candidate_files( $parms, $fullpath ) );
-            }
-        } # while
+    App::Ack::say( $grid );
 
-        return;
-    }; # iterator
+    return;
 }
 
 
+sub show_man {
+    require Pod::Usage;
+    Pod::Usage::pod2usage({
+        -input   => $App::Ack::ORIGINAL_PROGRAM_NAME,
+        -verbose => 2,
+        -exitval => 0,
+    });
 
+    return;
+}
 
 
 
-sub from_file {
-    die _bad_invocation() if @_ && defined($_[0]) && ($_[0] eq __PACKAGE__);
-
-    my ($parms,@queue) = _setup( \%files_defaults, @_ );
-    my $err  = $parms->{error_handler};
-    my $warn = $parms->{error_handler};
-
-    my $filename = $queue[1];
+sub get_version_statement {
+    require Config;
 
-    if ( !defined($filename) ) {
-        $err->( 'Must pass a filename to from_file()' );
-        return undef;
+    my $copyright = $App::Ack::COPYRIGHT;
+    my $this_perl = $Config::Config{perlpath};
+    if ($^O ne 'VMS') {
+        my $ext = $Config::Config{_exe};
+        $this_perl .= $ext unless $this_perl =~ m/$ext$/i;
     }
+    my $perl_ver = sprintf( 'v%vd', $^V );
 
-    my $fh;
-    if ( $filename eq '-' ) {
-        $fh = \*STDIN;
-    }
-    else {
-        if ( !open( $fh, '<', $filename ) ) {
-            $err->( "Unable to open $filename: $!" );
-            return undef;
-        }
-    }
-    my $filter = $parms->{file_filter};
+    my $build_type = $App::Ack::STANDALONE ? 'standalone version' : 'standard build';
 
-    return sub {
-        local $/ = $parms->{nul_separated} ? "\x00" : $/;
-        while ( my $fullpath = <$fh> ) {
-            chomp $fullpath;
-            next unless $fullpath =~ /./;
-            if ( not ( -f $fullpath || -p _ ) ) {
-                $warn->( "$fullpath: No such file" );
-                next;
-            }
+    return <<"END_OF_VERSION";
+ack $App::Ack::VERSION ($build_type)
+Running under Perl $perl_ver at $this_perl
 
-            my ($volume,$dirname,$file) = File::Spec->splitpath( $fullpath );
-            if ( $filter ) {
-                local $_ = $file;
-                local $File::Next::dir  = $dirname;
-                local $File::Next::name = $fullpath;
-                next if not $filter->();
-            }
-            return wantarray ? ($dirname,$file,$fullpath) : $fullpath;
-        } # while
-        close $fh;
+$copyright
 
-        return;
-    }; # iterator
+This program is free software.  You may modify or distribute it
+under the terms of the Artistic License v2.0.
+END_OF_VERSION
 }
 
-sub _bad_invocation {
-    my $good = (caller(1))[3];
-    my $bad  = $good;
-    $bad =~ s/(.+)::/$1->/;
-    return "$good must not be invoked as $bad";
-}
 
-sub sort_standard($$)   { return $_[0]->[1] cmp $_[1]->[1] }
-sub sort_reverse($$)    { return $_[1]->[1] cmp $_[0]->[1] }
+sub print            { print {$fh} @_; return; }
+sub say              { print {$fh} @_, $ors; return; }
+sub print_blank_line { print {$fh} "\n"; return; }
 
-sub reslash {
-    my $path = shift;
+sub set_up_pager {
+    my $command = shift;
 
-    my @parts = split( /\//, $path );
+    return if App::Ack::output_to_pipe();
 
-    return $path if @parts < 2;
+    my $pager;
+    if ( not open( $pager, '|-', $command ) ) {
+        App::Ack::die( qq{Unable to pipe to pager "$command": $!} );
+    }
+    $fh = $pager;
 
-    return File::Spec->catfile( @parts );
+    return;
 }
 
 
+sub output_to_pipe {
+    return $output_to_pipe;
+}
 
-sub _setup {
-    my $defaults = shift;
-    my $passed_parms = ref $_[0] eq 'HASH' ? {%{+shift}} : {}; # copy parm hash
 
-    my %passed_parms = %{$passed_parms};
+sub exit_from_ack {
+    my $nmatches = shift;
 
-    my $parms = {};
-    for my $key ( keys %{$defaults} ) {
-        $parms->{$key} =
-            exists $passed_parms{$key}
-                ? delete $passed_parms{$key}
-                : $defaults->{$key};
-    }
+    my $rc = $nmatches ? 0 : 1;
+    exit $rc;
+}
 
-    # Any leftover keys are bogus
-    for my $badkey ( keys %passed_parms ) {
-        my $sub = (caller(1))[3];
-        $parms->{error_handler}->( "Invalid option passed to $sub(): $badkey" );
-    }
 
-    # If it's not a code ref, assume standard sort
-    if ( $parms->{sort_files} && ( ref($parms->{sort_files}) ne 'CODE' ) ) {
-        $parms->{sort_files} = \&sort_standard;
-    }
-    my @queue;
+sub show_types {
+    my $file = shift;
 
-    for ( @_ ) {
-        my $start = reslash( $_ );
-        if (-d $start) {
-            push @queue, ($start,undef,$start);
-        }
-        else {
-            push @queue, (undef,$start,$start);
-        }
-    }
+    my @types = filetypes( $file );
+    my $arrow = @types ? ' => ' : ' =>';
+    App::Ack::say( $file->name, $arrow, join( ',', @types ) );
 
-    return ($parms,@queue);
+    return;
 }
 
 
-sub _candidate_files {
-    my $parms   = shift;
-    my $dirname = shift;
-
-    my $dh;
-    if ( !opendir $dh, $dirname ) {
-        $parms->{error_handler}->( "$dirname: $!" );
-        return;
-    }
-
-    my @newfiles;
-    my $descend_filter = $parms->{descend_filter};
-    my $follow_symlinks = $parms->{follow_symlinks};
-    my $sort_sub = $parms->{sort_files};
+sub filetypes {
+    my ( $file ) = @_;
 
-    for my $file ( grep { !exists $skip_dirs{$_} } readdir $dh ) {
-        my $has_stat;
+    my @matches;
 
-        # Only do directory checking if we have a descend_filter
-        my $fullpath = File::Spec->catdir( $dirname, $file );
-        if ( !$follow_symlinks ) {
-            next if -l $fullpath;
-            $has_stat = 1;
-        }
+    foreach my $k (keys %App::Ack::mappings) {
+        my $filters = $App::Ack::mappings{$k};
 
-        if ( $descend_filter ) {
-            if ( $has_stat ? (-d _) : (-d $fullpath) ) {
-                local $File::Next::dir = $fullpath;
-                local $_ = $file;
-                next if not $descend_filter->();
+        foreach my $filter (@{$filters}) {
+            # Clone the file.
+            my $clone = $file->clone;
+            if ( $filter->filter($clone) ) {
+                push @matches, $k;
+                last;
             }
         }
-        if ( $sort_sub ) {
-            push( @newfiles, [ $dirname, $file, $fullpath ] );
-        }
-        else {
-            push( @newfiles, $dirname, $file, $fullpath );
-        }
     }
-    closedir $dh;
 
-    if ( $sort_sub ) {
-        return map { @{$_} } sort $sort_sub @newfiles;
-    }
+    # https://metacpan.org/pod/distribution/Perl-Critic/lib/Perl/Critic/Policy/Subroutines/ProhibitReturnSort.pm
+    @matches = sort @matches;
+    return @matches;
+}
+
+
+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 @newfiles;
+    return 0;
 }
 
 
-1; # End of File::Next
-package App::Ack;
+1; # End of App::Ack
+package App::Ack::ConfigDefault;
 
 use warnings;
 use strict;
 
 
-our $VERSION;
-our $GIT_REVISION;
-our $COPYRIGHT;
-BEGIN {
-    $VERSION = '2.04';
-    $COPYRIGHT = 'Copyright 2005-2013 Andy Lester.';
-    $GIT_REVISION = q{8f405b7};
-}
 
-our $fh;
 
-BEGIN {
-    $fh = *STDOUT;
+sub options {
+    return split( /\n/, _options_block() );
 }
 
 
-our %types;
-our %type_wanted;
-our %mappings;
-our %ignore_dirs;
+sub options_clean {
+    return grep { /./ && !/^#/ } options();
+}
 
-our $is_filter_mode;
-our $output_to_pipe;
 
-our $dir_sep_chars;
-our $is_cygwin;
-our $is_windows;
+sub _options_block {
+    my $lines = <<'HERE';
+# This is the default ackrc for ack version ==VERSION==.
 
-use File::Spec 1.00015 ();
+# There are four different ways to match
+#
+# is:  Match the filename exactly
+#
+# ext: Match the extension of the filename exactly
+#
+# match: Match the filename against a Perl regular expression
+#
+# firstlinematch: Match the first 250 characters of the first line
+#   of text against a Perl regular expression.  This is only for
+#   the --type-add option.
 
-BEGIN {
-    # These have to be checked before any filehandle diddling.
-    $output_to_pipe  = not -t *STDOUT;
-    $is_filter_mode = -p STDIN;
 
-    $is_cygwin       = ($^O eq 'cygwin');
-    $is_windows      = ($^O =~ /MSWin32/);
-    $dir_sep_chars   = $is_windows ? quotemeta( '\\/' ) : quotemeta( File::Spec->catfile( '', '' ) );
-}
+### Directories to ignore
 
+# Bazaar
+# https://bazaar.canonical.com/
+--ignore-directory=is:.bzr
 
+# Codeville
+# http://freshmeat.sourceforge.net/projects/codeville
+--ignore-directory=is:.cdv
 
-sub remove_dir_sep {
-    my $path = shift;
-    $path =~ s/[$dir_sep_chars]$//;
+# Interface Builder (Xcode)
+# https://en.wikipedia.org/wiki/Interface_Builder
+--ignore-directory=is:~.dep
+--ignore-directory=is:~.dot
+--ignore-directory=is:~.nib
+--ignore-directory=is:~.plst
 
-    return $path;
-}
+# Git
+# https://git-scm.com/
+--ignore-directory=is:.git
+# When submodules are used, .git is a file.
+--ignore-file=is:.git
 
+# Mercurial
+# https://www.mercurial-scm.org/
+--ignore-directory=is:.hg
 
+# Quilt
+# https://directory.fsf.org/wiki/Quilt
+--ignore-directory=is:.pc
 
-sub warn {
-    return CORE::warn( _my_program(), ': ', @_, "\n" );
-}
+# Subversion
+# https://subversion.apache.org/
+--ignore-directory=is:.svn
 
+# Monotone
+# https://www.monotone.ca/
+--ignore-directory=is:_MTN
 
-sub die {
-    return CORE::die( _my_program(), ': ', @_, "\n" );
-}
+# CVS
+# https://savannah.nongnu.org/projects/cvs
+--ignore-directory=is:CVS
 
-sub _my_program {
-    require File::Basename;
-    return File::Basename::basename( $0 );
-}
+# RCS
+# https://www.gnu.org/software/rcs/
+--ignore-directory=is:RCS
 
+# SCCS
+# https://en.wikipedia.org/wiki/Source_Code_Control_System
+--ignore-directory=is:SCCS
 
+# darcs
+# http://darcs.net/
+--ignore-directory=is:_darcs
 
-sub filetypes_supported {
-    return keys %mappings;
-}
+# Vault/Fortress
+--ignore-directory=is:_sgbak
 
-sub _get_thpppt {
-    my $y = q{_   /|,\\'!.x',=(www)=,   U   };
-    $y =~ tr/,x!w/\nOo_/;
-    return $y;
-}
+# autoconf
+# https://www.gnu.org/software/autoconf/
+--ignore-directory=is:autom4te.cache
 
-sub _thpppt {
-    my $y = _get_thpppt();
-    App::Ack::print( "$y ack $_[0]!\n" );
-    exit 0;
-}
+# Perl module building
+--ignore-directory=is:blib
+--ignore-directory=is:_build
 
-sub _bar {
-    my $x;
-    $x = <<'_BAR';
- 6?!I'7!I"?%+!
- 3~!I#7#I"7#I!?!+!="+"="+!:!
- 2?#I!7!I!?#I!7!I"+"=%+"=#
- 1?"+!?*+!=#~"=!+#?"="+!
- 0?"+!?"I"?&+!="~!=!~"=!+%="+"
- /I!+!?)+!?!+!=$~!=!~!="+!="+"?!="?!
- .?%I"?%+%='?!=#~$="
- ,,!?%I"?(+$=$~!=#:"~$:!~!
- ,I!?!I!?"I"?!+#?"+!?!+#="~$:!~!:!~!:!,!:!,":#~!
- +I!?&+!="+!?#+$=!~":!~!:!~!:!,!:#,!:!,%:"
- *+!I!?!+$=!+!=!+!?$+#=!~":!~":#,$:",#:!,!:!
- *I!?"+!?!+!=$+!?#+#=#~":$,!:",!:!,&:"
- )I!?$=!~!=#+"?!+!=!+!=!~!="~!:!~":!,'.!,%:!~!
- (=!?"+!?!=!~$?"+!?!+!=#~"=",!="~$,$.",#.!:!=!
- (I"+"="~"=!+&=!~"=!~!,!~!+!=!?!+!?!=!I!?!+"=!.",!.!,":!
- %I$?!+!?!=%+!~!+#~!=!~#:#=!~!+!~!=#:!,%.!,!.!:"
- $I!?!=!?!I!+!?"+!=!~!=!~!?!I!?!=!+!=!~#:",!~"=!~!:"~!=!:",&:" '-/
- $?!+!I!?"+"=!+"~!,!:"+#~#:#,"=!~"=!,!~!,!.",!:".!:! */! !I!t!'!s! !a! !g!r!e!p!!! !/!
- $+"=!+!?!+"~!=!:!~!:"I!+!,!~!=!:!~!,!:!,$:!~".&:"~!,# (-/
- %~!=!~!=!:!.!+"~!:!,!.!,!~!=!:$.!,":!,!.!:!~!,!:!=!.#="~!,!:" ./!
- %=!~!?!+"?"+!=!~",!.!:!?!~!.!:!,!:!,#.!,!:","~!:!=!~!=!:",!~! ./!
- %+"~":!~!=#~!:!~!,!.!~!:",!~!=!~!.!:!,!.",!:!,":!=":!.!,!:!7! -/!
- %~",!:".#:!=!:!,!:"+!:!~!:!.!,!~!,!.#,!.!,$:"~!,":"~!=! */!
- &=!~!=#+!=!~",!.!:",#:#,!.",+:!,!.",!=!+!?!
- &~!=!~!=!~!:"~#:",!.!,#~!:!.!+!,!.",$.",$.#,!+!I!?!
- &~!="~!:!~":!~",!~!=!~":!,!:!~!,!:!,&.$,#."+!?!I!?!I!
- &~!=!~!=!+!,!:!~!:!=!,!:!~&:$,!.!,".!,".!,#."~!+!?$I!
- &~!=!~!="~!=!:!~":!,!~%:#,!:",!.!,#.",#I!7"I!?!+!?"I"
- &+!I!7!:#~"=!~!:!,!:"~$.!=!.!,!~!,$.#,!~!7!I#?!+!?"I"7!
- %7#?!+!~!:!=!~!=!~":!,!:"~":#.!,)7#I"?"I!7&
- %7#I!=":!=!~!:"~$:"~!:#,!:!,!:!~!:#,!7#I!?#7)
- $7$+!,!~!=#~!:!~!:!~$:#,!.!~!:!=!,":!7#I"?#7+=!?!
- $7#I!~!,!~#=!~!:"~!:!,!:!,#:!=!~",":!7$I!?#I!7*+!=!+"
- "I!7$I!,":!,!.!=":$,!:!,$:$7$I!+!?"I!7+?"I!7!I!7!,!
- !,!7%I!:",!."~":!,&.!,!:!~!I!7$I!+!?"I!7,?!I!7',!
- !7(,!.#~":!,%.!,!7%I!7!?#I"7,+!?!7*
-7+:!,!~#,"=!7'I!?#I"7/+!7+
-77I!+!7!?!7!I"71+!7,
-_BAR
+# Perl Devel::Cover module's output directory
+# https://metacpan.org/release/Devel-Cover
+--ignore-directory=is:cover_db
 
-    $x =~ s/(.)(.)/$1x(ord($2)-32)/eg;
-    App::Ack::print( $x );
-    exit 0;
-}
+# Node modules created by npm
+--ignore-directory=is:node_modules
 
+# CMake cache
+# https://www.cmake.org/
+--ignore-directory=is:CMakeFiles
 
-sub show_help {
-    my $help_arg = shift || 0;
+# Eclipse workspace folder
+# https://eclipse.org/
+--ignore-directory=is:.metadata
 
-    return show_help_types() if $help_arg =~ /^types?/;
+# Cabal (Haskell) sandboxes
+# https://www.haskell.org/cabal/users-guide/installing-packages.html
+--ignore-directory=is:.cabal-sandbox
 
-    App::Ack::print( <<"END_OF_HELP" );
-Usage: ack [OPTION]... PATTERN [FILES OR DIRECTORIES]
+# Python caches
+# https://docs.python.org/3/tutorial/modules.html
+--ignore-directory=is:__pycache__
+--ignore-directory=is:.pytest_cache
 
-Search for PATTERN in each source file in the tree from the current
-directory on down.  If any files or directories are specified, then
-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 "-".
+# macOS Finder remnants
+--ignore-directory=is:__MACOSX
+--ignore-file=is:.DS_Store
 
-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.
+### Files to ignore
 
-Example: ack -i select
+# Backup files
+--ignore-file=ext:bak
+--ignore-file=match:/~$/
 
-Searching:
-  -i, --ignore-case             Ignore case distinctions in PATTERN
-  --[no]smart-case              Ignore case distinctions in PATTERN,
-                                only if PATTERN contains no upper case.
-                                Ignored if -i is specified
-  -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
+# Emacs swap files
+--ignore-file=match:/^#.+#$/
 
-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
+# vi/vim swap files https://www.vim.org/
+--ignore-file=match:/[._].*[.]swp$/
 
-  -A NUM, --after-context=NUM   Print NUM lines of trailing context after matching
-                                lines.
-  -B NUM, --before-context=NUM  Print NUM lines of leading context before matching
-                                lines.
-  -C [NUM], --context[=NUM]     Print NUM lines (default 2) of output context.
+# core dumps
+--ignore-file=match:/core[.]\d+$/
 
-  --print0                      Print null byte as separator between filenames,
-                                only works with -f, -g, -l, -L or -c.
+# minified Javascript
+--ignore-file=match:/[.-]min[.]js$/
+--ignore-file=match:/[.]js[.]min$/
 
-  -s                            Suppress error messages about nonexistent or
-                                unreadable files.
+# minified CSS
+--ignore-file=match:/[.]min[.]css$/
+--ignore-file=match:/[.]css[.]min$/
 
+# JS and CSS source maps
+--ignore-file=match:/[.]js[.]map$/
+--ignore-file=match:/[.]css[.]map$/
 
-File presentation:
-  --pager=COMMAND               Pipes all ack output through COMMAND.  For example,
-                                --pager="less -R".  Ignored if output is redirected.
-  --nopager                     Do not send output through a pager.  Cancels any
-                                setting in ~/.ackrc, ACK_PAGER or ACK_PAGER_COLOR.
-  --[no]heading                 Print a filename heading above each file's results.
-                                (default: on when used interactively)
-  --[no]break                   Print a break between results from different 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
-                                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.
-  --flush                       Flush output immediately, even when ack is used
-                                non-interactively (when output goes to a pipe or
-                                file).
+# PDFs, because they pass Perl's -T detection
+--ignore-file=ext:pdf
 
+# Common graphics, just as an optimization
+--ignore-file=ext:gif,jpg,jpeg,png
 
-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.
-  -x                            Read the list of files to search from STDIN.
+# Common archives, as an optimization
+--ignore-file=ext:gz,tar,tgz,zip
 
-File inclusion/exclusion:
-  --[no]ignore-dir=name         Add/Remove directory from the list of ignored dirs
-  --[no]ignore-directory=name   Synonym for ignore-dir
-  --ignore-file=filter          Add filter for ignoring files
-  -r, -R, --recurse             Recurse into subdirectories (ack's default behavior)
-  -n, --no-recurse              No descending into subdirectories
-  --[no]follow                  Follow symlinks.  Default is off.
-  -k, --known-types             Include only files with types that ack recognizes.
+# Python compiles modules
+--ignore-file=ext:pyc,pyd,pyo
 
-  --type=X                      Include only X files, where X is a recognized filetype.
-  --type=noX                    Exclude X files.
-                                See "ack --help-types" for supported filetypes.
+# C extensions
+--ignore-file=ext:so
 
-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 of type TYPE.
-  --type-del TYPE               Removes all filters associated with TYPE.
+# Compiled gettext files
+--ignore-file=ext:mo
 
+### Filetypes defined
 
-Miscellaneous:
-  --[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 the default definitions that ack includes.
-  --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
-  --[no]filter                  Force ack to treat standard input as a pipe (--filter) or tty (--nofilter)
-  --man                         Man page
-  --version                     Display version & copyright
-  --thpppt                      Bill the Cat
-  --bar                         The warning admiral
+# Makefiles
+# https://www.gnu.org/s/make/
+--type-add=make:ext:mk
+--type-add=make:ext:mak
+--type-add=make:is:makefile
+--type-add=make:is:Makefile
+--type-add=make:is:Makefile.Debug
+--type-add=make:is:Makefile.Release
+--type-add=make:is:GNUmakefile
 
-Exit status is 0 if match, 1 if no match.
+# Rakefiles
+# https://rake.rubyforge.org/
+--type-add=rake:is:Rakefile
 
-This is version $VERSION of ack.
-END_OF_HELP
+# CMake
+# https://cmake.org/
+--type-add=cmake:is:CMakeLists.txt
+--type-add=cmake:ext:cmake
 
-    return;
- }
+# Actionscript
+--type-add=actionscript:ext:as,mxml
 
+# Ada
+# https://www.adaic.org/
+--type-add=ada:ext:ada,adb,ads
 
+# ASP
+# https://docs.microsoft.com/en-us/previous-versions/office/developer/server-technologies/aa286483(v=msdn.10)
+--type-add=asp:ext:asp
 
-sub show_help_types {
-    App::Ack::print( <<'END_OF_HELP' );
-Usage: ack [OPTION]... PATTERN [FILES OR DIRECTORIES]
+# ASP.Net
+# https://dotnet.microsoft.com/apps/aspnet
+--type-add=aspx:ext:master,ascx,asmx,aspx,svc
 
-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.
+# Assembly
+--type-add=asm:ext:asm,s
 
-Note that some extensions may appear in multiple types.  For example,
-.pod files are both Perl and Parrot.
+# DOS/Windows batch
+--type-add=batch:ext:bat,cmd
 
-END_OF_HELP
+# ColdFusion
+# https://en.wikipedia.org/wiki/ColdFusion
+--type-add=cfmx:ext:cfc,cfm,cfml
 
-    my @types = filetypes_supported();
-    my $maxlen = 0;
-    for ( @types ) {
-        $maxlen = length if $maxlen < length;
-    }
-    for my $type ( sort @types ) {
-        next if $type =~ /^-/; # Stuff to not show
-        my $ext_list = $mappings{$type};
+# Clojure
+# https://clojure.org/
+--type-add=clojure:ext:clj,cljs,edn,cljc
 
-        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 ) );
-    }
+# C
+# .xs are Perl C files
+--type-add=cc:ext:c,h,xs
 
-    return;
-}
+# C header files
+--type-add=hh:ext:h
 
-sub show_man {
-    require Pod::Usage;
+# CoffeeScript
+# https://coffeescript.org/
+--type-add=coffeescript:ext:coffee
 
-    Pod::Usage::pod2usage({
-        -input   => $App::Ack::orig_program_name,
-        -verbose => 2,
-        -exitval => 0,
-    });
+# C++
+--type-add=cpp:ext:cpp,cc,cxx,m,hpp,hh,h,hxx
 
-    return;
-}
+# C++ header files
+--type-add=hpp:ext:hpp,hh,h,hxx
 
+# C#
+--type-add=csharp:ext:cs
 
-sub get_version_statement {
-    require Config;
+# CSS
+# https://www.w3.org/Style/CSS/
+--type-add=css:ext:css
 
-    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 );
+# Dart
+# https://dart.dev/
+--type-add=dart:ext:dart
 
-    my $git_revision = $GIT_REVISION ? " (git commit $GIT_REVISION)" : '';
+# Delphi
+# https://en.wikipedia.org/wiki/Embarcadero_Delphi
+--type-add=delphi:ext:pas,int,dfm,nfm,dof,dpk,dproj,groupproj,bdsgroup,bdsproj
 
-    return <<"END_OF_VERSION";
-ack ${VERSION}${git_revision}
-Running under Perl $ver at $this_perl
+# Elixir
+# https://elixir-lang.org/
+--type-add=elixir:ext:ex,exs
 
-$copyright
+# Emacs Lisp
+# https://www.gnu.org/software/emacs
+--type-add=elisp:ext:el
 
-This program is free software.  You may modify or distribute it
-under the terms of the Artistic License v2.0.
-END_OF_VERSION
-}
+# Erlang
+# https://www.erlang.org/
+--type-add=erlang:ext:erl,hrl
 
+# Fortran
+# https://en.wikipedia.org/wiki/Fortran
+--type-add=fortran:ext:f,f77,f90,f95,f03,for,ftn,fpp
 
-sub print_version_statement {
-    App::Ack::print( get_version_statement() );
+# Go
+# https://golang.org/
+--type-add=go:ext:go
 
-    return;
-}
+# Groovy
+# https://www.groovy-lang.org/
+--type-add=groovy:ext:groovy,gtmpl,gpp,grunit,gradle
 
+# GSP
+# https://gsp.grails.org/
+--type-add=gsp:ext:gsp
 
-sub get_copyright {
-    return $COPYRIGHT;
-}
+# Haskell
+# https://www.haskell.org/
+--type-add=haskell:ext:hs,lhs
 
+# HTML
+--type-add=html:ext:htm,html,xhtml
 
-# print subs added in order to make it easy for a third party
-# module (such as App::Wack) to redefine the display methods
-# and show the results in a different way.
-sub print                   { print {$fh} @_; return; }
-sub print_first_filename    { App::Ack::print( $_[0], "\n" ); return; }
-sub print_blank_line        { App::Ack::print( "\n" ); return; }
-sub print_separator         { App::Ack::print( "--\n" ); return; }
-sub print_filename          { App::Ack::print( $_[0], $_[1] ); return; }
-sub print_line_no           { App::Ack::print( $_[0], $_[1] ); return; }
-sub print_column_no         { App::Ack::print( $_[0], $_[1] ); return; }
-sub print_count {
-    my $filename = shift;
-    my $nmatches = shift;
-    my $ors = shift;
-    my $count = shift;
-    my $show_filename = shift;
+# Jade
+# http://jade-lang.com/
+--type-add=jade:ext:jade
 
-    if ($show_filename) {
-        App::Ack::print( $filename );
-        App::Ack::print( ':', $nmatches ) if $count;
-    }
-    else {
-        App::Ack::print( $nmatches ) if $count;
-    }
-    App::Ack::print( $ors );
+# Java
+# https://www.oracle.com/technetwork/java/index.html
+--type-add=java:ext:java,properties
 
-    return;
-}
+# JavaScript
+--type-add=js:ext:js
 
-sub print_count0 {
-    my $filename = shift;
-    my $ors = shift;
-    my $show_filename = shift;
+# JSP
+# https://www.oracle.com/technetwork/java/javaee/jsp/index.html
+--type-add=jsp:ext:jsp,jspx,jspf,jhtm,jhtml
 
-    if ($show_filename) {
-        App::Ack::print( $filename, ':0', $ors );
-    }
-    else {
-        App::Ack::print( '0', $ors );
-    }
+# JSON
+# https://json.org/
+--type-add=json:ext:json
 
-    return;
-}
+# Kotlin
+# https://kotlinlang.org/
+--type-add=kotlin:ext:kt,kts
 
-sub set_up_pager {
-    my $command = shift;
+# Less
+# http://www.lesscss.org/
+--type-add=less:ext:less
 
-    return if App::Ack::output_to_pipe();
+# Common Lisp
+# https://common-lisp.net/
+--type-add=lisp:ext:lisp,lsp
 
-    my $pager;
-    if ( not open( $pager, '|-', $command ) ) {
-        App::Ack::die( qq{Unable to pipe to pager "$command": $!} );
-    }
-    $fh = $pager;
+# Lua
+# https://www.lua.org/
+--type-add=lua:ext:lua
+--type-add=lua:firstlinematch:/^#!.*\blua(jit)?/
 
-    return;
-}
+# 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
 
-sub output_to_pipe {
-    return $output_to_pipe;
-}
+# Objective-C
+--type-add=objc:ext:m,h
 
+# Objective-C++
+--type-add=objcpp:ext:mm,h
 
-sub exit_from_ack {
-    my $nmatches = shift;
+# OCaml
+# https://ocaml.org/
+--type-add=ocaml:ext:ml,mli,mll,mly
 
-    my $rc = $nmatches ? 0 : 1;
-    exit $rc;
-}
+# 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
 
+# Perl's Plain Old Documentation format, POD
+--type-add=pod:ext:pod
 
-1; # End of App::Ack
-package App::Ack::Resource;
+# PHP
+# https://www.php.net/
+--type-add=php:ext:php,phpt,php3,php4,php5,phtml
+--type-add=php:firstlinematch:/^#!.*\bphp/
 
+# Plone
+# https://plone.org/
+--type-add=plone:ext:pt,cpt,metadata,cpy,py
 
-use warnings;
-use strict;
-use overload
-    '""' => 'name';
+# Python
+# https://www.python.org/
+--type-add=python:ext:py
+--type-add=python:firstlinematch:/^#!.*\bpython/
 
-sub FAIL {
-    require Carp;
-    Carp::confess( 'Must be overloaded' );
-}
+# R
+# https://www.r-project.org/
+--type-add=rr:ext:R
 
+# reStructured Text
+# https://docutils.sourceforge.io/rst.html
+--type-add=rst:ext:rst
 
-sub new {
-    return FAIL();
-}
+# Ruby
+# https://www.ruby-lang.org/
+--type-add=ruby:ext:rb,rhtml,rjs,rxml,erb,rake,spec
+--type-add=ruby:is:Rakefile
+--type-add=ruby:firstlinematch:/^#!.*\bruby/
 
+# Rust
+# https://www.rust-lang.org/
+--type-add=rust:ext:rs
 
-sub name {
-    return FAIL();
-}
+# Sass
+# https://sass-lang.com
+--type-add=sass:ext:sass,scss
 
+# Scala
+# https://www.scala-lang.org/
+--type-add=scala:ext:scala
 
-sub is_binary {
-    return FAIL();
-}
+# Scheme
+# https://groups.csail.mit.edu/mac/projects/scheme/
+--type-add=scheme:ext:scm,ss
 
+# Shell
+--type-add=shell:ext:sh,bash,csh,tcsh,ksh,zsh,fish
+--type-add=shell:firstlinematch:/^#!.*\b(?:ba|t?c|k|z|fi)?sh\b/
 
-sub open {
-    return FAIL();
-}
+# Smalltalk
+# http://www.smalltalk.org/
+--type-add=smalltalk:ext:st
 
+# Smarty
+# https://www.smarty.net/
+--type-add=smarty:ext:tpl
 
-sub needs_line_scan {
-    return FAIL();
-}
+# SQL
+# https://www.iso.org/standard/45498.html
+--type-add=sql:ext:sql,ctl
 
+# Stylus
+# http://stylus-lang.com/
+--type-add=stylus:ext:styl
 
-sub reset {
-    return FAIL();
-}
+# 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/
 
-sub close {
-    return FAIL();
-}
+# Tcl
+# https://www.tcl.tk/
+--type-add=tcl:ext:tcl,itcl,itk
 
+# TeX & LaTeX
+# https://www.latex-project.org/
+--type-add=tex:ext:tex,cls,sty
 
-sub clone {
-    return FAIL();
-}
+# Template Toolkit (Perl)
+# http//template-toolkit.org/
+--type-add=ttml:ext:tt,tt2,ttml
 
+# TOML
+# https://toml.io/
+--type-add=toml:ext:toml
 
-sub firstliney {
-    return FAIL();
-}
+# Typescript
+# https://www.typescriptlang.org/
+--type-add=ts:ext:ts,tsx
 
-1;
-package App::Ack::Resources;
+# Visual Basic
+--type-add=vb:ext:bas,cls,frm,ctl,vb,resx
 
+# Verilog
+--type-add=verilog:ext:v,vh,sv
 
+# VHDL
+# http://www.eda.org/twiki/bin/view.cgi/P1076/WebHome
+--type-add=vhdl:ext:vhd,vhdl
 
-use warnings;
-use strict;
+# Vim
+# https://www.vim.org/
+--type-add=vim:ext:vim
 
+# XML
+# https://www.w3.org/TR/REC-xml/
+--type-add=xml:ext:xml,dtd,xsd,xsl,xslt,ent,wsdl
+--type-add=xml:firstlinematch:/<[?]xml/
 
-sub from_argv {
-    my $class = shift;
-    my $opt   = shift;
-    my $start = shift;
+# YAML
+# https://yaml.org/
+--type-add=yaml:ext:yaml,yml
+HERE
+    $lines =~ s/==VERSION==/$App::Ack::VERSION/sm;
 
-    my $self = bless {}, $class;
+    return $lines;
+}
 
-    my $file_filter    = undef;
-    my $descend_filter = $opt->{descend_filter};
+1;
+package App::Ack::ConfigFinder;
 
-    if( $opt->{n} ) {
-        $descend_filter = sub {
-            return 0;
-        };
-    }
 
-    $self->{iter} =
-        File::Next::files( {
-            file_filter     => $opt->{file_filter},
-            descend_filter  => $descend_filter,
-            error_handler   => sub { my $msg = shift; App::Ack::warn( $msg ) },
-            sort_files      => $opt->{sort_files},
-            follow_symlinks => $opt->{follow},
-        }, @{$start} );
+use strict;
+use warnings;
 
-    return $self;
-}
+use Cwd 3.00 ();
+use File::Spec 3.00 ();
 
+use if ($^O eq 'MSWin32'), 'Win32';
 
-sub from_file {
-    my $class = shift;
-    my $opt   = shift;
-    my $file  = shift;
 
-    my $iter =
-        File::Next::from_file( {
-            error_handler   => sub { my $msg = shift; App::Ack::warn( $msg ) },
-            warning_handler => sub { my $msg = shift; App::Ack::warn( $msg ) },
-            sort_files      => $opt->{sort_files},
-        }, $file ) or return undef;
+sub new {
+    my ( $class ) = @_;
 
-    return bless {
-        iter => $iter,
-    }, $class;
+    return bless {}, $class;
 }
 
-# This is for reading input lines from STDIN, not the list of files from STDIN
-sub from_stdin {
-    my $class = shift;
-    my $opt   = shift;
-
-    my $self  = bless {}, $class;
-
-    my $has_been_called = 0;
 
-    $self->{iter} = sub {
-        if ( !$has_been_called ) {
-            $has_been_called = 1;
-            return '-';
+sub _remove_redundancies {
+    my @configs = @_;
+
+    my %seen;
+    my @uniq;
+    foreach my $config (@configs) {
+        my $path = $config->{path};
+        my $key = -e $path ? Cwd::realpath( $path ) : $path;
+        if ( not $App::Ack::is_windows ) {
+            # On Unix, uniquify on inode.
+            my ($dev, $inode) = (stat $key)[0, 1];
+            $key = "$dev:$inode" if defined $dev;
         }
-        return;
-    };
-
-    return $self;
+        push( @uniq, $config ) unless $seen{$key}++;
+    }
+    return @uniq;
 }
 
-sub next {
-    my $self = shift;
-
-    my $file = $self->{iter}->() or return;
 
-    return App::Ack::Resource::Basic->new( $file );
-}
+sub _check_for_ackrc {
+    return unless defined $_[0];
 
-1;
-package App::Ack::Resource::Basic;
+    my @files = grep { -f }
+                map { File::Spec->catfile(@_, $_) }
+                qw(.ackrc _ackrc);
 
+    App::Ack::die( File::Spec->catdir(@_) . ' contains both .ackrc and _ackrc. Please remove one of those files.' )
+        if @files > 1;
 
-use warnings;
-use strict;
+    return wantarray ? @files : $files[0];
+} # end _check_for_ackrc
 
-use Fcntl ();
 
-BEGIN {
-    our @ISA = 'App::Ack::Resource';
-}
 
+sub find_config_files {
+    my @config_files;
 
-sub new {
-    my $class    = shift;
-    my $filename = shift;
+    if ( $App::Ack::is_windows ) {
+        push @config_files, map { +{ path => File::Spec->catfile($_, 'ackrc') } } (
+            Win32::GetFolderPath(Win32::CSIDL_COMMON_APPDATA()),
+            Win32::GetFolderPath(Win32::CSIDL_APPDATA()),
+        );
+    }
+    else {
+        push @config_files, { path => '/etc/ackrc' };
+    }
 
-    my $self = bless {
-        filename => $filename,
-        fh       => undef,
-        opened   => 0,
-    }, $class;
 
-    if ( $self->{filename} eq '-' ) {
-        $self->{fh}     = *STDIN;
-        $self->{opened} = 1;
+    if ( $ENV{'ACKRC'} && -f $ENV{'ACKRC'} ) {
+        push @config_files, { path => $ENV{'ACKRC'} };
+    }
+    else {
+        push @config_files, map { +{ path => $_ } } _check_for_ackrc($ENV{'HOME'});
     }
 
-    return $self;
-}
+    my $cwd = Cwd::getcwd();
+    return () unless defined $cwd;
 
+    # XXX This should go through some untainted cwd-fetching function, and not get untainted brute-force like this.
+    $cwd =~ /(.+)/;
+    $cwd = $1;
+    my @dirs = File::Spec->splitdir( $cwd );
+    while ( @dirs ) {
+        my $ackrc = _check_for_ackrc(@dirs);
+        if ( defined $ackrc ) {
+            push @config_files, { project => 1, path => $ackrc };
+            last;
+        }
+        pop @dirs;
+    }
 
-sub name {
-    return $_[0]->{filename};
+    # We only test for existence here, so if the file is deleted out from under us, this will fail later.
+    return _remove_redundancies( @config_files );
 }
 
+1;
+package App::Ack::ConfigLoader;
 
+use strict;
+use warnings;
+use 5.010;
 
-sub needs_line_scan {
-    my $self  = shift;
-    my $opt   = shift;
-
-    return 1 if $opt->{v};
-
-    my $size = -s $self->{fh};
-    if ( $size == 0 ) {
-        return 0;
-    }
-    elsif ( $size > 100_000 ) {
-        return 1;
-    }
+use File::Spec 3.00 ();
+use Getopt::Long 2.39 ();
+use Text::ParseWords 3.1 ();
 
-    my $buffer;
-    my $rc = sysread( $self->{fh}, $buffer, $size );
-    if ( !defined($rc) && $App::Ack::report_bad_filenames ) {
-        App::Ack::warn( "$self->{filename}: $!" );
-        return 1;
-    }
-    return 0 unless $rc && ( $rc == $size );
+sub opt_parser {
+    my @opts = @_;
 
-    my $regex = $opt->{regex};
-    return $buffer =~ /$regex/m;
+    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 {
+    my ( $option_name, $opt ) = @_;
 
-sub reset {
-    my $self = shift;
-
-    # return if we haven't opened the file yet
-    if ( !defined($self->{fh}) ) {
-        return;
-    }
-
-    if( !seek( $self->{fh}, 0, 0 ) && $App::Ack::report_bad_filenames ) {
-        App::Ack::warn( "$self->{filename}: $!" );
-    }
+    my $is_inverted = $option_name =~ /^--no/;
 
-    return;
-}
+    return sub {
+        my ( undef, $dir ) = @_;
 
+        $dir = _remove_directory_separator( $dir );
+        if ( $dir !~ /:/ ) {
+            $dir = 'is:' . $dir;
+        }
 
-sub close {
-    my $self = shift;
+        my ( $filter_type, $args ) = split /:/, $dir, 2;
 
-    # return if we haven't opened the file yet
-    if ( !defined($self->{fh}) ) {
-        return;
-    }
+        if ( $filter_type eq 'firstlinematch' ) {
+            App::Ack::die( qq{Invalid filter specification "$filter_type" for option '$option_name'} );
+        }
 
-    if ( !close($self->{fh}) && $App::Ack::report_bad_filenames ) {
-        App::Ack::warn( $self->name() . ": $!" );
-    }
+        my $filter = App::Ack::Filter->create_filter($filter_type, split(/,/, $args));
+        my $collection;
 
-    $self->{opened} = 0;
+        my $previous_inversion_matches = $opt->{idirs} && !($is_inverted xor $opt->{idirs}[-1]->is_inverted());
 
-    return;
-}
+        if ( $previous_inversion_matches ) {
+            $collection = $opt->{idirs}[-1];
 
+            if ( $is_inverted ) {
+                # This relies on invert of an inverted filter to return the original.
+                $collection = $collection->invert();
+            }
+        }
+        else {
+            $collection = App::Ack::Filter::Collection->new();
+            push @{ $opt->{idirs} }, $is_inverted ? $collection->invert() : $collection;
+        }
 
-sub clone {
-    my ( $self ) = @_;
+        $collection->add($filter);
 
-    return __PACKAGE__->new($self->name);
+        if ( $filter_type eq 'is' ) {
+            $collection->add(App::Ack::Filter::IsPath->new($args));
+        }
+    };
 }
 
-sub firstliney {
-    my ( $self ) = @_;
 
-    my $fh = $self->open();
+sub _remove_directory_separator {
+    my $path = shift;
 
-    unless(exists $self->{firstliney}) {
-        my $buffer = '';
-        my $rc     = sysread( $fh, $buffer, 250 );
-        unless($rc) { # XXX handle this better?
-            $buffer = '';
-        }
-        $buffer =~ s/[\r\n].*//s;
-        $self->{firstliney} = $buffer;
-        $self->reset;
-    }
+    state $dir_sep_chars = $App::Ack::is_windows ? quotemeta( '\\/' ) : quotemeta( File::Spec->catfile( '', '' ) );
 
-    $self->close;
+    $path =~ s/[$dir_sep_chars]$//;
 
-    return $self->{firstliney};
+    return $path;
 }
 
-sub open {
-    my ( $self ) = @_;
 
-    return $self->{fh} if $self->{opened};
+sub _process_filter_spec {
+    my ( $spec ) = @_;
 
-    unless ( open $self->{fh}, '<', $self->{filename} ) {
-        return;
+    if ( $spec =~ /^(\w+):(\w+):(.*)/ ) {
+        my ( $type_name, $ext_type, $arguments ) = ( $1, $2, $3 );
+
+        return ( $type_name,
+            App::Ack::Filter->create_filter($ext_type, split(/,/, $arguments)) );
     }
+    elsif ( $spec =~ /^(\w+)=(.*)/ ) { # Check to see if we have ack1-style argument specification.
+        my ( $type_name, $extensions ) = ( $1, $2 );
 
-    $self->{opened} = 1;
+        my @extensions = split(/,/, $extensions);
+        foreach my $extension ( @extensions ) {
+            $extension =~ s/^[.]//;
+        }
 
-    return $self->{fh};
+        return ( $type_name, App::Ack::Filter->create_filter('ext', @extensions) );
+    }
+    else {
+        App::Ack::die( "Invalid filter specification '$spec'" );
+    }
 }
 
-1;
-package App::Ack::Filter;
-
-use strict;
-use warnings;
-use overload
-    '""' => 'to_string';
-
-use Carp 1.04 ();
 
-my %filter_types;
+sub _uninvert_filter {
+    my ( $opt, @filters ) = @_;
 
+    return unless defined $opt->{filters} && @filters;
 
-sub create_filter {
-    my ( undef, $type, @args ) = @_;
+    # Loop through all the registered filters.  If we hit one that
+    # matches this extension and it's inverted, we need to delete it from
+    # the options.
+    for ( my $i = 0; $i < @{ $opt->{filters} }; $i++ ) {
+        my $opt_filter = @{ $opt->{filters} }[$i];
 
-    if ( my $package = $filter_types{$type} ) {
-        return $package->new(@args);
+        # XXX Do a real list comparison? This just checks string equivalence.
+        if ( $opt_filter->is_inverted() && "$opt_filter->{filter}" eq "@filters" ) {
+            splice @{ $opt->{filters} }, $i, 1;
+            $i--;
+        }
     }
-    Carp::croak "Unknown filter type '$type'";
-}
-
-
-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 _process_filetypes {
+    my ( $opt, $arg_sources ) = @_;
 
+    my %additional_specs;
 
-sub to_string {
-    my ( $self ) = @_;
+    my $add_spec = sub {
+        my ( undef, $spec ) = @_;
 
-    return '(unimplemented to_string)';
-}
+        my ( $name, $filter ) = _process_filter_spec($spec);
 
+        push @{ $App::Ack::mappings{$name} }, $filter;
 
-sub inspect {
-    my ( $self ) = @_;
+        $additional_specs{$name . '!'} = sub {
+            my ( undef, $value ) = @_;
 
-    return ref($self);
-}
+            my @filters = @{ $App::Ack::mappings{$name} };
+            if ( not $value ) {
+                @filters = map { $_->invert() } @filters;
+            }
+            else {
+                _uninvert_filter( $opt, @filters );
+            }
 
-1;
-package App::Ack::Filter::Extension;
+            push @{ $opt->{'filters'} }, @filters;
+        };
+    };
 
-use strict;
-use warnings;
-BEGIN {
-    our @ISA = 'App::Ack::Filter';
-}
+    my $set_spec = sub {
+        my ( undef, $spec ) = @_;
 
-sub new {
-    my ( $class, @extensions ) = @_;
+        my ( $name, $filter ) = _process_filter_spec($spec);
 
-    my $exts = join('|', map { "\Q$_\E"} @extensions);
-    my $re   = qr/[.](?:$exts)$/i;
+        $App::Ack::mappings{$name} = [ $filter ];
 
-    return bless {
-        extensions => \@extensions,
-        regex      => $re,
-    }, $class;
-}
+        $additional_specs{$name . '!'} = sub {
+            my ( undef, $value ) = @_;
 
-sub filter {
-    my ( $self, $resource ) = @_;
+            my @filters = @{ $App::Ack::mappings{$name} };
+            if ( not $value ) {
+                @filters = map { $_->invert() } @filters;
+            }
 
-    my $re = $self->{'regex'};
+            push @{ $opt->{'filters'} }, @filters;
+        };
+    };
 
-    return $resource->name =~ /$re/;
-}
+    my $delete_spec = sub {
+        my ( undef, $name ) = @_;
 
-sub inspect {
-    my ( $self ) = @_;
+        delete $App::Ack::mappings{$name};
+        delete $additional_specs{$name . '!'};
+    };
 
-    my $re = $self->{'regex'};
+    my %type_arg_specs = (
+        'type-add=s' => $add_spec,
+        'type-set=s' => $set_spec,
+        'type-del=s' => $delete_spec,
+    );
 
-    return ref($self) . " - $re";
-}
+    my $p = opt_parser( 'no_auto_abbrev', 'pass_through' );
+    foreach my $source (@{$arg_sources}) {
+        my $args = $source->{contents};
 
-sub to_string {
-    my ( $self ) = @_;
+        if ( ref($args) ) {
+            # $args are modified in place, so no need to munge $arg_sources
+            $p->getoptionsfromarray( $args, %type_arg_specs );
+        }
+        else {
+            ( undef, $source->{contents} ) =
+                $p->getoptionsfromstring( $args, %type_arg_specs );
+        }
+    }
 
-    my $exts = $self->{'extensions'};
+    $additional_specs{'k|known-types'} = sub {
+        my @filters = map { @{$_} } values(%App::Ack::mappings);
 
-    return join(' ', map { ".$_" } @{$exts});
-}
+        push @{ $opt->{'filters'} }, @filters;
+    };
 
-BEGIN {
-    App::Ack::Filter->register_filter(ext => __PACKAGE__);
+    return \%additional_specs;
 }
 
-1;
-package App::Ack::Filter::FirstLineMatch;
-
-use strict;
-use warnings;
-BEGIN {
-    our @ISA = 'App::Ack::Filter';
-}
 
-sub new {
-    my ( $class, $re ) = @_;
+sub get_arg_spec {
+    my ( $opt, $extra_specs ) = @_;
 
-    $re =~ s{^/|/$}{}g; # XXX validate?
-    $re = qr{$re}i;
 
-    return bless {
-        regex => $re,
-    }, $class;
-}
+    sub _type_handler {
+        my ( $getopt, $value ) = @_;
 
-# 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.
+        my $cb_value = 1;
+        if ( $value =~ s/^no// ) {
+            $cb_value = 0;
+        }
 
-sub filter {
-    my ( $self, $resource ) = @_;
+        my $callback;
+        {
+            no warnings;
+            $callback = $extra_specs->{ $value . '!' };
+        }
 
-    my $re = $self->{'regex'};
+        if ( $callback ) {
+            $callback->( $getopt, $cb_value );
+        }
+        else {
+            App::Ack::die( "Unknown type '$value'" );
+        }
 
-    my $line = $resource->firstliney;
+        return;
+    }
 
-    return $line =~ /$re/;
-}
+    return {
+        1                   => sub { $opt->{1} = $opt->{m} = 1 },
+        '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|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},
+        'create-ackrc'      => sub { say for ( '--ignore-ack-defaults', App::Ack::ConfigDefault::options() ); exit; },
+        'debug'             => \$opt->{debug},
+        'env!'              => sub {
+            my ( undef, $value ) = @_;
 
-sub inspect {
-    my ( $self ) = @_;
+            if ( !$value ) {
+                $opt->{noenv_seen} = 1;
+            }
+        },
+        f                   => \$opt->{f},
+        'files-from=s'      => \$opt->{files_from},
+        'filter!'           => \$App::Ack::is_filter_mode,
+        flush               => sub { $| = 1 },
+        'follow!'           => \$opt->{follow},
+        g                   => \$opt->{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'     => 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 $re = $self->{'regex'};
+            my ( $filter_type, $args ) = split /:/, $file, 2;
 
-    return ref($self) . " - $re";
-}
+            my $filter = App::Ack::Filter->create_filter($filter_type, split(/,/, $args//''));
 
-sub to_string {
-    my ( $self ) = @_;
+            if ( !$opt->{ifiles} ) {
+                $opt->{ifiles} = App::Ack::Filter::Collection->new();
+            }
+            $opt->{ifiles}->add($filter);
+        },
+        'l|files-with-matches'
+                            => \$opt->{l},
+        'L|files-without-matches'
+                            => \$opt->{L},
+        'm|max-count=i'     => \$opt->{m},
+        'match=s'           => \$opt->{regex},
+        'n|no-recurse'      => \$opt->{n},
+        o                   => sub { $opt->{output} = '$&' },
+        'output=s'          => \$opt->{output},
+        'pager:s'           => sub {
+            my ( undef, $value ) = @_;
 
-    (my $re = $self->{regex}) =~ s{\([^:]*:(.*)\)$}{$1};
+            $opt->{pager} = $value || $ENV{PAGER};
+        },
+        'noignore-directory|noignore-dir=s' => _generate_ignore_dir('--noignore-dir', $opt),
+        '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 },
+        '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},
+        'S|smart-case!'     => sub { my (undef,$value) = @_; $opt->{S} = $value; $opt->{i} = 0 if $value; },
+        'sort-files'        => \$opt->{sort_files},
+        '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} = '-' },
 
-    return "first line matches /$re/";
+        'help'              => sub { App::Ack::show_help(); exit; },
+        'help-types'        => sub { App::Ack::show_help_types(); 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
 }
 
-BEGIN {
-    App::Ack::Filter->register_filter(firstlinematch => __PACKAGE__);
-}
 
-1;
-package App::Ack::Filter::Is;
+sub _context_value {
+    my $val = shift;
 
-use strict;
-use warnings;
-BEGIN {
-    our @ISA = 'App::Ack::Filter';
+    # Contexts default to 2.
+    return (!defined($val) || ($val < 0)) ? 2 : $val;
 }
 
-use File::Spec 3.00 ();
-
-sub new {
-    my ( $class, $filename ) = @_;
 
-    return bless {
-        filename => $filename,
-    }, $class;
-}
+sub _process_other {
+    my ( $opt, $extra_specs, $arg_sources ) = @_;
 
-sub filter {
-    my ( $self, $resource ) = @_;
+    my $argv_source;
+    my $is_help_types_active;
 
-    my $filename = $self->{'filename'};
-    my $base     = (File::Spec->splitpath($resource->name))[2];
+    foreach my $source (@{$arg_sources}) {
+        if ( $source->{name} eq 'ARGV' ) {
+            $argv_source = $source->{contents};
+            last;
+        }
+    }
 
-    return $base eq $filename;
-}
+    if ( $argv_source ) { # This *should* always be true, but you never know...
+        my $p = opt_parser( 'pass_through' );
+        $p->getoptionsfromarray( [ @{$argv_source} ],
+            'help-types' => \$is_help_types_active,
+        );
+    }
 
-sub inspect {
-    my ( $self ) = @_;
+    my $arg_specs = get_arg_spec( $opt, $extra_specs );
 
-    my $filename = $self->{'filename'};
+    my $p = opt_parser();
+    foreach my $source (@{$arg_sources}) {
+        my ( $source_name, $args ) = @{$source}{qw/name contents/};
 
-    return ref($self) . " - $filename";
-}
+        my $args_for_source = { %{$arg_specs} };
 
-sub to_string {
-    my ( $self ) = @_;
+        if ( $source->{is_ackrc} ) {
+            my $illegal = sub {
+                my $name = shift;
+                App::Ack::die( "Option --$name is forbidden in .ackrc files." );
+            };
 
-    my $filename = $self->{'filename'};
-}
+            $args_for_source = {
+                %{$args_for_source},
+                'output=s' => $illegal,
+                'match=s'  => $illegal,
+            };
+        }
+        if ( $source->{project} ) {
+            my $illegal = sub {
+                my $name = shift;
+                App::Ack::die( "Option --$name is forbidden in project .ackrc files." );
+            };
+
+            $args_for_source = {
+                %{$args_for_source},
+                'pager:s' => $illegal,
+            };
+        }
 
-BEGIN {
-    App::Ack::Filter->register_filter(is => __PACKAGE__);
-}
+        my $ret;
+        if ( ref($args) ) {
+            $ret = $p->getoptionsfromarray( $args, %{$args_for_source} );
+        }
+        else {
+            ( $ret, $source->{contents} ) =
+                $p->getoptionsfromstring( $args, %{$args_for_source} );
+        }
+        if ( !$ret ) {
+            if ( !$is_help_types_active ) {
+                my $where = $source_name eq 'ARGV' ? 'on command line' : "in $source_name";
+                App::Ack::die( "Invalid option $where" );
+            }
+        }
+        if ( $opt->{noenv_seen} ) {
+            App::Ack::die( "--noenv found in $source_name" );
+        }
+    }
 
-1;
-package App::Ack::Filter::Match;
+    # XXX We need to check on a -- in the middle of a non-ARGV source
 
-use strict;
-use warnings;
-BEGIN {
-    our @ISA = 'App::Ack::Filter';
+    return;
 }
 
-use File::Spec 3.00;
-
-sub new {
-    my ( $class, $re ) = @_;
 
-    $re =~ s{^/|/$}{}g; # XXX validate?
-    $re = qr/$re/i;
+sub _explode_sources {
+    my ( $sources ) = @_;
 
-    return bless {
-        regex => $re,
-    }, $class;
-}
+    my @new_sources;
 
-sub filter {
-    my ( $self, $resource ) = @_;
+    my %opt;
+    my $arg_spec = get_arg_spec( \%opt, {} );
 
-    my $re   = $self->{'regex'};
-    my $base = (File::Spec->splitpath($resource->name))[2];
+    my $dummy_sub = sub {};
+    my $add_type = sub {
+        my ( undef, $arg ) = @_;
 
-    return $base =~ /$re/;
-}
+        if ( $arg =~ /(\w+)=/) {
+            $arg_spec->{$1} = $dummy_sub;
+        }
+        else {
+            ( $arg ) = split /:/, $arg;
+            $arg_spec->{$arg} = $dummy_sub;
+        }
+    };
 
-sub inspect {
-    my ( $self ) = @_;
+    my $del_type = sub {
+        my ( undef, $arg ) = @_;
 
-    my $re = $self->{'regex'};
+        delete $arg_spec->{$arg};
+    };
 
-    print ref($self) . " - $re";
-}
+    my $p = opt_parser( 'pass_through' );
+    foreach my $source (@{$sources}) {
+        my ( $name, $options ) = @{$source}{qw/name contents/};
+        if ( ref($options) ne 'ARRAY' ) {
+            $source->{contents} = $options =
+                [ Text::ParseWords::shellwords($options) ];
+        }
 
-sub to_string {
-    my ( $self ) = @_;
+        for my $j ( 0 .. @{$options}-1 ) {
+            next unless $options->[$j] =~ /^-/;
+            my @chunk = ( $options->[$j] );
+            push @chunk, $options->[$j] while ++$j < @{$options} && $options->[$j] !~ /^-/;
+            $j--;
 
-    my $re = $self->{'regex'};
+            my @copy = @chunk;
+            $p->getoptionsfromarray( [@chunk],
+                'type-add=s' => $add_type,
+                'type-set=s' => $add_type,
+                'type-del=s' => $del_type,
+                %{$arg_spec}
+            );
 
-    return "filename matches $re";
-}
+            push @new_sources, {
+                name     => $name,
+                contents => \@copy,
+            };
+        }
+    }
 
-BEGIN {
-    App::Ack::Filter->register_filter(match => __PACKAGE__);
+    return \@new_sources;
 }
 
-1;
-package App::Ack::Filter::Default;
-
-use strict;
-use warnings;
-BEGIN {
-    our @ISA = 'App::Ack::Filter';
-}
 
-sub new {
-    my ( $class ) = @_;
+sub _compare_opts {
+    my ( $a, $b ) = @_;
 
-    return bless {}, $class;
-}
+    my $first_a = $a->[0];
+    my $first_b = $b->[0];
 
-sub filter {
-    my ( $self, $resource ) = @_;
+    $first_a =~ s/^--?//;
+    $first_b =~ s/^--?//;
 
-    return -T $resource->name;
+    return $first_a cmp $first_b;
 }
 
-1;
-package App::Ack::Filter::Inverse;
-
-use strict;
-use warnings;
-BEGIN {
-    our @ISA = 'App::Ack::Filter';
-}
 
-sub new {
-    my ( $class, $filter ) = @_;
+sub _dump_options {
+    my ( $sources ) = @_;
 
-    return bless {
-        filter => $filter,
-    }, $class;
-}
+    $sources = _explode_sources($sources);
 
-sub filter {
-    my ( $self, $resource ) = @_;
+    my %opts_by_source;
+    my @source_names;
 
-    my $filter = $self->{'filter'};
-    return !$filter->filter( $resource );
-}
+    foreach my $source (@{$sources}) {
+        my $name = $source->{name};
+        if ( not $opts_by_source{$name} ) {
+            $opts_by_source{$name} = [];
+            push @source_names, $name;
+        }
+        push @{$opts_by_source{$name}}, $source->{contents};
+    }
 
-sub invert {
-    my $self = shift;
+    foreach my $name (@source_names) {
+        my $contents = $opts_by_source{$name};
 
-    return $self->{'filter'};
-}
+        say $name;
+        say '=' x length($name);
+        say '  ', join(' ', @{$_}) for sort { _compare_opts($a, $b) } @{$contents};
+    }
 
-sub is_inverted {
-    return 1;
+    return;
 }
 
-sub inspect {
-    my ( $self ) = @_;
 
-    my $filter = $self->{'filter'};
+sub _remove_default_options_if_needed {
+    my ( $sources ) = @_;
 
-    return "!$filter";
-}
+    my $default_index;
 
-1;
-package App::Ack::ConfigFinder;
+    foreach my $index ( 0 .. $#{$sources} ) {
+        if ( $sources->[$index]{'name'} eq 'Defaults' ) {
+            $default_index = $index;
+            last;
+        }
+    }
 
+    return $sources unless defined $default_index;
 
-use strict;
-use warnings;
+    my $should_remove = 0;
 
-use Cwd 3.00 ();
-use File::Spec 3.00;
+    my $p = opt_parser( 'no_auto_abbrev', 'pass_through' );
 
-use if ($^O =~ /MSWin32/ ? 1 : 0), "Win32";
+    foreach my $index ( $default_index + 1 .. $#{$sources} ) {
+        my $args = $sources->[$index]->{contents};
 
+        if (ref($args)) {
+            $p->getoptionsfromarray( $args,
+                'ignore-ack-defaults' => \$should_remove,
+            );
+        }
+        else {
+            ( undef, $sources->[$index]{contents} ) = $p->getoptionsfromstring( $args,
+                'ignore-ack-defaults' => \$should_remove,
+            );
+        }
+    }
 
-our $is_win = 0;
+    return $sources unless $should_remove;
 
-sub new {
-    my ( $class ) = @_;
+    my @copy = @{$sources};
+    splice @copy, $default_index, 1;
+    return \@copy;
+}
 
-    $is_win = $^O =~ /MSWin32/,
 
-    return bless {}, $class;
-}
+sub process_args {
+    my $arg_sources = \@_;
 
-sub _remove_redundancies {
-    my ( @configs ) = @_;
+    my %opt = (
+        pager => $ENV{ACK_PAGER_COLOR} || $ENV{ACK_PAGER},
+    );
 
-    if ( $is_win ) {
-        # inode stat always returns 0 on windows,
-        # so just check filenames
-        my (%seen, @uniq);
+    $arg_sources = _remove_default_options_if_needed($arg_sources);
 
-        foreach my $path (@configs) {
-            push @uniq, $path unless $seen{$path};
-            $seen{$path} = 1;
+    # 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);
+            }
         }
-
-        return @uniq;
     }
 
-    else {
+    my $type_specs = _process_filetypes(\%opt, $arg_sources);
 
-        my %dev_and_inode_seen;
+    _check_for_mutex_options( $type_specs );
 
-        foreach my $path ( @configs ) {
-            my ( $dev, $inode ) = (stat $path)[0, 1];
+    _process_other(\%opt, $type_specs, $arg_sources);
+    while ( @{$arg_sources} ) {
+        my $source = shift @{$arg_sources};
+        my $args = $source->{contents};
 
-            if( defined($dev) ) {
-                if( $dev_and_inode_seen{"$dev:$inode"} ) {
-                    undef $path;
-                }
-                else {
-                    $dev_and_inode_seen{"$dev:$inode"} = 1;
-                }
+        # 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}) {
+                App::Ack::die( "Source '$source_name' has extra arguments!" );
             }
         }
+        else {
+            App::Ack::die( 'The impossible has occurred!' );
+        }
+    }
+    my $filters = ($opt{filters} ||= []);
 
-        return grep { defined() } @configs;
-
+    # Throw the default filter in if no others are selected.
+    if ( not grep { !$_->is_inverted() } @{$filters} ) {
+        push @{$filters}, App::Ack::Filter::Default->new();
     }
+    return \%opt;
 }
 
-sub _check_for_ackrc {
-    return unless defined $_[0];
-
-    my @files = grep { -f }
-                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;
+sub retrieve_arg_sources {
+    my @arg_sources;
 
-    return wantarray ? @files : $files[0];
-} # end _check_for_ackrc
+    my $noenv;
+    my $ackrc;
 
+    my $p = opt_parser( 'no_auto_abbrev', 'pass_through' );
+    $p->getoptions(
+        'noenv'   => \$noenv,
+        'ackrc=s' => \$ackrc,
+    );
 
-sub find_config_files {
-    my @config_files;
+    my @files;
 
-    if( $is_win ) {
-        push @config_files, map { File::Spec->catfile($_, 'ackrc') } (
-            Win32::GetFolderPath(Win32::CSIDL_COMMON_APPDATA()),
-            Win32::GetFolderPath(Win32::CSIDL_APPDATA()),
-        );
+    if ( !$noenv ) {
+        my $finder = App::Ack::ConfigFinder->new;
+        @files  = $finder->find_config_files;
     }
-    else {
-        push @config_files, '/etc/ackrc';
+    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 ) {
+            close $fh;
+        }
+        else {
+            App::Ack::die( "Unable to load ackrc '$ackrc': $!" );
+        }
+        push( @files, { path => $ackrc } );
     }
 
+    push @arg_sources, {
+        name     => 'Defaults',
+        contents => [ App::Ack::ConfigDefault::options_clean() ],
+    };
 
-    if ( $ENV{'ACKRC'} && -f $ENV{'ACKRC'} ) {
-        push @config_files, $ENV{'ACKRC'};
-    }
-    else {
-        push @config_files, _check_for_ackrc($ENV{'HOME'});
-    }
-
-    my @dirs = File::Spec->splitdir(Cwd::getcwd());
-    while(@dirs) {
-        my $ackrc = _check_for_ackrc(@dirs);
-        if(defined $ackrc) {
-            push @config_files, $ackrc;
-            last;
+    foreach my $file ( @files) {
+        my @lines = read_rcfile($file->{path});
+        if ( @lines ) {
+            push @arg_sources, {
+                name     => $file->{path},
+                contents => \@lines,
+                project  => $file->{project},
+                is_ackrc => 1,
+            };
         }
-        pop @dirs;
     }
 
-    # XXX we only test for existence here, so if the file is
-    #     deleted out from under us, this will fail later. =(
-    return _remove_redundancies( @config_files );
+    push @arg_sources, {
+        name     => 'ARGV',
+        contents => [ @ARGV ],
+    };
+
+    return @arg_sources;
 }
 
 
@@ -3620,972 +4377,1545 @@ sub read_rcfile {
     my @lines;
 
     open( my $fh, '<', $file ) or App::Ack::die( "Unable to read $file: $!" );
-    while ( my $line = <$fh> ) {
+    while ( defined( my $line = <$fh> ) ) {
         chomp $line;
         $line =~ s/^\s+//;
         $line =~ s/\s+$//;
 
         next if $line eq '';
-        next if $line =~ /^#/;
+        next if $line =~ /^\s*#/;
 
         push( @lines, $line );
     }
-    close $fh;
+    close $fh or App::Ack::die( "Unable to close $file: $!" );
 
     return @lines;
 }
 
-1;
-package App::Ack::ConfigLoader;
 
-use strict;
-use warnings;
+# Verifies no mutex options were passed.  Dies if they were.
+sub _check_for_mutex_options {
+    my $type_specs = shift;
 
-use Carp 1.04 ();
-use Getopt::Long 2.35 ();
-use Text::ParseWords 3.1 ();
+    my $mutex = mutex_options();
 
+    my ($raw,$used) = _options_used( $type_specs );
 
-my @INVALID_COMBINATIONS;
+    my @used = sort { lc $a cmp lc $b } keys %{$used};
 
-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],
-    );
+    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 process_filter_spec {
-    my ( $spec ) = @_;
 
-    if ( $spec =~ /^(\w+):(\w+):(.*)/ ) {
-        my ( $type_name, $ext_type, $arguments ) = ( $1, $2, $3 );
+# 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 ( $type_name,
-            App::Ack::Filter->create_filter($ext_type, split(/,/, $arguments)) );
-    }
-    elsif ( $spec =~ /^(\w+)=(.*)/ ) { # Check to see if we have ack1-style argument specification.
-        my ( $type_name, $extensions ) = ( $1, $2 );
+    my %dummy_opt;
+    my $real_spec = get_arg_spec( \%dummy_opt, $type_specs );
 
-        my @extensions = split(/,/, $extensions);
-        foreach my $extension ( @extensions ) {
-            $extension =~ s/^[.]//;
-        }
+    # 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,
+    };
 
-        return ( $type_name, App::Ack::Filter->create_filter('ext', @extensions) );
-    }
-    else {
-        Carp::croak "invalid filter specification '$spec'";
-    }
-}
+    my %parsed;
+    my @raw;
+    my %spec_capture_parsed;
+    my %spec_capture_raw;
 
-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;
+    # 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}
+        );
+    }
 
-    my $add_spec = sub {
-        my ( undef, $spec ) = @_;
+    # Capture the @raw array.
+    CAPTURE_RAW: {
+        my $raw_pos = 0;
+        %spec_capture_raw = (
+            '<>' => sub { $raw_pos++ }, # Bump forward one pos for non-options.
+        );
 
-        my ( $name, $filter ) = process_filter_spec($spec);
+        my $sub_count = sub {
+            my $arg = shift;
 
-        push @{ $App::Ack::mappings{$name} }, $filter;
+            $arg = "$arg";
+            $raw[$raw_pos] = length($arg) == 1 ? "-$arg" : "--$arg";
+            $raw_pos++;
+        };
 
-        $additional_specs{$name . '!'} = sub {
-            my ( undef, $value ) = @_;
+        for my $opt_spec ( keys %{$real_spec} ) {
+            my $negatable;
+            my $type;
+            my $default;
 
-            my @filters = @{ $App::Ack::mappings{$name} };
-            if ( not $value ) {
-                @filters = map { $_->invert() } @filters;
+            $negatable = ($opt_spec =~ s/!$//);
+
+            if ( $opt_spec =~ s/(=[si])$// ) {
+                $type = $1;
+            }
+            if ( $opt_spec =~ s/(:.+)$// ) {
+                $default = $1;
             }
 
-            push @{ $opt->{'filters'} }, @filters;
-        };
-    };
+            my @aliases = split( /\|/, $opt_spec );
+            for my $alias ( @aliases ) {
+                $alias .= $type    if defined $type;
+                $alias .= $default if defined $default;
+                $alias .= '!'      if $negatable;
 
-    my $set_spec = sub {
-        my ( undef, $spec ) = @_;
+                $spec_capture_raw{$alias} = $sub_count;
+            }
+        }
+    }
 
-        my ( $name, $filter ) = process_filter_spec($spec);
+    # 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 );
 
-        $App::Ack::mappings{$name} = [ $filter ];
+    return (\@raw,\%parsed);
+}
 
-        $additional_specs{$name . '!'} = sub {
-            my ( undef, $value ) = @_;
 
-            my @filters = @{ $App::Ack::mappings{$name} };
-            if ( not $value ) {
-                @filters = map { $_->invert() } @filters;
-            }
+sub mutex_options {
+    # This list is machine-generated by dev/crank-mutex.  Do not modify it by hand.
 
-            push @{ $opt->{'filters'} }, @filters;
-        };
+    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,
+        },
     };
 
-    my $delete_spec = sub {
-        my ( undef, $name ) = @_;
-
-        delete $App::Ack::mappings{$name};
-        delete $additional_specs{$name . '!'};
-    };
+}   # End of mutex_options()
 
-    my %type_arg_specs = (
-        'type-add=s' => $add_spec,
-        'type-set=s' => $set_spec,
-        'type-del=s' => $delete_spec,
-    );
 
-    for ( my $i = 0; $i < @{$arg_sources}; $i += 2) {
-        my ( $source_name, $args ) = @{$arg_sources}[ $i, $i + 1];
+1; # End of App::Ack::ConfigLoader
+package App::Ack::File;
 
-        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;
-        }
-        else {
-            ( undef, $arg_sources->[$i + 1] ) =
-                Getopt::Long::GetOptionsFromString($args, %type_arg_specs);
-        }
-    }
+use warnings;
+use strict;
 
-    $additional_specs{'k|known-types'} = sub {
-        my ( undef, $value ) = @_;
+use File::Spec ();
 
-        my @filters = map { @{$_} } values(%App::Ack::mappings);
 
-        push @{ $opt->{'filters'} }, @filters;
-    };
+sub new {
+    my $class    = shift;
+    my $filename = shift;
 
-    return \%additional_specs;
-}
+    my $self = bless {
+        filename => $filename,
+        fh       => undef,
+    }, $class;
 
-sub removed_option {
-    my ( $option, $explanation ) = @_;
+    if ( $self->{filename} eq '-' ) {
+        $self->{fh}     = *STDIN;
+    }
 
-    $explanation ||= '';
-    return sub {
-        warn "Option '$option' is not valid in ack 2\n$explanation";
-        exit 1;
-    };
+    return $self;
 }
 
-sub get_arg_spec {
-    my ( $opt, $extra_specs ) = @_;
-
-    my $dash_a_explanation = <<EOT;
-This is because we now have -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).
-EOT
 
-    return {
-        1                   => sub { $opt->{1} = $opt->{m} = 1 },
-        'A|after-context=i' => \$opt->{after_context},
-        'B|before-context=i'
-                            => \$opt->{before_context},
-        'C|context:i'       => sub { shift; my $val = shift; $opt->{before_context} = $opt->{after_context} = ($val || 2) },
-        'a'                 => removed_option('-a', $dash_a_explanation),
-        'all'               => removed_option('--all', $dash_a_explanation),
-        'break!'            => \$opt->{break},
-        c                   => \$opt->{count},
-        'color|colour!'     => \$opt->{color},
-        'color-match=s'     => \$ENV{ACK_COLOR_MATCH},
-        'color-filename=s'  => \$ENV{ACK_COLOR_FILENAME},
-        '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; },
-        'env!'              => sub {
-            my ( undef, $value ) = @_;
 
-            if ( !$value ) {
-                $opt->{noenv_seen} = 1;
-            }
-        },
-        f                   => \$opt->{f},
-        'files-from=s'      => \$opt->{files_from},
-        'filter!'           => \$App::Ack::is_filter_mode,
-        flush               => \$opt->{flush},
-        'follow!'           => \$opt->{follow},
-        g                   => \$opt->{g},
-        G                   => removed_option('-G'),
-        'group!'            => sub { shift; $opt->{heading} = $opt->{break} = shift },
-        'heading!'          => \$opt->{heading},
-        'h|no-filename'     => \$opt->{h},
-        'H|with-filename'   => \$opt->{H},
-        'i|ignore-case'     => \$opt->{i},
-        'ignore-directory|ignore-dir=s' # XXX Combine this version with the negated version below
-                            => sub {
-                                my ( undef, $dir ) = @_;
-
-                                $dir = App::Ack::remove_dir_sep( $dir );
-                                if ( $dir !~ /^(?:is|match):/ ) {
-                                    $dir = 'is:' . $dir;
-                                }
-                                push @{ $opt->{idirs} }, $dir;
-                               },
-        'ignore-file=s'    => sub {
-                                    my ( undef, $file ) = @_;
-                                    push @{ $opt->{ifiles} }, $file;
-                               },
-        'lines=s'           => sub { shift; my $val = shift; push @{$opt->{lines}}, $val },
-        'l|files-with-matches'
-                            => \$opt->{l},
-        'L|files-without-matches'
-                            => \$opt->{L},
-        'm|max-count=i'     => \$opt->{m},
-        'match=s'           => \$opt->{regex},
-        'n|no-recurse'      => \$opt->{n},
-        o                   => sub { $opt->{output} = '$&' },
-        'output=s'          => \$opt->{output},
-        'pager=s'           => \$opt->{pager},
-        'noignore-directory|noignore-dir=s'
-                            => sub {
-                                my ( undef, $dir ) = @_;
-
-                                # XXX can you do --noignore-dir=match,...?
-                                $dir = App::Ack::remove_dir_sep( $dir );
-                                if ( $dir !~ /^(?:is|match):/ ) {
-                                    $dir = 'is:' . $dir;
-                                }
-                                if ( $dir !~ /^(?:is|match):/ ) {
-                                    Carp::croak("invalid noignore-directory argument: '$dir'");
-                                }
-
-                                @{ $opt->{idirs} } = grep {
-                                    $_ ne $dir
-                                } @{ $opt->{idirs} };
-
-                                push @{ $opt->{no_ignore_dirs} }, $dir;
-                               },
-        'nopager'           => sub { $opt->{pager} = undef },
-        'passthru'          => \$opt->{passthru},
-        'print0'            => \$opt->{print0},
-        'Q|literal'         => \$opt->{Q},
-        'r|R|recurse'       => sub { $opt->{n} = 0 },
-        's'                 => \$opt->{dont_report_bad_filenames},
-        'show-types'        => \$opt->{show_types},
-        'smart-case!'       => \$opt->{smart_case},
-        'sort-files'        => \$opt->{sort_files},
-        'type=s'            => sub {
-            my ( $getopt, $value ) = @_;
+sub name {
+    return $_[0]->{filename};
+}
 
-            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'),
-        'v|invert-match'    => \$opt->{v},
-        'w|word-regexp'     => \$opt->{w},
-        'x'                 => sub { $opt->{files_from} = '-' },
+sub basename {
+    my ( $self ) = @_;
 
-        'version'           => sub { App::Ack::print_version_statement(); exit; },
-        'help|?:s'          => sub { shift; App::Ack::show_help(@_); exit; },
-        'help-types'        => sub { App::Ack::show_help_types(); exit; },
-        'man'               => sub { App::Ack::show_man(); exit; },
-        $extra_specs ? %{$extra_specs} : (),
-    }; # arg_specs
+    return $self->{basename} //= (File::Spec->splitpath($self->name))[2];
 }
 
-sub process_other {
-    my ( $opt, $extra_specs, $arg_sources ) = @_;
-
-    Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version'); # start with default options, minus some annoying ones
-    Getopt::Long::Configure(
-        'bundling',
-        'no_ignore_case',
-    );
 
-    my $argv_source;
-    my $is_help_types_active;
 
-    for ( my $i = 0; $i < @{$arg_sources}; $i += 2 ) {
-        my ( $source_name, $args ) = @{$arg_sources}[ $i, $i + 1 ];
+sub open {
+    my ( $self ) = @_;
 
-        if ( $source_name eq 'ARGV' ) {
-            $argv_source = $args;
-            last;
+    if ( !$self->{fh} ) {
+        if ( open $self->{fh}, '<', $self->{filename} ) {
+            # Do nothing.
+        }
+        else {
+            $self->{fh} = undef;
         }
     }
 
-    if ( $argv_source ) { # this *should* always be true, but you never know...
-        my @copy = @{$argv_source};
-        local @ARGV = @copy;
+    return $self->{fh};
+}
 
-        Getopt::Long::Configure('pass_through');
 
-        Getopt::Long::GetOptions(
-            'help-types' => \$is_help_types_active,
-        );
+sub may_be_present {
+    my $self  = shift;
+    my $regex = shift;
 
-        Getopt::Long::Configure('no_pass_through');
-    }
+    # 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.
 
-    my $arg_specs = get_arg_spec($opt, $extra_specs);
+    # 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.
 
-    for ( my $i = 0; $i < @{$arg_sources}; $i += 2) {
-        my ($source_name, $args) = @{$arg_sources}[$i, $i + 1];
+    # The $regex may be undef if it had a "$" in it, and is therefore unsuitable for this heuristic.
 
-        my $ret;
-        if ( ref($args) ) {
-            local @ARGV = @{$args};
-            $ret = Getopt::Long::GetOptions( %{$arg_specs} );
-            @{$args} = @ARGV;
+    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 {
-            ( $ret, $arg_sources->[$i + 1] ) =
-                Getopt::Long::GetOptionsFromString( $args, %{$arg_specs} );
-        }
-        if ( !$ret ) {
-            if ( !$is_help_types_active ) {
-                my $where = $source_name eq 'ARGV' ? 'on command line' : "in $source_name";
-                App::Ack::die( "Invalid option $where" );
+            # 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;
+                }
             }
-        }
-        if ( $opt->{noenv_seen} ) {
-            App::Ack::die( "--noenv found in $source_name" );
         }
     }
 
-    # XXX We need to check on a -- in the middle of a non-ARGV source
+    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 should_dump_options {
-    my ( $sources ) = @_;
 
-    for(my $i = 0; $i < @{$sources}; $i += 2) {
-        my ( $name, $options ) = @{$sources}[$i, $i + 1];
-        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;
+
+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 explode_sources {
-    my ( $sources ) = @_;
 
-    my @new_sources;
 
-    Getopt::Long::Configure('default', 'pass_through', 'no_auto_help', 'no_auto_version');
+sub clone {
+    my ( $self ) = @_;
+
+    return __PACKAGE__->new($self->name);
+}
 
-    my %opt;
-    my $arg_spec = get_arg_spec(\%opt);
 
-    my $add_type = sub {
-        my ( undef, $arg ) = @_;
 
-        # XXX refactor?
-        if ( $arg =~ /(\w+)=/) {
-            $arg_spec->{$1} = sub {};
+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 {
-            ( $arg ) = split /:/, $arg;
-            $arg_spec->{$arg} = sub {};
+            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;
         }
-    };
+    }
 
-    my $del_type = sub {
-        my ( undef, $arg ) = @_;
+    return $self->{firstliney};
+}
 
-        delete $arg_spec->{$arg};
-    };
+1;
+package App::Ack::Files;
 
-    for(my $i = 0; $i < @{$sources}; $i += 2) {
-        my ( $name, $options ) = @{$sources}[$i, $i + 1];
-        if ( ref($options) ne 'ARRAY' ) {
-            $sources->[$i + 1] = $options =
-                [ Text::ParseWords::shellwords($options) ];
-        }
-        for ( my $j = 0; $j < @{$options}; $j++ ) {
-            next unless $options->[$j] =~ /^-/;
-            my @chunk = ( $options->[$j] );
-            push @chunk, $options->[$j] while ++$j < @{$options} && $options->[$j] !~ /^-/;
-            $j--;
 
-            my @copy = @chunk;
-            local @ARGV = @chunk;
-            Getopt::Long::GetOptions(
-                'type-add=s' => $add_type,
-                'type-set=s' => $add_type,
-                'type-del=s' => $del_type,
-            );
-            Getopt::Long::GetOptions( %{$arg_spec} );
 
-            push @new_sources, $name, \@copy;
+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 {};
+    }
+}
 
-    return \@new_sources;
+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 compare_opts {
-    my ( $a, $b ) = @_;
 
-    my $first_a = $a->[0];
-    my $first_b = $b->[0];
+sub register_filter {
+    my ( undef, $type, $package ) = @_;
 
-    $first_a =~ s/^--?//;
-    $first_b =~ s/^--?//;
+    $filter_types{$type} = $package;
+
+    return;
+}
+
+
+sub invert {
+    my ( $self ) = @_;
+
+    return App::Ack::Filter::Inverse->new( $self );
+}
+
+
+sub is_inverted {
+    return 0;
+}
+
+
+sub to_string {
+    return '(unimplemented to_string)';
+}
+
+
+sub inspect {
+    my ( $self ) = @_;
+
+    return ref($self);
+}
+
+1;
+package App::Ack::Filter::Collection;
+
+
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
+
+sub new {
+    my ( $class ) = @_;
+
+    return bless {
+        groups => {},
+        ungrouped => [],
+    }, $class;
+}
+
+sub filter {
+    my ( $self, $file ) = @_;
+
+    for my $group (values %{$self->{groups}}) {
+        return 1 if $group->filter($file);
+    }
+
+    for my $filter (@{$self->{ungrouped}}) {
+        return 1 if $filter->filter($file);
+    }
+
+    return 0;
+}
+
+sub add {
+    my ( $self, $filter ) = @_;
+
+    if (exists $filter->{'groupname'}) {
+        my $group = ($self->{groups}->{$filter->{groupname}} ||= $filter->create_group());
+        $group->add($filter);
+    }
+    else {
+        push @{$self->{'ungrouped'}}, $filter;
+    }
+
+    return;
+}
+
+sub inspect {
+    my ( $self ) = @_;
+
+    return ref($self) . " - $self";
+}
+
+sub to_string {
+    my ( $self ) = @_;
+
+    return join(', ', map { "($_)" } @{$self->{ungrouped}});
+}
+
+1;
+package App::Ack::Filter::Default;
+
+
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
+
+sub new {
+    my ( $class ) = @_;
+
+    return bless {}, $class;
+}
+
+sub filter {
+    my ( undef, $file ) = @_;
+
+    return -T $file->name;
+}
+
+1;
+package App::Ack::Filter::Extension;
+
+
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
+
+
+sub new {
+    my ( $class, @extensions ) = @_;
+
+    my $exts = join('|', map { "\Q$_\E"} @extensions);
+    my $re   = qr/[.](?:$exts)$/i;
+
+    return bless {
+        extensions => \@extensions,
+        regex      => $re,
+        groupname  => 'ExtensionGroup',
+    }, $class;
+}
+
+sub create_group {
+    return App::Ack::Filter::ExtensionGroup->new();
+}
+
+sub filter {
+    my ( $self, $file ) = @_;
+
+    return $file->name =~ /$self->{regex}/;
+}
+
+sub inspect {
+    my ( $self ) = @_;
+
+    return ref($self) . ' - ' . $self->{regex};
+}
+
+sub to_string {
+    my ( $self ) = @_;
+
+    return join( ' ', map { ".$_" } @{$self->{extensions}} );
+}
+
+BEGIN {
+    App::Ack::Filter->register_filter(ext => __PACKAGE__);
+}
+
+1;
+package App::Ack::Filter::ExtensionGroup;
+
+
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
+
+sub new {
+    my ( $class ) = @_;
+
+    return bless {
+        data => {},
+    }, $class;
+}
+
+sub add {
+    my ( $self, $filter ) = @_;
+
+    foreach my $ext (@{$filter->{extensions}}) {
+        $self->{data}->{lc $ext} = 1;
+    }
+
+    return;
+}
+
+sub filter {
+    my ( $self, $file ) = @_;
+
+    if ($file->name =~ /[.]([^.]*)$/) {
+        return exists $self->{'data'}->{lc $1};
+    }
+
+    return 0;
+}
+
+sub inspect {
+    my ( $self ) = @_;
+
+    return ref($self) . " - $self";
+}
+
+sub to_string {
+    my ( $self ) = @_;
+
+    return join(' ', map { ".$_" } sort keys %{$self->{data}});
+}
+
+1;
+package App::Ack::Filter::FirstLineMatch;
+
+
+
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
+
+sub new {
+    my ( $class, $re ) = @_;
+
+    $re =~ s{^/|/$}{}g; # XXX validate?
+    $re = qr{$re}i;
+
+    return bless {
+        regex => $re,
+    }, $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, $file ) = @_;
+
+    return $file->firstliney =~ /$self->{regex}/;
+}
+
+sub inspect {
+    my ( $self ) = @_;
+
+
+    return ref($self) . ' - ' . $self->{regex};
+}
+
+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::Inverse;
+
+
+
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
+
+sub new {
+    my ( $class, $filter ) = @_;
+
+    return bless {
+        filter => $filter,
+    }, $class;
+}
+
+sub filter {
+    my ( $self, $file ) = @_;
+
+    return !$self->{filter}->filter( $file );
+}
+
+sub invert {
+    my $self = shift;
 
-    return $first_a cmp $first_b;
+    return $self->{'filter'};
 }
 
-sub dump_options {
-    my ( $sources ) = @_;
+sub is_inverted {
+    return 1;
+}
 
-    $sources = explode_sources($sources);
+sub inspect {
+    my ( $self ) = @_;
 
-    my %opts_by_source;
-    my @source_names;
+    my $filter = $self->{'filter'};
 
-    for(my $i = 0; $i < @{$sources}; $i += 2) {
-        my ( $name, $contents ) = @{$sources}[$i, $i + 1];
-        if ( not $opts_by_source{$name} ) {
-            $opts_by_source{$name} = [];
-            push @source_names, $name;
-        }
-        push @{$opts_by_source{$name}}, $contents;
-    }
+    return "!$filter";
+}
 
-    foreach my $name (@source_names) {
-        my $contents = $opts_by_source{$name};
+1;
+package App::Ack::Filter::Is;
 
-        print $name, "\n";
-        print '=' x length($name), "\n";
-        print '  ', join(' ', @{$_}), "\n" foreach sort { compare_opts($a, $b) } @{$contents};
-    }
 
-    return;
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
 }
 
-sub remove_default_options_if_needed {
-    my ( $sources ) = @_;
-
-    my $default_index;
+use File::Spec 3.00 ();
 
-    foreach my $index ( 0 .. $#$sources ) {
-        if ( $sources->[$index] eq 'Defaults' ) {
-            $default_index = $index;
-            last;
-        }
-    }
+sub new {
+    my ( $class, $filename ) = @_;
 
-    return $sources unless defined $default_index;
+    return bless {
+        filename => $filename,
+        groupname => 'IsGroup',
+    }, $class;
+}
 
-    my $should_remove = 0;
+sub create_group {
+    return App::Ack::Filter::IsGroup->new();
+}
 
-    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',
-    );
+sub filter {
+    my ( $self, $file ) = @_;
 
-    foreach my $index ( $default_index + 2 .. $#$sources ) {
-        next if $index % 2 != 0;
+    return (File::Spec->splitpath($file->name))[2] eq $self->{filename};
+}
 
-        my ( $name, $args ) = @{$sources}[ $index, $index + 1 ];
+sub inspect {
+    my ( $self ) = @_;
 
-        if (ref($args)) {
-            local @ARGV = @{$args};
-            Getopt::Long::GetOptions(
-                'ignore-ack-defaults' => \$should_remove,
-            );
-            @{$args} = @ARGV;
-        }
-        else {
-            ( undef, $sources->[$index + 1] ) = Getopt::Long::GetOptionsFromString($args,
-                'ignore-ack-defaults' => \$should_remove,
-            );
-        }
-    }
+    return ref($self) . ' - ' . $self->{filename};
+}
 
-    Getopt::Long::Configure('default');
-    Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version');
+sub to_string {
+    my ( $self ) = @_;
 
-    return $sources unless $should_remove;
+    return $self->{filename};
+}
 
-    my @copy = @{$sources};
-    splice @copy, $default_index, 2;
-    return \@copy;
+BEGIN {
+    App::Ack::Filter->register_filter(is => __PACKAGE__);
 }
 
-sub check_for_mutually_exclusive_options {
-    my ( $arg_sources ) = @_;
+1;
+package App::Ack::Filter::IsGroup;
 
-    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 ];
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
 
-        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;
-            }
-        }
-    }
+sub new {
+    my ( $class ) = @_;
 
-    while( @copy ) {
-        my %set_opts;
+    return bless {
+        data => {},
+    }, $class;
+}
 
-        my ( $source_name, $args ) = splice @copy, 0, 2;
-        $args = ref($args) ? [ @{$args} ] : [ Text::ParseWords::shellwords($args) ];
+sub add {
+    my ( $self, $filter ) = @_;
 
-        foreach my $opt ( @{$args} ) {
-            next unless $opt =~ /^[-+]/;
-            last if $opt eq '--';
+    $self->{data}->{ $filter->{filename} } = 1;
 
-            if( $opt =~ /^(.*)=/ ) {
-                $opt = $1;
-            }
-            elsif ( $opt =~ /^(-[^-]).+/ ) {
-                $opt = $1;
-            }
+    return;
+}
 
-            $set_opts{ $opt } = 1;
+sub filter {
+    my ( $self, $file ) = @_;
 
-            my $mutex_opts = $mutually_exclusive_with{ $opt };
+    return exists $self->{data}->{ $file->basename };
+}
 
-            next unless $mutex_opts;
+sub inspect {
+    my ( $self ) = @_;
 
-            foreach my $mutex_opt ( @{$mutex_opts} ) {
-                if($set_opts{ $mutex_opt }) {
-                    die "Options '$mutex_opt' and '$opt' are mutually exclusive\n";
-                }
-            }
-        }
-    }
+    return ref($self) . " - $self";
 }
 
-sub process_args {
-    my $arg_sources = \@_;
+sub to_string {
+    my ( $self ) = @_;
 
-    my %opt;
+    return join(' ', keys %{$self->{data}});
+}
 
-    check_for_mutually_exclusive_options($arg_sources);
+1;
+package App::Ack::Filter::IsPath;
 
-    $arg_sources = remove_default_options_if_needed($arg_sources);
 
-    if ( should_dump_options($arg_sources) ) {
-        dump_options($arg_sources);
-        exit(0);
-    }
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
 
-    my $type_specs = process_filetypes(\%opt, $arg_sources);
-    process_other(\%opt, $type_specs, $arg_sources);
-    while ( @{$arg_sources} ) {
-        my ( $source_name, $args ) = splice( @{$arg_sources}, 0, 2 );
 
-        # All of our sources should be transformed into an array ref
-        if ( ref($args) ) {
-            if ( $source_name eq 'ARGV' ) {
-                @ARGV = @{$args};
-            }
-            elsif (@{$args}) {
-                Carp::croak "source '$source_name' has extra arguments!";
-            }
-        }
-        else {
-            Carp::croak 'The impossible has occurred!';
-        }
-    }
-    my $filters = ($opt{filters} ||= []);
+sub new {
+    my ( $class, $filename ) = @_;
 
-    # throw the default filter in if no others are selected
-    if ( not grep { !$_->is_inverted() } @{$filters} ) {
-        push @{$filters}, App::Ack::Filter::Default->new();
-    }
-    return \%opt;
+    return bless {
+        filename => $filename,
+        groupname => 'IsPathGroup',
+    }, $class;
 }
 
+sub create_group {
+    return App::Ack::Filter::IsPathGroup->new();
+}
 
-sub retrieve_arg_sources {
-    my @arg_sources;
-
-    my $noenv;
-    my $ackrc;
+sub filter {
+    my ( $self, $file ) = @_;
 
-    Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version');
-    Getopt::Long::Configure('pass_through');
-    Getopt::Long::Configure('no_auto_abbrev');
+    return $file->name eq $self->{filename};
+}
 
-    Getopt::Long::GetOptions(
-        'noenv'   => \$noenv,
-        'ackrc=s' => \$ackrc,
-    );
+sub inspect {
+    my ( $self ) = @_;
 
-    Getopt::Long::Configure('default', 'no_auto_help', 'no_auto_version');
+    return ref($self) . ' - ' . $self->{filename};
+}
 
-    my @files;
+sub to_string {
+    my ( $self ) = @_;
 
-    if ( !$noenv ) {
-        my $finder = App::Ack::ConfigFinder->new;
-        @files  = $finder->find_config_files;
-    }
-    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) {
-            close $fh;
-        }
-        else {
-            die "Unable to load ackrc '$ackrc': $!"
-        }
-        push( @files, $ackrc );
-    }
+    return $self->{filename};
+}
 
-    push @arg_sources, Defaults => [ App::Ack::ConfigDefault::options() ];
+1;
+package App::Ack::Filter::IsPathGroup;
 
-    foreach my $file ( @files) {
-        my @lines = App::Ack::ConfigFinder::read_rcfile($file);
-        push ( @arg_sources, $file, \@lines ) if @lines;
-    }
 
-    if ( $ENV{ACK_OPTIONS} && !$noenv ) {
-        push( @arg_sources, 'ACK_OPTIONS' => $ENV{ACK_OPTIONS} );
-    }
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
 
-    push( @arg_sources, 'ARGV' => [ @ARGV ] );
+sub new {
+    my ( $class ) = @_;
 
-    return @arg_sources;
+    return bless {
+        data => {},
+    }, $class;
 }
 
-1; # End of App::Ack::ConfigLoader
-package App::Ack::ConfigDefault;
-
-use warnings;
-use strict;
+sub add {
+    my ( $self, $filter ) = @_;
 
-sub options {
-    my @options = split( /\n/, _options_block() );
-    @options = grep { /./ && !/^#/ } @options;
+    $self->{data}->{ $filter->{filename} } = 1;
 
-    return @options;
+    return;
 }
 
-sub _options_block {
-    return <<'HERE';
-# This is the default ackrc for ack 2.0
+sub filter {
+    my ( $self, $file ) = @_;
 
-# There are four different ways to match
-#
-# is:  Match the filename exactly
-#
-# ext: Match the extension of the filename exactly
-#
-# match: Match the filename against a Perl regular expression
-#
-# firstlinematch: Match the first 250 characters of the first line
-#   of text against a Perl regular expression.  This is only for
-#   the --type-add option.
+    return exists $self->{data}->{$file->name};
+}
 
+sub inspect {
+    my ( $self ) = @_;
 
-# Directories to ignore
-# Bazaar
---ignore-directory=is:.bzr
+    return ref($self) . " - $self";
+}
 
-# Codeville
---ignore-directory=is:.cdv
+sub to_string {
+    my ( $self ) = @_;
 
-# Interface Builder
---ignore-directory=is:~.dep
---ignore-directory=is:~.dot
---ignore-directory=is:~.nib
---ignore-directory=is:~.plst
+    return join(' ', keys %{$self->{data}});
+}
 
-# Git
---ignore-directory=is:.git
+1;
+package App::Ack::Filter::Match;
 
-# Mercurial
---ignore-directory=is:.hg
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
 
-# quilt
---ignore-directory=is:.pc
 
-# Subversion
---ignore-directory=is:.svn
 
-# Monotone
---ignore-directory=is:_MTN
+sub new {
+    my ( $class, $re ) = @_;
 
-# CVS
---ignore-directory=is:CVS
+    $re =~ s{^/|/$}{}g; # XXX validate?
+    $re = qr/$re/i;
 
-# RCS
---ignore-directory=is:RCS
+    return bless {
+        regex => $re,
+        groupname => 'MatchGroup',
+    }, $class;
+}
 
-# SCCS
---ignore-directory=is:SCCS
+sub create_group {
+    return App::Ack::Filter::MatchGroup->new;
+}
 
-# darcs
---ignore-directory=is:_darcs
+sub filter {
+    my ( $self, $file ) = @_;
 
-# Vault/Fortress
---ignore-directory=is:_sgbak
+    return $file->basename =~ /$self->{regex}/;
+}
 
-# autoconf
---ignore-directory=is:autom4te.cache
+sub inspect {
+    my ( $self ) = @_;
 
-# Perl module building
---ignore-directory=is:blib
---ignore-directory=is:_build
+    return ref($self) . ' - ' . $self->{regex};
+}
 
-# Perl Devel::Cover module's output directory
---ignore-directory=is:cover_db
+sub to_string {
+    my ( $self ) = @_;
+
+    return "Filename matches $self->{regex}";
+}
 
+BEGIN {
+    App::Ack::Filter->register_filter(match => __PACKAGE__);
+}
 
+1;
+package App::Ack::Filter::MatchGroup;
 
-# Files to ignore
-# Backup files
---ignore-file=ext:bak
---ignore-file=match:/~$/
 
-# Emacs swap files
---ignore-file=match:/^#.+#$/
+use strict;
+use warnings;
+BEGIN {
+    our @ISA = 'App::Ack::Filter';
+}
 
-# vi/vim swap files
---ignore-file=match:/[._].*\.swp$/
+sub new {
+    my ( $class ) = @_;
 
-# core dumps
---ignore-file=match:/core\.\d+$/
+    return bless {
+        matches => [],
+        big_re  => undef,
+    }, $class;
+}
 
-# minified Javascript
---ignore-file=match:/[.]min[.]js$/
---ignore-file=match:/[.]js[.]min$/
+sub add {
+    my ( $self, $filter ) = @_;
 
-# minified CSS
---ignore-file=match:/[.]min[.]css$/
---ignore-file=match:/[.]css[.]min$/
+    push @{ $self->{matches} }, $filter->{regex};
 
+    my $re = join('|', map { "(?:$_)" } @{ $self->{matches} });
+    $self->{big_re} = qr/$re/;
 
-# Filetypes defined
+    return;
+}
 
-# Perl http://perl.org/
---type-add=perl:ext:pl,pm,pod,t
---type-add=perl:firstlinematch:/^#!.*\bperl/
+sub filter {
+    my ( $self, $file ) = @_;
 
-# Makefiles http://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:GNUmakefile
+    return $file->basename =~ /$self->{big_re}/;
+}
 
-# Rakefiles http://rake.rubyforge.org/
---type-add=rake:is:Rakefile
+# This class has no inspect() or to_string() method.
+# It will just use the default one unless someone writes something useful.
 
-# CMake http://www.cmake.org/
---type-add=cmake:is:CMakeLists.txt
---type-add=cmake:ext:cmake
+1;
+package File::Next;
 
-# Actionscript
---type-add=actionscript:ext:as,mxml
+use strict;
+use warnings;
 
-# Ada http://www.adaic.org/
---type-add=ada:ext:ada,adb,ads
 
-# ASP http://msdn.microsoft.com/en-us/library/aa286483.aspx
---type-add=asp:ext:asp
+our $VERSION = '1.18';
 
-# ASP.Net http://www.asp.net/
---type-add=aspx:ext:master,ascx,asmx,aspx,svc
 
-# Assembly
---type-add=asm:ext:asm,s
 
-# Batch
---type-add=batch:ext:bat,cmd
+use File::Spec ();
 
-# ColdFusion http://en.wikipedia.org/wiki/ColdFusion
---type-add=cfmx:ext:cfc,cfm,cfml
+our $name; # name of the current file
+our $dir;  # dir of the current file
 
-# Clojure http://clojure.org/
---type-add=clojure:ext:clj
+our %files_defaults;
+our %skip_dirs;
 
-# C
-# .xs are Perl C files
---type-add=cc:ext:c,h,xs
+BEGIN {
+    %files_defaults = (
+        file_filter     => undef,
+        descend_filter  => undef,
+        error_handler   => sub { CORE::die $_[0] },
+        warning_handler => sub { CORE::warn @_ },
+        sort_files      => undef,
+        follow_symlinks => 1,
+        nul_separated   => 0,
+    );
+    %skip_dirs = map {($_,1)} (File::Spec->curdir, File::Spec->updir);
+}
 
-# C header files
---type-add=hh:ext:h
 
-# C++
---type-add=cpp:ext:cpp,cc,cxx,m,hpp,hh,h,hxx
+sub files {
+    die _bad_invocation() if @_ && defined($_[0]) && ($_[0] eq __PACKAGE__);
 
-# C#
---type-add=csharp:ext:cs
+    my ($parms,@queue) = _setup( \%files_defaults, @_ );
 
-# CSS http://www.w3.org/Style/CSS/
---type-add=css:ext:css
+    my $filter = $parms->{file_filter};
+    return sub {
+        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;
+                    local $File::Next::name = $fullpath;
+                    next if not $filter->();
+                }
+                return wantarray ? ($dirname,$file,$fullpath) : $fullpath;
+            }
+            if ( $is_dir ) {
+                unshift( @queue, _candidate_files( $parms, $fullpath ) );
+            }
+        } # while
 
-# Dart http://www.dartlang.org/
---type-add=dart:ext:dart
+        return;
+    }; # iterator
+}
 
-# Delphi http://en.wikipedia.org/wiki/Embarcadero_Delphi
---type-add=delphi:ext:pas,int,dfm,nfm,dof,dpk,dproj,groupproj,bdsgroup,bdsproj
 
-# Emacs Lisp http://www.gnu.org/software/emacs
---type-add=elisp:ext:el
 
-# Erlang http://www.erlang.org/
---type-add=erlang:ext:erl,hrl
 
-# Fortran http://en.wikipedia.org/wiki/Fortran
---type-add=fortran:ext:f,f77,f90,f95,f03,for,ftn,fpp
 
-# Google Go http://golang.org/
---type-add=go:ext:go
 
-# Groovy http://groovy.codehaus.org/
---type-add=groovy:ext:groovy,gtmpl,gpp,grunit,gradle
 
-# Haskell http://www.haskell.org/
---type-add=haskell:ext:hs,lhs
+sub from_file {
+    die _bad_invocation() if @_ && defined($_[0]) && ($_[0] eq __PACKAGE__);
 
-# HTML
---type-add=html:ext:htm,html
+    my ($parms,@queue) = _setup( \%files_defaults, @_ );
+    my $err  = $parms->{error_handler};
+    my $warn = $parms->{warning_handler};
 
-# Java http://www.oracle.com/technetwork/java/index.html
---type-add=java:ext:java,properties
+    my $filename = $queue[0]->[1];
 
-# JavaScript
---type-add=js:ext:js
+    if ( !defined($filename) ) {
+        $err->( 'Must pass a filename to from_file()' );
+        return undef;
+    }
 
-# JSP http://www.oracle.com/technetwork/java/javaee/jsp/index.html
---type-add=jsp:ext:jsp,jspx,jhtm,jhtml
+    my $fh;
+    if ( $filename eq '-' ) {
+        $fh = \*STDIN;
+    }
+    else {
+        if ( !open( $fh, '<', $filename ) ) {
+            $err->( "Unable to open $filename: $!", $! + 0 );
+            return undef;
+        }
+    }
 
-# Common Lisp http://common-lisp.net/
---type-add=lisp:ext:lisp,lsp
+    my $filter = $parms->{file_filter};
+    return sub {
+        local $/ = $parms->{nul_separated} ? "\x00" : $/;
+        while ( my $fullpath = <$fh> ) {
+            chomp $fullpath;
+            next unless $fullpath =~ /./;
+            if ( not ( -f $fullpath || -p _ ) ) {
+                $warn->( "$fullpath: No such file" );
+                next;
+            }
 
-# Lua http://www.lua.org/
---type-add=lua:ext:lua
+            my ($volume,$dirname,$file) = File::Spec->splitpath( $fullpath );
+            if ( $filter ) {
+                local $_ = $file;
+                local $File::Next::dir  = $dirname;
+                local $File::Next::name = $fullpath;
+                next if not $filter->();
+            }
+            return wantarray ? ($dirname,$file,$fullpath) : $fullpath;
+        } # while
+        close $fh;
 
-# Objective-C
---type-add=objc:ext:m,h
+        return;
+    }; # iterator
+}
 
-# Objective-C++
---type-add=objcpp:ext:mm,h
+sub _bad_invocation {
+    my $good = (caller(1))[3];
+    my $bad  = $good;
+    $bad =~ s/(.+)::/$1->/;
+    return "$good must not be invoked as $bad";
+}
 
-# OCaml http://caml.inria.fr/
---type-add=ocaml:ext:ml,mli
+sub sort_standard($$)   { return $_[0]->[1] cmp $_[1]->[1] }
+sub sort_reverse($$)    { return $_[1]->[1] cmp $_[0]->[1] }
 
-# Parrot http://www.parrot.org/
---type-add=parrot:ext:pir,pasm,pmc,ops,pod,pg,tg
+sub reslash {
+    my $path = shift;
 
-# PHP http://www.php.net/
---type-add=php:ext:php,phpt,php3,php4,php5,phtml
---type-add=php:firstlinematch:/^#!.*\bphp/
+    my @parts = split( /\//, $path );
 
-# Plone http://plone.org/
---type-add=plone:ext:pt,cpt,metadata,cpy,py
+    return $path if @parts < 2;
 
-# Python http://www.python.org/
---type-add=python:ext:py
---type-add=python:firstlinematch:/^#!.*\bpython/
+    return File::Spec->catfile( @parts );
+}
 
-# R http://www.r-project.org/
---type-add=rr:ext:R
 
-# Ruby http://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/
---type-add=rust:ext:rs
+sub _setup {
+    my $defaults = shift;
+    my $passed_parms = ref $_[0] eq 'HASH' ? {%{+shift}} : {}; # copy parm hash
 
-# Scala http://www.scala-lang.org/
---type-add=scala:ext:scala
+    my %passed_parms = %{$passed_parms};
 
-# Scheme http://groups.csail.mit.edu/mac/projects/scheme/
---type-add=scheme:ext:scm,ss
+    my $parms = {};
+    for my $key ( keys %{$defaults} ) {
+        $parms->{$key} =
+            exists $passed_parms{$key}
+                ? delete $passed_parms{$key}
+                : $defaults->{$key};
+    }
 
-# Shell
---type-add=shell:ext:sh,bash,csh,tcsh,ksh,zsh,fish
---type-add=shell:firstlinematch:/^#!.*\b(?:ba|t?c|k|z|fi)?sh\b/
+    # Any leftover keys are bogus
+    for my $badkey ( sort keys %passed_parms ) {
+        my $sub = (caller(1))[3];
+        $parms->{error_handler}->( "Invalid option passed to $sub(): $badkey" );
+    }
 
-# Smalltalk http://www.smalltalk.org/
---type-add=smalltalk:ext:st
+    # If it's not a code ref, assume standard sort
+    if ( $parms->{sort_files} && ( ref($parms->{sort_files}) ne 'CODE' ) ) {
+        $parms->{sort_files} = \&sort_standard;
+    }
+    my @queue;
 
-# SQL http://www.iso.org/iso/catalogue_detail.htm?csnumber=45498
---type-add=sql:ext:sql,ctl
+    for ( @_ ) {
+        my $start = reslash( $_ );
+        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 ];
+    }
 
-# Tcl http://www.tcl.tk/
---type-add=tcl:ext:tcl,itcl,itk
+    return ($parms,@queue);
+}
 
-# LaTeX http://www.latex-project.org/
---type-add=tex:ext:tex,cls,sty
 
-# Template Toolkit http://template-toolkit.org/
---type-add=tt:ext:tt,tt2,ttml
+sub _candidate_files {
+    my $parms   = shift;
+    my $dirname = shift;
 
-# Visual Basic
---type-add=vb:ext:bas,cls,frm,ctl,vb,resx
+    my $dh;
+    if ( !opendir $dh, $dirname ) {
+        $parms->{error_handler}->( "$dirname: $!", $! + 0 );
+        return;
+    }
 
-# Verilog
---type-add=verilog:ext:v,vh,sv
+    my @newfiles;
+    my $descend_filter = $parms->{descend_filter};
+    my $follow_symlinks = $parms->{follow_symlinks};
 
-# VHDL http://www.eda.org/twiki/bin/view.cgi/P1076/WebHome
---type-add=vhdl:ext:vhd,vhdl
+    for my $file ( grep { !exists $skip_dirs{$_} } readdir $dh ) {
+        my $fullpath = File::Spec->catdir( $dirname, $file );
+        if ( !$follow_symlinks ) {
+            next if -l $fullpath;
+        }
+        else {
+            stat($fullpath);
+        }
+        my $is_dir  = -d _;
+        my $is_file = -f _;
+        my $is_fifo = (-p _) || ($fullpath =~ m{^/dev/fd});
 
-# Vim http://www.vim.org/
---type-add=vim:ext:vim
+        # Only do directory checking if we have a descend_filter
+        if ( $descend_filter ) {
+            if ( $is_dir ) {
+                local $File::Next::dir = $fullpath;
+                local $_ = $file;
+                next if not $descend_filter->();
+            }
+        }
+        push @newfiles, [ $dirname, $file, $fullpath, $is_dir, $is_file, $is_fifo ];
+    }
+    closedir $dh;
 
-# XML http://www.w3.org/TR/REC-xml/
---type-add=xml:ext:xml,dtd,xsl,xslt,ent
---type-add=xml:firstlinematch:/<[?]xml/
+    my $sort_sub = $parms->{sort_files};
+    if ( $sort_sub ) {
+        @newfiles = sort $sort_sub @newfiles;
+    }
 
-# YAML http://yaml.org/
---type-add=yaml:ext:yaml,yml
-HERE
+    return @newfiles;
 }
 
-1;
+
+
+1; # End of File::Next