File Coverage

blib/lib/App/DocKnot.pm
Criterion Covered Total %
statement 67 72 93.0
branch 22 28 78.5
condition n/a
subroutine 10 10 100.0
pod 2 2 100.0
total 101 112 90.1


line stmt bran cond sub pod time code
1             # Dispatch code for the DocKnot application.
2             #
3             # DocKnot provides various commands for generating documentation, web pages,
4             # and software releases. This module provides command-line parsing and
5             # dispatch of commands to the various App::DocKnot modules.
6             #
7             # SPDX-License-Identifier: MIT
8              
9             ##############################################################################
10             # Modules and declarations
11             ##############################################################################
12              
13             package App::DocKnot 2.00;
14              
15 6     6   684801 use 5.024;
  6         77  
16 6     6   40 use autodie;
  6         42  
  6         50  
17 6     6   33021 use warnings;
  6         12  
  6         248  
18              
19 6     6   3310 use App::DocKnot::Generate;
  6         22  
  6         245  
20 6     6   4408 use Getopt::Long;
  6         62648  
  6         28  
21              
22             # Defines the subcommands, their options, and the module and method that
23             # implements them. The keys are the names of the commands. Each value is a
24             # hash with one or more of the following keys:
25             #
26             # code
27             # A reference to a function to call to implement this command. If set,
28             # overrides method and module. The function will be passed a reference to
29             # the hash resulting from option parsing as its first argument and any
30             # other command-line arguments as its remaining arguments.
31             #
32             # maximum
33             # The maximum number of positional arguments this command takes.
34             #
35             # method
36             # The name of the method to run to implement this command. It is passed
37             # as arguments any remaining command-line arguments after option parsing.
38             #
39             # minimum
40             # The minimum number of positional arguments this command takes.
41             #
42             # module
43             # The name of the module that implements this command. Its constructor
44             # (which must be named new) will be passed as its sole argument a
45             # reference to the hash containing the results of parsing any options.
46             #
47             # options
48             # A reference to an array of Getopt::Long option specifications defining
49             # the arguments that can be passed to this subcommand.
50             #
51             # required
52             # A reference to an array of required option names (the part before any |
53             # in the option specification for that option). If any of these options
54             # are not set, an error will be thrown.
55             our %COMMANDS = (
56             generate => {
57             method => 'generate_output',
58             module => 'App::DocKnot::Generate',
59             options => ['metadata|m=s', 'width|w=i'],
60             maximum => 2,
61             minimum => 1,
62             },
63             'generate-all' => {
64             method => 'generate_all',
65             module => 'App::DocKnot::Generate',
66             options => ['metadata|m=s', 'width|w=i'],
67             maximum => 0,
68             },
69             );
70              
71             ##############################################################################
72             # Option parsing
73             ##############################################################################
74              
75             # Parse command-line options and do any required error handling.
76             #
77             # $self - The App::DocKnot object
78             # $command - The command being run or undef for top-level options
79             # $options_ref - A reference to the options specification
80             # @args - The arguments to the command
81             #
82             # Returns: A list composed of a reference to a hash of options and values,
83             # followed by a reference to the remaining arguments after options
84             # have been extracted
85             # Throws: A text error message if the options are invalid
86             sub _parse_options {
87 21     21   60 my ($self, $command, $options_ref, @args) = @_;
88              
89             # Use the object-oriented syntax to isolate configuration options from the
90             # rest of the program.
91 21         98 my $parser = Getopt::Long::Parser->new;
92 21         407 $parser->configure(qw(bundling no_ignore_case require_order));
93              
94             # Parse the options and capture any errors, turning them into exceptions.
95             # The first letter of the Getopt::Long warning message will be capitalized
96             # but we want it to be lowercase to follow our error message standard.
97 21         1596 my %opts;
98             {
99 21         36 my $error = 'option parsing failed';
  21         33  
100 21     2   135 local $SIG{__WARN__} = sub { ($error) = @_ };
  2         518  
101 21 100       83 if (!$parser->getoptionsfromarray(\@args, \%opts, $options_ref->@*)) {
102 2         120 $error =~ s{ \n+ \z }{}xms;
103 2         12 $error =~ s{ \A (\w) }{ lc($1) }xmse;
  2         7  
104 2 100       7 if ($command) {
105 1         13 die "$0 $command: $error\n";
106             } else {
107 1         15 die "$0: $error\n";
108             }
109             }
110             }
111              
112             # Success. Return the options and the remaining arguments.
113 19         4754 return (\%opts, \@args);
114             }
115              
116             # Parse command-line options for a given command.
117             #
118             # $self - The App::DocKnot object
119             # $command - The command being run
120             # @args - The arguments to the command
121             #
122             # Returns: A list composed of a reference to a hash of options and values,
123             # followed by a reference to the remaining arguments after options
124             # have been extracted
125             # Throws: A text error message if the options are invalid
126             sub _parse_command {
127 9     9   30 my ($self, $command, @args) = @_;
128 9         22 my $options_ref = $COMMANDS{$command}{options};
129 9         27 return $self->_parse_options($command, $options_ref, @args);
130             }
131              
132             ##############################################################################
133             # Public interface
134             ##############################################################################
135              
136             # Create a new App::DocKnot object.
137             #
138             # $class - Class of object to create
139             #
140             # Returns: Newly created object
141             sub new {
142 2     2 1 210 my ($class) = @_;
143 2         7 my $self = {};
144 2         4 bless($self, $class);
145 2         6 return $self;
146             }
147              
148             # Parse command-line options to determine which command to run, and then
149             # dispatch that command.
150             #
151             # $self - The App::DocKnot object
152             # @args - Command-line arguments (optional, default: @ARGV)
153             #
154             # Returns: undef
155             # Throws: A text error message for invalid arguments
156             sub run {
157 12     12 1 12659 my ($self, @args) = @_;
158 12 100       46 if (!@args) {
159 2         7 @args = @ARGV;
160             }
161              
162             # Parse the initial options and extract the subcommand to run, preserving
163             # any options after the subcommand.
164 12         32 my $spec = ['help|h'];
165 12         44 my ($opts_ref, $args_ref) = $self->_parse_options(undef, $spec, @args);
166 11 50       38 if ($opts_ref->{help}) {
167 0         0 pod2usage(0);
168             }
169 11 100       36 if (!$args_ref->@*) {
170 1         8 die "$0: no subcommand given\n";
171             }
172 10         19 my $command = shift($args_ref->@*);
173 10 100       46 if (!$COMMANDS{$command}) {
174 1         10 die "$0: unknown command $command\n";
175             }
176              
177             # Parse the arguments for the command and check for required arguments.
178 9         31 ($opts_ref, $args_ref) = $self->_parse_command($command, $args_ref->@*);
179 8 50       39 if (exists($COMMANDS{$command}{required})) {
180 0         0 for my $required ($COMMANDS{$command}{required}->@*) {
181 0 0       0 if (!exists($opts_ref->{$required})) {
182 0         0 die "$0 $command: missing required option --$required\n";
183             }
184             }
185             }
186              
187             # Check that we have the correct number of remaining arguments.
188 8 50       30 if (exists($COMMANDS{$command}{maximum})) {
189 8 100       32 if (scalar($args_ref->@*) > $COMMANDS{$command}{maximum}) {
190 2         19 die "$0 $command: too many arguments\n";
191             }
192             }
193 6 100       24 if (exists($COMMANDS{$command}{minimum})) {
194 5 100       22 if (scalar($args_ref->@*) < $COMMANDS{$command}{minimum}) {
195 1         9 die "$0 $command: too few arguments\n";
196             }
197             }
198              
199             # Dispatch the command and turn exceptions into error messages.
200 5         61 eval {
201 5 50       21 if ($COMMANDS{$command}{code}) {
202 0         0 $COMMANDS{$command}{code}->($opts_ref, $args_ref->@*);
203             } else {
204 5         46 my $object = $COMMANDS{$command}{module}->new($opts_ref);
205 4         14 my $method = $COMMANDS{$command}{method};
206 4         23 $object->$method($args_ref->@*);
207             }
208             };
209 5 100       57 if ($@) {
210 1         2 my $error = $@;
211 1         3 chomp($error);
212 1         9 $error =~ s{ \s+ at \s+ \S+ \s+ line \s+ \d+ [.]? \z }{}xms;
213 1         9 die "$0 $command: $error\n";
214             }
215 4         34 return;
216             }
217              
218             1;
219             __END__