summaryrefslogtreecommitdiff
path: root/lib/App/DocKnot/Command.pm
diff options
context:
space:
mode:
authorRuss Allbery <rra@cpan.org>2019-03-16 15:53:54 -0700
committerRuss Allbery <rra@cpan.org>2019-03-16 15:53:54 -0700
commit6030c61d9a418d85a789bcc8bdfd751e8afd2a93 (patch)
tree4d04e7001d4df549e1cc8c7c8c5ee8311eebc117 /lib/App/DocKnot/Command.pm
parent1c1515b5d74f26125f61ffb0dd70ac0c7d28b278 (diff)
Separate App::DocKnot::Command, add base App::DocKnot
Move the entry point for command-line commands from App::DocKnot to App::DocKnot::Command. The App::DocKnot module now only provides some helper methods to load application data, used by both App::DocKnot::Config and App::DocKnot::Generate. It's no longer necessary to explicitly load App::DocKnot before using one of the submodules.
Diffstat (limited to 'lib/App/DocKnot/Command.pm')
-rw-r--r--lib/App/DocKnot/Command.pm312
1 files changed, 312 insertions, 0 deletions
diff --git a/lib/App/DocKnot/Command.pm b/lib/App/DocKnot/Command.pm
new file mode 100644
index 0000000..f7fcf3e
--- /dev/null
+++ b/lib/App/DocKnot/Command.pm
@@ -0,0 +1,312 @@
+# Dispatch code for the DocKnot application.
+#
+# DocKnot provides various commands for generating documentation, web pages,
+# and software releases. This module provides command-line parsing and
+# dispatch of commands to the various App::DocKnot modules.
+#
+# SPDX-License-Identifier: MIT
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package App::DocKnot::Command 2.00;
+
+use 5.024;
+use autodie;
+use warnings;
+
+use App::DocKnot::Generate;
+use Getopt::Long;
+
+# Defines the subcommands, their options, and the module and method that
+# implements them. The keys are the names of the commands. Each value is a
+# hash with one or more of the following keys:
+#
+# code
+# A reference to a function to call to implement this command. If set,
+# overrides method and module. The function will be passed a reference to
+# the hash resulting from option parsing as its first argument and any
+# other command-line arguments as its remaining arguments.
+#
+# maximum
+# The maximum number of positional arguments this command takes.
+#
+# method
+# The name of the method to run to implement this command. It is passed
+# as arguments any remaining command-line arguments after option parsing.
+#
+# minimum
+# The minimum number of positional arguments this command takes.
+#
+# module
+# The name of the module that implements this command. Its constructor
+# (which must be named new) will be passed as its sole argument a
+# reference to the hash containing the results of parsing any options.
+#
+# options
+# A reference to an array of Getopt::Long option specifications defining
+# the arguments that can be passed to this subcommand.
+#
+# required
+# A reference to an array of required option names (the part before any |
+# in the option specification for that option). If any of these options
+# are not set, an error will be thrown.
+our %COMMANDS = (
+ generate => {
+ method => 'generate_output',
+ module => 'App::DocKnot::Generate',
+ options => ['metadata|m=s', 'width|w=i'],
+ maximum => 2,
+ minimum => 1,
+ },
+ 'generate-all' => {
+ method => 'generate_all',
+ module => 'App::DocKnot::Generate',
+ options => ['metadata|m=s', 'width|w=i'],
+ maximum => 0,
+ },
+);
+
+##############################################################################
+# Option parsing
+##############################################################################
+
+# Parse command-line options and do any required error handling.
+#
+# $self - The App::DocKnot::Command object
+# $command - The command being run or undef for top-level options
+# $options_ref - A reference to the options specification
+# @args - The arguments to the command
+#
+# Returns: A list composed of a reference to a hash of options and values,
+# followed by a reference to the remaining arguments after options
+# have been extracted
+# Throws: A text error message if the options are invalid
+sub _parse_options {
+ my ($self, $command, $options_ref, @args) = @_;
+
+ # Use the object-oriented syntax to isolate configuration options from the
+ # rest of the program.
+ my $parser = Getopt::Long::Parser->new;
+ $parser->configure(qw(bundling no_ignore_case require_order));
+
+ # Parse the options and capture any errors, turning them into exceptions.
+ # The first letter of the Getopt::Long warning message will be capitalized
+ # but we want it to be lowercase to follow our error message standard.
+ my %opts;
+ {
+ my $error = 'option parsing failed';
+ local $SIG{__WARN__} = sub { ($error) = @_ };
+ if (!$parser->getoptionsfromarray(\@args, \%opts, $options_ref->@*)) {
+ $error =~ s{ \n+ \z }{}xms;
+ $error =~ s{ \A (\w) }{ lc($1) }xmse;
+ if ($command) {
+ die "$0 $command: $error\n";
+ } else {
+ die "$0: $error\n";
+ }
+ }
+ }
+
+ # Success. Return the options and the remaining arguments.
+ return (\%opts, \@args);
+}
+
+# Parse command-line options for a given command.
+#
+# $self - The App::DocKnot::Command object
+# $command - The command being run
+# @args - The arguments to the command
+#
+# Returns: A list composed of a reference to a hash of options and values,
+# followed by a reference to the remaining arguments after options
+# have been extracted
+# Throws: A text error message if the options are invalid
+sub _parse_command {
+ my ($self, $command, @args) = @_;
+ my $options_ref = $COMMANDS{$command}{options};
+ return $self->_parse_options($command, $options_ref, @args);
+}
+
+##############################################################################
+# Public interface
+##############################################################################
+
+# Create a new App::DocKnot::Command object.
+#
+# $class - Class of object to create
+#
+# Returns: Newly created object
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+# Parse command-line options to determine which command to run, and then
+# dispatch that command.
+#
+# $self - The App::DocKnot::Command object
+# @args - Command-line arguments (optional, default: @ARGV)
+#
+# Returns: undef
+# Throws: A text error message for invalid arguments
+sub run {
+ my ($self, @args) = @_;
+ if (!@args) {
+ @args = @ARGV;
+ }
+
+ # Parse the initial options and extract the subcommand to run, preserving
+ # any options after the subcommand.
+ my $spec = ['help|h'];
+ my ($opts_ref, $args_ref) = $self->_parse_options(undef, $spec, @args);
+ if ($opts_ref->{help}) {
+ pod2usage(0);
+ }
+ if (!$args_ref->@*) {
+ die "$0: no subcommand given\n";
+ }
+ my $command = shift($args_ref->@*);
+ if (!$COMMANDS{$command}) {
+ die "$0: unknown command $command\n";
+ }
+
+ # Parse the arguments for the command and check for required arguments.
+ ($opts_ref, $args_ref) = $self->_parse_command($command, $args_ref->@*);
+ if (exists($COMMANDS{$command}{required})) {
+ for my $required ($COMMANDS{$command}{required}->@*) {
+ if (!exists($opts_ref->{$required})) {
+ die "$0 $command: missing required option --$required\n";
+ }
+ }
+ }
+
+ # Check that we have the correct number of remaining arguments.
+ if (exists($COMMANDS{$command}{maximum})) {
+ if (scalar($args_ref->@*) > $COMMANDS{$command}{maximum}) {
+ die "$0 $command: too many arguments\n";
+ }
+ }
+ if (exists($COMMANDS{$command}{minimum})) {
+ if (scalar($args_ref->@*) < $COMMANDS{$command}{minimum}) {
+ die "$0 $command: too few arguments\n";
+ }
+ }
+
+ # Dispatch the command and turn exceptions into error messages.
+ eval {
+ if ($COMMANDS{$command}{code}) {
+ $COMMANDS{$command}{code}->($opts_ref, $args_ref->@*);
+ } else {
+ my $object = $COMMANDS{$command}{module}->new($opts_ref);
+ my $method = $COMMANDS{$command}{method};
+ $object->$method($args_ref->@*);
+ }
+ };
+ if ($@) {
+ my $error = $@;
+ chomp($error);
+ $error =~ s{ \s+ at \s+ \S+ \s+ line \s+ \d+ [.]? \z }{}xms;
+ die "$0 $command: $error\n";
+ }
+ return;
+}
+
+##############################################################################
+# Module return value and documentation
+##############################################################################
+
+1;
+__END__
+
+=for stopwords
+Allbery DocKnot docknot MERCHANTABILITY NONINFRINGEMENT sublicense
+
+=head1 NAME
+
+App::DocKnot::Command - Run DocKnot commands
+
+=head1 SYNOPSIS
+
+ my $docknot = App::DocKnot::Command->new();
+ $docknot->run();
+
+=head1 REQUIREMENTS
+
+Perl 5.24 or later and the modules File::BaseDir, File::ShareDir, JSON,
+Perl6::Slurp, and Template (part of Template Toolkit), all of which are
+available from CPAN.
+
+=head1 DESCRIPTION
+
+The App::DocKnot::Command module implements the B<docknot> command-line
+interface to all of the functions of DocKnot. It is an implementation detail
+of the B<docknot> command-line tool and is normally only called by that
+program.
+
+For full documentation, see L<docknot(1)>.
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item new()
+
+Create a new App::DocKnot::Command object.
+
+=back
+
+=head1 INSTANCE METHODS
+
+=over 4
+
+=item run([ARGS])
+
+Run the DocKnot action specified by ARGS, which are parsed as command-line
+arguments to B<docknot>. If ARGS is not given or is empty, C<@ARGV> will be
+parsed instead.
+
+=back
+
+=head1 AUTHOR
+
+Russ Allbery <rra@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2018-2019 Russ Allbery <rra@cpan.org>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+L<App::DocKnot::Generate>, L<docknot(1)>
+
+This module is part of the App-DocKnot distribution. The current version of
+App::DocKnot is available from CPAN, or directly from its web site at
+L<https://www.eyrie.org/~eagle/software/docknot/>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End: