# $Id: Handy.pm,v 1.114 2018-08-09 13:21:10-04 roderick Exp roderick $ # # Copyright (c) 2000 Roderick Schertler. All rights reserved. This # program is free software; you can redistribute it and/or modify it # under the same terms as Perl itself. use strict; =head1 NAME RS::Handy - a grab-bag of useful routines =head1 SYNOPSIS use RS::Handy qw(:stat xdie); my @st = stat $path or xdie "can't stat $path:"; print "$path modified ", scalar localtime $st[ST_MTIME], "\n"; # and many more, sorry for leaving them out of the synopsis =head1 DESCRIPTION This module provides some assorted functions I like to use in my programs. I've tossed many of my generic routines in here, I should really have more discipline about categorizing all these things and creating separate modules for them. I've split some modules out of here in the past (Proc::SyncExec, IPC::Signal, Proc::WaitStat, String::ShellQuote, plus some which made it into the core), if you find any of these compellingly useful let me know so I can prioritize splitting them out, too. Nothing is automatically exported into your namespace. Almost all of these functions die() if they encounter any sort of problem. =head1 IMPORTABLE SYMBOLS =over 4 =cut package RS::Handy; use Exporter (); use Carp; use vars qw(@ISA @EXPORT @EXPORT_OK @EXPORT_FAIL %EXPORT_TAGS $Me $VERSION); $VERSION = q$Revision: 1.114 $ =~ /(\d\S+)/ ? $1 : '?'; @ISA = qw(Exporter); @EXPORT = (); @EXPORT_OK = qw($Me fileline chompexpr_fileline subname subcall_info badinvo xwarn xdie xwarn_caller xdie_caller xcarp xcluck xcroak xconfess process_arg_pairs dirents dirents_qualified fopen_mode fdopen fhbits f_getfl f_setfl exclusive_create binary_scaled plural pluralx uncontrol uncontrol_emacs getopt tmpfile safe_tmp home home_of full_name_uid full_name pwuid grgid cat catslurp xsrand shuffle yorn prompt data_dump data_dump_unsorted untaint tainted rename_nounlink rename_unique filter_string mbox_read_head mbox_read_body mbox_read mbox_escape_body_part_in_place mbox_escape daemonize url_decode url_encode_c html_attr_encode html_attrs html_escape create_index_subs_pkg create_index_subs create_constant_subs_pkg create_constant_subs get_win_size even_elements odd_elements define command_with_stdin sendmail ordinal wrap max min max_not_undef min_not_undef fuser make_check_digit valid_check_digit replace_uids replace_ugids cmp_strnum cmp_strnum_transform have_prog rfc822_dt iso_date iso_dt interval_to_secs secs_to_dhms secs_to_dhms_str mtime flush commify mime_token_quote unique cache_url set_autoflush remove_at_exit struct_linger_pack struct_linger_unpack equal dstr expand_field_list_specs non_comments discard_zombies list_length decompressing_open_command eq_undef inverse_hash G_PER_LB KG_PER_LB LB_PER_KG lb_to_kg lb_to_g kg_to_lb g_to_lb longest_common_prefix longest_common_parent_directory create_lock_file prompt_readline center_string crack_time trim iso_date_fuzzy_re); # Set up constant subs used as struct indices at compile time so they're # available to code here. BEGIN { my %struct = ( 'stat' => ['st', qw(dev ino mode nlink uid gid rdev size atime mtime ctime blksize blocks)], 'gr' => ['gr', qw(name passwd gid mem)], 'pw' => ['pw', qw(name passwd uid gid quota comment gcos dir shell expire)], 'tm' => ['tm', qw(sec min hour mday mon year wday yday isdst)], ); %EXPORT_TAGS = (); for my $tag (keys %struct) { my ($prefix, @field) = @{ $struct{$tag} }; for my $i (0..$#field) { my $name = uc "${prefix}_$field[$i]"; push @{ $EXPORT_TAGS{$tag} }, $name; no strict 'refs'; *$name = sub { $i }; } } } Exporter::export_ok_tags; $EXPORT_TAGS{all} = [@EXPORT_OK]; my %moved_syms = qw( gensym Symbol ref_qualify Symbol=qualify_to_ref fork_retry Proc::SyncExec sync_exec Proc::SyncExec sync_fhpopen_noshell Proc::SyncExec sync_popen_noshell Proc::SyncExec sync_open Proc::SyncExec %sig_no IPC::Signal=%Sig_num @sig_name IPC::Signal=@Sig_name sig_setup IPC::Signal=sig_translate_setup sig_no IPC::Signal=sig_num sig_name IPC::Signal exitstat Proc::WaitStat=waitstat exitstat_die Proc::WaitStat=waitstat_die exitstat_reuse Proc::WaitStat=waitstat_reuse close_die Proc::WaitStat shell_quote String::ShellQuote shell_comment_quote String::ShellQuote popen_noshell Proc::SafePipe backtick_noshell Proc::SafePipe ); my @moved_syms; push @EXPORT_FAIL, keys %moved_syms; push @EXPORT_OK, keys %moved_syms; sub export_fail { my $self = shift; my (@ret, $new_pkg, $sym, $new_sym, $name_changed); for $sym (@_) { if ($new_pkg = $moved_syms{$sym}) { if (1) { push @moved_syms, $sym; } else { carp "Load $sym from $new_pkg instead of $self"; } if ($new_pkg =~ s/=(.*)//) { $new_sym = $1; $name_changed = 1; } else { $new_sym = $sym; $name_changed = 0; } eval "require $new_pkg"; die if $@; $new_pkg->import($new_sym); if ($name_changed) { $new_sym =~ s/^(?=\w)/&/; $sym =~ s/^\W//; eval "*$sym = \\$new_sym"; die if $@; } } else { push @ret, $sym; } } return @ret; } END { if (@moved_syms) { local ($?, $\); my $subject = "moved syms for $0"; $subject =~ s/\n.*//s; open MAIL, '|/usr/lib/sendmail -oeq -t'; print MAIL "To: roderick\n"; print MAIL "Subject: $subject\n"; print MAIL "\n"; print MAIL "Symbols used by $0:\n"; print MAIL "\t", join("\n\t", map { sprintf "%-14s => %s", $_, $moved_syms{$_} } @moved_syms), "\n"; print MAIL "\n"; print MAIL -f '/dgux' ? `/home/roderick/bin/share/ps-climb $$` : `ps -fp $$`; close MAIL; } } use subs qw(badinvo); sub dstr ($); sub my_gensym { require Symbol; return Symbol::gensym(); } =item B<$Me> The basename of the currently running script. =cut #use File::Basename qw(basename); #$Me = basename $0 eq '-e' ? $^X : $0; ($Me = $0 ne '-e' ? $0 : $^X) =~ s-.*/--; =item B [I] Return a string describing the caller's file and line number. If I is given it's an additional number of stack frames to go back. =cut sub fileline { @_ <= 1 || croak 'bogus invocation of ', (caller 0)[3]; my $level = shift || 0; my ($package, $file, $line) = caller $level; return "$file line $line"; } =item B I Remove a trailing newline from I and then try to remove a trailing "at $file line $num\n" or similar from the result. In a scalar context return just the initial part of the string, in a list context also include the $file and $num. =cut sub chompexpr_fileline { @_ == 1 || badinvo; local $_ = shift; chomp; if ($_ =~ /^(.+) at (.+) (?:chunk|line) (\d+)\.\z/s) { return wantarray ? ($1, $2, $3) : $1; } else { return wantarray ? ($_, undef, undef) : $_; } } =item B [I] Return the name of the current subroutine (that is, the one which invokes B). If I is given, go back that many additional stack frames and give the name that sub instead. =cut sub subname { @_ <= 1 || croak 'bogus invocation of ', (caller 0)[3]; my $level = shift || 0; return +(caller $level + 1)[3]; } =item B [I] Return a text string which describes the invocation of the current subroutine. Like B, current really means B's caller, and you can specify a I to go back to a different stack frame. =cut #' sub subcall_info { @_ <= 1 || croak 'bogus invocation of ', (caller 0)[3]; my $level = shift || 0; my $fileline = fileline $level + 2; # The $subname in that stack frame is the name of the sub invoked, # not the invoker. So, the invoker is one frame further up. my $sub = subname $level + 2; return defined $sub ? "$sub ($fileline)" : "$fileline"; } =item B [I [I]] Die with a message indicating that the current subroutine was invoked improperly. If I is given go back that many additional levels before the current one. If I is also given include it in the die message. =cut sub badinvo { my $level = @_ ? shift : 0; my $msg = @_ ? " (" . join('', @_) . ")" : ''; die "$Me: bogus invocation of ", subname($level + 1), " from ", subcall_info($level + 1), "$msg\n"; } sub badinvo_warn { my @arg = @_; # Add 1 for the invocation of this sub, 1 for the eval. if (@arg) { $arg[0] += 2; } else { push @arg, 2; } eval { badinvo @arg }; warn $@; } =item B [I]... =item B [I]... Like C and C but these functions prepend the message with the name of the script and a colon. Further, if the last I ends with a colon the current string value of C<$!> and a newline are appended to it. =cut sub xwarndie_mess { @_ >= 1 || badinvo; my $level = shift; if (!@_) { @_ = (caller 1)[3] =~ /die/ ? ('died') : ("something's wrong"); } my @mess = ("$Me: ", @_); $mess[-1] =~ s/:\z/: $!\n/; if ($mess[-1] !~ /\n\z/) { # Parse warn()'s output to get at last_in_gv (the most recently # used filehandle), which isn't otherwise available. my $last_read = ''; { local $SIG{__WARN__} = sub { # except the trailing ., which doesn't fit this style $last_read = $1 if shift =~ / at .*(, <.*> (chunk|line) .*?)\.?$/; }; warn "extract file"; } push @mess, " at ", fileline($level + 1), "$last_read\n" } @mess; } sub xwarn { warn xwarndie_mess 1, @_ } sub xdie { die xwarndie_mess 1, @_ } =item B I, [I]... =item B I, [I]... Like C and C but the given additional number of stack frames are climbed to find the file/line from which to warn. An I argument of 0 is equivalent to the non-C<_caller> form. =cut sub xwarndie_caller { my $type = shift; my ($level); if (@_ && $_[0] =~ /^\s*\d+$/) { $level = shift; } else { badinvo_warn 1, "additional frames not specified"; $level = 0; } # 1 for the caller of this helper, 1 for the caller of that wrapper. $level += 2; my @mess = xwarndie_mess $level, @_; if ($type eq 'warn') { warn @mess; } else { die @mess; } } sub xwarn_caller { xwarndie_caller 'warn', @_; } sub xdie_caller { xwarndie_caller 'die', @_; } =item B I... =item B I... =item B I... =item B I... Like the corresponding function from the Carp module, but as with B supply the script's name. =cut sub xcarp { require Carp; unshift @_, "$Me: "; goto &Carp::carp; } sub xcluck { require Carp; unshift @_, "$Me: "; goto &Carp::cluck; } sub xcroak { require Carp; unshift @_, "$Me: "; goto &Carp::croak; } sub xconfess { require Carp; unshift @_, "$Me: "; goto &Carp::confess; } =item B I [I => I]... This sub is used to translate argument lists of key/value pairs. The I is a reference to the list of arguments which were given to your sub. The I/I pairs are your subs arguments and the variables they set. If an invalid argument is present in the I list this sub will die. =cut sub process_arg_pairs { my ($rarg, %desc) = @_; my @arg = @$rarg; my $frames = 2; @arg % 2 == 0 or xdie_caller $frames, "odd number of elements in key/value list"; while (@arg) { my ($key, $val) = splice @arg, 0, 2; if ($desc{$key}) { ${ $desc{$key} } = $val; } else { xdie_caller $frames, "invalid arg key `$key'"; } } } =item B I Returns all the entries in I except dot and dotdot. =cut sub dirents { @_ == 1 or badinvo; my $dir = shift; my $dh = my_gensym; opendir $dh, $dir or croak "Can't opendir $dir: $!"; my @ents = grep { $_ ne '.' and $_ ne '..' } readdir $dh; closedir $dh or croak "Error running closedir($dir): $!"; @ents; } =item B [I]... Returns fully qualified pathnames of all the entries in each listed I except dot and dotdot. If no Is are given then an empty list is returned. =cut sub dirents_qualified { my ($dir, $name, @ents); foreach $dir (@_) { $name = $dir eq '/' ? '' : $dir; push @ents, map { "$name/$_" } dirents $dir; } @ents; } =item B I This function turns an fopen()-type mode string (like C<'r+'>) and returns the Perl equivalent (C<'+E'>). =cut my %fopen_mode = (); sub fopen_mode { @_ == 1 or badinvo; my $fopen_mode = shift; %fopen_mode = ( 'r' => '<', 'rb' => '<', 'w' => '>', 'wb' => '>', 'a' => '>>', 'ab' => '>>', 'r+' => '+<', 'r+b' => '+<', 'rb+' => '+>', 'w+' => '+>', 'w+b' => '+>', 'wb+' => '+<', 'a+' => '+>>', 'a+b' => '+>>', 'ab+' => '+>>', ) unless %fopen_mode; $fopen_mode{$fopen_mode}; } =item B I I Like the system fdopen(). I dislike that POSIX.pm tells you to use Cnew_from_fd>. Further, FileHandle's new_from_fd() is broken, it does not get the mode right if it's read/write. =cut sub fdopen { @_ == 2 or badinvo; my ($fd, $mode) = @_; my $glob = my_gensym; $mode = fopen_mode($mode) || croak "Invalid mode `$mode'"; open($glob, "$mode&=$fd") and bless $glob, 'FileHandle'; } =item B I... This routine returns a bitmask which includes all the specified filehandles. =cut sub fhbits { my @fh = @_; my ($fh, $bits); require Symbol; @fh = map { Symbol::qualify($_) } @fh; $bits = ''; no strict 'refs'; foreach $fh (@fh) { defined fileno $fh or croak "`$fh' isn't an open filehandle"; vec($bits, fileno $fh, 1) = 1; } $bits; } my %binary_scaled_suffix; my $binary_scaled_suffix_pat; sub binary_scaled { my (@arg) = @_; my ($arg, @ret); unless (%binary_scaled_suffix) { %binary_scaled_suffix = ( 'b' => 512, 'k' => 1024, 'm' => 1024**2, 'g' => 1024**3, ); # Note, this assumes suffixes are single characters. $binary_scaled_suffix_pat = '[' . join('', keys %binary_scaled_suffix) . ']'; } foreach $arg (@arg) { $arg =~ /^(\d+)\s*($binary_scaled_suffix_pat)?$/io or croak "Invalid binary-scaled number `$arg'"; push @ret, $1 * (defined($2) ? $binary_scaled_suffix{"\L$2"} : 1); } wantarray ? @ret : $ret[$#ret]; } sub plural { @_ == 1 or @_ == 2 or badinvo; my ($n, $p) = @_; $n == 1 ? '' : (defined $p ? $p : 's'); } sub pluralx ($$$) { my ($n, $s, $p) = @_; $n == 1 ? $s : $p; } =item B [I]... This function joins all the I arguments and expands control characters as ^A and meta characters as \200 and the like. It returns the concatenated result. =cut my @uncontrol_tab; sub uncontrol { unless (@uncontrol_tab) { foreach (0..037) { $uncontrol_tab[$_] = '^' . chr($_ + ord '@'); } foreach (0177) { $uncontrol_tab[$_] = '^' . chr($_ - ord '@'); } foreach (040..0176) { $uncontrol_tab[$_] = chr $_; } foreach (0200..0377) { $uncontrol_tab[$_] = sprintf '\\%03o', $_; } } my $ret = ''; foreach (split //, join('', @_)) { $ret .= $uncontrol_tab[ord $_]; } $ret; } =item B [I] This function joins all the I arguments together and expands control characters to C-A and meta characters to M-A and whitespace to SPC and RET and so on in the emacs fashion. It returns the concatenated result. =cut my %emacs_xlate; sub uncontrol_emacs { unless (%emacs_xlate) { %emacs_xlate = ( "\0" => 'NUL', "\040" => 'SPC', "\r" => 'RET', "\n" => 'LFD', "\t" => 'TAB', "\f" => 'FF', "\177" => 'DEL', "\e" => 'ESC', ); } my $ret = ''; my ($ord, $meta, $space); $space = ''; foreach (split //, join('', @_)) { $ord = ord $_; if ($ord > 0177) { $meta = 'M-'; $ord -= 0200; $_ = chr $ord; } else { $meta = ''; } if (defined $emacs_xlate{$_}) { $ret .= $space . $meta . $emacs_xlate{$_} . ' '; $space = ''; } elsif (ord $_ < 040) { $ret .= $space . 'C-' . $meta . chr($ord + 0140) . ' '; $space = ''; } else { $ret .= $meta . $_ . ($meta ? ' ' : ''); $space = $meta ? '' : ' '; } } chop $ret if substr($ret, -1) eq ' '; $ret; } =item B I This performs an F_GETFL on I and returns the flags. =cut sub f_getfl { @_ == 1 or badinvo; my $fh = shift; require Fcntl; my $flags = fcntl $fh, Fcntl::F_GETFL(), 0 or croak "Can't F_GETFL on $fh: $!"; local $^W; $flags+0; } =item B I I This performs an F_SETFL using the arguments. =cut sub f_setfl { @_ == 2 or badinvo; my ($fh, $flags) = @_; require Fcntl; fcntl $fh, Fcntl::F_SETFL(), 0+$flags or croak "Can't F_SETFL on $fh: $!"; } =item B [B<-bundle> | B<-bundling>] [I...] This is basically Getopt::Long but it has the defaults set up the way I think they should be. =cut sub getopt { # Don't bother if there aren't any switches. This test works because # I'm setting $REQUIRE_ORDER. return 1 unless @ARGV && substr($ARGV[0], 0, 1) eq '-'; my $bundling = 0; if (@_ && ($_[0] eq -bundle || $_[0] eq -bundling)) { $bundling = 1; shift; } { # I'm setting this environment variable when loading Getopt::Long # so that the defaults for options added later (which aren't set # explicitly below) are more likely to match what I'd like. local $ENV{POSIXLY_CORRECT} = 1; require Getopt::Long; } Getopt::Long->VERSION(2.19); Getopt::Long::Configure( 'no_auto_abbrev', 'no_getopt_compat', 'require_order', $bundling ? 'bundling' : (), 'no_ignore_case', 'prefix_pattern=(--|-)', ) if 1; # The getopt function puts the vars into its caller's package so # it's necessary to jump to it so that its caller is my caller. goto &Getopt::Long::GetOptions; } =item B I This is like a call to the system open() with flags O_CREAT, O_EXCL and O_RDWR set. A Perl filehandle is returned. =cut sub exclusive_create { @_ == 1 or @_ == 2 or badinvo; my $file = shift; my $mode = @_ ? shift : 0666; require FileHandle; require Fcntl; return new FileHandle $file, Fcntl::O_CREAT()|Fcntl::O_EXCL()|Fcntl::O_RDWR(), $mode } =item B This is just like the C function tmpfile(). I dislike that POSIX.pm tells you to use Cnew_tmpfile> (though I'm unsure why I don't just have this function call that one). =cut sub tmpfile { @_ == 0 or badinvo; my $base = "/tmp/$Me.$$"; my $ext = 0; my $file = $base; my $fh; until ($fh = exclusive_create $file, 0666) { $ext++ > 100 and xdie "&tmpfile: can't open a file named like $base.\n"; $file = "$base.$ext"; } unlink $file or xdie "&tmpfile: can't unlink $file:"; $fh; } =item B [I]... This routine safely creates temporary files and directories. The default is files, specify C as an arg to make a directory instead. In scalar context the return value is the name of the file or directory created. In array context the file-creating version also returns a filehandle opened in read/write mode on the file. Right now it's an error to call the mkdir version in array context. Valid args are: =over 4 =item B This boolean option indicates that a directory rather than a file should be created. =item B => I Create the file using the given filehandle. If you don't specify this a filehandle is generated for you. You can retrieve it if you call B in array context. =item B => I Place the created file in this directory. The default is the user's $TMPDIR or F. Note that both the B arg and $TMPDIR are ignored if the C contains a C. =item B => I Specify the part of the file name which precedes the random part. The default comes from $0 or (if that doesn't work out) the user's name. If this value contains a C the C argument is ignored. =item B Use the user name, not the script name, as the preferred default B. =item B => I Specify the part of the file name which comes after the random part. There is no default, as you don't normally need one. It's useful for programs which require that files have a certain extension. $zip_file = safe_tmp postfix => '.zip' or xdie "can't create temporary file:"; =item B => I Specify the file creation mode. The default is 0600 for files and 0700 for directories. =back =cut sub safe_tmp { my @arg = @_; my ($fh, $mkdir, $dir, $prefix, $loginprefix, $postfix, $mode); require Fcntl; while (@arg) { my $k = shift @arg; my $rval; if ($k eq 'mkdir') { $mkdir = 1 } elsif ($k eq 'fh') { $rval = \$fh } elsif ($k eq 'dir') { $rval = \$dir } elsif ($k eq 'prefix') { $rval = \$prefix } elsif ($k eq 'loginprefix') { $loginprefix = 1 } elsif ($k eq 'postfix') { $rval = \$postfix } elsif ($k eq 'mode') { $rval = \$mode } else { croak "Invalid arg `$k' passed to safe_tmp"; } if ($rval) { @arg or croak "No arg specified for $k"; $$rval = shift @arg; } } if ($mkdir) { defined $fh and croak 'Both mkdir and fh specified'; } else { require Symbol; if (defined $fh) { $fh = Symbol::qualify_to_ref($fh, scalar caller); } else { $fh = my_gensym; } } if (!defined $prefix) { my $cmd = $loginprefix ? '' : $0; if ($cmd eq '-e') { $cmd = $^X; } elsif ($cmd eq '') { $cmd = $ENV{LOGNAME} || $ENV{USER} || getlogin || getpwuid $< || $<; } $cmd =~ s-.*/--; $cmd =~ s/^\.+//; $cmd =~ tr/a-zA-Z0-9_.-/_/cs; $cmd = ($cmd =~ /^([\w.\-]+)$/) ? $1 : ''; # untaint $cmd = 'safe_tmp' if $cmd eq ''; # can't happen $prefix = "$cmd."; } if ($prefix =~ m-/-) { $dir = ''; } elsif (!defined $dir) { $dir = $ENV{TMPDIR} if !tainted($ENV{TMPDIR}); $dir = '/tmp' if !defined $dir; } $dir .= '/' if $dir ne '' && substr($dir, -1) ne '/'; $postfix = '' if !defined $postfix; $mode = $mkdir ? 0700 : 0600 if !defined $mode; # 36^6 == 2 billion possible names my @alphabet = ('a'..'z', 0..9); xsrand(); for (my $attempts = 0; $attempts < 10; $attempts++) { my $file = "$dir$prefix"; $file .= join '', map { $alphabet[rand @alphabet] } 1..6; $file .= $postfix; if ($mkdir) { wantarray and croak 'safe_tmp mkdir called in array context'; mkdir $file, $mode and return $file; } elsif (sysopen $fh, $file, Fcntl::O_RDWR() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), $mode) { return wantarray ? ($file, $fh) : $file; } { local $!; require POSIX; } $! == POSIX::EEXIST() or return; } # I couldn't find a name which didn't exist, return error with # existing $! (== EEXIST). return; } =item B This function returns the current home directory, preferring $HOME to looking it up in the password file. It doesn't cache the result. =cut #' sub home { @_ == 0 or badinvo; $ENV{HOME} || (getpwuid($<))[7] || xdie "can't get your home directory\n"; } =item B I This function returns the home directory for the given user, or dies. =cut sub home_of { @_ == 1 or badinvo; my ($user) = @_; my @pw = getpwnam $user or xdie_caller 1, "invalid user ", dstr $user; my $dir = $pw[PW_DIR]; defined $dir && $dir ne '' or xdie_caller 1, "user $user has invalid home directory ", dstr $dir; return $dir; } =item B<:stat> This tag gives you subs named ST_DEV, ST_INO, ST_MODE and so on which return indices into a stat() or lstat() array. =item B<:tm> This tag gives you subs named TM_SEC, TM_MIN, and so on which return indices into a gmtime() or localtime() array. =item B I Return a best guess at the full name for the user with uid I, but always return something usable (rather than dying or returning undef). =cut # A cache mapping uid => full name. my %full_name_uid; sub full_name_uid { @_ == 1 or badinvo; my $uid = shift; my ($login, $name); return $full_name_uid{$uid} if exists $full_name_uid{$uid}; if (($login, $name) = (getpwuid($uid))[0, 6]) { # BSD-type (?) gecos has 4 comma-separated fields (full name, # office location, office phone, home phone). I'm just # stripping everything after a comma in case some people use a # different number of fields. This loses for `Joe User, Jr.', # alas. $name =~ s/,.*//; # if ($name =~ tr/,/,/) == 3; # & in the full name field means to insert the capitalized # login name. $name =~ s/&/ucfirst $login/eg; } else { $name = "uid-$uid"; } $full_name_uid{$uid} = $name; return $name; } =item B Call B with the real user id. =cut sub full_name { @_ == 0 or badinvo; full_name_uid $<; } =item B I Return the user name associated with I. =cut my %pwuid; sub pwuid ($) { my $uid = shift; $pwuid{$uid} ||= getpwuid($uid) || "uid$uid" } =item B I Return the group name associated with I. =cut my %grgid; sub grgid ($) { my $gid = shift; $grgid{$gid} ||= getgrgid($gid) || "group$gid" } =item B [I]... Return the contents of each I, one line per element. =cut sub cat (@) { require FileHandle; my $fh; map { $fh = new FileHandle $_ or croak "Can't read $_: $!"; $fh->getlines; } @_; } =item B [I]... Return the contents of each I, in a list context with each file in a separate returned value, in a scalar context all joined together. =cut sub catslurp (@) { local *FH; my @r; local $/ = undef; for my $file (@_) { open FH, $file or xdie_caller 1, "can't read $file:"; push @r, ; } close FH; return wantarray ? @r : join '', @r; } =item B A better srand(). =cut my $xsrand_called; sub xsrand () { if ($xsrand_called++) { # Do nothing, already called. } elsif ($] >= 5.003_90) { # Do nothing, automatic srand. } elsif ($] >= 5.003_01) { # Improved default seed. srand; } else { srand(time ^ (($$ << 15) + $$)); } } =item B [I]... Return all the Is in random order. =cut # Fisher/Yates shuffle from PCB. sub shuffle { my @a = @_; my ($i, $j); return unless @a; xsrand; for ($i = @a; --$i; ) { $j = int rand $i+1; next if $i == $j; @a[$i, $j] = @a[$j, $i]; } return @a; } =item B I I [I] [I