#!/usr/bin/perl
#: search in, or list all, executable commands in your $PATH
#======================================================================
# Time-stamp: "2009-12-20 05:07:18 AKST sburke@cpan.org"
use constant DEBUG => 0; use strict; use warnings;

my $Cache_Max_Lifetime_In_Days = 1;

#
# allx - list all your executables in your $PATH, or filter for some.
#        The master list is cached in ~/.allx, and is rebuilt
#         periodically.
#
# If called as allxk or allxapropos, we try to list descriptions from 'whatis'
#
# allx               lists all executable files
# allx foo           lists all executable files matching the regexp "foo"
# allx foo bar baz   lists all executable files matching any of those regexps
# allx / ...         forces a rebuild of ~/.allx, then handles the ... items
#
# Note: Like grep, we return an exit status of 1 if nothing was found.
#
#======================================================================
#
# If you want to force a rebuild but not show anything, just use this hack:
#   allx / > /dev/null
#
#======================================================================
#
# (FYI: I chose "/" as the magic rebuild flag because there's just no way
# an executable could be named "/", or even contain the "/" character.)
#
#======================================================================
my $Home = $ENV{'HOME'} || die "No home directory?";

my $Cache_Plain       = "$Home/allx.txt";
my $Cache_Pointery    = "$Home/allx_wheres.txt";

my $Descriptions_File = "$Home/whatis.txt";  # a cache file

my $Descriptions_Rebuilder_Command =
  '_rebuild_my_text_whatis'
  # "whatis -r . > $Descriptions_File"
;

my $Skip_Path_Current = 1; # Whether to skip any '.' in PATH

my $Show_Descriptions = 0;
my %Descriptions;
{
  DEBUG and print "Considering \$0 value of \"$0\"\n";
  if($0 =~ m/allx-?k/ or $0 =~ m/allk-?apro/) {
    DEBUG and print "OK, we'll try to show descriptions.\n";
    $Show_Descriptions = 1;
  } else {
    DEBUG and print "(We won't show descriptions.)\n";
  }
}

#======================================================================

my $A = "\nAborting";
my $Force_Rebuild_Cache = 0;
my @Patterns;
my %Name2Where;
run();
exit;

#======================================================================

sub run {
  args_processing();

  maybe_read_descriptions();

  my @items = all_x_maybe_cached();

  my $found = 0;
  my $desc;

  if(@Patterns) { # we have criteria
   Item:
    foreach my $item (@items) {
      foreach my $p (@Patterns) { # print the item if ANY match
        if($item =~ $p) {

	  $desc = $Descriptions{$item} || '';
	  if($desc) {
	    $desc =~ s<\A ([^\t]+) \t ><\t\t($1\)>x;
	    $desc = "\t$desc";
	  }

          print $item, "$desc\n";
          $found++;
	  next Item;
        }
      }
    }

  } else { # No patterns, we're just dumping:
    foreach my $item (@items) {

      $desc = $Descriptions{$item} || '';
      if($desc) {
        $desc =~ s<\A ([^\t]+) \t ><\t\t($1\)>x;
	$desc = "\t$desc";
      }

      print $item, "$desc\n";
      $found++;
    }
  }

  DEBUG and print "Items found: $found\n";
  exit 1 unless $found;
  exit 0;
  return;
}

#======================================================================

sub args_processing {
  if(@ARGV and $ARGV[0] eq '/') { # the rebuild flag
    shift @ARGV;
    $Force_Rebuild_Cache = 1;
  }
  pattern_compiling( splice @ARGV );

  return;
}

#======================================================================

sub pattern_compiling {
  my(@ins) = @_;
   
  my $this_pat;
  foreach my $in (@ins) {
    eval { $this_pat = qr/$in/i };
    if($@) {
      die "$in isn't a valid regexp: $@\n";
    }
    push @Patterns, $this_pat;
  }

  if(DEBUG) {
    if(@Patterns) {
      print "Patterns to try matching against:\n";
      for(@Patterns) { print "  $_\n" }
      print "\n";
    } else {
      print "No patterns to match against.  Will just dump all.\n";
    }
  }

  return;
}


#======================================================================

sub should_write_to_cache {

  if( $Force_Rebuild_Cache ) {
    DEBUG and print "As I'm being forced to...\n";
    return 1;
  }

  unless( -e $Cache_Plain ) {
    DEBUG and print "As $Cache_Plain doesn't exist...\n";
    return 1;
  }

  if( -M $Cache_Plain >= $Cache_Max_Lifetime_In_Days ) {
    DEBUG and print "As $Cache_Plain is stale...\n";
    return 1;
  }

  die "$Cache_Plain isn't writeable$A" unless -w $Cache_Plain;

  DEBUG and print "As $Cache_Plain is fine...\n";
  return; # extant and not stale, and not forcing
}

#======================================================================

sub all_x_maybe_cached {
  return to_and_from_cache() if should_write_to_cache();
  return from_cache();
}

#======================================================================

sub from_cache {
  my @items;
  open my $IN, "<", $Cache_Plain or die "Couldn't read-open $Cache_Plain - $!$A";
  chomp(@items = <$IN>);
  close($IN) or die "Can't close the readfile $Cache_Plain - $!"; #unexpected!
  return @items;
}

sub to_and_from_cache {
  DEBUG and print "I'm re-scanning the dirs and writing to $Cache_Plain\n";
  open my $OUT, ">", $Cache_Plain or die "Couldn't write-open $Cache_Plain - $!$A";

  my @items = all_x_scanning();

  for(@items) {    print $OUT $_, "\n";    }

  close($OUT) or die "Can't close the writefile $Cache_Plain - $!"; #unexpected!
  DEBUG and print "Done writing ", scalar(@items), " items to $Cache_Plain\n";

  return @items;
}

#======================================================================

sub all_x_scanning { # no parameters, returns a list

  DEBUG > 1 and print "About to scan all path-dirs for executables...\n";

  my @path = grep m/\S/, split ':', $ENV{'PATH'} || die "No path!?";
  
  my %seen;
  my $all_count = 0;

  if($Skip_Path_Current) {
    $seen{ '.'  } = 1;
    $seen{ './' } = 1;
  }


  foreach my $p (@path) {
    next if $seen{$p};
    $seen{$p} = 1;
  
    opendir(D, $p) or next;
  
    DEBUG and print " Looking at path dir $p\n";
    my $this_count = 0;
  
    while(defined($_ = readdir(D))) {
      next if $_ eq '.' or $_ eq '..' or m/~/;
      if( -e "$p/$_" and -x _ and -r _ and -f _ ) {
        DEBUG > 15 and print "    x: $_\n";
        push @{ $Name2Where{$_} }, "$p/$_";
        ++$this_count;
      }
    }
    closedir(D) or warn "Can't closedir $p - $!";
    DEBUG and print " Saw $this_count items in $p\n";
    $all_count += $this_count;
  }

  my @keys = sort {lc($a) cmp lc($b)} keys %Name2Where;
  if($Cache_Pointery) {
    open my $WHERES, ">", $Cache_Pointery
     or die "Couldn't write-open $Cache_Pointery: $!\n";
    foreach my $key (@keys) {
      print $WHERES join("\t", $key, @{$Name2Where{$key}}), "\n";
    }
    close $WHERES or die "Can't close writefile $Cache_Pointery - $!"; #unexpected!
  }

  my $uniques_count = scalar @keys;
  DEBUG and print "Saw $all_count items in all paths (uniq: $uniques_count)\n";

  return @keys;
}

#======================================================================
#======================================================================


sub maybe_rebuild_descriptions_cache {
  my $rebuild = '';

  if($Force_Rebuild_Cache) {
    $rebuild = 1;
  } elsif(! -e $Descriptions_File ) {
    DEBUG and print "Will build $Descriptions_File (none extant)\n";
    $rebuild = 1;

  } elsif( -M $Descriptions_File >= $Cache_Max_Lifetime_In_Days ) {
    DEBUG and print "As $Descriptions_File is stale...\n";
    $rebuild = 1;
  } else {
    DEBUG and print "Stet $Descriptions_File\n";
  }
  return unless $rebuild;
  return rebuild_descriptions_cache();
}

sub rebuild_descriptions_cache {
  0==system($Descriptions_Rebuilder_Command)
   or die "Running \"$Descriptions_Rebuilder_Command\" failed - $?";
  return;
}



sub maybe_read_descriptions { 
  return unless $Show_Descriptions;


  maybe_rebuild_descriptions_cache();

  DEBUG and print "Reading $Descriptions_File ...\n";
  open my $F, "<", $Descriptions_File
    or die "Can't read-open $Descriptions_File - $!$A";
  my $seen = 0;

=begin notes

Test cases of odd lines from whatis...

cpp (1)              - The C Preprocessor
Crypt::SSLeay (3pm)  - OpenSSL glue that provides LWP https support
cups-genppd (8) [cups-genppd.5.0] - generate Gutenprint PPD files for use with CUPS
ctags (1emacs21)     - generate tag file for Emacs, vi

=end notes

=cut

  # In processing, we'll take the one with the simplest, lowest-sectioned line.
  while(<$F>) {
    chomp;

    my($comm, $sect, $desc) = 
      m<\A
        (\S+)  # command name

	\s+
	\(
          ( .*? )  # section "number" 
	\)

	( .+ )  # absolutely everything else
       >x
    ;

    unless(defined $comm) {
      DEBUG > 5 and print " Can't parse whatis line: \"$_\"\n";
      next;
    }

    DEBUG > 15 and print " Considering the whatis line: $_ {$comm} {$sect} {$desc}\n";

    $desc =~ s< - >< >;
    $desc =~ s<\s+>< >g;
    unless($desc) {
      DEBUG > 5 and print " Odd, no sensible description in: \"$_\"\n";
      next;
    }
    $desc = "$sect\t$desc";

    ++$seen;

    if(!defined $Descriptions{$comm}) {
      $Descriptions{$comm} = $desc;
      if(DEBUG > 14) {
	print " ~For $comm, desc: $desc\n";
      }
      next;      
    }

    # Otherwise, uhoh, here we go, trying to compare
    if( $desc lt $Descriptions{$comm}) {
      if(DEBUG > 14) {
	my $saying = join '', " +For $comm, replacing description\n\t",
	  $Descriptions{$comm}, "\n with simpler\n\t$desc\n\n";
	print $saying;
      }

      Descriptions{$comm} = $desc;

    } else {
      if(DEBUG > 14) {
        my $saying = join '', " -For $comm, stet. Already-seen description\n\t",
	  $Descriptions{$comm}, "\n  is simpler than under-consideration\n\t$desc\n\n";
	print $saying;
      }
    }

  }
  close $F or die "Can't close readfile $Descriptions_File - $!"; #unexpected!

  DEBUG and print "Done reading $Descriptions_File -- $seen descs seen, ",
    scalar keys %Descriptions, " descs stored.\n";

  return;
}

#======================================================================

__END__
