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 5     5   98293 use strict;
  5         10  
  5         150  
9 5     5   29 use File::Basename;
  5         9  
  5         485  
10 5     5   5599 use Getopt::Long qw(:config no_ignore_case bundling);
  5         65854  
  5         33  
11 5     5   1044 use Carp;
  5         11  
  5         348  
12 5     5   3019 use Params::Validate qw(:all);
  5         38566  
  5         1051  
13 5     5   35 use base qw(Class::Accessor);
  5         10  
  5         15159  
14              
15 5     5   16458 use Nagios::Monitoring::Plugin::Functions;
  5         15  
  5         466  
16 5     5   3146 use Nagios::Monitoring::Plugin::Config;
  5         11  
  5         158  
17 5     5   29 use vars qw($VERSION);
  5         9  
  5         14159  
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 14     14   71 my $self = shift;
61 14         27 my ($msg) = @_;
62 14 100       69 $msg .= "\n" unless substr($msg, -1) eq "\n";
63 14         74 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   32 my $self = shift;
70 24         39 my ($item, $extra) = @_;
71 24 100       55 $extra = '' unless defined $extra;
72 24 100       69 return '' unless $self->{_attr}->{$item};
73 19         120 $self->{_attr}->{$item} . "\n" . $extra;
74             }
75              
76             # Turn argument spec into help-style output
77             sub _spec_to_help
78             {
79 26     26   37 my ($self, $spec, $label) = @_;
80              
81 26         53 my ($opts, $type) = split /=/, $spec, 2;
82 26         28 my (@short, @long);
83 26         49 for (split /\|/, $opts) {
84 44 100       126 if (length $_ == 1) {
85 16         37 push @short, "-$_";
86             } else {
87 28         54 push @long, "--$_";
88             }
89             }
90              
91 26         43 my $help = join(', ', @short, @long);
92 26 100       51 if ($type) {
    50          
93 24 100       35 if ($label) {
94 10         82 $help .= '=' . $label;
95             }
96             else {
97 14 100       32 $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         28 $help .= "\n ";
104 26         71 return $help;
105             }
106              
107             # Options output for plugin -h
108             sub _options
109             {
110 8     8   14 my $self = shift;
111              
112 8         16 my @args = ();
113 8         14 my @defer = ();
114 8         12 for (@{$self->{_args}}) {
  8         21  
115 72 100       135 if (exists $DEFER_ARGS{$_->{name}}) {
116 16         23 push @defer, $_;
117             } else {
118 56         82 push @args, $_;
119             }
120             }
121              
122 8         15 my @options = ();
123 8         16 for my $arg (@args, @defer) {
124 72 100 66     244 my $help_array = ref $arg->{help} && ref $arg->{help} eq 'ARRAY' ? $arg->{help} : [ $arg->{help} ];
125 72 100 66     272 my $label_array = $arg->{label} && ref $arg->{label} && ref $arg->{label} eq 'ARRAY' ? $arg->{label} : [ $arg->{label} ];
126 72         125 my $help_string = '';
127 72         201 for (my $i = 0; $i <= $#$help_array; $i++) {
128 78         95 my $help = $help_array->[$i];
129             # Add spec arguments to help if not already there
130 78 100       191 if ($help =~ m/^\s*-/) {
131 52         172 $help_string .= $help;
132             }
133             else {
134 26         59 $help_string .= $self->_spec_to_help($arg->{spec}, $label_array->[$i]) . $help;
135 26 100       92 $help_string .= "\n " if $i < $#$help_array;
136             }
137             }
138              
139             # Add help_string to @options
140 72 100       145 if ($help_string =~ m/%s/) {
141 12 50       30 my $default = defined $arg->{default} ? $arg->{default} : '';
142             # We only handle '%s' formats here, so escape everything else
143 12         33 $help_string =~ s/%(?!s)/%%/g;
144 12         50 push @options, sprintf($help_string, $default, $default, $default, $default);
145             } else {
146 60         131 push @options, $help_string;
147             }
148             }
149              
150 8         83 return ' ' . join("\n ", @options);
151             }
152              
153             # Output for plugin -? (or missing/invalid args)
154             sub _usage
155             {
156 12     12   19 my $self = shift;
157 12         29 sprintf $self->_attr('usage'), $self->{_attr}->{plugin};
158             }
159              
160             # Output for plugin -V
161             sub _revision
162             {
163 6     6   12 my $self = shift;
164 6         43 my $revision = sprintf "%s %s", $self->{_attr}->{plugin}, $self->{_attr}->{version};
165 6 100       36 $revision .= sprintf " [%s]", $self->{_attr}->{url} if $self->{_attr}->{url};
166 6         12 $revision .= "\n";
167 6         21 $revision;
168             }
169              
170             # Output for plugin -h
171             sub _help
172             {
173 4     4   9 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         13 $help .= $self->_attr('blurb', "\n");
178 4 50       20 $help .= $self->_usage ? $self->_usage . "\n" : '';
179 4 50       18 $help .= $self->_options ? $self->_options . "\n" : '';
180 4         15 $help .= $self->_attr('extra', "\n");
181 4         16 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 35     35   50 my $self = shift;
188              
189 35         61 my @opts = ();
190 35         51 for my $arg (@{$self->{_args}}) {
  35         99  
191 404         742 push @opts, $arg->{spec};
192             # Setup names and defaults
193 404         557 my $spec = $arg->{spec};
194             # Use first arg as name (like Getopt::Long does)
195 404         982 $spec =~ s/[=:].*$//;
196 404         1159 my $name = (split /\s*\|\s*/, $spec)[0];
197 404         783 $arg->{name} = $name;
198 404 100       847 if (defined $self->{$name}) {
199 35         87 $arg->{default} = $self->{$name};
200             } else {
201 369         1156 $self->{$name} = $arg->{default};
202             }
203             }
204              
205 35         208 return @opts;
206             }
207              
208             # Check for existence of required arguments
209             sub _check_required_opts
210             {
211 24     24   33 my $self = shift;
212              
213 24         35 my @missing = ();
214 24         34 for my $arg (@{$self->{_args}}) {
  24         61  
215 288 100 100     681 if ($arg->{required} && ! defined $self->{$arg->{name}}) {
216 2         7 push @missing, $arg->{name};
217             }
218             }
219 24 100       87 if (@missing) {
220             $self->_die($self->_usage . "\n" .
221 2         7 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 32     32   53 my $self = shift;
229              
230             # Print message and exit for usage, version, help
231 32 100       95 $self->_die($self->_usage) if $self->{usage};
232 30 100       82 $self->_die($self->_revision) if $self->{version};
233 28 100       83 $self->_die($self->_help) if $self->{help};
234             }
235              
236             # -------------------------------------------------------------------------
237             # Default opts methods
238              
239             sub _load_config_section
240             {
241 15     15   24 my $self = shift;
242 15         23 my ($section, $file, $flags) = @_;
243 15   33     27 $section ||= $self->{_attr}->{plugin};
244              
245 15         19 my $Config;
246 15         22 eval { $Config = Nagios::Monitoring::Plugin::Config->read($file); };
  15         128  
247 15 50       38 $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 15   33     73 $file ||= $Config->np_getfile();
253             $self->_die("Invalid section '$section' in config file '$file'")
254 15 100       76 unless exists $Config->{$section};
255              
256 12         108 return $Config->{$section};
257             }
258              
259             # Helper method to setup a hash of spec definitions for _cmdline
260             sub _setup_spec_index
261             {
262 25     25   29 my $self = shift;
263 25 100       65 return if defined $self->{_spec};
264 13         18 $self->{_spec} = { map { $_->{name} => $_->{spec} } @{$self->{_args}} };
  208         1953  
  13         29  
265             }
266              
267             # Quote values that require it
268             sub _cmdline_value
269             {
270 65     65   77 my $self = shift;
271 65         114 local $_ = shift;
272 65 50 33     493 if (m/\s/ && (m/^[^"']/ || m/[^"']$/)) {
    100 66        
273 0         0 return qq("$_");
274             }
275             elsif ($_ eq '') {
276 1         4 return q("");
277             }
278             else {
279 64         158 return $_;
280             }
281             }
282              
283             # Helper method to format key/values in $hash in a quasi-commandline format
284             sub _cmdline
285             {
286 25     25   521 my $self = shift;
287 25         32 my ($hash) = @_;
288 25   66     80 $hash ||= $self;
289              
290 25         278 $self->_setup_spec_index;
291              
292 25         294 my @args = ();
293 25         169 for my $key (sort keys %$hash) {
294             # Skip internal keys
295 268 100       1431 next if $key =~ m/^_/;
296              
297             # Skip defaults and internals
298 229 100 66     1027 next if exists $DEFAULT{$key} && $hash->{$key} eq $DEFAULT{$key};
299 203 100       986 next if grep { $key eq $_ } qw(help usage version extra-opts);
  812         2432  
300 151 100       628 next unless defined $hash->{$key};
301              
302             # Render arg
303 49   50     131 my $spec = $self->{_spec}->{$key} || '';
304 49 100       163 if ($spec =~ m/[=:].+$/) {
305             # Arg takes value - may be a scalar or an arrayref
306 47 100       359 for my $value (ref $hash->{$key} eq 'ARRAY' ? @{$hash->{$key}} : ( $hash->{$key} )) {
  30         60  
307 65         132 $value = $self->_cmdline_value($value);
308 65 100       121 if (length($key) > 1) {
309 33         150 push @args, sprintf "--%s=%s", $key, $value;
310             }
311             else {
312 32         96 push @args, "-$key", $value;
313             }
314             }
315             }
316              
317             else {
318             # Flag - render long or short based on option length
319 2 50       7 push @args, (length($key) > 1 ? '--' : '-') . $key;
320             }
321             }
322              
323 25 100       426 return wantarray ? @args : join(' ', @args);
324             }
325              
326             # Process and load extra-opts sections
327             sub _process_extra_opts
328             {
329 35     35   57 my $self = shift;
330 35         52 my ($args) = @_;
331              
332 35         60 my $extopts_list = $args->{'extra-opts'};
333              
334 35         60 my @sargs = ();
335 35         89 for my $extopts (@$extopts_list) {
336 15   66     51 $extopts ||= $self->{_attr}->{plugin};
337 15         22 my $section = $extopts;
338 15         21 my $file = '';
339              
340             # Parse section@file
341 15 50       38 if ($extopts =~ m/^([^@]*)@(.*?)\s*$/) {
342 0         0 $section = $1;
343 0         0 $file = $2;
344             }
345              
346             # Load section args
347 15         38 my $shash = $self->_load_config_section($section, $file);
348              
349             # Turn $shash into a series of commandline-like arguments
350 12         80 push @sargs, $self->_cmdline($shash);
351             }
352              
353             # Reset ARGV to extra-opts + original
354 32         58 @ARGV = ( @sargs, @{$self->{_attr}->{argv}} );
  32         110  
355              
356             printf "[extra-opts] %s %s\n", $self->{_attr}->{plugin}, join(' ', @ARGV)
357 32 100 100     487 if $args->{verbose} && $args->{verbose} >= 3;
358             }
359              
360             # -------------------------------------------------------------------------
361             # Public methods
362              
363             # Define plugin argument
364             sub arg
365             {
366 194     194 0 6994 my $self = shift;
367 194         236 my %args;
368              
369             # Named args
370 194 100 66     1072 if ($_[0] =~ m/^(spec|help|required|default)$/ && scalar(@_) % 2 == 0) {
371 177         2849 %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         177 my @args = validate_pos(@_, 1, 1, 0, 0, 0);
383 17         112 %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 194         781 push @{$self->{_args}}, \%args;
  194         786  
394             }
395              
396             # Process the @ARGV array using the current _args list (possibly exiting)
397             sub getopts
398             {
399 35     35 0 4787 my $self = shift;
400              
401             # Collate spec arguments for Getopt::Long
402 35         109 my @opt_array = $self->_process_specs_getopt_long;
403              
404             # Capture original @ARGV (for extra-opts games)
405 35         138 $self->{_attr}->{argv} = [ @ARGV ];
406              
407             # Call GetOptions using @opt_array
408 35         104 my $args1 = {};
409 35         158 my $ok = GetOptions($args1, @opt_array);
410             # Invalid options - give usage message and exit
411 35 50       28962 $self->_die($self->_usage) unless $ok;
412              
413             # Process extra-opts
414 35         107 $self->_process_extra_opts($args1);
415              
416             # Call GetOptions again, this time including extra-opts
417 32         111 $ok = GetOptions($self, @opt_array);
418             # Invalid options - give usage message and exit
419 32 50       26372 $self->_die($self->_usage) unless $ok;
420              
421             # Process immediate options (possibly exiting)
422 32         107 $self->_process_opts;
423              
424             # Required options (possibly exiting)
425 24         61 $self->_check_required_opts;
426              
427             # Setup accessors for options
428 22         271 $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   2000503 my $plugin = uc $self->{_attr}->{plugin};
433 1         9 $plugin =~ s/^check_//;
434 1         17 $self->_die(
435             sprintf("%s UNKNOWN - plugin timed out (timeout %ss)",
436             $plugin, $self->timeout));
437 22         16019 };
438             }
439              
440             # -------------------------------------------------------------------------
441             # Constructor
442              
443             sub _init
444             {
445 35     35   53 my $self = shift;
446              
447             # Check params
448 35   33     1443 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 35         1479 });
460              
461             # Add attr to private _attr hash (except timeout)
462 35         367 $self->{timeout} = delete $attr{timeout};
463 35         171 $self->{_attr} = { %attr };
464             # Chomp _attr values
465 35         69 chomp foreach values %{$self->{_attr}};
  35         233  
466              
467             # Setup initial args list
468 35         124 $self->{_args} = [ @ARGS ];
469              
470 35         185 $self
471             }
472              
473             sub new
474             {
475 35     35 1 20371 my $class = shift;
476 35         102 my $self = bless {}, $class;
477 35         123 $self->_init(@_);
478             }
479              
480             # -------------------------------------------------------------------------
481              
482             1;
483              
484             __END__