#!/usr/bin/perl
###############################################################################
## ----------------------------------------------------------------------------
## A MCE-driven 'wrapper' script for grep-like C binaries.
##
## Making a wrapper for other grep-like binaries is easy. Simply link this
## script or make a copy. The prefix 'mce_' is stripped from the name for
## determining the actual binary to use. A trailing '.pl' extension is
## optional. Please ensure the binary is in your path.
##
##    agrep.exe (z)grep.exe (z)egrep.exe (z)fgrep.exe tre-agrep.exe
##    agrep     (z)grep     (z)egrep     (z)fgrep     tre-agrep
##
##    ln mce_grep mce_egrep ; ln mce_grep mce_zegrep ; (or) ln -s ...
##    ln mce_grep mce_fgrep ; ln mce_grep mce_zfgrep ; (or) cp ...
##    ln mce_grep mce_zgrep
##
##    ln mce_grep mce_tre-agrep  # binary is named tre-agrep
##    ln mce_grep mce_agrep      # or agrep
##
## Caveat for (z)grep, (z)egrep, and (z)fgrep. When '--chunklevel=file' is
## specified or passing a single file, MCE workers read the file in smaller
## chunks. This is fine, typically. However, the following grep options may
## report inaccurately due to crossing boundaries in regards to chunks.
## To ensure accuracy, run with '--chunklevel=list'.
##
##    -A NUM, --after-context=NUM
##    -B NUM, --before-context=NUM
##    -C NUM, --context=NUM
##
## ============================================================================
## ----------------------------------------------------------------------------
## 2014-01-21  v1.008
##   Initial release by Mario Roy.
##
## 2014-07-23  v1.009
##   ${^CHILD_ERROR_NATIVE} is not defined in Perl 5.8.x. Use $? instead.
##   Compute chunk_level => 'auto' to use 'file' when reading STDIN.
##   Set chunk_size to 8M when not specified (from 4M previously).
##
## 2014-12-22  v1.010
##   Small code refactoring.
##
## 2017-02-25  v1.011
##   When -r is specified and zero paths are given, start recursively in the
##   current directory. Set chunk-level accordingly to list mode.
##
## 2017-03-01  v1.012
##   Updated logic for determining chunk level mode. Ditto for chunk size.
##   Fixed an issue for not seeing STDERR output with '--chunk-level=file'.
##   Added support for zgrep, zegrep, and zfgrep. Thank you, Jeff Rouse.
##   https://www.activestate.com/blog/2016/12/grep-losing-its-grip
##
## 2017-03-27  v1.013
##   Check for $!{'EINTR'} during syswrite.
##
###############################################################################

###############################################################################
## ----------------------------------------------------------------------------
## Which to choose? bin/mce_grep or mce-examples/other/egrep.pl
##
## (A) This wrapper script is good for expensive pattern matching, especially
##     with agrep and tre-agrep. It supports more options due to being passed
##     to the binary. It supports two levels of chunking specified with the
##     --chunk-level={auto|file|list} option. Choose file for large files.
##
## (B) The egrep.pl script is a pure-Perl implementation with fewer options.
##     It's strengh is searching a single file and/or with many expressions.
##     https://github.com/marioroy/mce-examples/blob/master/other/egrep.pl
##
###############################################################################

use strict; use warnings;

## no critic (InputOutput::ProhibitBarewordFileHandles)
## no critic (InputOutput::ProhibitTwoArgOpen)

use Cwd 'abs_path'; ## Insert lib-path at the head of @INC.
use lib abs_path($0 =~ m{^(.*)[\\/]} && $1 || abs_path) . '/../lib';

my ($prog_name, $prog_dir);

BEGIN {
   $prog_name = $0;             $prog_name =~ s{^.*[\\/]}{}g;
   $prog_dir  = abs_path($0);   $prog_dir  =~ s{[\\/][^\\/]*$}{};

   $ENV{PATH} .= ($^O eq 'MSWin32' ? ';' : ':') . $prog_dir;
}
sub INIT {
   ## Provide file globbing support under Windows similar to Unix.
   @ARGV = <@ARGV> if ($^O eq 'MSWin32');
}

use Getopt::Long qw(
   :config bundling pass_through no_ignore_case no_auto_abbrev
);
use Fcntl qw( O_RDONLY );
use Scalar::Util qw( looks_like_number );
use Errno ();

use MCE::Signal qw( -use_dev_shm );
use MCE 1.5;  # or later release

###############################################################################
## ----------------------------------------------------------------------------
## Usage and validation.
##
###############################################################################

sub usage {
   print <<"::_USAGE_BLOCK_END_::";

Options for Script:
  --max-workers=NUM         override max workers (default auto, 8 maximum)
  --maxworkers=NUM            e.g. auto*2, 16

  --chunk-level=LEVEL       override chunk level (default auto)
  --chunklevel=LEVEL          chunk at [file] or [list] level

  --chunk-size=NUM[KM]      override chunk size (set at limit if under or over)
  --chunksize=NUM[KM]         [file] default: 8M  minimum: 200K  maximum: 20M
                              [list] default: 1   minimum: 1     maximum: 200

  --lang=LOCALE             override locale
                              e.g. C, en_US.UTF-8, en_US.ISO-8859-1

Options for Binary:
::_USAGE_BLOCK_END_::

   return;
}

my $is_mswin32 = $^O eq 'MSWin32';
my ($cmd_name, $cmd_path);

$cmd_name = $prog_name;
$cmd_name =~ s{^mce_}{};
$cmd_name =~ s{\.pl$}{};

if ($is_mswin32) {
   $cmd_name .= '.exe';
   for ( split ';', $ENV{'PATH'} ) {
      $cmd_path = "$_\\$cmd_name", last if (-x "$_\\$cmd_name");
   }
}
else {
   $cmd_name .= '.exe' if $^O eq 'cygwin';
   for ( split ':', $ENV{'PATH'} ) {
      $cmd_path = "$_/$cmd_name", last if (-x "$_/$cmd_name");
   }
}

unless (defined $cmd_path) {
   print {*STDERR} "$prog_name: $cmd_name: command not found\n";
   exit 2;
}
{
   my %valid_names = map { $_ => 1 } qw(
      grep.exe egrep.exe fgrep.exe zgrep.exe zegrep.exe zfgrep.exe
      grep     egrep     fgrep     zgrep     zegrep     zfgrep

      agrep.exe tre-agrep.exe agrep tre-agrep
   );
   unless (exists $valid_names{$cmd_name}) {
      print {*STDERR} "$prog_name: $cmd_name: command not supported\n";
      exit 2;
   }
}

###############################################################################
## ----------------------------------------------------------------------------
## Process arguments.
##
###############################################################################

my ($h_patn, $b_flag, $c_flag, $H_flag, $h_flag, $n_flag, $q_flag) = ((0) x 7);
my (@r_patn, @args, $arg, @files, $file); my ($f_list, $r_flag) = (0, 0);
my ($exit_status, $found_match, $skip_args, $w_filename) = (0, 0, 0, 0);

my $max_workers = 'auto'; my $chunk_level = 'auto'; my $chunk_size;
my $max_count = 0; my $no_msg = 0; my @TMP_ARGV;

## Option parsing step 1.

for my $i (0 .. @ARGV - 1) {
   if ($ARGV[$i] eq '--') {
      @TMP_ARGV = @ARGV[$i .. @ARGV - 1]; @ARGV = @ARGV[0 .. $i - 1];
      last;
   }
}

{
   local $SIG{__WARN__} = sub { };

   GetOptions(
      'maxworkers|max-workers|max_workers=s' => \$max_workers,
      'chunklevel|chunk-level|chunk_level=s' => \$chunk_level,
      'chunksize|chunk-size|chunk_size=s'    => \$chunk_size,

      'lang=s' => sub {
         my ($self, $lang) = @_;
         delete @ENV{ qw( LC_MESSAGES LC_COLLATE LC_CTYPE LC_ALL ) };
         $ENV{'LANG'} = $lang;
      },
      'help' => sub {
         usage(); system $cmd_path, '--help'; print "\n";
         exit 0;
      },
      'V|version' => sub {
         system $cmd_path, '--version'; exit 0;
      },

      'q|quiet|silent'  => \$q_flag,
      'H|with-filename' => sub { $H_flag = 1; $h_flag = 0; },
      'h|no-filename'   => sub { $H_flag = 0; $h_flag = 1; },
      'm|max-count=s'   => \$max_count,
      'R|r|recursive'   => \$r_flag
   );

   if ($max_workers !~ /^auto/) {
      unless (looks_like_number($max_workers) && $max_workers > 0) {
         print {*STDERR} "$prog_name: invalid max workers\n";
         exit 2;
      }
   }
   if ($chunk_level !~ /^(?:auto|file|list)$/) {
      print {*STDERR} "$prog_name: invalid chunk level\n";
      exit 2;
   }
   if (defined $chunk_size) {
      if ($chunk_size =~ /^(\d+)K/i) {
         $chunk_size = $1 * 1024;
      }
      elsif ($chunk_size =~ /^(\d+)M/i) {
         $chunk_size = $1 * 1024 * 1024;
      }
      if (!looks_like_number($chunk_size) || $chunk_size < 1) {
         print {*STDERR} "$prog_name: invalid chunk size\n";
         exit 2;
      }
   }
   if ($max_count) {
      unless (looks_like_number($max_count) && $max_count >= 0) {
         print {*STDERR} "$prog_name: invalid max count\n";
         exit 2;
      }
   }
}

## Option parsing step 2.

if (@TMP_ARGV) {
   @ARGV = (@ARGV, @TMP_ARGV); undef @TMP_ARGV;
   if ($ARGV[0] eq '--') {
      shift @ARGV; $skip_args = 1; push @args, '--';
   }
}

while ( @ARGV ) {
   $arg = shift @ARGV; $arg =~ s/ /\\ /g;

   if ($skip_args) {
      push @files, $arg;
   }
   elsif (substr($arg, 0, 2) eq '--') {           ## --OPTION
      if ($arg eq '--') {
         $skip_args = 1; push @args, $arg;
         next;
      }
      $h_patn = 1 if $arg =~ /^--regexp=/;
      $h_patn = 1 if $arg =~ /^--file=/;
      $b_flag = 1 if $arg eq '--byte-offset';
      $c_flag = 1 if $arg eq '--count';
      $f_list = 1 if $arg eq '--files-without-match';
      $f_list = 1 if $arg eq '--files-with-matches';
      $n_flag = 1 if $arg eq '--record-number';
      $n_flag = 1 if $arg eq '--line-number';
      $no_msg = 1 if $arg eq '--no-messages';

      if ($arg =~ /^--directories=(.+)/) {
         if ($1 ne 'recurse') {
            push @args, $arg;
         } else {
            $r_flag = 1;
         }
      }
      elsif ($arg =~ /^--include=.+/) {
         push @r_patn, $arg;
      }
      elsif ($arg =~ /^--exclude=.+/) {
         push @r_patn, $arg;
      }
      elsif ($arg =~ /^--exclude-from=.+/) {
         push @r_patn, $arg;
      }
      elsif ($arg =~ /^--exclude-dir=.+/) {
         push @r_patn, $arg;
      }
      else {
         ## Pass arguments to the C binary
         push @args, $arg;
      }
   }
   elsif (substr($arg, 0, 1) eq '-') {            ## -OPTION
      if ($arg eq '-') {
         push @files, $arg;
         next;
      }
      my $len = length $arg;

      for (my $x = 1; $x < $len; $x++) {
         my $a = substr($arg, $x, 1);

         $f_list = 1 if $a eq 'L' || $a eq 'l';
         $h_patn = 1 if $a eq 'e' || $a eq 'f';
         $b_flag = 1 if $a eq 'b';
         $c_flag = 1 if $a eq 'c';
         $n_flag = 1 if $a eq 'n';

         $no_msg = 1
            if ($a eq 's' && $cmd_name !~ /agrep/);
      }

      next if $arg eq '-';

      ## Pass arguments to the C binary
      if ($cmd_name =~ /agrep/) {
         push @args, $arg;
         if (substr($arg, -1) =~ /[efDISEd]/) {
            $arg =  shift @ARGV;
            $arg =~ s/ /\\ /g;
            push @args, $arg;
         }
      }
      else {
         my $a = substr($arg, -1);
         push @args, $arg if ($arg ne '-d');
         if ($a =~ /[efABCD]/) {
            $arg =  shift @ARGV;
            $arg =~ s/ /\\ /g;
            push @args, $arg;
         }
         elsif ($a eq 'd') {
            $arg = shift @ARGV;
            if ($arg ne 'recurse') {
               push @args, '-d', $arg;
            }
            else {
               $r_flag = 1;
            }
         }
      }
   }
   else {                                         ## FILE
      push @files, $arg;
   }
}

## Option parsing step 3.

push @args, shift @files if ($h_patn == 0 && @files > 0);

if ((!$h_flag && @files > 1) || (!$h_flag && $r_flag) || $H_flag) {
   $w_filename = 1;
}

if ($r_flag && !@files) {
   push @files, '.';
}

if (@args == 0) {
   system $cmd_path;
   exit 2;
}

###############################################################################
## ----------------------------------------------------------------------------
## MCE callback functions: Error, File, and Count.
##
###############################################################################

my ($_order_id, %_tmp, %_nrecs, %_nsize, $_start_nrecs, $_start_nsize);
my ($_abort_all, $_abort_job, $_total_found);

sub _error {

   my ($msg) = @_;

   print {*STDERR} $msg;
   $exit_status = 2;

   return;
}

sub _abort_job {

   if (!$_abort_job) {
      MCE->abort;
      $_abort_job = $_total_found = $found_match = 1;
      $_abort_all = 1 if $q_flag;
   }

   return;
}

sub _output_cnt {

   my ($chunk_id, $out_file, @_rest) = @_;
   my $cnt;

   if (-s $out_file) {
      $found_match = 1;

      open my $fh, '<', $out_file;
      chomp($cnt = <$fh>);
      close $fh;

      $_total_found += $cnt;

      if ($q_flag && !$_abort_all) {
         MCE->abort; $_abort_all = $_abort_job = 1;
      }
   }

   unlink $out_file;

   return;
}

sub _set_found_match {

   $found_match = 1;

   return;
}

###############################################################################
## ----------------------------------------------------------------------------
## MCE callback function: Output without line-number or byte-offset
##
###############################################################################

sub _output_n0 {

   my ($chunk_id, $out_file, @_rest) = @_;

   $_tmp{ $chunk_id } = $out_file;

   return unless exists $_tmp{ $_order_id };

   do {
      my $out_file = $_tmp{ $_order_id };

      if (!$_abort_job && -s $out_file) {
         my ($fh, $buffer); $found_match = 1;

         if ($q_flag) {
            unless ($_abort_all) {
               MCE->abort; $_abort_all = $_abort_job = 1;
            }
         }
         else {
            if ($w_filename) {
               open $fh, '<', $out_file;
               while (<$fh>) {
                  print $file . ':' . $_;
                  if ($max_count && ++$_total_found == $max_count) {
                     MCE->abort; $_abort_job = 1;
                     last;
                  }
               }
               close $fh;
            }
            else {
               if ($max_count) {
                  open  $fh, '<', $out_file;
                  while (<$fh>) {
                     print $_;
                     if ($max_count && ++$_total_found == $max_count) {
                        MCE->abort; $_abort_job = 1;
                        last;
                     }
                  }
                  close $fh;
               }
               else {
                  sysopen $fh, $out_file, O_RDONLY;
                  sysread $fh, $buffer, -s $fh;
                  close   $fh;

                  print $buffer;
               }
            }
         }
      }

      delete $_tmp{ $_order_id };
      unlink $out_file;

   } while (exists $_tmp{ ++$_order_id });

   return;
}

###############################################################################
## ----------------------------------------------------------------------------
## MCE callback function: Output with line-number or byte-offset
##
###############################################################################

sub _output_n1 {

   my ($chunk_id, $out_file, $n_records, $size) = @_;

   $_tmp{ $chunk_id }   = $out_file;
   $_nsize{ $chunk_id } = $n_flag ? $n_records : $size;

   return unless exists $_tmp{ $_order_id };

   do {
      my $out_file = $_tmp{ $_order_id };

      if ($_order_id > 1) {
         $_start_nsize += $_nsize{ $_order_id - 1 };
         delete $_nsize{ $_order_id - 1 };
      }

      if (!$_abort_job && -s $out_file) {
         my ($p1, $size); $found_match = 1;

         if ($q_flag) {
            unless ($_abort_all) {
               MCE->abort; $_abort_all = $_abort_job = 1;
            }
         }
         else {
            open my $fh, '<', $out_file;

            if ($w_filename) {
               while (<$fh>) {
                  $p1   = index($_, ':');
                  $size = $_start_nsize + substr($_, 0, $p1);

                  print $file . ':' . $size . substr($_, $p1);
                  if ($max_count && ++$_total_found == $max_count) {
                     MCE->abort; $_abort_job = 1;
                     last;
                  }
               }
            }
            else {
               while (<$fh>) {
                  $p1   = index($_, ':');
                  $size = $_start_nsize + substr($_, 0, $p1);

                  print $size . substr($_, $p1);
                  if ($max_count && ++$_total_found == $max_count) {
                     MCE->abort; $_abort_job = 1;
                     last;
                  }
               }
            }

            close $fh;
         }
      }

      delete $_tmp{ $_order_id };
      unlink $out_file;

   } while (exists $_tmp{ ++$_order_id });

   return;
}

###############################################################################
## ----------------------------------------------------------------------------
## MCE callback function: Output with line-number and byte-offset
##
###############################################################################

sub _output_n2 {

   my ($chunk_id, $out_file, $n_records, $size) = @_;

   $_tmp{ $chunk_id }   = $out_file;
   $_nrecs{ $chunk_id } = $n_records;
   $_nsize{ $chunk_id } = $size;

   return unless exists $_tmp{ $_order_id };

   do {
      my $out_file = $_tmp{ $_order_id };

      if ($_order_id > 1) {
         $_start_nrecs += $_nrecs{ $_order_id - 1 };
         delete $_nrecs{ $_order_id - 1 };

         $_start_nsize += $_nsize{ $_order_id - 1 };
         delete $_nsize{ $_order_id - 1 };
      }

      if (!$_abort_job && -s $out_file) {
         my ($p1, $p2, $recs, $size); $found_match = 1;

         if ($q_flag) {
            unless ($_abort_all) {
               MCE->abort; $_abort_all = $_abort_job = 1;
            }
         }
         else {
            open my $fh, '<', $out_file;

            if ($w_filename) {
               while (<$fh>) {
                  $p1   = index($_, ':');
                  $recs = $_start_nrecs + substr($_, 0, $p1++);

                  $p2   = index($_, ':', $p1);
                  $size = $_start_nsize + substr($_, $p1, $p2 - $p1);

                  print $file . ':' . $recs . ':' . $size . substr($_, $p2);
                  if ($max_count && ++$_total_found == $max_count) {
                     MCE->abort; $_abort_job = 1;
                     last;
                  }
               }
            }
            else {
               while (<$fh>) {
                  $p1   = index($_, ':');
                  $recs = $_start_nrecs + substr($_, 0, $p1++);

                  $p2   = index($_, ':', $p1);
                  $size = $_start_nsize + substr($_, $p1, $p2 - $p1);

                  print $recs . ':' . $size . substr($_, $p2);
                  if ($max_count && ++$_total_found == $max_count) {
                     MCE->abort; $_abort_job = 1;
                     last;
                  }
               }
            }

            close $fh;
         }
      }

      delete $_tmp{ $_order_id };
      unlink $out_file;

   } while (exists $_tmp{ ++$_order_id });

   return;
}

###############################################################################
## ----------------------------------------------------------------------------
## MCE user functions: run-mode = file.
##
###############################################################################

sub user_begin_file {

   $0 = $^X;

   return;
}

sub make_user_func_file {

   my $first_time = 1;

   return sub {
      my ($self, $chunk_ref, $chunk_id) = @_;

      my ($out_fh, $err_fh, $cmd_fh, $has_err); my $n_records = 0;
      my $out_file = MCE->sess_dir .'/'. $chunk_id;

      if ($n_flag) {
         $n_records++ while ($$chunk_ref =~ m!\n!mg);
      }

      if ($is_mswin32) {
         $out_file =~ s{/}{\\\\}g;

         open my $in_fh, '+>', $out_file . '.in'; binmode $in_fh, ':raw';
         print {$in_fh} $$chunk_ref;
         close $in_fh;

         my $err_file = $first_time ? "2> $out_file.err" : '';
         system("$cmd_path < $out_file.in @args > $out_file $err_file");

         unlink "$out_file.in";
      }
      else {
         ## Borrowed bits from IPC::Run3 for STDOUT/ERR. However, I settled
         ## on passing STDIN via open, for lesser overhead, versus calling
         ## system from observations made during testing.

         local (*STDOUT_SAVE, *STDERR_SAVE);

         open STDOUT_SAVE, '>&STDOUT';
         open $out_fh, '+>', $out_file; binmode $out_fh, ':raw';
         open STDOUT, '>&' . fileno $out_fh;

         if ($first_time) {
            open STDERR_SAVE, '>&STDERR';
            open $err_fh, '+>', "$out_file.err"; binmode $err_fh, ':raw';
            open STDERR, '>&' . fileno $err_fh;
         }

         ## Got "maximal count of pending signals (NUM) exceeded" message.
         ## Thus the reason for using syswrite versus print below.

         local $SIG{PIPE} = sub { };

         open $cmd_fh, '|-', $cmd_path, @args;     # Run external command

         my $wrote = 0;                            # Write to STDIN

         WRITE: {
            $wrote += ( syswrite (
               $cmd_fh, $$chunk_ref, length($$chunk_ref) - $wrote, $wrote
            )) or do {
               redo WRITE if $! == Errno::EINTR();
            };
         }

         close $cmd_fh;

         open  STDOUT, '>&STDOUT_SAVE';
         close $out_fh;

         if ($first_time) {
            open  STDERR, '>&STDERR_SAVE';
            close $err_fh;
         }
      }

      MCE->abort if ($q_flag && -s $out_file);

      ## Send error.

      if ($first_time) {
         my $err_file = "$out_file.err";

         if (-s $err_file) {
            $has_err = 1; MCE->abort;
            if ($chunk_id == 1) {
               open  $err_fh, '<', $err_file;
               local $/ = undef; MCE->do('_error', <$err_fh>);
               close $err_fh;
            }
         }

         unlink $err_file;
         $first_time = 0;
      }

      ## Gather output.

      if ($f_list) {
         MCE->do('_abort_job') if (!$has_err && -s $out_file);
         unlink $out_file;
      }
      else {
         MCE->gather($chunk_id, $out_file, $n_records, length $$chunk_ref)
            unless $has_err;
      }

      return;
   };
}

###############################################################################
## ----------------------------------------------------------------------------
## MCE user functions: run-mode = list.
##
###############################################################################

sub user_begin_list {

   $0 = $^X;

   use vars qw( $child_found_match );
   our $child_found_match = 0;

   return;
}

sub user_end_list {

   MCE->do('_set_found_match') if $child_found_match;

   return;
}

sub user_func_list {

   my ($self, $chunk_ref, $chunk_id) = @_;

   my ($output, $err_fh, $status);
   my $err_file = MCE->sess_dir .'/'. $chunk_id . '.err';

   $$chunk_ref =~ s/\n/ /mg;
   local $?;

   if ($is_mswin32) {
      $err_file =~ s{/}{\\\\}g;
      $output = `$cmd_path @args $$chunk_ref 2> $err_file`;
      $status = $? >> 8;
   }
   else {
      local *STDERR_SAVE;

      open STDERR_SAVE, '>&STDERR';
      open $err_fh, '+>', $err_file; binmode $err_fh, ':raw';
      open STDERR, '>&' . fileno $err_fh;

      $output = `$cmd_path @args $$chunk_ref`;
      $status = $? >> 8;

      open  STDERR, '>&STDERR_SAVE';
      close $err_fh;
   }

   MCE->abort if ($q_flag && length $output);

   ## Send error.

   if (-s $err_file) {
      open  $err_fh, '<', $err_file;
      local $/ = undef; MCE->do('_error', <$err_fh>);
      close $err_fh;
   }

   unlink $err_file;

   ## Gather output.

   if ($q_flag) {
      MCE->do('_abort_job') if ($status == 0);
   }
   else {
      if (length $output) {
         MCE->print($output);
         $child_found_match = 1;
      }
   }

   return;
}

###############################################################################
## ----------------------------------------------------------------------------
## Process routines: run-mode = file.
##
###############################################################################

sub process_file {

   ($file) = @_;

   if ($file eq '-') {
      open(STDIN, '<', ($is_mswin32) ? 'CON' : '/dev/tty') or die $!;
      process_stdin();
   }
   elsif (! -e $file) {
      $exit_status = 2;

      print {*STDERR} "$prog_name: $file: No such file or directory\n"
         unless $no_msg;
   }
   elsif (-d $file) {
      $exit_status = 1;
   }
   else {
      $_abort_job = $_start_nrecs = $_start_nsize = $_total_found = 0;
      $_order_id  = 1;

      MCE->process($file);
      %_nrecs = (); %_nsize = ();

      if (!$q_flag && $f_list) {
         print "$file\n" if $_total_found;
      }
      elsif (!$q_flag && $c_flag) {
         $_total_found = $max_count
            if ($max_count && $_total_found > $max_count);

         print "$file:" if $w_filename;
         print "$_total_found\n";
      }
   }

   return;
}

sub process_stdin {

   $file = '(standard input)';

   $_abort_job = $_start_nrecs = $_start_nsize = $_total_found = 0;
   $_order_id  = 1;

   MCE->process(\*STDIN);
   %_nrecs = (); %_nsize = ();

   if (!$q_flag && $f_list) {
      print "$file\n" if $_total_found;
   }
   elsif (!$q_flag && $c_flag) {
      $_total_found = $max_count
         if ($max_count && $_total_found > $max_count);

      print "$file:" if $w_filename;
      print "$_total_found\n";
   }

   return;
}

###############################################################################
## ----------------------------------------------------------------------------
## Configure Many-Core Engine.
##
###############################################################################

my $gather_func;

if ($chunk_level eq 'auto') {
   if ((@files <= 1 || $files[0] eq '-') && !$r_flag && $cmd_name !~ /^z/i) {
      $chunk_level = 'file';
   } else {
      $chunk_level = 'list';
   }
}

if ($chunk_level eq 'list') {
   $chunk_size = 200   if (!defined $chunk_size && @files > 200);
   $chunk_size = 1     if (!defined $chunk_size);
   $chunk_size = 200   if ($chunk_size > 200);
   $chunk_size = 1     if ($chunk_size < 1);

   unshift @args, '-H' if (!$h_flag && ($H_flag || $r_flag || @files > 1));
   unshift @args, '-h' if ($h_flag);
   unshift @args, '-q' if ($q_flag);

   MCE->new(
      max_workers => $max_workers, chunk_size => $chunk_size, use_slurpio => 1,
      user_begin => \&user_begin_list, user_func => \&user_func_list,
      user_end => \&user_end_list
   );
}
else {
   $chunk_size =  8_388_608 unless defined $chunk_size;   ## 8M
   $chunk_size = 20_971_520 if $chunk_size > 20_971_520;  ## 20M
   $chunk_size =    204_800 if $chunk_size <    204_800;  ## 200K

   if ($f_list) {
      $gather_func = undef;
   }
   elsif ($c_flag) {
      $gather_func = \&_output_cnt;
   }
   elsif ($n_flag && $b_flag) {
      $gather_func = \&_output_n2;
   }
   elsif ($n_flag || $b_flag) {
      $gather_func = \&_output_n1;
   }
   else {
      $gather_func = \&_output_n0;
   }

   MCE->new(
      max_workers => $max_workers, chunk_size => $chunk_size, use_slurpio => 1,
      user_begin => \&user_begin_file, user_func => make_user_func_file(),
      gather => $gather_func
   );
}

###############################################################################
## ----------------------------------------------------------------------------
## Run.
##
###############################################################################

if ($r_flag && @files > 0) {
   my ($list_fh, $list);

   MCE->spawn;

   if ($is_mswin32) {
      $list = `egrep -lsr @r_patn ^ @files`;
      open $list_fh, '<', \$list;
   }
   else {
      open $list_fh, '-|', 'egrep', '-lsr', @r_patn, '^', @files;
   }

   if ($chunk_level eq 'list') {
      MCE->process($list_fh);
   }
   else {
      while (<$list_fh>) {
         chomp;
         process_file($_);
         last if $_abort_all;
      }
   }

   close $list_fh;
}
elsif (@files > 0) {
   if ($chunk_level eq 'list') {
      my $list = join("\n", @files) . "\n"; undef @files;
      open my $list_fh, '<', \$list;
      MCE->process($list_fh);
      close $list_fh;
   }
   else {
      foreach (@files) {
         process_file($_);
         last if $_abort_all;
      }
   }
}
else {
   if ($chunk_level eq 'list') {
      my $status = system($cmd_path, @args);
      exit($status >> 8);
   }
   else {
      process_stdin();
   }
}

###############################################################################
## ----------------------------------------------------------------------------
## Finish.
##
###############################################################################

MCE->shutdown;

if (!$q_flag && $exit_status) {
   exit($exit_status);
}
else {
   exit($found_match ? 0 : ($exit_status ? $exit_status : 1));
}

