App-Pfind

 view release on metacpan or  search on metacpan

lib/App/Pfind.pm  view on Meta::CPAN

  'follow-fast|ff!' => \$options{follow_fast},
  'recurse|r!' => \$options{recurse},
  'type|t=s@' => $options{type},
  'chdir!' => \$options{chdir},
  'print|p=s' => \$options{print},
  'begin|BEGIN|B=s@' => $options{begin},
  'end|END|E=s@' => $options{end},
  'pre|pre-process=s@' => $options{pre},
  'post|post-process=s@' => $options{post},
  'exec|e=s@' => $options{exec},
  'verbose|v|V+' => \$options{verbose},
)}

sub eval_code {
  my ($code, $flag) = @_;
  my $r = $safe->reval($code);
  if ($@) {
    die "Failure in the code given to --${flag}: ${@}\n";
  }
  return $r;
}

sub wrap_code_blocks {
  my ($code_blocks, $default_variable_value, $flag) = @_;
  # We're building a sub that will execute each given piece of code in a block.
  # That way we can evaluate this code in the safe once and get the sub
  # reference (so that it does not need to be recompiled for each file). In
  # addition, control flow keywords (mainly next, redo and return) can be used
  # in each block.
  my $block_start = '{ my $tmp_pfind_default = '.$default_variable_value.'; ';
  $block_start .= "print { *STDERR } 'Executing code $flag:'.\$internal_pfind_block_count++;" if $options{verbose};
  $block_start .= 'local $_ = $tmp_pfind_default;'
                  .'local $dir = $internal_pfind_dir;'
                  .'local $name = $internal_pfind_name;';
  my $block_end = $options{catch_errors} ? '} die "$!\n" if $!;' : '} ';
  my $all_exec_code = 'sub { my $internal_pfind_block_count = 1;'
                      .${block_start}.join("${block_end} \n ${block_start}", @$code_blocks).${block_end}
                      .' }';
  print STDERR $all_exec_code if $options{verbose} > 2;
  return eval_code($all_exec_code, 'exec');
}

sub run_wrapped_sub {
  my ($wrapped_code, $flag) = @_;
  # Instead of using our local variables, we could share the real one here:
  # $safe->share_from('File::Find', ['$dir', '$name']);
  # They have to be shared inside the sub as they are 'localized' each time.
  # That approach would be slower by a small factor though.
  $dir_setter->set($File::Find::dir);
  $name_setter->set($File::Find::name);
  undef $!;
  $wrapped_code->();
  die "Failure in the code given to --${flag}: $!\n" if $!;
}

# Perform a real-stat or just reads the result from the previous stat. Returns
# just the mode part of the stat.
my $last_stated_file = '';
sub cheap_stat {
  my ($relative_file_name, $full_file_name) = @_;
  return (stat(_))[2] if $full_file_name eq $last_stated_file;
  $last_stated_file = $full_file_name;
  # When executing find in follow mode, the current file has already been
  # stat-ed (this is guaranteed by find), so we can re-use the value using `_`.
  return (stat(_))[2] if $options{follow} || $options{follow_fast};
  return (stat($relative_file_name))[2];
}

my %file_mode = (
    f => S_IFREG,   # regular file
    d => S_IFDIR,   # directory
    l => S_IFLNK,   # symbolic link
    b => S_IFBLK,   # block special file
    c => S_IFCHR,   # character special file
    p => S_IFIFO,   # fifo (pipe)
    s => S_IFSOCK,  # socket
    # S_IFWHT and S_ENFMT are not supported (they're Sys-V specific features)
  );

sub should_skip_file {
  my ($relative_file_name, $full_file_name) = @_;
  return 0 unless %{$options{type}};
  my $mode = cheap_stat($relative_file_name, $full_file_name);
  for my $m (keys(%{$options{type}})) {
    if (($mode & $file_mode{$m}) xor $options{type}{$m}) {
      print STDERR "Skipping '$full_file_name' due to '$m' type" if $options{verbose};
      return 1;
    }
  }
  return 0;
}

sub Run {
  my ($argv) = @_;
  
  reset_options();
  # After the GetOptions call this will contain the input directories.
  my @inputs = @$argv;
  GetOptionsFromArray(\@inputs, all_options())
    or pod2usage(-exitval => 2, -verbose => 0);
    
  # With this the -v option (--verbose) will still trigger the behavior of the
  # --version option (although it won't stop the execution).
  Getopt::Long::VersionMessage({-exitval => 'NOEXIT', -output => \*STDERR}) if $options{verbose};
    
  if (not @{$options{exec}}) {
    $options{exec} = ['print $name'];
  }
  
  $options{type} = { map { lc() => ($_ eq lc()) } split(//, join('', @{$options{type}})) };
  
  print STDERR "options = ".Dumper({%options}) if $options{verbose} > 1;
  
  if ($options{follow} && $options{follow_fast}) {
    die "The --follow and --follow-fast options cannot be used together.\n";
  }
  my $follow_mode = $options{follow} || $options{follow_fast};
  if (@{$options{pre}} && $follow_mode) {
    die "The --pre-process option cannot be used with --follow or --follow-fast.\n";
  }
  if (@{$options{post}} && $follow_mode) {
    die "The --post-process option cannot be used with --follow or --follow-fast.\n";
  }
  if (not $options{recurse} and $options{depth_first}) {
    die "The --no-recurse option cannot be used with --depth-first.\n";
  }
  if (join('', keys %{$options{type}}) !~ /^[fdlpsbc]*$/) {
    die "Unsupported value for the --type option.\n";
  }
  if (not @inputs) {
    print STDERR "No input given on the command-line. Exiting without doing any work.";
    exit 1;
  }

  $\ = $options{print};
  
  for my $c (@{$options{begin}}) {
    eval_code($c, 'BEGIN');
  }
  
  my $wrapped_exec = wrap_code_blocks($options{exec}, '$_', 'exec');
  # The $_ variable inside these method will be set to $File::Find::dir (as the
  # real $_ does not contain anything useful in that case).
  my $wrapped_pre;
  if (@{$options{pre}}) {
    $wrapped_pre = wrap_code_blocks($options{pre}, '$internal_pfind_dir', 'pre');
  }
  my $wrapped_post;
  if (@{$options{post}}) {
    $wrapped_post = wrap_code_blocks($options{post}, '$internal_pfind_dir', 'post');
  }

  find({
    bydepth => $options{depth_first},
    follow => $options{follow},
    follow_fast => $options{follow_fast},
    no_chdir => !$options{chdir},
    wanted => sub {
      if (not $options{recurse} and cheap_stat($_, $File::Find::name) & S_IFDIR) {
        print "Will not recurse into $File::Find::name" if $options{verbose};
        $File::Find::prune = 1;
      }
      print STDERR "Looking at file: $File::Find::name" if $options{verbose};
      return if should_skip_file($_, $File::Find::name);
      run_wrapped_sub($wrapped_exec, 'exec');
    },
    preprocess => sub {
      print STDERR "Entering: $File::Find::dir" if $options{verbose};
      run_wrapped_sub($wrapped_pre, 'pre-process') if $wrapped_pre;
      return @_;  # Needed by find().
    },
    postprocess => sub {
      print STDERR "Exiting: $File::Find::dir" if $options{verbose};
      run_wrapped_sub($wrapped_post, 'post-process') if $wrapped_post;
    }
  }, @inputs);

  for my $c (@{$options{end}}) {
    eval_code($c, 'BEGIN');
  }
}

1;



( run in 3.167 seconds using v1.01-cache-2.11-cpan-5837b0d9d2c )