# (X)Emacs mode: -*- cperl -*- # Check working/testing without Term::ProgressBar # Document SUPER:: calling of check, etc. # Add validity checks to check (e.g., modes set ok) # Add check to mode to check mode is valid # Document mode_info (esp. in init) (and add to SYNOPSIS) # Document package Getopt::Plus; =head1 NAME Getopt::Plus - Options wrapper with standard options, help system and more =head1 SYNOPSIS Z<> =head1 DESCRIPTION Z<> =cut # ---------------------------------------------------------------------------- # Pragmas ----------------------------- require 5.005_62; use strict; use warnings; # Inheritance ------------------------- use base qw( Exporter ); our @EXPORT_OK = qw( OPT_FLOAT OPT_INT OPT_STRING OPT_BOOLEAN OPT_FDLEVEL ERR_OK ERR_ABNORMAL ERR_UTILITY ERR_USAGE ERR_IO_READ ERR_IO_WRITE ERR_DB_READ ERR_DB_WRITE ERR_RDBMS_READ ERR_RDBMS_WRITE ERR_EXTERNAL ERR_INTERNAL ERR_INPUT ERR_UNKNOWN find_exec ftime commify human_file_size $PACKAGE $VERSION ); our %EXPORT_TAGS = ( opt_types => [qw/ OPT_FLOAT OPT_INT OPT_STRING OPT_BOOLEAN OPT_FDLEVEL /], exit_codes => [qw/ ERR_OK ERR_ABNORMAL ERR_UTILITY ERR_USAGE ERR_IO_READ ERR_IO_WRITE ERR_DB_READ ERR_DB_WRITE ERR_RDBMS_READ ERR_RDBMS_WRITE ERR_EXTERNAL ERR_INTERNAL ERR_INPUT ERR_UNKNOWN /], ); # Utility ----------------------------- use Carp qw( carp croak ); use Class::MethodMaker 1.04 qw( ); use Data::Dumper 2.102 qw( ); use Env qw( @PATH ); use Fatal 1.02 qw( :void close open seek sysopen ); use Fcntl 1.03 qw( :seek ); use File::Basename 2.6 qw( fileparse ); use File::Spec::Functions 1.1 qw( catfile ); use File::Temp 0.12 qw( tempfile ); use Getopt::Long 2.25 qw( ); use IPC::Run 0.44 qw( harness ); use List::Util 1.06 qw( first min max sum ); use Log::Info 1.13 qw( :DEFAULT :log_levels :default_channels :trap ); use Pod::Select 1.13 qw( podselect ); use Pod::Text 2.08 qw( ); use Pod::Usage 1.12 qw( pod2usage ); use Text::Tabs 98.112801 qw( expand ); use Text::Wrap 2001.0131 qw( wrap ); my ($ReadKeyPresent); BEGIN { eval 'use Term::ReadKey 2.14 qw( );'; $ReadKeyPresent = $@ ? 0 : 1; } BEGIN { select((select(STDOUT), $| = 1)[0]); } # ---------------------------------------------------------------------------- # CLASS METHODS -------------------------------------------------------------- # ------------------------------------- # CLASS CONSTANTS # ------------------------------------- =head1 CLASS CONSTANTS Z<> =cut # Maximum width of option name column in opt output use constant MAX_OPT_WIDTH => 13; =head2 FILE_SIZE_HUMAN Map from file size in bytes to human name, as hashref, keys being name (full name, lowercase, no trailing 's') and abbrev (one/two-letter abbreviation). =cut use constant FILE_SIZE_HUMAN => +{ 1024**0 => +{ name => 'byte', abbrev => 'b' }, 1024**1 => +{ name => 'kilobyte', abbrev => 'Kb' }, 1024**2 => +{ name => 'megabyte', abbrev => 'Mb' }, 1024**3 => +{ name => 'gigabyte', abbrev => 'Gb' }, 1024**4 => +{ name => 'terabyte', abbrev => 'Tb' }, }; # OPTION TYPES ------------------------ =head2 Option Types Permissable values to the C field of an option specifier. =over 4 =item OPT_FLOAT =item OPT_INT =item OPT_STRING =item OPT_FDLEVEL =item OPT_BOOLEAN =back =cut use constant OPT_FLOAT => 'f'; use constant OPT_INT => 'i'; use constant OPT_STRING => 's'; use constant OPT_FDLEVEL => 'F'; use constant OPT_BOOLEAN => '!'; use constant OPTION_NAMES => { OPT_FLOAT , 'float' , OPT_INT , 'int' , OPT_STRING , 'string' , OPT_FDLEVEL , 'fd/level' , }; use constant GETOPT_TYPE_MAP => { OPT_FLOAT , { char => 'f' } , OPT_INT , { char => 'i' } , OPT_STRING , { char => 's' } , OPT_FDLEVEL , { char => 's' } , OPT_BOOLEAN , { char => '!' } , }; # DEFAULT OPTIONS --------------------- use constant STANDARD_OPTIONS => [ { names => [qw/ v verbose /], type => OPT_FDLEVEL, arg_reqd => 0, mandatory => 0, summary => 'output informational messages', desc => <<'END', Enable informational messages about choices made, etc. to stderr. This option may be invoked multiple times to increase the level of verbosity. END default => 0, linkage => sub { my ($rse, $opt, $value) = @_; Log::Info::enable_file_channel(CHAN_INFO, $value, 'verbose',SINK_STDERR); } }, { names => [qw/ progress /], type => OPT_FDLEVEL, arg_reqd => 0, mandatory => 0, summary => 'output progress messages', desc => <<'END', Enable regular messages to inform the user of progress made. These may be in simple text form, or where appropriate, progress bars or the like may be used (when connected to a suitable terminal). END default => 0, linkage => sub { my ($rse, $opt, $value) = @_; Log::Info::enable_file_channel(CHAN_PROGRESS, $value, 'progress', 'p-out', 1); } }, { names => [qw/ stats /], type => OPT_FDLEVEL, arg_reqd => 0, mandatory => 0, summary => 'output statistical information', desc => 'Enable statistical information to be output to the user.', default => 0, linkage => sub { my ($rse, $opt, $value) = @_; Log::Info::enable_file_channel(CHAN_STATS, $value, 'stats', 's-out'); } }, undef, { names => [qw/ help /], type => OPT_STRING, arg_reqd => 0, mandatory => 0, summary => 'produce summary help on stdout', desc => <<'END', Print a brief help message and exit. If an argument is given, then it is treated as an option name, and the description for that option is given (a la longhelp). END default => 0, linkage => sub { $_[0]->dump_help(undef, $_[2]) }, }, { names => [qw/ longhelp /], arg_reqd => 0, mandatory => 0, summary => 'produce long help on stdout', desc => 'Print a longer help message and exit.', default => 0, linkage => sub { $_[0]->dump_longhelp }, }, { names => [qw/ man /], arg_reqd => 0, mandatory => 0, summary => 'produce full man page on stdout', desc => 'Print the manual page and exit.', default => 0, linkage => sub { $_[0]->dump_man }, }, { names => [qw/ version /], arg_reqd => 0, mandatory => 0, summary => 'produce full version on stdout', desc => <<'END', Print the version info (as for C) and the copyright notice, and exit. END default => 0, linkage => sub { $_[0]->dump_version }, }, { names => [qw/ V briefversion /], arg_reqd => 0, mandatory => 0, summary => 'produce brief version on stdout', desc => <<'END', Print the version number (of the source package), in the form scriptname (packagename): version and exit. scriptname is the canonical installed name of the script. END default => undef, linkage => sub { $_[0]->dump_briefversion }, }, { names => [qw/ copyright /], arg_reqd => 0, mandatory => 0, summary => 'produce full copyright on stdout', desc => 'Print the copyright notice, and exit.', default => 0, linkage => sub { $_[0]->dump_copyright }, arg_trigger => 1, }, undef, { names => [qw/ dry-run /], arg_reqd => 0, mandatory => 0, summary => "don't really do anything", desc => <<'END', Do not write any files (other than temporary files), nor make any changes to any RDBMS (other than disposable ones). END default => 0, arg_trigger => 1, }, { names => [qw/ debug /], type => OPT_FDLEVEL, arg_reqd => 0, mandatory => 0, summary => '', desc => 'Enable debugging output.', default => 0, linkage => sub { my ($rse, $opt, $value) = @_; Log::Info::enable_file_channel(CHAN_DEBUG, $value, 'debug', 'd-out'); } }, { names => [qw/ dump-pod /], type => OPT_FDLEVEL, arg_reqd => 0, mandatory => 0, summary => 'dump generated pod', default => 0, linkage => sub { $_[0]->dump_as_pod(1); $_[0]->dump_man }, hidden => 1, }, ]; # STANDARD TEXT -------------------------------------------------------------- use constant OPTION_TEXT => <<'END'; Options come in short (single-character) and/or long forms. Short options are preceded by a single dash (-x), whilst long options are preceded by a double-dash (--option). Long options may be abbreviated to the minimal distinct prefix. Single char options may not be bundled (-v -a -x != -vax). Options taking string values will assume that anything immediately following is a string value, even if the string is optional and/or the "value" could be interpreted as another option (if -v takes a string, -vax will give the value "ax" to the option -v). Options which are boolean must use the long form (if available) to negate, prefixed by "no" (--foo may be negated by --nofoo). Options which are repeating may be invoked multiple times for greater effect. Option & argument order does not matter: all options will be processed prior to any arguments. A lone "--" may be used to terminate options processing; any text(s) following this will be treated as arguments, rather than options. Some options are marked as type 'fd/level'. These take options of the form C<+([0-9]+)> to set a specific level, and/or either a simple file name ([A-Za-z0-9_-.\/]+) or a file-descriptor number (preceded by a colon). They come in the order file,level,fd (but it is illegal to specify a filename and a file descriptor together). E.g., C<+1> sets to level one (to the default filehandle), C sets it to output to F (at the default level); C<+2:3> outputs at level 2 to file descriptor 3. If a filename is given, an error will ensue if that file already exists (and is a plain file). This is to avoid accidents due to the optional string syntax. Beware optional arguments; if you use an option that takes an optional argument, then any likely-looking (in the case of string arguments, anything) following it will be treated as an argument to the option. If you mean for an argument-looking thing to be an argument to the option, use C<--foo=bob> (for clarity). If you want to follow it with a value that looks like an argument to the option (but you intend to be a value for the program), follow it with C<-->, e.g., C END use constant DEFAULT_ENV_TEXT => 'This program has no special environment handling'; # ERROR CODES ------------------------- use constant DEFAULT_ERR => [ 'Successful termination', 'Successful, but abnormal termination', 'A utility function was requested (--help, --version etc.)', 'Incorrect usage', 'Filesystem error on open/read', 'Filesystem error on close/write', 'RDBMS access error on read/connect', 'RDBMS access error on on write', 'Unexpected exit status from external program', ]; BEGIN { DEFAULT_ERR->[255] = 'Unknown Error'; } =head2 Error Codes =over 4 =cut =item ERR_OK Not an error at all. Hence the name. =cut use constant ERR_OK => 0; =item ERR_ABNORMAL Not so much an error as a non-erroneous circumstance worthy of signalling, e.g., grep finding no matches. =cut use constant ERR_ABNORMAL => 1; =item ERR_UTILITY Again, not really an error, rather a utility function being called --- e.g., the --help or --version. This gets an error code because it is almost certainly an error to call from batch scripts. =cut use constant ERR_UTILITY => 2; =item ERR_USAGE The program was called wrong. =cut use constant ERR_USAGE => 3; =item ERR_IO_READ Some problem reading from disk or network (system read). =cut use constant ERR_IO_READ => 4; =item ERR_IO_WRITE Some problem writing to disk or network (system write). =cut use constant ERR_IO_WRITE => 5; =item ERR_DB_READ Some problem reading from db or similar (application read). =cut use constant ERR_RDBMS_READ => 6; use constant ERR_DB_READ => 6; =item ERR_DB_WRITE Some problem writing to db or similar (application write). =cut use constant ERR_RDBMS_WRITE => 7; use constant ERR_DB_WRITE => 7; =item ERR_EXTERNAL Some problem with an external application. =cut use constant ERR_EXTERNAL => 8; =item ERR_INTERNAL An internal logic error (the sort of thing that I never happen, but has been caught by an internal assertion or sanity check). =cut use constant ERR_INTERNAL => 9; =item ERR_INPUT Some problem with the input file (which was read fine, but contains bad data). =cut use constant ERR_INPUT => 10; =item ERR_UNKNOWN =cut use constant ERR_UNKNOWN => 255; =back =cut # ------------------------------------- our $PACKAGE = 'Getopt-Plus'; our $VERSION = '0.96'; # ------------------------------------- # CLASS CONSTRUCTION # ------------------------------------- # ------------------------------------- # CLASS COMPONENTS # ------------------------------------- =head1 CLASS COMPONENTS Z<> =cut my $DEFAULT_VERSION = undef; # ------------------------------------- # CLASS HIGHER-LEVEL FUNCTIONS # ------------------------------------- =head1 CLASS HIGHER-LEVEL FUNCTIONS Z<> =cut # ------------------------------------- # CLASS HIGHER-LEVEL PROCEDURES # ------------------------------------- =head1 CLASS HIGHER-LEVEL PROCEDURES Z<> =cut sub VERSION { $DEFAULT_VERSION = $_[1]; $_[0]->SUPER::VERSION($_[1]) } # ------------------------------------- # CLASS UTILITY FUNCTIONS # ------------------------------------- =head2 find_exec For each directory P of the current path (in order), check if the named program exists in P and is executable (just as the shell would when executing a command). =over 4 =item ARGUMENTS =over 4 =item exec The name of the command to execute =back =item RETURNS =over 4 =item path If the command exists in the path, the path to the command. The path will be relative if the given path segment is. If the command does not exist in the path, then nothing (undef or the empty list) shall be returned. =back =back =cut sub find_exec { my ($exec) = @_; return $_ for grep -x $_, map catfile($_, $exec), @PATH; return; } # ------------------------------------- sub columns { my ($outfh) = @_; return $ENV{COLUMNS} if exists $ENV{COLUMNS} and $ENV{COLUMNS} =~ /^\d+$/; my $columns = 72; if ( defined $outfh ) { if ( -t $outfh ) { if ( $ReadKeyPresent ) { eval { $columns = (Term::ReadKey::GetTerminalSize($outfh))[0]; }; if ( $@ ) { warn $@; } } else { if ( my $stty = find_exec('stty') ) { my ($readfh, $writefh); pipe $readfh, $writefh or croak "Failed to forge pipe: $!\n"; my $pid = fork; croak "Fork failed: $!\n" if ! defined $pid; my $sttyout; if ( $pid ) { # Parent close $writefh; local $/ = undef; $sttyout = <$readfh>; close $readfh; my $rv = waitpid($pid, 0); croak "waitpid returned $rv (expected $pid)\n" unless $rv == $pid; } else { # Child open STDOUT, ">&" . fileno $writefh; exec $stty, '-a'; } if ( $sttyout =~ /(?:^|;)\s*columns\s+(\d+)\;/m ) { $columns = $1; } elsif ( $sttyout =~ /(?:^|;)\s*(\d+)\s+columns\s*\;/m ) { $columns = $1; } } } } } return $columns; } # ------------------------------------- # Merge a set of values so that they use up the min. possible lines, subject # to a max. line length & join field (and preserving order). sub _merge_words { my ($words, $max_length, $join) = @_; my @lines; my $current = $words->[0]; for (@{$words}[1..$#$words]) { if ( length($current) + length($_) + length($join) > $max_length ) { push @lines, $current; $current = $_; } else { $current = length($current) ? join($join,$current,$_) : $_; } } push @lines, $current; return @lines; } # ------------------------------------- =head2 ftime This function is exported upon request. =over =item SYNOPSIS print ftime 86500; # 1d0h0m40s print ftime 357; # 5m57s =item ARGUMENTS =over =item time time (duration) to format, as a number of seconds =back =item RETURNS =over =item * The input time, formatted as days/hours/minutes/seconds (larger exponents produced only as needed) =back =back =cut # Format time sub ftime { my ($time) = @_; if ( $time < 60 ) { return sprintf '%ds', $time; } elsif ( $time < 60 * 60 ) { return sprintf '%dm%ds', int($time/60), $time % 60; } elsif ( $time < 60 * 60 * 24 ) { return sprintf('%dh%dm%ds', int($time/(60*60)), int(($time%60)/60), $time % 60); } else { return sprintf('%dd%dh%dm%ds', int($time/(24*60*60)), int($time%(60*60)/(60*60)), int(($time%60)/60), $time % 60); } } # ------------------------------------- =head2 commify This function is exported upon request. =over =item SYNOPSIS print commify 1_535_343; # 1,535,343 print commify 1_535_343.45459845; # 1,535,343.454,598,45 =item ARGUMENTS =over =item number number to commify. =back =item RETURNS =over =item * The input number, with commas between groups 3 digits. =back =back =cut sub commify ($) { (my $text = reverse $_[0]) =~ s/(\d{3})(?=\d)(?!\d*\.)/$1,/g; $text = reverse $text; 1 while $text =~ s/([.,])(\d{3})(?=\d)/$1$2,/g; return $text; } # ------------------------------------- =head2 human_file_size This function is exported upon request. =over =item SYNOPSIS print human_file_size(1_000); # 1000b print human_file_size(1_024); # 1Kb print human_file_size(1_535); # 1Kb print human_file_size(1_535_343); #1Mb =item ARGUMENTS =over =item bytes An integer being a number of bytes =back =item RETURNS =over =item * A human-readable representation of the size. That is, the bytes suffixed with the appropriate b/Kb/Mb/etc. exponent. Note that the mantissa is rounded to the nearest integer =back =back =cut sub human_file_size { my ($bytes) = @_; carp ("human_file_size: bytes not defined\n"), return '' unless defined $bytes; return $bytes if $bytes < 1; my $exponent = first { $bytes >= $_ } sort {$b<=>$a} keys %{FILE_SIZE_HUMAN()}; return join('', sprintf('%1.0f', ($bytes / $exponent)), FILE_SIZE_HUMAN->{$exponent}->{abbrev}); } # INSTANCE METHODS ----------------------------------------------------------- # ------------------------------------- # INSTANCE CONSTRUCTION # ------------------------------------- =head1 INSTANCE CONSTRUCTION Z<> =cut =head2 new Create & return a new thing. =over 4 =item SYNOPSIS my $RSE = Getopt::Plus->new(scriptname => 'exec-monitor', scriptsumm => 'Exec a process, monitor resources', copyright => <<'END', This program is copyright __CYEARS__ Martyn J. Pearce. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. END main => sub {}, argtype => 'exec', arg_ary => '+', options => [{ names => [qw( output o )], type => OPT_FDLEVEL, arg_reqd => 1, mandatory => 0, summary => 'No meaning', desc => 'No description', default => 'foo', linkage => sub { my ($rse, $opt, $value) = @_; Log::Info::enable_file_channel(MONITOR_CHANNEL, $value, 'output', MONITOR_SINK); $sink_added = 1; }, }, ], ); $RSE->run; =item ARGUMENTS Arguments are taken as key => value pairs. Recognized keys are: =over 4 =item scriptname B The canonical name of the script. This should I be $0 --- it should have no path, and be the I name. Hence, for C , this would be C. =item scriptsumm B A one-line summary of the purpose of the script; suitable for the header (C) line of a man page. =item copyright B A (possibly multi-line) summary of the copyright status of this program. B. If the copyright contains the text C<__CYEARS__>, this will be replaced with the approraite copyright years. =item main B This must be a coderef. It will be called once for each argument on the command line after options processing. Its arguments will be: =over 4 =item rse This instance of Getopt::Plus. =item arg_name The ARGV item in question =item output_fns If output_suffix has any members, then this contains one filename for each member, constructed appending the member onto the basename of the arg_name, with any (single) trailing suffix stripped. The value is an arrayref. Hence, if C is F, and C is set to C<(jim, kate)>, then C is C<[blibble.foo.jim, blibble.foo.kate]>. =back =item c_years B An arrayref of copyright years. This is required if the C option contains the text __CYEARS__. =item package B The package from which this program comes. Please set this correctly, so a user can determine which package to install on their box to install this program (this is useful when, for example, asking a friend or colleague the origin a your cool script). The package name should not be a class name, e.g., C, but a partial file name, e.g., F. =item version B A version number. If the script comes from a package, then please use the version number of the I here, not some individual concept of version for the executable. This is for two reasons: =over 4 =item * Since the executable is a part of the package, it presumably utilizes common libraries which have likely changed as the package got updated. Therefore the executable behaviour will have changed even if the specific script code has not. =item * Users typically install the package as a whole (after all, that's why they're distributed as packages...), so the version of the installed package is more useful than a script version number which has no direct connection. =back =item options B An arrayref of option specifications. Each specification is a hashref, with the following keys: =over 4 =item names B An arrayref of available names for this option. Both short & long options are given here; any single-char option is a short option, any multi-char option is a long option. There is no meaning to the order, other than the "default" name comes first; this is used only by the C specifier. =item type B A specifier of the type of the argument, if any. Any value from L