File Coverage

blib/lib/Nagios/Monitoring/Plugin/Getopt.pm
Criterion Covered Total %
statement 208 212 98.1
branch 76 86 88.3
condition 25 41 60.9
subroutine 29 29 100.0
pod 1 3 33.3
total 339 371 91.3


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