#!/usr/bin/perl

use v5.20.0;
use warnings;

use Cron::Sequencer::CLI qw(parse_argv);
require Cron::Sequencer::Output;
require Cron::Sequencer;
require Pod::Usage;

$main::VERSION = $Cron::Sequencer::VERSION;
my ($start, $end, $output, @input) = parse_argv(\&Pod::Usage::pod2usage, @ARGV);

=head1 NAME

cron-sequencer - show the sequence of commands that cron would run

=head1 SYNOPSIS

  cron-sequencer crontab-file             # show today's events
  cron-sequencer --show tomorrow cronfile # or tomorrow's
  cron-sequencer cronfile1 cronfile2      # show events from both crontabs
  cron-sequencer --ignore 6,9,43 file     # ignore events from these lines

=head1 DESCRIPTION

B<cron-sequencer> takes one or more crontab files, and shows the sequence of
commands that C<cron> would run, with the source line and time fields that
triggered the event at that timestamp. The default is to show events for
I<today>, and to show environment variables set in the crontab file.

=head1 GLOBAL OPTIONS

=over 4

=item --show I<period>

Show events for the given period. Options are

=over 4

=item *

today

=item *

yesterday

=item *

tomorrow

=item *

last week

=item *

this week

=item *

next week

=item *

last hour

=item *

this hour

=item *

next hour

=item *

I<(next|last) (\d*) (minutes|hours|days|weeks)>

=back

Single days and weeks are treated as starting at midnight (inclusive) and ending
at midnight (exclusive). Single hours start at the start of the current hour.
The default is I<today>

The counted forms (I<next 2 days>, I<last 24 hours>, I<etc>) start or end from
the current time. Truncating to the day or hour would introduce ambiguities -
doing so would mean "next 2 hours" usually wouldn't show events 119 minutes in
the future, or it would mean showing between 120 and 179 minutes of events.
Neither choice makes sense.

(For completeness and consistency, C<this day> is provided as an alias for
C<today>, as well as aliases C<next day> and C<last day>, but it's probably less
clear to use these. Likewise C<last minute> I<etc> also exist, but they seem
unlikely to be useful as the results shown will rapidly change.)

=item --from I<epoch time>

=item --to I<epoch time>

Specify an exact range of events to show. The start time is inclusive, the end
time exclusive. Values can be

=over 4

=item I<positive integer>

Absolute time in Unix epoch seconds (I<ie> seconds since 1970-01-01)

=item I<+integer>

Relative time offset in seconds. For I<from>, this specifies a start time
relative to midnight gone. For I<to> this specifies an end time relative to the
start time (so gives the number of seconds of crontab events to show)

=item I<-integer>

A negative integer is only permitted for I<from>, and specifies a start time
before midnight gone.

=back

Hence C<--from +0 --to +3600> shows events from midnight until 00:59 inclusive.

Beware - because of how Unix shells split command line arguments, and how option
parsing requires a single argument to be the value, for the options that have
spaces you will need to escape the spaces. So you'd need to write:

  cron-sequencer --show "last week" cronfile

or

  cron-sequencer "--show=last 3 days" cronfile

=item --hide-env

Don't show environment variables in the output.

=item --no-group

Don't group events that happen at the same time. By default, the timestamp is
shown once and then all events firing at that time are grouped together, to make
it clearer that they are run simultaneously. With the C<--no-group> option,
each event is treated independently and shown with its own timestamp.

=item --json

Generate JSON output. This intended for machine consumption, and the plan is
to keep it stable (deletion or changes subject to deprecation cycles, but be
preparated for new keys to be added). Please don't parse the default text
output, as this is intended to be human-readable and might have minor changes
without warning.

The default JSON output is a single JSON value with the top level an array.
Within this, the default is arrays grouping events that happen at the same time,
with the events represented as hashes. If C<--no-group> is used, then the inner
arrays are removed, and the top level array contains the hashes directly.

If you need JSON output in structures that these options don't offer, consider
piping the output of this program to a tool such as C<jq>.

Value(s) can be provided to --json to tweak the output

=over 4

=item pretty

=item canonical

Set these options on the JSON serialiser. See L<JSON::PP/pretty> and
L<JSON::PP/canonical> for details.

=item split

Instead of serialising a single JSON value encoding an array of structures
(arrays of hashes by default, hashes with C<--no-group>) instead output that
list of structures as a sequence of JSON values, with newlines between them.

=item seq

Similar to C<split>, but output C<application/json-seq> format - each value
is preceded with an ASCII C<record separator> character and followed by a
newline. See L<RFC 7464|https://www.rfc-editor.org/rfc/rfc7464.txt>

=back

C<split> and C<seq> are mutually exclusive.

The C<--json> option is parsed differently from other options with arguments -
to specify arguments you must use the C<=> form - I<eg> C<--json=pretty>
Without this special parsing you could also have written C<--json pretty>, but
that would mean either that C<--json> would B<always> need an argument, or that
C<--json pretty> would be an option but C<--json foobar> would be "display
I<foobar> as I<JSON>" -- with silent breakage if we ever wanted to introduce a
C<--json=foobar> option.

The first choice JSON serialiser is L<Cpanel::JSON::XS> which behaves
identically, but as it doesn't provide direct link targets in its documentation
this document links to the relevant sections in C<JSON::PP>.

=item --help

Shows this documentation

=item --version

Shows the version

=back

=head1 PER-FILE OPTIONS

These options can be specified independently for each crontab file (or group of
crontab files), and can be repeated multiple times.

=over 4

=item --ignore <line numbers>

Ignore the given line(s) in the crontab. Specify line numbers as comma-separated
lists of integers or integer ranges (just like crontab time files, aside from
you can't use I<*> or skip syntax such as I</2>). Line numbers start at 1.

"Ignore" is the first action of the parser, so you can ignore all of

=over 4

=item *

command entries (particularly "chatty" entries such as C<* * * * *>)

=item *

setting environment variables

=item *

lines with syntax errors that otherwise would abort the parse

=back

=item --env I<NAME=value>

Pre-define an environment variable for this crontab file. The variable
declaration B<won't> be shown in the output if the crontab defines the variable
with the same value. Without this a crontab that starts

     MAILTO=god@heaven.mil

and has 42 events to show would generate 42 lines of C<MAILTO=god@heaven.mil>
output, once for each command.

If you define an environment variable on that command line that isn't set in
scope in the crontab file then an C<unset ...> line is shown. This makes it
clear that the event doesn't match your expected default value.

You can't declare both I<--env> and I<--hide-env>

=item --

Use a pair of dashes to separate options for different files on the command
line. Effectively C<--> resets the state to no lines ignored and no environment
variables defined.

=back

=head1 EXAMPLES

  cron-sequencer cronfile1 cronfile2

Shows events from both crontab files for today, in time order, annotated with
file name, line number, time specification and environment variables.

  cron-sequencer --env MAILTO=alice cron1 -- --env MAILTO=bob cron2

Shows events from both files, but will create clearer output if F<cron1>
declares C<MAILTO=alice> and F<cron2> declares C<MAILTO=bob>

  cron-sequencer -- cron1 --env MAILTO=alice -- --env MAILTO=bob cron2

Identical output. (This is a side effect of how options are parsed first, and
then filenames.)

  cron-sequencer --env MAILTO=bob cron1 cron2 -- --ignore 3-5 cron3

Shows events from the first two files assuming the both declare C<MAILTO=bob>,
along with events from cron3 except for lines 3, 4 and 5 (with all environment
variables shown, unless they were declared on the ignored lines 3, 4 and 5)

=head1 BUGS

Currently the code assumes that all crontabs are run with a system timezone of
B<UTC>. Similarly all display output is shown for UTC. The work systems all run
in UTC, so we don't have pressing need to fix this ourselves.

=head1 LICENSE

This library is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. If you would like to contribute documentation,
features, bug fixes, or anything else then please raise an issue / pull request:

    https://github.com/Humanstate/cron-sequencer

=head1 AUTHOR

Nicholas Clark - C<nick@ccl4.org>

=cut

my $crontab = Cron::Sequencer->new(@input);

my $formatter = Cron::Sequencer::Output->new(@$output);

print $formatter->render($crontab->sequence($start, $end));
