File Coverage

blib/lib/Cron/Sequencer/CLI.pm
Criterion Covered Total %
statement 128 129 99.2
branch 57 58 98.2
condition 20 20 100.0
subroutine 8 8 100.0
pod 0 2 0.0
total 213 217 98.1


line stmt bran cond sub pod time code
1             #!perl
2              
3 2     2   196771 use v5.20.0;
  2         15  
4 2     2   10 use warnings;
  2         3  
  2         55  
5              
6             # The parts of this that we use have been stable and unchanged since v5.20.0:
7 2     2   9 use feature qw(postderef);
  2         4  
  2         256  
8 2     2   11 no warnings 'experimental::postderef';
  2         4  
  2         113  
9              
10              
11             use parent qw(Exporter);
12 2     2   771 require DateTime;
  2         509  
  2         8  
13             require DateTime::Format::ISO8601;
14             use Getopt::Long qw(GetOptionsFromArray);
15 2     2   1393  
  2         17403  
  2         8  
16             our $VERSION = '0.05';
17             our @EXPORT_OK = qw(calculate_start_end parse_argv);
18              
19             my %known_json = map { $_, 1 } qw(seq split pretty canonical);
20              
21             my ($pod2usage, @argv) = @_;
22              
23 44     44 0 221859 my @groups;
24             my $current = [];
25 44         100 my $json;
26 44         96  
27 44         74 # Split the command line into sections:
28             for my $item (@argv) {
29             if ($item eq '--') {
30 44         105 push @groups, $current;
31 158 100 100     625 $current = [];
    100          
32 20         44 } elsif ($item =~ /\A--json(?:=(.*)|)\z/s && !@groups) {
33 20         41 # GetOpt::Long doesn't appear to have a way to specify to specify an
34             # option that can take an argument, but it so, the argument must be
35             # given in the '=' form.
36             # We'd like to support `--json` and `--json=pretty` but have
37             # `--json pretty` mean the same as `./pretty --json`
38              
39             if (length $1) {
40             for my $opt (split ',', $1) {
41 19 100       66 $pod2usage->(exitval => 255,
42 15         48 message => "Unknown --json option '$opt'")
43             unless $known_json{$opt};
44             $json->{$opt} = 1;
45 20 100       64 }
46 19         41 $pod2usage->(exitval => 255,
47             message => "Can't use --json=seq with --json=split")
48             if $json->{seq} && $json->{split};
49             } else {
50 14 100 100     62 # Flag that we saw --json
51             $json //= {};
52             }
53 4   100     17 } else {
54             push @$current, $item;
55             }
56 119         196 }
57             push @groups, $current;
58              
59 41         85 my %global_options = (
60             group => 1,
61 41         148 );
62              
63             Getopt::Long::Configure('pass_through', 'auto_version', 'auto_help');
64             unless(GetOptionsFromArray($groups[0], \%global_options,
65 41         223 'show=s',
66 41 50       2278 'from=s',
67             'to=s',
68             'hide-env',
69             'group!',
70             )) {
71             $pod2usage->(exitval => 255, verbose => 1);
72             }
73 0         0  
74             my ($start, $end) = calculate_start_end(\%global_options);
75              
76 37         15708 my @input;
77              
78 37         91 Getopt::Long::Configure('no_pass_through', 'no_auto_version');
79             for my $group (@groups) {
80 37         161 my %options;
81 37         1599 unless(GetOptionsFromArray($group, \%options,
82 55         82 'env=s@',
83 55 100       155 'ignore=s@'
84             )) {
85             $pod2usage->(exitval => 255, verbose => 1);
86             }
87 4         982 $pod2usage->(exitval => 255,
88             message => "--env and --hide-env options can't be used together")
89             if $global_options{'hide-env'} && $options{env};
90              
91 50 100 100     10780 push @input, map {{ source => $_, %options{qw(env ignore)} }} @$group;
92             }
93 48         114  
  42         217  
94             $pod2usage->(exitval => 255)
95             unless @input;
96 30 100       85  
97             my $output = [%global_options{qw(hide-env group)}, count => scalar @input];
98              
99 29         96 push @$output, json => $json
100             if $json;
101 29 100       94  
102             return ($start, $end, $output, @input);
103             }
104 29         284  
105             my $options = shift;
106              
107             my ($start, $end);
108 89     89 0 98106  
109             if (defined $options->{from} || defined $options->{to}) {
110 89         165 die "$0: Can't use --show with --from or --to\n"
111             if defined $options->{show};
112 89 100 100     430  
113             # Default is midnight gone
114 27 100       75 my $from = $options->{from} // '+0';
115             if ($from =~ /\A[1-9][0-9]*\z/) {
116             # Absolute epoch seconds
117 25   100     64 $start = $from;
118 25 100       127 } elsif ($from =~ /\A[-+](?:0|[1-9][0-9]*)\z/) {
    100          
119             # Seconds relative to midnight gone
120 9         23 $start = DateTime->today()->epoch() + $from;
121             } else {
122             my $line;
123 11         40 eval {
124             $line = __LINE__ + 1;
125 5         9 $start = DateTime::Format::ISO8601->parse_datetime($from)->epoch();
126 5         6 };
127 5         6 unless (defined $start) {
128 5         18 # Seems that we can't override DateTime::Format::ISO8601 on_fail
129             # *cleanly*. DateTime::Format::Builder on_fail is trying to use
130 5 100       2301 # $Carp::CarpLevel to keep itself and its calls to
131             # DateTime::Format::Builder::Parser out of the backtrace, but it
132             # and Carp don't quite agree on the right number to use, with
133             # the unfortunate result that it triggers a full backtrace
134             # (effectively croak turns into confess, because Carp runs out
135             # of call stack before $Carp::CarpLevel is exhausted. No, I
136             # didn't know that it did this. "Today I Learned")
137              
138             # I don't want to confuse the user with a backtrace. This seems
139             # like the simplest solution.
140              
141             my $message = $@;
142             my $file = quotemeta __FILE__;
143             $message =~ s/ at $file line $line.*//s;
144 1         3 die "$0: Can't parse --from: $message\n";
145 1         2 }
146 1         25 }
147 1         8  
148             # Default is to show 1 hour
149             my $to = $options->{to} // '+3600';
150             if ($to =~ /\A[1-9][0-9]+\z/) {
151             # Absolute epoch seconds
152 24   100     5039 $end = $to;
153 24 100       106 } elsif ($to =~ /\A\+[1-9][0-9]*\z/) {
    100          
154             # Seconds relative to from
155 10         15 # As $end >= $start, '+0' doesn't make sense
156             $end = $start + $to;
157             } else {
158             my $line;
159 11         21 eval {
160             $line = __LINE__ + 1;
161 3         4 $end = DateTime::Format::ISO8601->parse_datetime($to)->epoch();
162 3         6 };
163 3         3 unless (defined $end) {
164 3         8 my $message = $@;
165             my $file = quotemeta __FILE__;
166 3 100       1338 $message =~ s/ at $file line $line.*//s;
167 1         3 die "$0: Can't parse --to: $message\n";
168 1         2 }
169 1         15 }
170 1         7  
171             die "$0: End $end must be after start $start (--from=$from --to=$to)\n"
172             if $end <= $start;
173             } else {
174 23 100       57 my $show = $options->{show} // 'today';
175             if ($show =~ /\A\s*(last|this|next)\s+(minute|hour|day|week)\s*\z/) {
176             my $which = $1;
177 62   100     227 my $what = $2;
178 62 100       542 my $start_of_period = DateTime->now()->truncate(to => $what);
    100          
    100          
    100          
    100          
179 12         26 if ($which eq 'last') {
180 12         16 $end = $start_of_period->epoch();
181 12         39 $start_of_period->subtract($what . 's' => 1);
182 12 100       8072 $start = $start_of_period->epoch();
183 4         12 } else {
184 4         35 $start_of_period->add($what . 's' => 1)
185 4         3362 if $which eq 'next';
186             $start = $start_of_period->epoch();
187 8 100       29 $start_of_period->add($what . 's' => 1);
188             $end = $start_of_period->epoch();
189 8         2907 }
190 8         65 } elsif ($show =~ /\A\s*(last|next)\s+([1-9][0-9]*)\s+(minute|hour|day|week)s\s*\z/) {
191 8         5647 my $which = $1;
192             my $count = $2;
193             my $what = $3;
194 4         10  
195 4         5 # I was going to name this $now, but then realised that if I add or
196 4         7 # subtract on it, it won't be very "now" any more...
197             my $aargh_mutable = DateTime->now();
198              
199             if ($which eq 'last') {
200 4         13 $end = $aargh_mutable->epoch();
201             $aargh_mutable->subtract($what . 's' => $count);
202 4 100       884 $start = $aargh_mutable->epoch();
203 2         6 } else {
204 2         17 $start= $aargh_mutable->epoch();
205 2         1693 $aargh_mutable->add($what . 's' => $count);
206             $end = $aargh_mutable->epoch();
207 2         8 }
208 2         17 } elsif ($show =~ /\A\s*yesterday\s*\z/) {
209 2         1417 my $midnight = DateTime->today();
210             $end = $midnight->epoch();
211             $midnight->subtract(days => 1);
212 2         30 $start = $midnight->epoch();
213 2         882 } elsif ($show =~ /\A\s*(today|tomorrow)\s*\z/) {
214 2         18 my $midnight = DateTime->today();
215 2         1632 $midnight->add(days => 1)
216             if $1 eq 'tomorrow';
217 39         242 $start = $midnight->epoch();
218 39 100       23573 $midnight->add(days => 1);
219             $end = $midnight->epoch();
220 39         1632 } elsif ($show =~ /\A(?:last|this|next)\z/) {
221 39         444 die "$0: Unknown time period '$show' for --show (did you forget to escape the space after it?)\n";
222 39         35070 } else {
223             my ($line, $midnight);
224 3         19 eval {
225             $line = __LINE__ + 1;
226 2         4 $midnight = DateTime::Format::ISO8601->parse_datetime($show)->truncate(to => 'day');
227 2         4 };
228 2         3 unless (defined $midnight) {
229 2         15 my $message = $@;
230             my $file = quotemeta __FILE__;
231 2 100       1502 $message =~ s/ at $file line $line.*//s;
232 1         3 die "$0: Can't parse --show: $message\n";
233 1         2 }
234 1         16 $start = $midnight->epoch();
235 1         7 $midnight->add(days => 1);
236             $end = $midnight->epoch();
237 1         3 }
238 1         8 }
239 1         866  
240             return ($start, $end);
241             }
242              
243 80         766 =head1 NAME
244              
245             Cron::Sequencer::CLI
246              
247             =head1 SYNOPSIS
248              
249             This module exists to make it easy to test the command line option parsing
250             of L<bin/cron-sequencer>. It's for "internal use only", and subject to change
251             (or deletion) without warning.
252              
253             =head1 LICENSE
254              
255             This library is free software; you can redistribute it and/or modify it under
256             the same terms as Perl itself. If you would like to contribute documentation,
257             features, bug fixes, or anything else then please raise an issue / pull request:
258              
259             https://github.com/Humanstate/cron-sequencer
260              
261             =head1 AUTHOR
262              
263             Nicholas Clark - C<nick@ccl4.org>
264              
265             =cut
266              
267             1;