1 | # ------------------------------------------------------------------------------ |
---|
2 | # Copyright (C) 2006-2021 British Crown (Met Office) & Contributors. |
---|
3 | # |
---|
4 | # This file is part of FCM, tools for managing and building source code. |
---|
5 | # |
---|
6 | # FCM is free software: you can redistribute it and/or modify |
---|
7 | # it under the terms of the GNU General Public License as published by |
---|
8 | # the Free Software Foundation, either version 3 of the License, or |
---|
9 | # (at your option) any later version. |
---|
10 | # |
---|
11 | # FCM is distributed in the hope that it will be useful, |
---|
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
14 | # GNU General Public License for more details. |
---|
15 | # |
---|
16 | # You should have received a copy of the GNU General Public License |
---|
17 | # along with FCM. If not, see <http://www.gnu.org/licenses/>. |
---|
18 | # ------------------------------------------------------------------------------ |
---|
19 | use strict; |
---|
20 | use warnings; |
---|
21 | |
---|
22 | # ------------------------------------------------------------------------------ |
---|
23 | package FCM::CLI::Parser; |
---|
24 | use base qw{FCM::Class::CODE}; |
---|
25 | |
---|
26 | use FCM::CLI::Exception; |
---|
27 | use Getopt::Long qw{GetOptions :config bundling}; |
---|
28 | |
---|
29 | use constant { |
---|
30 | OPT_INCR => q{+}, # no argument, but incremental |
---|
31 | OPT_BOOL => q{}, # no argument |
---|
32 | OPT_SCAL => q{=s}, # single argument |
---|
33 | OPT_LIST => q{=s@}, # multiple argument |
---|
34 | }; |
---|
35 | |
---|
36 | # Option hash, key = preferred name of option, value = HASH reference where: |
---|
37 | # arg => argument flag |
---|
38 | # letters => ARRAY reference of a list of option letters |
---|
39 | # names => ARRAY reference of a list of names |
---|
40 | our %OPTION_OF = map { |
---|
41 | ($_->[0][0], {arg => $_->[2], letters => $_->[1], names => $_->[0]}); |
---|
42 | } ( |
---|
43 | [['archive' , ], ['a'], OPT_BOOL], |
---|
44 | [['auto-log' , ], [ ], OPT_BOOL], |
---|
45 | [['branch' , ], ['b'], OPT_BOOL], |
---|
46 | [['branch-of-branch' , 'bob' ], [ ], OPT_BOOL], |
---|
47 | [['browser' , ], ['b'], OPT_SCAL], |
---|
48 | [['check' , ], ['c'], OPT_BOOL], |
---|
49 | [['clean' , ], [ ], OPT_BOOL], |
---|
50 | [['create' , ], ['c'], OPT_BOOL], |
---|
51 | [['config-file' , 'file' ], ['f'], OPT_LIST], |
---|
52 | [['config-file-path' , ], ['F'], OPT_LIST], |
---|
53 | [['custom' , ], [ ], OPT_BOOL], |
---|
54 | [['delete' , ], ['d'], OPT_BOOL], |
---|
55 | [['diff-cmd' , ], [ ], OPT_SCAL], |
---|
56 | [['directory' , ], ['C'], OPT_SCAL], |
---|
57 | [['dry-run' , ], [ ], OPT_BOOL], |
---|
58 | [['exclude' , ], [ ], OPT_LIST], |
---|
59 | [['extensions' , ], ['x'], OPT_SCAL], |
---|
60 | [['graphical' , ], ['g'], OPT_BOOL], |
---|
61 | [['fcm1' , ], ['1'], OPT_BOOL], |
---|
62 | [['full' , ], ['f'], OPT_BOOL], |
---|
63 | [['help' , 'usage' ], ['h'], OPT_BOOL], |
---|
64 | [['ignore-lock' , ], [ ], OPT_BOOL], |
---|
65 | [['info' , ], ['i'], OPT_BOOL], |
---|
66 | [['jobs' , ], ['j'], OPT_SCAL], |
---|
67 | [['list' , ], ['l'], OPT_BOOL], |
---|
68 | [['name' , ], ['n'], OPT_SCAL], |
---|
69 | [['new' , ], ['N'], OPT_BOOL], |
---|
70 | [['non-interactive' , ], [ ], OPT_BOOL], |
---|
71 | [['only' , ], [ ], OPT_LIST], |
---|
72 | [['organisation' , ], [ ], OPT_SCAL], |
---|
73 | [['password' , ], [ ], OPT_SCAL], |
---|
74 | [['quiet' , ], ['q'], OPT_INCR], |
---|
75 | [['relocate' , ], [ ], OPT_BOOL], |
---|
76 | [['reverse' , ], [ ], OPT_BOOL], |
---|
77 | [['revision' , ], ['r'], OPT_SCAL], |
---|
78 | [['rev-flag' , ], [ ], OPT_SCAL], |
---|
79 | [['show-all' , ], ['a'], OPT_BOOL], |
---|
80 | [['show-children' , ], [ ], OPT_BOOL], |
---|
81 | [['show-other' , ], [ ], OPT_BOOL], |
---|
82 | [['show-siblings' , ], [ ], OPT_BOOL], |
---|
83 | [['stage' , ], ['s'], OPT_SCAL], |
---|
84 | [['summarize' , 'summarise'], [ ], OPT_BOOL], |
---|
85 | [['svn-non-interactive', ], [ ], OPT_BOOL], |
---|
86 | [['switch' , ], ['s'], OPT_BOOL], |
---|
87 | [['targets' , ], ['t'], OPT_LIST], |
---|
88 | [['ticket' , ], ['k'], OPT_LIST], |
---|
89 | [['trac' , ], ['t'], OPT_BOOL], |
---|
90 | [['type' , ], ['t'], OPT_SCAL], |
---|
91 | [['url' , ], [ ], OPT_BOOL], |
---|
92 | [['user' , ], ['u'], OPT_LIST], |
---|
93 | [['verbose' , ], ['v'], OPT_INCR], |
---|
94 | [['verbosity' , ], ['v'], OPT_SCAL], |
---|
95 | [['wiki' , ], ['w'], OPT_BOOL], |
---|
96 | [['wiki-format' , 'wiki' ], ['w'], OPT_SCAL], |
---|
97 | [['xml' , ], [ ], OPT_BOOL], |
---|
98 | ); |
---|
99 | # Hook command before parsing the options |
---|
100 | our %HOOK_BEFORE_FOR = ( |
---|
101 | 'add' => _get_code_to_match($OPTION_OF{check}), |
---|
102 | 'delete' => _get_code_to_match($OPTION_OF{check}), |
---|
103 | 'diff' => sub { |
---|
104 | _get_code_to_replace( |
---|
105 | $OPTION_OF{graphical}, [qw{ |
---|
106 | --config-option config:working-copy:exclusive-locking-clients= |
---|
107 | --diff-cmd fcm_graphic_diff |
---|
108 | }] |
---|
109 | )->(@_); |
---|
110 | _get_code_to_replace($OPTION_OF{summarize}, ['--summarize'])->(@_); |
---|
111 | _get_code_to_match($OPTION_OF{branch})->(@_); |
---|
112 | }, |
---|
113 | 'switch' => sub {!_get_code_to_match($OPTION_OF{relocate})->(@_)}, |
---|
114 | ); |
---|
115 | our $HELP_APP = 'help'; |
---|
116 | # Options for known applications |
---|
117 | our %OPTIONS_FOR = ( |
---|
118 | 'add' => [$OPTION_OF{check}], |
---|
119 | 'branch' => [@OPTION_OF{ |
---|
120 | qw{ branch-of-branch create delete info list name non-interactive |
---|
121 | password quiet revision rev-flag show-all show-children |
---|
122 | show-siblings svn-non-interactive ticket type user verbose |
---|
123 | } |
---|
124 | }], |
---|
125 | 'branch-create' => [@OPTION_OF{ |
---|
126 | qw{ branch-of-branch non-interactive password rev-flag |
---|
127 | svn-non-interactive switch ticket type |
---|
128 | } |
---|
129 | }], |
---|
130 | 'branch-delete' => [@OPTION_OF{ |
---|
131 | qw{ non-interactive password quiet show-all show-children show-siblings |
---|
132 | svn-non-interactive switch verbose |
---|
133 | } |
---|
134 | }], |
---|
135 | 'branch-diff' => [@OPTION_OF{ |
---|
136 | qw{diff-cmd graphical extensions summarize trac wiki xml} |
---|
137 | }], |
---|
138 | 'branch-info' => [@OPTION_OF{ |
---|
139 | qw{quiet show-all show-children show-siblings verbose} |
---|
140 | }], |
---|
141 | 'branch-list' => [@OPTION_OF{ |
---|
142 | qw{only quiet show-all url user verbose} |
---|
143 | }], |
---|
144 | 'browse' => [$OPTION_OF{browser}], |
---|
145 | 'build' => [@OPTION_OF{ |
---|
146 | qw{archive clean full ignore-lock jobs stage targets verbosity} |
---|
147 | }], |
---|
148 | 'cfg-print' => [$OPTION_OF{fcm1}], |
---|
149 | 'cmp-ext-cfg' => [@OPTION_OF{qw{quiet verbose wiki-format}}], |
---|
150 | 'commit' => [@OPTION_OF{ |
---|
151 | qw{dry-run password svn-non-interactive} |
---|
152 | }], |
---|
153 | 'conflicts' => [], |
---|
154 | 'delete' => [$OPTION_OF{check}], |
---|
155 | 'diff' => [@OPTION_OF{ |
---|
156 | qw{branch diff-cmd extensions summarize trac wiki} |
---|
157 | }], |
---|
158 | 'export-items' => [@OPTION_OF{qw{directory config-file new}}], |
---|
159 | 'extract' => [@OPTION_OF{qw{clean full ignore-lock verbosity}}], |
---|
160 | 'gui' => [], |
---|
161 | $HELP_APP => [@OPTION_OF{qw{quiet verbose}}], |
---|
162 | 'keyword-print' => [@OPTION_OF{qw{verbose}}], |
---|
163 | 'loc-layout' => [@OPTION_OF{qw{verbose}}], |
---|
164 | 'make' => [@OPTION_OF{ |
---|
165 | qw{ archive directory ignore-lock jobs config-file config-file-path name |
---|
166 | new quiet verbose |
---|
167 | } |
---|
168 | }], |
---|
169 | 'merge' => [@OPTION_OF{ |
---|
170 | qw{ auto-log custom dry-run non-interactive quiet reverse revision |
---|
171 | verbose} |
---|
172 | }], |
---|
173 | 'mkpatch' => [@OPTION_OF{qw{exclude organisation revision}}], |
---|
174 | 'project-create'=> [@OPTION_OF{ |
---|
175 | qw{non-interactive password svn-non-interactive} |
---|
176 | }], |
---|
177 | 'switch' => [@OPTION_OF{qw{non-interactive revision quiet verbose}}], |
---|
178 | 'update' => [@OPTION_OF{qw{non-interactive revision quiet verbose}}], |
---|
179 | ); |
---|
180 | # Preferred names of known applications with aliases |
---|
181 | our %PREF_NAME_OF = ( |
---|
182 | 'ann' => 'blame', |
---|
183 | 'annotate' => 'blame', |
---|
184 | 'bcreate' => 'branch-create', |
---|
185 | 'bc' => 'branch-create', |
---|
186 | 'bdel' => 'branch-delete', |
---|
187 | 'bdelete' => 'branch-delete', |
---|
188 | 'bdi' => 'branch-diff', |
---|
189 | 'bdiff' => 'branch-diff', |
---|
190 | 'binfo' => 'branch-info', |
---|
191 | 'bld' => 'build', |
---|
192 | 'blist' => 'branch-list', |
---|
193 | 'bls' => 'branch-list', |
---|
194 | 'br' => 'branch', |
---|
195 | 'brm' => 'branch-delete', |
---|
196 | 'cfg' => 'cfg-print', |
---|
197 | 'ci' => 'commit', |
---|
198 | 'cf' => 'conflicts', |
---|
199 | 'co' => 'checkout', |
---|
200 | 'cp' => 'copy', |
---|
201 | 'del' => 'delete', |
---|
202 | 'di' => 'diff', |
---|
203 | 'ext' => 'extract', |
---|
204 | 'h' => $HELP_APP, |
---|
205 | 'kp' => 'keyword-print', |
---|
206 | 'ls' => 'list', |
---|
207 | 'mv' => 'move', |
---|
208 | 'pd' => 'propdel', |
---|
209 | 'pdel' => 'propdel', |
---|
210 | 'pe' => 'propedit', |
---|
211 | 'pedit' => 'propedit', |
---|
212 | 'pg' => 'propget', |
---|
213 | 'pget' => 'propget', |
---|
214 | 'pl' => 'proplist', |
---|
215 | 'plist' => 'proplist', |
---|
216 | 'praise' => 'blame', |
---|
217 | 'ps' => 'propset', |
---|
218 | 'pset' => 'propset', |
---|
219 | 'ren' => 'move', |
---|
220 | 'rename' => 'move', |
---|
221 | 'rm' => 'delete', |
---|
222 | 'remove' => 'delete', |
---|
223 | 'st' => 'status', |
---|
224 | 'sw' => 'switch', |
---|
225 | 'stat' => 'status', |
---|
226 | 'trac' => 'browse', |
---|
227 | 'up' => 'update', |
---|
228 | 'usage' => $HELP_APP, |
---|
229 | 'www' => 'browse', |
---|
230 | '?' => $HELP_APP, |
---|
231 | '-V' => 'version', |
---|
232 | '--help' => $HELP_APP, |
---|
233 | '--usage' => $HELP_APP, |
---|
234 | '--version'=> 'version', |
---|
235 | ); |
---|
236 | |
---|
237 | # Creates the class. |
---|
238 | __PACKAGE__->class( |
---|
239 | { help_app => {isa => '$', default => $HELP_APP }, |
---|
240 | help_option => {isa => '%', default => {%{$OPTION_OF{help}}}}, |
---|
241 | hook_before_for => {isa => '%', default => {%HOOK_BEFORE_FOR} }, |
---|
242 | options_for => {isa => '%', default => {%OPTIONS_FOR} }, |
---|
243 | pref_name_of => {isa => '%', default => {%PREF_NAME_OF} }, |
---|
244 | }, |
---|
245 | {action_of => {parse => \&_parse}}, |
---|
246 | ); |
---|
247 | |
---|
248 | # Parses the options and arguments. |
---|
249 | sub _parse { |
---|
250 | my ($attrib_ref, @argv) = @_; |
---|
251 | my @args = @argv; |
---|
252 | my $option_hash_ref = {}; |
---|
253 | if (!@args) { |
---|
254 | return ($attrib_ref->{help_app}, $option_hash_ref); |
---|
255 | } |
---|
256 | my $app = shift(@args); |
---|
257 | if (exists($attrib_ref->{pref_name_of}{$app})) { |
---|
258 | $app = $attrib_ref->{pref_name_of}{$app}; |
---|
259 | } |
---|
260 | if (_get_code_to_match($attrib_ref->{help_option})->(\@args)) { |
---|
261 | return ($attrib_ref->{help_app}, {}, $app); |
---|
262 | } |
---|
263 | if (exists($attrib_ref->{hook_before_for}{$app})) { |
---|
264 | if (!$attrib_ref->{hook_before_for}{$app}->(\@args)) { |
---|
265 | return ($app, $option_hash_ref, @args); |
---|
266 | } |
---|
267 | } |
---|
268 | if (!exists($attrib_ref->{options_for}{$app})) { |
---|
269 | return ($app, $option_hash_ref, @args); |
---|
270 | } |
---|
271 | my @option_strings = map { |
---|
272 | join('|', @{$_->{names}}, @{$_->{letters}}) . $_->{arg}; |
---|
273 | } @{$attrib_ref->{options_for}{$app}}; |
---|
274 | local(@ARGV) = @args; |
---|
275 | my @warnings; |
---|
276 | local($SIG{__WARN__}) = sub {push(@warnings, @_)}; |
---|
277 | if (!GetOptions($option_hash_ref, @option_strings)) { |
---|
278 | my $E = 'FCM::CLI::Exception'; |
---|
279 | for (@warnings) { |
---|
280 | chomp(); |
---|
281 | } |
---|
282 | return $E->throw($E->OPT, \@argv, join('|', @warnings)); |
---|
283 | } |
---|
284 | @args = @ARGV; |
---|
285 | return ($app, $option_hash_ref, @args); |
---|
286 | } |
---|
287 | |
---|
288 | # Returns a CODE reference for matching a simple option to a string. |
---|
289 | sub _get_option_matcher { |
---|
290 | my ($option_ref) = @_; |
---|
291 | return sub { |
---|
292 | grep {$_[0] eq $_} ( |
---|
293 | (map {"--$_"} @{$option_ref->{names} }), |
---|
294 | (map { "-$_"} @{$option_ref->{letters}}), |
---|
295 | ); |
---|
296 | }; |
---|
297 | } |
---|
298 | |
---|
299 | # Returns a CODE reference for matching a simple option to a string. |
---|
300 | sub _get_code_to_match { |
---|
301 | my ($option_ref) = @_; |
---|
302 | my $grepper = _get_option_matcher($option_ref); |
---|
303 | return sub {grep {$grepper->($_)} @{$_[0]}}; |
---|
304 | } |
---|
305 | |
---|
306 | # Returns a CODE reference to replace a simple option in the argument list. |
---|
307 | sub _get_code_to_replace { |
---|
308 | my ($option_ref, $replacement) = @_; |
---|
309 | my @replacements = ref($replacement) ? @{$replacement} : $replacement; |
---|
310 | my $grepper = _get_option_matcher($option_ref); |
---|
311 | return sub { |
---|
312 | @{$_[0]} = map {($grepper->($_) ? @replacements : $_)} @{$_[0]}; |
---|
313 | return 1; |
---|
314 | }; |
---|
315 | } |
---|
316 | |
---|
317 | # ------------------------------------------------------------------------------ |
---|
318 | 1; |
---|
319 | __END__ |
---|
320 | |
---|
321 | =head1 NAME |
---|
322 | |
---|
323 | FCM::CLI::Parser |
---|
324 | |
---|
325 | =head1 SYNOPSIS |
---|
326 | |
---|
327 | use FCM::CLI::Parser; |
---|
328 | my $cli = FCM::CLI::Parser->new(\%attrib); |
---|
329 | my ($app, $opt_hash_ref, @args) = $cli->(@ARGV); |
---|
330 | |
---|
331 | =head1 DESCRIPTION |
---|
332 | |
---|
333 | This class provides an option/argument parser for the FCM command line |
---|
334 | interface. The parser, when called with some arguments, returns a list. The 1st |
---|
335 | element is the name of the application, the 2nd element is a HASH reference |
---|
336 | containing the option names and their values. The remaining elements are the |
---|
337 | remaining arguments. |
---|
338 | |
---|
339 | =head1 METHODS |
---|
340 | |
---|
341 | =over 4 |
---|
342 | |
---|
343 | =item $class->new(\%attrib) |
---|
344 | |
---|
345 | Returns a new instance. The %attrib HASH may contain the following elements: |
---|
346 | |
---|
347 | =over 4 |
---|
348 | |
---|
349 | =item help_app |
---|
350 | |
---|
351 | The name of the I<help> application. Default = $FCM::CLI::Parser::HELP_APP. |
---|
352 | |
---|
353 | =item help_option |
---|
354 | |
---|
355 | An option that represents I<help>. If this option is encountered in the command |
---|
356 | line, the CODE reference returns (help_app, {}, $app) regardless of the other |
---|
357 | command line options and arguments. Default = |
---|
358 | $FCM::CLI::Parser::OPTIONS_FOR{help}. |
---|
359 | |
---|
360 | =item hook_before_for |
---|
361 | |
---|
362 | Hook commands for the applications, which are executed before the option parser. |
---|
363 | See the L</CONFIGURATIONS> section for detail. Default = |
---|
364 | $FCM::CLI::Parser::HOOK_BEFORE_FOR. |
---|
365 | |
---|
366 | =item options_for |
---|
367 | |
---|
368 | The options for each application. See the L</CONFIGURATIONS> section for detail. |
---|
369 | Default = $FCM::CLI::Parser::OPTIONS_FOR. |
---|
370 | |
---|
371 | =item pref_name_of |
---|
372 | |
---|
373 | The preferred names for the applications. See the L</CONFIGURATIONS> section for |
---|
374 | detail. Default = $FCM::CLI::Parser::PREF_NAME_OF. |
---|
375 | |
---|
376 | =back |
---|
377 | |
---|
378 | =item $instance->(@args) |
---|
379 | |
---|
380 | =back |
---|
381 | |
---|
382 | =head1 CONFIGURATIONS |
---|
383 | |
---|
384 | The following should only be used as read-only variables. The |
---|
385 | $class->new(\%attrib) method should be used to configure a parser. |
---|
386 | |
---|
387 | =over 4 |
---|
388 | |
---|
389 | =item $FCM::CLI::Parser::HELP_APP |
---|
390 | |
---|
391 | The name of the I<help> application. |
---|
392 | |
---|
393 | =item %FCM::CLI::Parser::HOOK_BEFORE_FOR |
---|
394 | |
---|
395 | A hash containing the hook commands, which are invoked before calling the option |
---|
396 | parser. The hash keys are names of the applications, and the values are CODE |
---|
397 | references to invoke. If a hook exists for an application, it is called as |
---|
398 | $hook->(\@args) where @args is the current command line arguments (with the |
---|
399 | first argument, i.e. the application name removed). If the hook returns a false |
---|
400 | value, the parser will return immediately. |
---|
401 | |
---|
402 | =item %FCM::CLI::Parser::OPTION_OF |
---|
403 | |
---|
404 | A hash containing the known options. The key is the preferred name of the |
---|
405 | option, and the value is a HASH reference, where C<names> (=> ARRAY reference) |
---|
406 | are the long names of the option, C<letters> (=> ARRAY reference) are the |
---|
407 | option letters, C<arg> (=> integer) is a flag. (See L</CONSTANTS> section for |
---|
408 | detail.) |
---|
409 | |
---|
410 | =item %FCM::CLI::Parser::OPTIONS_FOR |
---|
411 | |
---|
412 | A hash containing the known applications. The keys are the names of the |
---|
413 | applications and the values are ARRAY references, each pointing to |
---|
414 | a list of options (as described in %FCM::CLIParser::OPTION_OF) for the |
---|
415 | application. |
---|
416 | |
---|
417 | =item %FCM::CLI::Parser::PREF_NAME_OF |
---|
418 | |
---|
419 | A hash containing the preferred names of an application. The keys are the |
---|
420 | aliases and the values are the preferred names. |
---|
421 | |
---|
422 | =back |
---|
423 | |
---|
424 | =head1 CONSTANTS |
---|
425 | |
---|
426 | =over 4 |
---|
427 | |
---|
428 | =item FCM::CLI::Parser->OPT_BOOL |
---|
429 | |
---|
430 | Option flag. Option is a boolean with no argument. |
---|
431 | |
---|
432 | =item FCM::CLI::Parser->OPT_INCR |
---|
433 | |
---|
434 | Option flag. Option has no argument but is incremental. |
---|
435 | |
---|
436 | =item FCM::CLI::Parser->OPT_LIST |
---|
437 | |
---|
438 | Option flag. Option has one or more arguments. |
---|
439 | |
---|
440 | =item FCM::CLI::Parser->OPT_SCAL |
---|
441 | |
---|
442 | Option flag. Option has a single argument. |
---|
443 | |
---|
444 | =back |
---|
445 | |
---|
446 | =head1 DIAGNOSTICS |
---|
447 | |
---|
448 | =over 4 |
---|
449 | |
---|
450 | =item FCM::CLI::Parser::Exception |
---|
451 | |
---|
452 | This exception is raised if an invalid command option is given. It inherits from |
---|
453 | L<FCM::Exception>. There is no error code associated with this exception. The |
---|
454 | $e->get_ctx() method returns an ARRAY reference containing the original |
---|
455 | arguments. |
---|
456 | |
---|
457 | =back |
---|
458 | |
---|
459 | =head1 COPYRIGHT |
---|
460 | |
---|
461 | Copyright (C) 2006-2021 British Crown (Met Office) & Contributors. |
---|
462 | |
---|
463 | =cut |
---|