File Coverage

blib/lib/Monitoring/Plugin/Getopt.pm
Criterion Covered Total %
statement 214 218 98.1
branch 76 86 88.3
condition 25 41 60.9
subroutine 31 31 100.0
pod 1 3 33.3
total 347 379 91.5


line stmt bran cond sub pod time code
1             package Monitoring::Plugin::Getopt;
2              
3             #
4             # Monitoring::Plugin::Getopt - OO perl module providing standardised argument
5             # processing for nagios plugins
6             #
7              
8 6     6   72518 use 5.006;
  6         16  
  6         213  
9 6     6   26 use strict;
  6         9  
  6         208  
10 6     6   26 use warnings;
  6         13  
  6         201  
11              
12 6     6   29 use File::Basename;
  6         7  
  6         582  
13 6     6   4406 use Getopt::Long qw(:config no_ignore_case bundling);
  6         65681  
  6         35  
14 6     6   1210 use Carp;
  6         11  
  6         439  
15 6     6   2004 use Params::Validate qw(:all);
  6         28555  
  6         1117  
16 6     6   38 use base qw(Class::Accessor);
  6         9  
  6         2298  
17              
18 6     6   7471 use Monitoring::Plugin::Functions;
  6         15  
  6         471  
19 6     6   3098 use Monitoring::Plugin::Config;
  6         11  
  6         209  
20 6     6   30 use vars qw($VERSION);
  6         8  
  6         13469  
21             $VERSION = $Monitoring::Plugin::Functions::VERSION;
22              
23             # Standard defaults
24             my %DEFAULT = (
25             timeout => 15,
26             verbose => 0,
27             license =>
28             "This nagios plugin is free software, and comes with ABSOLUTELY NO WARRANTY.
29             It may be used, redistributed and/or modified under the terms of the GNU
30             General Public Licence (see http://www.fsf.org/licensing/licenses/gpl.txt).",
31             );
32             # Standard arguments
33             my @ARGS = ({
34             spec => 'usage|?',
35             help => "-?, --usage\n Print usage information",
36             }, {
37             spec => 'help|h',
38             help => "-h, --help\n Print detailed help screen",
39             }, {
40             spec => 'version|V',
41             help => "-V, --version\n Print version information",
42             }, {
43             spec => 'extra-opts:s@',
44             help => "--extra-opts=[section][\@file]\n Read options from an ini file. See https://www.monitoring-plugins.org/doc/extra-opts.html\n for usage and examples.",
45             }, {
46             spec => 'timeout|t=i',
47             help => "-t, --timeout=INTEGER\n Seconds before plugin times out (default: %s)",
48             default => $DEFAULT{timeout},
49             }, {
50             spec => 'verbose|v+',
51             help => "-v, --verbose\n Show details for command-line debugging (can repeat up to 3 times)",
52             default => $DEFAULT{verbose},
53             },
54             );
55             # Standard arguments we traditionally display last in the help output
56             my %DEFER_ARGS = map { $_ => 1 } qw(timeout verbose);
57              
58             # -------------------------------------------------------------------------
59             # Private methods
60              
61             sub _die
62             {
63 14     14   83 my $self = shift;
64 14         17 my ($msg) = @_;
65 14 100       48 $msg .= "\n" unless substr($msg, -1) eq "\n";
66 14         45 Monitoring::Plugin::Functions::_plugin_exit(3, $msg);
67             }
68              
69             # Return the given attribute, if set, including a final newline
70             sub _attr
71             {
72 24     24   32 my $self = shift;
73 24         25 my ($item, $extra) = @_;
74 24 100       46 $extra = '' unless defined $extra;
75 24 100       54 return '' unless $self->{_attr}->{$item};
76 19         89 $self->{_attr}->{$item} . "\n" . $extra;
77             }
78              
79             # Turn argument spec into help-style output
80             sub _spec_to_help
81             {
82 26     26   27 my ($self, $spec, $label) = @_;
83              
84 26         53 my ($opts, $type) = split /=/, $spec, 2;
85 26         23 my (@short, @long);
86 26         40 for (split /\|/, $opts) {
87 44 100       54 if (length $_ == 1) {
88 16         32 push @short, "-$_";
89             } else {
90 28         49 push @long, "--$_";
91             }
92             }
93              
94 26         39 my $help = join(', ', @short, @long);
95 26 100       33 if ($type) {
    50          
96 24 100       29 if ($label) {
97 10         11 $help .= '=' . $label;
98             }
99             else {
100 14 100       28 $help .= $type eq 'i' ? '=INTEGER' : '=STRING';
101             }
102             }
103             elsif ($label) {
104 0         0 carp "Label specified, but there's no type in spec '$spec'";
105             }
106 26         26 $help .= "\n ";
107 26         53 return $help;
108             }
109              
110             # Options output for plugin -h
111             sub _options
112             {
113 8     8   42 my $self = shift;
114              
115 8         11 my @args = ();
116 8         9 my @defer = ();
117 8         8 for (@{$self->{_args}}) {
  8         19  
118 72 100       107 if (exists $DEFER_ARGS{$_->{name}}) {
119 16         18 push @defer, $_;
120             } else {
121 56         54 push @args, $_;
122             }
123             }
124              
125 8         11 my @options = ();
126 8         12 for my $arg (@args, @defer) {
127 72 100 66     204 my $help_array = ref $arg->{help} && ref $arg->{help} eq 'ARRAY' ? $arg->{help} : [ $arg->{help} ];
128 72 100 66     221 my $label_array = $arg->{label} && ref $arg->{label} && ref $arg->{label} eq 'ARRAY' ? $arg->{label} : [ $arg->{label} ];
129 72         59 my $help_string = '';
130 72         124 for (my $i = 0; $i <= $#$help_array; $i++) {
131 78         75 my $help = $help_array->[$i];
132             # Add spec arguments to help if not already there
133 78 100       163 if ($help =~ m/^\s*-/) {
134 52         135 $help_string .= $help;
135             }
136             else {
137 26         50 $help_string .= $self->_spec_to_help($arg->{spec}, $label_array->[$i]) . $help;
138 26 100       77 $help_string .= "\n " if $i < $#$help_array;
139             }
140             }
141              
142             # Add help_string to @options
143 72 100       111 if ($help_string =~ m/%s/) {
144 12 50       24 my $default = defined $arg->{default} ? $arg->{default} : '';
145             # We only handle '%s' formats here, so escape everything else
146 12         35 $help_string =~ s/%(?!s)/%%/g;
147 12         43 push @options, sprintf($help_string, $default, $default, $default, $default);
148             } else {
149 60         105 push @options, $help_string;
150             }
151             }
152              
153 8         68 return ' ' . join("\n ", @options);
154             }
155              
156             # Output for plugin -? (or missing/invalid args)
157             sub _usage
158             {
159 12     12   18 my $self = shift;
160 12         22 sprintf $self->_attr('usage'), $self->{_attr}->{plugin};
161             }
162              
163             # Output for plugin -V
164             sub _revision
165             {
166 6     6   9 my $self = shift;
167 6         31 my $revision = sprintf "%s %s", $self->{_attr}->{plugin}, $self->{_attr}->{version};
168 6 100       24 $revision .= sprintf " [%s]", $self->{_attr}->{url} if $self->{_attr}->{url};
169 6         8 $revision .= "\n";
170 6         15 $revision;
171             }
172              
173             # Output for plugin -h
174             sub _help
175             {
176 4     4   6 my $self = shift;
177 4         6 my $help = '';
178 4         12 $help .= $self->_revision . "\n";
179 4         15 $help .= $self->_attr('license', "\n");
180 4         11 $help .= $self->_attr('blurb', "\n");
181 4 50       11 $help .= $self->_usage ? $self->_usage . "\n" : '';
182 4 50       44 $help .= $self->_options ? $self->_options . "\n" : '';
183 4         14 $help .= $self->_attr('extra', "\n");
184 4         13 return $help;
185             }
186              
187             # Return a Getopt::Long-compatible option array from the current set of specs
188             sub _process_specs_getopt_long
189             {
190 35     35   33 my $self = shift;
191              
192 35         42 my @opts = ();
193 35         29 for my $arg (@{$self->{_args}}) {
  35         68  
194 404         397 push @opts, $arg->{spec};
195             # Setup names and defaults
196 404         335 my $spec = $arg->{spec};
197             # Use first arg as name (like Getopt::Long does)
198 404         630 $spec =~ s/[=:].*$//;
199 404         779 my $name = (split /\s*\|\s*/, $spec)[0];
200 404         393 $arg->{name} = $name;
201 404 100       505 if (defined $self->{$name}) {
202 35         55 $arg->{default} = $self->{$name};
203             } else {
204 369         567 $self->{$name} = $arg->{default};
205             }
206             }
207              
208 35         143 return @opts;
209             }
210              
211             # Check for existence of required arguments
212             sub _check_required_opts
213             {
214 24     24   26 my $self = shift;
215              
216 24         30 my @missing = ();
217 24         22 for my $arg (@{$self->{_args}}) {
  24         45  
218 288 100 100     504 if ($arg->{required} && ! defined $self->{$arg->{name}}) {
219 2         6 push @missing, $arg->{name};
220             }
221             }
222 24 100       57 if (@missing) {
223 2         12 $self->_die($self->_usage . "\n" .
224 2         7 join("\n", map { sprintf "Missing argument: %s", $_ } @missing) . "\n");
225             }
226             }
227              
228             # Process and handle any immediate options
229             sub _process_opts
230             {
231 32     32   34 my $self = shift;
232              
233             # Print message and exit for usage, version, help
234 32 100       77 $self->_die($self->_usage) if $self->{usage};
235 30 100       59 $self->_die($self->_revision) if $self->{version};
236 28 100       64 $self->_die($self->_help) if $self->{help};
237             }
238              
239             # -------------------------------------------------------------------------
240             # Default opts methods
241              
242             sub _load_config_section
243             {
244 15     15   15 my $self = shift;
245 15         20 my ($section, $file, $flags) = @_;
246 15   33     17 $section ||= $self->{_attr}->{plugin};
247              
248 15         13 my $Config;
249 15         14 eval { $Config = Monitoring::Plugin::Config->read($file); };
  15         68  
250 15 50       26 $self->_die($@) if ($@); #TODO: add test?
251              
252             # TODO: is this check sane? Does --extra-opts=foo require a [foo] section?
253             ## Nevertheless, if we die as UNKNOWN here we should do the same on default
254             ## file *added eval/_die above*.
255 15   33     53 $file ||= $Config->mp_getfile();
256 15 100       41 $self->_die("Invalid section '$section' in config file '$file'")
257             unless exists $Config->{$section};
258              
259 12         87 return $Config->{$section};
260             }
261              
262             # Helper method to setup a hash of spec definitions for _cmdline
263             sub _setup_spec_index
264             {
265 25     25   17 my $self = shift;
266 25 100       51 return if defined $self->{_spec};
267 13         14 $self->{_spec} = { map { $_->{name} => $_->{spec} } @{$self->{_args}} };
  208         344  
  13         24  
268             }
269              
270             # Quote values that require it
271             sub _cmdline_value
272             {
273 65     65   54 my $self = shift;
274 65         62 local $_ = shift;
275 65 50 33     176 if (m/\s/ && (m/^[^"']/ || m/[^"']$/)) {
    100 66        
276 0         0 return qq("$_");
277             }
278             elsif ($_ eq '') {
279 1         3 return q("");
280             }
281             else {
282 64         92 return $_;
283             }
284             }
285              
286             # Helper method to format key/values in $hash in a quasi-commandline format
287             sub _cmdline
288             {
289 25     25   230 my $self = shift;
290 25         26 my ($hash) = @_;
291 25   66     67 $hash ||= $self;
292              
293 25         39 $self->_setup_spec_index;
294              
295 25         49 my @args = ();
296 25         125 for my $key (sort keys %$hash) {
297             # Skip internal keys
298 268 100       377 next if $key =~ m/^_/;
299              
300             # Skip defaults and internals
301 229 100 66     396 next if exists $DEFAULT{$key} && $hash->{$key} eq $DEFAULT{$key};
302 203 100       159 next if grep { $key eq $_ } qw(help usage version extra-opts);
  812         965  
303 151 100       235 next unless defined $hash->{$key};
304              
305             # Render arg
306 49   50     91 my $spec = $self->{_spec}->{$key} || '';
307 49 100       140 if ($spec =~ m/[=:].+$/) {
308             # Arg takes value - may be a scalar or an arrayref
309 47 100       71 for my $value (ref $hash->{$key} eq 'ARRAY' ? @{$hash->{$key}} : ( $hash->{$key} )) {
  30         47  
310 65         127 $value = $self->_cmdline_value($value);
311 65 100       82 if (length($key) > 1) {
312 33         107 push @args, sprintf "--%s=%s", $key, $value;
313             }
314             else {
315 32         69 push @args, "-$key", $value;
316             }
317             }
318             }
319              
320             else {
321             # Flag - render long or short based on option length
322 2 50       8 push @args, (length($key) > 1 ? '--' : '-') . $key;
323             }
324             }
325              
326 25 100       144 return wantarray ? @args : join(' ', @args);
327             }
328              
329             # Process and load extra-opts sections
330             sub _process_extra_opts
331             {
332 35     35   40 my $self = shift;
333 35         41 my ($args) = @_;
334              
335 35         44 my $extopts_list = $args->{'extra-opts'};
336              
337 35         40 my @sargs = ();
338 35         67 for my $extopts (@$extopts_list) {
339 15   66     37 $extopts ||= $self->{_attr}->{plugin};
340 15         17 my $section = $extopts;
341 15         12 my $file = '';
342              
343             # Parse section@file
344 15 50       33 if ($extopts =~ m/^([^@]*)@(.*?)\s*$/) {
345 0         0 $section = $1;
346 0         0 $file = $2;
347             }
348              
349             # Load section args
350 15         31 my $shash = $self->_load_config_section($section, $file);
351              
352             # Turn $shash into a series of commandline-like arguments
353 12         21 push @sargs, $self->_cmdline($shash);
354             }
355              
356             # Reset ARGV to extra-opts + original
357 32         34 @ARGV = ( @sargs, @{$self->{_attr}->{argv}} );
  32         85  
358              
359 32 100 100     222 printf "[extra-opts] %s %s\n", $self->{_attr}->{plugin}, join(' ', @ARGV)
360             if $args->{verbose} && $args->{verbose} >= 3;
361             }
362              
363             # -------------------------------------------------------------------------
364             # Public methods
365              
366             # Define plugin argument
367             sub arg
368             {
369 194     194 0 3850 my $self = shift;
370 194         147 my %args;
371              
372             # Named args
373 194 100 66     812 if ($_[0] =~ m/^(spec|help|required|default)$/ && scalar(@_) % 2 == 0) {
374 177         1808 %args = validate( @_, {
375             spec => 1,
376             help => 1,
377             default => 0,
378             required => 0,
379             label => 0,
380             });
381             }
382              
383             # Positional args
384             else {
385 17         141 my @args = validate_pos(@_, 1, 1, 0, 0, 0);
386 17         80 %args = (
387             spec => $args[0],
388             help => $args[1],
389             default => $args[2],
390             required => $args[3],
391             label => $args[4],
392             );
393             }
394              
395             # Add to private args arrayref
396 194         452 push @{$self->{_args}}, \%args;
  194         519  
397             }
398              
399             # Process the @ARGV array using the current _args list (possibly exiting)
400             sub getopts
401             {
402 35     35 0 4083 my $self = shift;
403              
404             # Collate spec arguments for Getopt::Long
405 35         73 my @opt_array = $self->_process_specs_getopt_long;
406              
407             # Capture original @ARGV (for extra-opts games)
408 35         93 $self->{_attr}->{argv} = [ @ARGV ];
409              
410             # Call GetOptions using @opt_array
411 35         39 my $args1 = {};
412 35         112 my $ok = GetOptions($args1, @opt_array);
413             # Invalid options - give usage message and exit
414 35 50       21769 $self->_die($self->_usage) unless $ok;
415              
416             # Process extra-opts
417 35         106 $self->_process_extra_opts($args1);
418              
419             # Call GetOptions again, this time including extra-opts
420 32         88 $ok = GetOptions($self, @opt_array);
421             # Invalid options - give usage message and exit
422 32 50       20037 $self->_die($self->_usage) unless $ok;
423              
424             # Process immediate options (possibly exiting)
425 32         75 $self->_process_opts;
426              
427             # Required options (possibly exiting)
428 24         60 $self->_check_required_opts;
429              
430             # Setup accessors for options
431 22         231 $self->mk_ro_accessors(grep ! /^_/, keys %$self);
432              
433             # Setup default alarm handler for alarm($ng->timeout) in plugin
434             $SIG{ALRM} = sub {
435 1     1   2000406 my $plugin = uc $self->{_attr}->{plugin};
436 1         6 $plugin =~ s/^check_//;
437 1         12 $self->_die(
438             sprintf("%s UNKNOWN - plugin timed out (timeout %ss)",
439             $plugin, $self->timeout));
440 22         5726 };
441             }
442              
443             # -------------------------------------------------------------------------
444             # Constructor
445              
446             sub _init
447             {
448 35     35   39 my $self = shift;
449              
450             # Check params
451 35   33     1218 my $plugin = basename($ENV{PLUGIN_NAME} || $ENV{NAGIOS_PLUGIN} || $0);
452 35         986 my %attr = validate( @_, {
453             usage => 1,
454             version => 0,
455             url => 0,
456             plugin => { default => $plugin },
457             blurb => 0,
458             extra => 0,
459             'extra-opts' => 0,
460             license => { default => $DEFAULT{license} },
461             timeout => { default => $DEFAULT{timeout} },
462             });
463              
464             # Add attr to private _attr hash (except timeout)
465 35         282 $self->{timeout} = delete $attr{timeout};
466 35         108 $self->{_attr} = { %attr };
467             # Chomp _attr values
468 35         49 chomp foreach values %{$self->{_attr}};
  35         183  
469              
470             # Setup initial args list
471 35         146 $self->{_args} = [ @ARGS ];
472              
473 35         146 $self
474             }
475              
476             sub new
477             {
478 35     35 1 14557 my $class = shift;
479 35         77 my $self = bless {}, $class;
480 35         83 $self->_init(@_);
481             }
482              
483             # -------------------------------------------------------------------------
484              
485             1;
486              
487             __END__