]> git.donarmstrong.com Git - debbugs.git/blobdiff - Debbugs/Common.pm
add hash slice
[debbugs.git] / Debbugs / Common.pm
index cb788e329a5d831dcf57646e2b6a58f961912cd7..ab51dbacf5c86f5ab20ce9da4ff22f96a150f4b5 100644 (file)
@@ -1,3 +1,11 @@
+# This module is part of debbugs, and is released
+# under the terms of the GPL version 2, or any later
+# version at your option.
+# See the file README and COPYING for more information.
+#
+# [Other people have contributed to this file; their copyrights should
+# go here too.]
+# Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
 
 package Debbugs::Common;
 
 
 package Debbugs::Common;
 
@@ -16,12 +24,6 @@ This module is a replacement for the general parts of errorlib.pl.
 subroutines in errorlib.pl will be gradually phased out and replaced
 with equivalent (or better) functionality here.
 
 subroutines in errorlib.pl will be gradually phased out and replaced
 with equivalent (or better) functionality here.
 
-=head1 BUGS
-
-This module currently requires /etc/debbugs/config; it should use a
-general configuration module so that more intelligent things can be
-done.
-
 =head1 FUNCTIONS
 
 =cut
 =head1 FUNCTIONS
 
 =cut
@@ -36,96 +38,43 @@ BEGIN{
      $DEBUG = 0 unless defined $DEBUG;
 
      @EXPORT = ();
      $DEBUG = 0 unless defined $DEBUG;
 
      @EXPORT = ();
-     %EXPORT_TAGS = (#status => [qw(getbugstatus)],
-                    read   => [qw(readbug)],
-                    util   => [qw(getbugcomponent getbuglocation getlocationpath get_hashname),
+     %EXPORT_TAGS = (util   => [qw(getbugcomponent getbuglocation getlocationpath get_hashname),
+                               qw(appendfile buglog getparsedaddrs getmaintainers),
+                               qw(bug_status),
+                               qw(getmaintainers_reverse),
+                               qw(getpseudodesc),
+                               qw(package_maintainer),
+                               qw(sort_versions),
                               ],
                               ],
+                    misc   => [qw(make_list globify_scalar english_join checkpid),
+                               qw(cleanup_eval_fail),
+                               qw(hash_slice),
+                              ],
+                    date   => [qw(secs_to_english)],
+                    quit   => [qw(quit)],
+                    lock   => [qw(filelock unfilelock lockpid)],
                    );
      @EXPORT_OK = ();
                    );
      @EXPORT_OK = ();
-     Exporter::export_ok_tags(qw(read util));
+     Exporter::export_ok_tags(qw(lock quit date util misc));
      $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
 #use Debbugs::Config qw(:globals);
      $EXPORT_TAGS{all} = [@EXPORT_OK];
 }
 
 #use Debbugs::Config qw(:globals);
+
+use Carp;
+
 use Debbugs::Config qw(:config);
 use IO::File;
 use Debbugs::Config qw(:config);
 use IO::File;
+use IO::Scalar;
 use Debbugs::MIME qw(decode_rfc1522);
 use Debbugs::MIME qw(decode_rfc1522);
+use Mail::Address;
+use Cwd qw(cwd);
 
 
-=head2 readbug
-
-     readbug($bug_number,$location)
-
-Reads a summary file from the archive given a bug number and a bug
-location. Valid locations are those understood by L</getbugcomponent>
-
-=cut
-
-
-my %fields = (originator     => 'submitter',
-              date           => 'date',
-              subject        => 'subject',
-              msgid          => 'message-id',
-              'package'      => 'package',
-              keywords       => 'tags',
-              done           => 'done',
-              forwarded      => 'forwarded-to',
-              mergedwith     => 'merged-with',
-              severity       => 'severity',
-              owner          => 'owner',
-              found_versions => 'found-in',
-              fixed_versions => 'fixed-in',
-              blocks         => 'blocks',
-              blockedby      => 'blocked-by',
-             );
-
-# Fields which need to be RFC1522-decoded in format versions earlier than 3.
-my @rfc1522_fields = qw(originator subject done forwarded owner);
+use Params::Validate qw(validate_with :types);
 
 
-sub readbug {
-    my ($lref, $location) = @_;
-    my $status = getbugcomponent($lref, 'summary', $location);
-    return undef unless defined $status;
-    my $status_fh = new IO::File $status, 'r' or
-        warn "Unable to open $status for reading: $!" and return undef;
-
-    my %data;
-    my @lines;
-    my $version = 2;
-    local $_;
-
-    while (<$status_fh>) {
-        chomp;
-        push @lines, $_;
-        $version = $1 if /^Format-Version: ([0-9]+)/i;
-    }
-
-    # Version 3 is the latest format version currently supported.
-    return undef if $version > 3;
-
-    my %namemap = reverse %fields;
-    for my $line (@lines) {
-        if ($line =~ /(\S+?): (.*)/) {
-            my ($name, $value) = (lc $1, $2);
-            $data{$namemap{$name}} = $value if exists $namemap{$name};
-        }
-    }
-    for my $field (keys %fields) {
-        $data{$field} = '' unless exists $data{$field};
-    }
-
-    $data{severity} = $config{default_severity} if $data{severity} eq '';
-    $data{found_versions} = [split ' ', $data{found_versions}];
-    $data{fixed_versions} = [split ' ', $data{fixed_versions}];
-
-    if ($version < 3) {
-       for my $field (@rfc1522_fields) {
-           $data{$field} = decode_rfc1522($data{$field});
-       }
-    }
-
-    return \%data;
-}
+use Fcntl qw(:flock);
 
 
+our $DEBUG_FH = \*STDERR if not defined $DEBUG_FH;
 
 =head1 UTILITIES
 
 
 =head1 UTILITIES
 
@@ -152,10 +101,9 @@ sub getbugcomponent {
        return undef if defined $location and
                        ($location ne 'db' and $location ne 'db-h');
     }
        return undef if defined $location and
                        ($location ne 'db' and $location ne 'db-h');
     }
-    return undef if not defined $location;
     my $dir = getlocationpath($location);
     return undef if not defined $dir;
     my $dir = getlocationpath($location);
     return undef if not defined $dir;
-    if ($location eq 'db') {
+    if (defined $location and $location eq 'db') {
        return "$dir/$bugnum.$ext";
     } else {
        my $hash = get_hashname($bugnum);
        return "$dir/$bugnum.$ext";
     } else {
        my $hash = get_hashname($bugnum);
@@ -177,7 +125,6 @@ sub getbuglocation {
     my ($bugnum, $ext) = @_;
     my $archdir = get_hashname($bugnum);
     return 'archive' if -r getlocationpath('archive')."/$archdir/$bugnum.$ext";
     my ($bugnum, $ext) = @_;
     my $archdir = get_hashname($bugnum);
     return 'archive' if -r getlocationpath('archive')."/$archdir/$bugnum.$ext";
-    print STDERR getlocationpath('db-h')."/$archdir/$bugnum.$ext\n";
     return 'db-h' if -r getlocationpath('db-h')."/$archdir/$bugnum.$ext";
     return 'db' if -r getlocationpath('db')."/$bugnum.$ext";
     return undef;
     return 'db-h' if -r getlocationpath('db-h')."/$archdir/$bugnum.$ext";
     return 'db' if -r getlocationpath('db')."/$bugnum.$ext";
     return undef;
@@ -217,8 +164,665 @@ sub get_hashname {
     return sprintf "%02d", $_[ 0 ] % 100;
 }
 
     return sprintf "%02d", $_[ 0 ] % 100;
 }
 
+=head2 buglog
+
+     buglog($bugnum);
+
+Returns the path to the logfile corresponding to the bug.
+
+Returns undef if the bug does not exist.
+
+=cut
+
+sub buglog {
+    my $bugnum = shift;
+    my $location = getbuglocation($bugnum, 'log');
+    return getbugcomponent($bugnum, 'log', $location) if ($location);
+    $location = getbuglocation($bugnum, 'log.gz');
+    return getbugcomponent($bugnum, 'log.gz', $location) if ($location);
+    return undef;
+}
+
+=head2 bug_status
+
+     bug_status($bugnum)
+
+
+Returns the path to the summary file corresponding to the bug.
+
+Returns undef if the bug does not exist.
+
+=cut
+
+sub bug_status{
+    my ($bugnum) = @_;
+    my $location = getbuglocation($bugnum, 'summary');
+    return getbugcomponent($bugnum, 'summary', $location) if ($location);
+    return undef;
+}
+
+=head2 appendfile
+
+     appendfile($file,'data','to','append');
+
+Opens a file for appending and writes data to it.
+
+=cut
+
+sub appendfile {
+       my ($file,@data) = @_;
+       my $fh = IO::File->new($file,'a') or
+            die "Unable top open $file for appending: $!";
+       print {$fh} @data or die "Unable to write to $file: $!";
+       close $fh or die "Unable to close $file: $!";
+}
+
+=head2 getparsedaddrs
+
+     my $address = getparsedaddrs($address);
+     my @address = getparsedaddrs($address);
+
+Returns the output from Mail::Address->parse, or the cached output if
+this address has been parsed before. In SCALAR context returns the
+first address parsed.
+
+=cut
+
+
+our %_parsedaddrs;
+sub getparsedaddrs {
+    my $addr = shift;
+    return () unless defined $addr;
+    return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0]
+        if exists $_parsedaddrs{$addr};
+    {
+        # don't display the warnings from Mail::Address->parse
+        local $SIG{__WARN__} = sub { };
+        @{$_parsedaddrs{$addr}} = Mail::Address->parse($addr);
+    }
+    return wantarray?@{$_parsedaddrs{$addr}}:$_parsedaddrs{$addr}[0];
+}
+
+=head2 getmaintainers
+
+     my $maintainer = getmaintainers()->{debbugs}
+
+Returns a hashref of package => maintainer pairs.
+
+=cut
+
+our $_maintainer = undef;
+our $_maintainer_rev = undef;
+sub getmaintainers {
+    return $_maintainer if defined $_maintainer;
+    package_maintainer(rehash => 1);
+    return $_maintainer;
+}
+
+=head2 getmaintainers_reverse
+
+     my @packages = @{getmaintainers_reverse->{'don@debian.org'}||[]};
+
+Returns a hashref of maintainer => [qw(list of packages)] pairs.
+
+=cut
+
+sub getmaintainers_reverse{
+     return $_maintainer_rev if defined $_maintainer_rev;
+     package_maintainer(rehash => 1);
+     return $_maintainer_rev;
+}
+
+=head2 package_maintainer
+
+     my @s = package_maintainer(source => [qw(foo bar baz)],
+                                binary => [qw(bleh blah)],
+                               );
+
+=over
+
+=item source -- scalar or arrayref of source package names to return
+maintainers for, defaults to the empty arrayref.
+
+=item binary -- scalar or arrayref of binary package names to return
+maintainers for; automatically returns source package maintainer if
+the package name starts with 'src:', defaults to the empty arrayref.
+
+=item reverse -- whether to return the source/binary packages a
+maintainer maintains instead
+
+=item rehash -- whether to reread the maintainer and source maintainer
+files; defaults to 0
+
+=back
+
+=cut
+
+our $_source_maintainer = undef;
+our $_source_maintainer_rev = undef;
+sub package_maintainer {
+    my %param = validate_with(params => \@_,
+                             spec   => {source => {type => SCALAR|ARRAYREF,
+                                                   default => [],
+                                                  },
+                                        binary => {type => SCALAR|ARRAYREF,
+                                                   default => [],
+                                                  },
+                                        maintainer => {type => SCALAR|ARRAYREF,
+                                                       default => [],
+                                                      },
+                                        rehash => {type => BOOLEAN,
+                                                   default => 0,
+                                                  },
+                                        reverse => {type => BOOLEAN,
+                                                    default => 0,
+                                                   },
+                                       },
+                            );
+    my @binary = make_list($param{binary});
+    my @source = make_list($param{source});
+    my @maintainers = make_list($param{maintainer});
+    if ((@binary or @source) and @maintainers) {
+       croak "It is nonsensical to pass both maintainers and source or binary";
+    }
+    if ($param{rehash}) {
+       $_source_maintainer = undef;
+       $_source_maintainer_rev = undef;
+       $_maintainer = undef;
+       $_maintainer_rev = undef;
+    }
+    if (not defined $_source_maintainer or
+       not defined $_source_maintainer_rev) {
+       $_source_maintainer = {};
+       $_source_maintainer_rev = {};
+       for my $fn (@config{('source_maintainer_file',
+                            'source_maintainer_file_override',
+                            'pseudo_maint_file')}) {
+           next unless defined $fn;
+           if (not -e $fn) {
+               warn "Missing source maintainer file '$fn'";
+               next;
+           }
+           __add_to_hash($fn,$_source_maintainer,
+                         $_source_maintainer_rev);
+       }
+    }
+    if (not defined $_maintainer or
+       not defined $_maintainer_rev) {
+       $_maintainer = {};
+       $_maintainer_rev = {};
+       for my $fn (@config{('maintainer_file',
+                            'maintainer_file_override',
+                            'pseudo_maint_file')}) {
+           next unless defined $fn;
+           if (not -e $fn) {
+               warn "Missing maintainer file '$fn'";
+               next;
+           }
+           __add_to_hash($fn,$_maintainer,
+                             $_maintainer_rev);
+       }
+    }
+    my @return;
+    for my $binary (@binary) {
+       if (not $param{reverse} and $binary =~ /^src:/) {
+           push @source,$binary;
+           next;
+       }
+       push @return,grep {defined $_} make_list($_maintainer->{$binary});
+    }
+    for my $source (@source) {
+       $source =~ s/^src://;
+       push @return,grep {defined $_} make_list($_source_maintainer->{$source});
+    }
+    for my $maintainer (grep {defined $_} @maintainers) {
+       push @return,grep {defined $_}
+           make_list($_maintainer_rev->{$maintainer});
+       push @return,map {$_ !~ /^src:/?'src:'.$_:$_} 
+           grep {defined $_}
+               make_list($_source_maintainer_rev->{$maintainer});
+    }
+    return @return;
+}
+
+#=head2 __add_to_hash
+#
+#     __add_to_hash($file,$forward_hash,$reverse_hash,'address');
+#
+# Reads a maintainer/source maintainer/pseudo desc file and adds the
+# maintainers from it to the forward and reverse hashref; assumes that
+# the forward is unique; makes no assumptions of the reverse.
+#
+#=cut
+
+sub __add_to_hash {
+    my ($fn,$forward,$reverse,$type) = @_;
+    if (ref($forward) ne 'HASH') {
+       croak "__add_to_hash must be passed a hashref for the forward";
+    }
+    if (defined $reverse and not ref($reverse) eq 'HASH') {
+       croak "if reverse is passed to __add_to_hash, it must be a hashref";
+    }
+    $type //= 'address';
+    my $fh = IO::File->new($fn,'r') or
+       die "Unable to open $fn for reading: $!";
+    while (<$fh>) {
+       chomp;
+       next unless m/^(\S+)\s+(\S.*\S)\s*$/;
+       my ($key,$value)=($1,$2);
+       $key = lc $key;
+       $forward->{$key}= $value;
+       if (defined $reverse) {
+           if ($type eq 'address') {
+               for my $m (map {lc($_->address)} (getparsedaddrs($value))) {
+                   push @{$reverse->{$m}},$key;
+               }
+           }
+           else {
+               push @{$reverse->{$value}}, $key;
+           }
+       }
+    }
+}
+
+
+=head2 getpseudodesc
+
+     my $pseudopkgdesc = getpseudodesc(...);
+
+Returns the entry for a pseudo package from the
+$config{pseudo_desc_file}. In cases where pseudo_desc_file is not
+defined, returns an empty arrayref.
+
+This function can be used to see if a particular package is a
+pseudopackage or not.
+
+=cut
+
+our $_pseudodesc = undef;
+sub getpseudodesc {
+    return $_pseudodesc if defined $_pseudodesc;
+    $_pseudodesc = {};
+    __add_to_hash($config{pseudo_desc_file},$_pseudodesc) if
+       defined $config{pseudo_desc_file};
+    return $_pseudodesc;
+}
+
+=head2 sort_versions
+
+     sort_versions('1.0-2','1.1-2');
+
+Sorts versions using AptPkg::Versions::compare if it is available, or
+Debbugs::Versions::Dpkg::vercmp if it isn't.
+
+=cut
+
+our $vercmp;
+BEGIN{
+    use Debbugs::Versions::Dpkg;
+    $vercmp=\&Debbugs::Versions::Dpkg::vercmp;
+
+# eventually we'll use AptPkg:::Version or similar, but the current
+# implementation makes this *super* difficult.
+
+#     eval {
+#      use AptPkg::Version;
+#      $vercmp=\&AptPkg::Version::compare;
+#     };
+}
+
+sub sort_versions{
+    return sort {$vercmp->($a,$b)} @_;
+}
+
 
 
+=head1 DATE
 
 
+    my $english = secs_to_english($seconds);
+    my ($days,$english) = secs_to_english($seconds);
+
+XXX This should probably be changed to use Date::Calc
+
+=cut
+
+sub secs_to_english{
+     my ($seconds) = @_;
+
+     my $days = int($seconds / 86400);
+     my $years = int($days / 365);
+     $days %= 365;
+     my $result;
+     my @age;
+     push @age, "1 year" if ($years == 1);
+     push @age, "$years years" if ($years > 1);
+     push @age, "1 day" if ($days == 1);
+     push @age, "$days days" if ($days > 1);
+     $result .= join(" and ", @age);
+
+     return wantarray?(int($seconds/86400),$result):$result;
+}
+
+
+=head1 LOCK
+
+These functions are exported with the :lock tag
+
+=head2 filelock
+
+     filelock
+
+FLOCKs the passed file. Use unfilelock to unlock it.
+
+=cut
+
+our @filelocks;
+
+sub filelock {
+    # NB - NOT COMPATIBLE WITH `with-lock'
+    my ($lockfile) = @_;
+    if ($lockfile !~ m{^/}) {
+        $lockfile = cwd().'/'.$lockfile;
+    }
+    my ($count,$errors);
+    $count= 10; $errors= '';
+    for (;;) {
+       my $fh = eval {
+            my $fh2 = IO::File->new($lockfile,'w')
+                 or die "Unable to open $lockfile for writing: $!";
+            flock($fh2,LOCK_EX|LOCK_NB)
+                 or die "Unable to lock $lockfile $!";
+            return $fh2;
+       };
+       if ($@) {
+            $errors .= $@;
+       }
+       if ($fh) {
+            push @filelocks, {fh => $fh, file => $lockfile};
+            last;
+       }
+        if (--$count <=0) {
+            $errors =~ s/\n+$//;
+            die "failed to get lock on $lockfile -- $errors";
+        }
+        sleep 10;
+    }
+}
+
+# clean up all outstanding locks at end time
+END {
+     while (@filelocks) {
+         unfilelock();
+     }
+}
+
+
+=head2 unfilelock
+
+     unfilelock()
+
+Unlocks the file most recently locked.
+
+Note that it is not currently possible to unlock a specific file
+locked with filelock.
+
+=cut
+
+sub unfilelock {
+    if (@filelocks == 0) {
+        warn "unfilelock called with no active filelocks!\n";
+        return;
+    }
+    my %fl = %{pop(@filelocks)};
+    flock($fl{fh},LOCK_UN)
+        or warn "Unable to unlock lockfile $fl{file}: $!";
+    close($fl{fh})
+        or warn "Unable to close lockfile $fl{file}: $!";
+    unlink($fl{file})
+        or warn "Unable to unlink lockfile $fl{file}: $!";
+}
+
+
+=head2 lockpid
+
+      lockpid('/path/to/pidfile');
+
+Creates a pidfile '/path/to/pidfile' if one doesn't exist or if the
+pid in the file does not respond to kill 0.
+
+Returns 1 on success, false on failure; dies on unusual errors.
+
+=cut
+
+sub lockpid {
+     my ($pidfile) = @_;
+     if (-e $pidfile) {
+         my $pid = checkpid($pidfile);
+         die "Unable to read pidfile $pidfile: $!" if not defined $pid;
+         return 0 if $pid != 0;
+         unlink $pidfile or
+              die "Unable to unlink stale pidfile $pidfile $!";
+     }
+     my $pidfh = IO::File->new($pidfile,'w') or
+         die "Unable to open $pidfile for writing: $!";
+     print {$pidfh} $$ or die "Unable to write to $pidfile $!";
+     close $pidfh or die "Unable to close $pidfile $!";
+     return 1;
+}
+
+=head2 checkpid
+
+     checkpid('/path/to/pidfile');
+
+Checks a pid file and determines if the process listed in the pidfile
+is still running. Returns the pid if it is, 0 if it isn't running, and
+undef if the pidfile doesn't exist or cannot be read.
+
+=cut
+
+sub checkpid{
+     my ($pidfile) = @_;
+     if (-e $pidfile) {
+         my $pidfh = IO::File->new($pidfile, 'r') or
+              return undef;
+         local $/;
+         my $pid = <$pidfh>;
+         close $pidfh;
+         ($pid) = $pid =~ /(\d+)/;
+         if (defined $pid and kill(0,$pid)) {
+              return $pid;
+         }
+         return 0;
+     }
+     else {
+         return undef;
+     }
+}
+
+
+=head1 QUIT
+
+These functions are exported with the :quit tag.
+
+=head2 quit
+
+     quit()
+
+Exits the program by calling die.
+
+Usage of quit is deprecated; just call die instead.
+
+=cut
+
+sub quit {
+     print {$DEBUG_FH} "quitting >$_[0]<\n" if $DEBUG;
+     carp "quit() is deprecated; call die directly instead";
+}
+
+
+=head1 MISC
+
+These functions are exported with the :misc tag
+
+=head2 make_list
+
+     LIST = make_list(@_);
+
+Turns a scalar or an arrayref into a list; expands a list of arrayrefs
+into a list.
+
+That is, make_list([qw(a b c)]); returns qw(a b c); make_list([qw(a
+b)],[qw(c d)] returns qw(a b c d);
+
+=cut
+
+sub make_list {
+     return map {(ref($_) eq 'ARRAY')?@{$_}:$_} @_;
+}
+
+
+=head2 english_join
+
+     print english_join(list => \@list);
+     print english_join(\@list);
+
+Joins list properly to make an english phrase.
+
+=over
+
+=item normal -- how to separate most values; defaults to ', '
+
+=item last -- how to separate the last two values; defaults to ', and '
+
+=item only_two -- how to separate only two values; defaults to ' and '
+
+=item list -- ARRAYREF values to join; if the first argument is an
+ARRAYREF, it's assumed to be the list of values to join
+
+=back
+
+In cases where C<list> is empty, returns ''; when there is only one
+element, returns that element.
+
+=cut
+
+sub english_join {
+    if (ref $_[0] eq 'ARRAY') {
+       return english_join(list=>$_[0]);
+    }
+    my %param = validate_with(params => \@_,
+                             spec  => {normal => {type => SCALAR,
+                                                  default => ', ',
+                                                 },
+                                       last   => {type => SCALAR,
+                                                  default => ', and ',
+                                                 },
+                                       only_two => {type => SCALAR,
+                                                    default => ' and ',
+                                                   },
+                                       list     => {type => ARRAYREF,
+                                                   },
+                                      },
+                            );
+    my @list = @{$param{list}};
+    if (@list <= 1) {
+       return @list?$list[0]:'';
+    }
+    elsif (@list == 2) {
+       return join($param{only_two},@list);
+    }
+    my $ret = $param{last} . pop(@list);
+    return join($param{normal},@list) . $ret;
+}
+
+
+=head2 globify_scalar
+
+     my $handle = globify_scalar(\$foo);
+
+if $foo isn't already a glob or a globref, turn it into one using
+IO::Scalar. Gives a new handle to /dev/null if $foo isn't defined.
+
+Will carp if given a scalar which isn't a scalarref or a glob (or
+globref), and return /dev/null. May return undef if IO::Scalar or
+IO::File fails. (Check $!)
+
+=cut
+
+sub globify_scalar {
+     my ($scalar) = @_;
+     my $handle;
+     if (defined $scalar) {
+         if (defined ref($scalar)) {
+              if (ref($scalar) eq 'SCALAR' and
+                  not UNIVERSAL::isa($scalar,'GLOB')) {
+                   return IO::Scalar->new($scalar);
+              }
+              else {
+                   return $scalar;
+              }
+         }
+         elsif (UNIVERSAL::isa(\$scalar,'GLOB')) {
+              return $scalar;
+         }
+         else {
+              carp "Given a non-scalar reference, non-glob to globify_scalar; returning /dev/null handle";
+         }
+     }
+     return IO::File->new('/dev/null','w');
+}
+
+=head2 cleanup_eval_fail()
+
+     print "Something failed with: ".cleanup_eval_fail($@);
+
+Does various bits of cleanup on the failure message from an eval (or
+any other die message)
+
+Takes at most two options; the first is the actual failure message
+(usually $@ and defaults to $@), the second is the debug level
+(defaults to $DEBUG).
+
+If debug is non-zero, the code at which the failure occured is output.
+
+=cut
+
+sub cleanup_eval_fail {
+    my ($error,$debug) = @_;
+    if (not defined $error or not @_) {
+       $error = $@ // 'unknown reason';
+    }
+    if (@_ <= 1) {
+       $debug = $DEBUG // 0;
+    }
+    $debug = 0 if not defined $debug;
+
+    if ($debug > 0) {
+       return $error;
+    }
+    # ditch the "at foo/bar/baz.pm line 5"
+    $error =~ s/\sat\s\S+\sline\s\d+//;
+    # ditch trailing multiple periods in case there was a cascade of
+    # die messages.
+    $error =~ s/\.+$/\./;
+    return $error;
+}
+
+=head2 hash_slice
+
+     hash_slice(%hash,qw(key1 key2 key3))
+
+For each key, returns matching values and keys of the hash if they exist
+
+=cut
+
+
+# NB: We use prototypes here SPECIFICALLY so that we can be passed a
+# hash without uselessly making a reference to first. DO NOT USE
+# PROTOTYPES USELESSLY ELSEWHERE.
+sub hash_slice(\%@) {
+    my ($hashref,@keys) = @_;
+    return map {exists $hashref->{$_}?($_,$hashref->{$_}):()} @keys;
+}
 
 1;
 
 
 1;