The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

CLI::Startup - Simple initialization for command-line scripts

VERSION

Version 0.27

SYNOPSIS

CLI::Startup can export a single method, startup(), into the caller's namespace. It transparently handles config files, defaults, and command-line options.

  use CLI::Startup 'startup';

  # Returns the merged results of defaults, config file
  # and command-line options.
  my $options = startup({
    'opt1=s' => 'Option taking a string',
    'opt2:i' => 'Optional option taking an integer',
    ...
  });

It also supports an object-oriented interface with much more scope for customization. The basic usage looks like this:

  use CLI::Startup;

  # Parse command line and read config
  $app = CLI::Startup->new({
      usage            => '[options] [other args ...]', # Optional
      options          => $optspec,
      default_settings => $defaults,
  });
  $app->init;

  # Combined command line, config file and default options. Almost
  # always what you want.
  $opts = $app->get_options;

  # Information about the current invocation of the calling script:
  $opts = $app->get_raw_options;       # Actual command-line options
  $conf = $app->get_config;            # Options set in config file
  $dflt = $app->get_default_settings;  # Wired-in script defaults

Most scripts can then use $opts for all their customization needs.

You can also hide extra data in the config file, and access it through $app-get_config>. The app settings will be stored in a section of the config file named "default," so the rest of the file is yours to do with as you wish. See the example implemetation of an rsync wrapper, below, for one use of this.

DESCRIPTION

Good command-line scripts always support command-line options using Getopt::Long, and should support default configuration in a file in a standard format like YAML, JSON, XML, INI, etc. At minimum it should include a --help option that explains the other options. Supporting all this takes quite a bit of boilerplate code. In my experience, doing it right takes several hundred lines of code that are practically the same in every script.

CLI::Startup is intended to factor away almost all of that boilerplate. In the common case, all that's needed is a single hashref listing the options (using Getopt::Long syntax) as keys, and a bit of help text as values. CLI::Startup will automatically generate the command-line parsing, reading of an optional config file, merging of the two, and creation of a hash of the actual settings to be used for the current invocation. It automatically prints a usage message when it sees invalid options or the --help option. It automatically supports an option to save the current settings in an rc file. It supports a --version option that prints $::VERSION from the calling script, and a --manpage option that prints the formatted POD, if any, in the calling script. All the grunt work is handled for you.

Any of these auto-generated options, except --help, can be disabled by including the option's name in the hashref with a false value in place of the help text.

An additional hashref can be passed with default values for the various options.

CLI::Startup slightly enhances Getopt::Long behavior by allowing repeatable options to be specified either with multiple options or with a commalist honoring CSV quoting conventions. It also enhances INI file parsing to support hash-valued options of the form:

    [default]
    hash=a=1, b=2, c=3

For convenience, CLI::Support also supplies die() and warn() methods that prepend the name of the script and postpend a newline.

    use CLI::Startup;

    my $app = CLI::Startup->new({
        'infile=s'   => 'An option for specifying an input file',
        'outfile=s'  => 'An option for specifying an output file',
        'password=s' => 'A password to use for something',
        'email=s@'   => 'Some email addresses to notify of something',
        'map=s%'     => 'Some name/value pairs mapping something to something',
        'x|y=i'      => 'Integer --x, could also be called --y',
        'verbose'    => 'Verbose output flag',
        'lines:i'    => 'Optional - the number of lines to process',
        'retries:5'  => 'Optional - number of retries; defaults to 5',
        ...
    });

    # Process the command line and resource file (if any)
    $app->init;

    # Information about the current invocation of the calling
    # script:
    my $opts = $app->get_raw_options;       # Actual command-line options
    my $conf = $app->get_config;            # Options set in config file
    my $dflt = $app->get_default_settings;  # Wired-in script defaults

    # Get the applicable options for the current invocation of
    # the script by combining, in order of decreasing precedence:
    # the actual command-line options; the options set in the
    # config file; and any wired-in script defaults.
    my $opts = $app->get_options;

    # Print messages to the user, with helpful formatting
    $app->die_usage();      # Print a --help message and exit
    $app->print_manpage();  # Print the formatted POD for this script and exit
    $app->print_version();  # Print version information for the calling script
    $app->warn();           # Format warnings nicely
    $app->die();            # Die with a nicely-formatted message

DEFAULT OPTIONS

Here is a help message printed by CLI::Startup for a script that defines no command-line options:

  usage: test [options]
  Options:
      --help          - Print this help message
                        Aliases: -h
      --manpage       - Print the manpage for this script
                        Aliases: -H
      --rcfile        - Config file to load
      --rcfile-format - Format to write the config file
      --verbose       - Print verbose messages
                        Aliases: -v
      --version       - Print version information and exit
                        Aliases: -V
      --write-rcfile  - Write the current options to config file

The options work as follows:

--help | -h

Prints a help message like the one above. The message is dynamically created using the Getopt::Long-style optspec and the help text. It automatically identifies negatable options, and lists aliases for options that have any.

--manpage | -H

This option is a poor-man's version of the --more-help option some scripts provide: when the --manpage option is used, CLI::Startup calls perldoc on the script itself.

--rcfile

This option lets the user specify the .rc file from which to load saved arguments. Any options in that file will override the app defaults, and will in turn be overridden by any command-line options specified along with --rcfile.

--rcfile-format

Allows the user to specify the formay of the .rc file when saving a new one using the --write-rcfile option. Valid values are: INI, XML, JSON, YAML, PERL. The value is not case sensitive.

--verbose | -v

This option lets the user request verbose output. There's a certain amount of extra magic behind this "option," in order to support the prevailing paradigms:

    If you specify --verbose, then get_options() will return a hash with its 'verbose' key set to 1.

    If you specify --verbose=N or --verbose N, the 'verbose' key returned by get_options() will have the value N.

    If you specify -vvv or -v -v, etc., the 'verbose' key will have a value equal to the number of 'v' options that were specified.

    If you use a mixture of -v and --verbose options, the total values will be added up.

The point of this is to support -vvv, --verbose, and --verbose=5 style options. It allows the user to perform pathological combinations of them all, but the end result should do what the user meant. When generating verbose output, you can check the value of get_options()->{verbose} and print if it's non-zero, or print if it exceeds some threshold.

--version | -V

Note the capital -V, as distinct from the lowercase -v that is the alias of --verbose. This option prints the version, as found in $::VERSION, along with the path to the script and the Perl version.

--write-rcfile

This option takes the fully-processed options, including the command line, any .rc file options, and the app defaults, and writes the results back to the specified .rc file. This can be used to save and reuse invocations: build up the command line that you want, and then tack on the --write-rcfile option to save it. Next time you run the script with no options (or with --rcfile PATH if the .rc file isn't the default one), it will use the saved options.

COMMAND LINE PROCESSING

CLI::Startup ultimately calls Getopt::Long::GetOptions(), so processing works almost exactly like you're used to. The only major difference is in the handling of array and hash argument types.

When the option spec is something like 'email=s@', that means the --email option can be specified multiple times. The arguments are then collected in an array, so $app-get_options->{email}> will be an array reference. In addition, though, CLI::Startup will step through each of the arguments and parse them as CSV. As a result, the following are equivalent:

  foo --bar=baz --bar=qux --bar=quux
  foo --bar=baz,qux --bar=quux
  foo --bar=baz,qux,quux

Similarly, when the option spec is something like 'map=s%', that means that the --map option is hash-valued, and $app-get_options->{map}> will be a hash reference. The name/value pairs are delimited by an equals sign, and either separated by commas or given in separate repetitions of the option, like so:

  foo --bar=baz=1 --bar=qux=2
  foo --bar=baz=1,qux=2
  # etc...

If you're running the foo script, in this case, you probably want to omit the equals sign between the option and its argument, to reduce confusion, like so:

  foo --bar baz=1,qux=2

Also note that Getopt::Long is configured with the settings posix_default, gnu_compat, and bundling. That means most confusing options are turned off, like specifying options first, and their arguments in a big list at the end, or other things that you hopefully don't want to do anyway. Specifically,

  • Abbreviated versions of option names are not recognized.

  • Options can't be started with a '+' in lieu of '--'.

  • You can use --opt= to specify empty options.

  • Options must be immediately followed by their arguments.

  • Single-character options can be combined: -las for -l -a -s.

  • Option names are case insensitive.

EXAMPLES

The following is a complete implementation of a wrapper for rsync. Since rsync doesn't support a config file, this wrapper provides that feature in 33 lines of code (according to sloccount). Fully 1/3 of the file is simply a list of the rsync command-line options in the definition of $optspec; the rest is just a small amount of glue for invoking rsync with the requested options.

  #!/usr/bin/perl

  use File::Rsync;
  use CLI::Startup;
  use List::Util qw{ reduce };
  use Hash::Merge qw{ merge };

  # All the rsync command-line options
  $optspec = {
      'archive!'     => 'Use archive mode--see manpage for rsync',
      ...
      'verbose+'     => 'Increase rsync verbosity',
  };

  # Default settings
  $defaults = {
      archive  => 1,
      compress => 1,
      rsh      => 'ssh',
  };

  # Parse command line and read config
  $app = CLI::Startup->new({
      usage            => '[options] [module ...]',
      options          => $optspec,
      default_settings => $defaults,
  });
  $app->init;

  # Now @ARGV is a list of INI-file groups: run rsync for
  # each one in turn.
  do {
      # Combine the following, in this order of precedence:
      # 1) The actual command-line options
      # 2) The INI-file group requested in $ARGV[0]
      # 3) The INI-file [default] group
      # 4) The wired-in app defaults

      $options = reduce { merge($a, $b) } (
          $app->get_raw_options,
          $config->{shift @ARGV} || {},
          $app->get_config->{default},
          $defaults,
      );

      my $rsync = File::Rsync->new($options);

      $rsync->exec({
          src    => delete $options->{src},
          dest   => delete $options->{dest},
      }) or $app->warn("Rsync failed for $source -> $dest: $OS_ERROR");

  } while @ARGV;

My personal version of the above script uses strict and warnings, and includes a complete manpage in POD. The POD takes up 246 lines, while the body of the script contains only 67 lines of code (again according to sloccount). In other words, 80% of the script is documentation.

CLI::Startup saved a ton of effort writing this, by abstracting away the boilerplate code for making the script behave like a normal command-line utility. It consists of approximately 425 lines of code (sloccount again), so the same script without CLI::Startup would have been more than seven times longer, and would either have taken many extra hours to write, or else would lack the features that this version supports.

EXPORT

If you really don't like object-oriented coding, or your needs are super-simple, CLI::Startup optionally exports a single sub: startup().

startup

  use CLI::Startup 'startup';

  # Simple version--almost a drop-in replacement for
  # Getopt::Long:
  my $options = startup({
    'opt1=s' => 'Option taking a string',
    'opt2:i' => 'Optional option taking an integer',
    ...
  });

  # More complicated version, using more CLI::Startup
  # features.
  my $options = startup({
      usage            => '[options] [module ...]',
      options          => $optspec,
      default_settings => $defaults,
  });

Process command-line options specified in the argument hashref. The argument is passed to CLI::Startup-new>, so anything valid there is valid here.

Scripts using this function automatically respond to to the --help option, or to invalid options, by printing a help message and exiting. They can also write a config file and exit, or any of the other default features provided by CLI::Startup.

If it doesn't exit, it returns a hash (or hashref, depending on the calling context) of the options supplied on the command line. It also automatically checks for default options in a resource file named $HOME/.SCRIPTNAMErc and folds them into the returned hash.

If you want any fancy configuration, or you want to customize any behaviors, then you need to use the object-oriented interface.

ACCESSORS

The following methods are available when the object-oriented interface is used.

get_config

  $config = $app->get_config;

Returns the contents of the resource file as a hashref. This attribute is read-only; it is set when the config file is read, which happens when $app-init()> is called. The referece returned by this sub is to a clone of the settings, so although its contents can be modified, it will have no effect on the $app object.

It is a fatal error to call get_config() before init() is called.

get_default_settings

  $defaults = $app->get_default_settings;

Returns default settings as a hashref. Default settings are applied with lower precedence than the rcfile contents, which is in turn applied with lower precedence than command-line options.

set_default_settings

  $app->set_default_settings(\%settings);

Set the default settings for the command-line options.

It is a fatal error to call set_default_settings() after calling init().

get_initialized

  $app->init unless $app->get_initialized();

Read-only flag indicating whether the app is initialized. This is used internally; you probably shouldn't need it since you should only be calling $app-init()> once, near the start of your script.

get_options

  my $options = $app->get_options;

Read-only: the command options for the current invocation of the script. This includes the actual command-line options of the script, or the defaults found in the config file, if any, or the wired-in defaults from the script itself, in that order of precedence.

Usually, this information is all your script really cares about. It doesn't care about $app-get_config> or $app-get_optspec> or any other building blocks that were used to ultimately build $app-get_options>.

It is a fatal error to call get_options() before calling init().

get_optspec

  my $optspec = $app->get_optspec();

Returns the hash of command-line options. Keys are option specifications in the Getopt::Long syntax, and values are the short descriptions to be printed in a usage summary. See set_optspec for an example, and see Getopt::Long for the full syntax.

Note that this optspec does include the default options supplied by CLI::Startup.

set_optspec

  $app->set_optspec({
    'file=s'  => 'File to read',    # Option with string argument
    'verbose' => 'Verbose output',  # Boolean option
    'tries=i' => 'Number of tries', # Option with integer argument
    ...
  });

Set the hash of command-line options. The keys use Getopt::Long syntax, and the values are descriptions, to be printed in the usage message. CLI::Startup will automatically add the default options to the optspec you supply, so get_optspec() will generally not return the same structure spec that you gave to get_optspec().

It is a fatal error to call set_optspec() after calling init().

get_raw_options

  $options = $app->get_raw_options;

Returns the options actually supplied on the command line--i.e., without adding in any defaults from the rcfile. Useful for checking which settings were actually requested, in cases where one option on the command line disables multiple options from the config file.

get_rcfile

  my $path = $app->get_rcfile;

Get the full path of the rcfile to read or write.

set_rcfile

  $app->set_rcfile( $path_to_rcfile );

Set the path to the rcfile to read or write. This overrides the built-in default of $HOME/.SCRIPTNAMErc, but is in turn overridden by the --rcfile option supported automatically by CLI::Startup.

It is a fatal error to call set_rcfile() after calling init().

get_usage

  print "Usage: $0: " . $app->get_usage . "\n";

Returns the usage string printed as part of the --help output. Unlikely to be useful outside the module.

set_usage

  $app->set_usage("[options] FILE1 [FILE2 ...]");

Set a usage string for the script. Useful if the command options are followed by positional parameters; otherwise a default usage message is supplied automatically.

It is a fatal error to call set_usage() after calling init().

set_write_rcfile

  $app->set_write_rcfile( \&rcfile_writing_sub );

  sub rcfile_writing_sub
  {
      ($app, $filename) = @_;
      $config_data      = $app->get_options_as_defaults;

      # Do stuff with $config_data, and write it to
      # $filename in the desired format.
  }

A code reference for writing out the rc file, in case the default file formats aren't good enough. Setting this to undef disables the command-line options --write-rcfile and --rcfile-format. Those options are also disabled if reading rc files is disabled by setting the rcfile attribute to anything that evaluates to false.

For now, your writer will have to write in one of the formats supported by CLI::Startup, so this feature is mostly useful for providing prettier output, with things like nice formatting and helpful explanatory comments.

It is a fatal error to call set_write_rcfile() after calling init().

SUBROUTINES/METHODS

new

  # Normal: accept defaults and specify only options
  my $app = CLI::Startup->new( \%options );

  # Advanced: override some CLI::Startup defaults
  my $app = CLI::Startup->new({
    rcfile       => $rcfile_path, # Set to false to disable rc files
    write_rcfile => \&write_sub,  # Set to false to disable writing
    options      => \%options,
    defaults     => \%defaults,
  });

Create a new CLI::Startup object to process the options defined in \%options. Options not specified on the command line or in a config file will be set to the value contained in \%defaults, if any.

Setting the rcfile option to a false value disables the --rcfile option, which in turn prevents the script from reading config files.

Setting the write_rcfile option to a false value disables writing config files with the --write-rcfile option, but does not disable reading config files created some other way.

BUILD

An internal method called by new().

DEMOLISH

An internal method, called when an object goes out of scope.

init

  $app  = CLI::Startup->new( \%optspec );
  $app->init;
  $opts = $app->get_options;

Initialize command options by parsing the command line and merging in defaults from the rcfile, if any. This is where most of the work gets done. If you don't have any special needs, and want to use the Perl fourish interface, the startup() function basically does nothing more than the example code above.

After init() is called, most of the write accessors will throw a fatal exception. It's not quite true that this object becomes read-only, but you can think of it that way: the object has done its heavy lifting, and now exists mostly to answer questions about the app's configuration.

warn

  $app->warn("warning message");
  # Prints the following, for script "$BINDIR/foo":
  # foo: WARNING: warning message

Print a nicely-formatted warning message, identifying the script by name. Identical to calling CORE::warn, except for the formatting.

die

  $app->die("die message");
  # Prints the following, for script "$BINDIR/foo":
  # foo: FATAL: die message

Die with a nicely-formatted message, identifying the script that died. This is exactly the same as calling CORE::die, except for the standardized formatting of the message. It also suppresses any backtrace, by postpending a newline to your message.

die_usage

  $app->die_usage if $something_wrong;
  $app->die_usage($message);

Print a help message and exit. This is called internally if the user supplies a --help option on the command-line. If the optional $message is specified, then precedes the usage message with something like:

  program_name: FATAL: $message
  $app->print_manpage;

Prints the formatted POD contained in the calling script. If there's no POD content in the file, then the --help usage is printed instead.

  $app->print_version;

Prints the version of the calling script, if the variable $VERSION is defined in the top-level scope.

write_rcfile

  $app->write_rcfile();      # Overwrite the rc file for this script
  $app->write_rcfile($path); # Write an rc file to a new location

Write the current settings for this script to an rcfile--by default, the rcfile read for this script, but optionally a different file specified by the caller. The automatic --write-rcfile option always writes to the script specified in the --rcfile option.

The file format can be specified by the --rcfile-format option, and must be one of: ini, yaml, json, xml, and perl. By default this method will attempt to use .ini file format, because it's the simplest and most readable for most option specification needs. If the module Config::INI::Writer isn't installed, it will fall back on perl format, which looks like the output of Data::Dumper.

The prettiest formats are ini, yaml, and perl. The others will tend to be harder to read.

The simplest format for users is ini. It's good enough, if you don't have complicated command-line options, or additional data hidden in your config files.

The most powerful formats are json and yaml, of which yaml is the most readable. It will let you put pretty much any data structure you desire in your config files.

It's a fatal error to call write_rcfile() before calling init().

get_options_as_defaults

    $options = $app->get_options_as_defaults;

Returns the same hashref as $app-get_config> would do, except that $options-{default}> is overridden with the current settings of the app. This is a helper method if you write a function to write config files in some format not natively supported by CLI::Startup. It lets you freeze the state of the current command line as the default for future runs.

This sub will also strip out any of the auto-generated options, like --help and --rcfile, since they don't belong in a config file.

AUTHOR

Len Budney, <len.budney at gmail.com>

BUGS AND LIMITATIONS

CLI::Startup tries reasonably to keep things consistent, but it doesn't stop you from shooting yourself in the foot if you try at all hard. For example: it will let you specify defaults for nonexistent options; it will let you use a hashref as the default value for a boolean option; etc..

For now, you should also not define aliases for default options. If you try to define an option like 'help|?', expecting to get --help and --? as synonyms, something will break. Basically, CLI::Startup isn't quite smart enough to recognize that your help option is a suitable replacement for the builtin one.

You can delete default options by supplying the primary name of the option with a help-text value that evaluates to false. CLI::Startup will delete that option from the defaults before merging with your options. If you try that with --help, though, CLI::Startup will put that option back in. Its main use is to disable .rc file handing.

Please report any bugs or feature requests to bug-cli-startup at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CLI-Startup. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc CLI::Startup

You can also look for information at:

ACKNOWLEDGEMENTS

LICENSE AND COPYRIGHT

Copyright 2011-2017 Len Budney.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.