#!/usr/bin/env perl

use 5.14.2;
use warnings;

use Zonemaster::CLI;
use File::Spec;
use autodie;

sub read_conf_file {
    # Returns list of command line parameters. List can be empty.
    my ( $conf_file ) = @_;
    my @lines;
    open my $fh, '<', $conf_file;
    while ( <$fh> ) {
        chomp;
        next if /^\s*$/;
        next if /^\s*#/;
        push @lines, $_;
    }
    return @lines;
}

# Load default arguments from file in home directory, if any
# This must be loaded before any global file to make the local
# file take precedence
my $home_dir       = ( ( getpwuid( $< ) )[7] ) || $ENV{HOME};
my $home_conf_file = File::Spec->catfile( $home_dir, '.zonemaster', 'cli.args' );

if ( -r $home_conf_file ) {
    my @lines = read_conf_file( $home_conf_file );
    unshift @ARGV, @lines;
}

# Load default arguments from global file, if any
my @global_conf = ( '/etc/zonemaster/cli.args', '/usr/local/etc/zonemaster/cli.args' );    # Order is significant.
my $global_conf_file;

for my $p ( @global_conf ) {
    if ( -e $p and -r $p ) {
        $global_conf_file = $p;
        last;
    }
}

if ( defined $global_conf_file ) {
    my @lines = read_conf_file( $global_conf_file );
    unshift @ARGV, @lines;

}

eval {
    my $exitstatus = Zonemaster::CLI->run( @ARGV );
    exit $exitstatus;
};

print STDERR $@;
exit $Zonemaster::CLI::EXIT_GENERIC_ERROR;

=head1 NAME

zonemaster-cli - run Zonemaster tests from the command line

=head1 SYNOPSIS

    zonemaster-cli [--help | --version | --list-tests]
    zonemaster-cli [OPTIONS] --dump-profile
    zonemaster-cli [OPTIONS] DOMAINNAME

=head1 DESCRIPTION

L<zonemaster-cli> is a command-line interface to the Zonemaster test engine.
It takes instructions the user provides as command line arguments, transforms
them into suitable API calls to the engine, runs the test suite and prints the
resulting messages. By default, the messages will be translated by the engine's
translation module, with the corresponding timestamp and logging level when
printed. See the available options below.

=head1 OPTIONS

=head2 Special Options

=over 4

=item B<-h>, B<--help>

Print brief usage information and exit.
(run `man zonemaster-cli` for the full manual page)

=item B<--version>

Print version information and exit.

=for :man The printed version numbers are the versions of this program as well as the ones from the underlying
Zonemaster test engine.

=item B<--list-tests>

Print all test cases listed in the test modules, then exit.

=item B<--dump-profile>

Print the effective profile used in JSON format, then exit.

=back

=head2 Testing Options

=over 4

=item B<--test>=EXPR

Specify which test cases to run by setting the C<test_cases> property in the profile.
The EXPR is a flexible expression consisting of terms (individual test cases, test
modules, or all) and operators (C<+> to add, C<-> to remove).
For details, see C<man zonemaster-cli>.

=begin :man

An EXPR starting with a term (e.g., C<all>, C<nameserver01>) I<replaces>
the current selection.
An EXPR starting with an operator (C<+> or C<->) I<modifies> the current selection.
An empty EXPR (C<--test=>) does not modify the selection (i.e., it is a no-op).

Multiple C<--test> options may be specified; they are applied in the order they are given.
When used together with the C<--profile>, the profile option is applied first.

Use C<--list-tests> to list all test cases and test modules in the default profile.

Examples:

=over 4

=item --test=B<all>

selects all test cases in the default profile, i.e. overrides any custom profile.

=item --test=B<nameserver>

selects all test cases in the Nameserver test module in the default profile.

=item --test=B<nameserver01>

selects only the Nameserver01 test case.

=item --test=B<dnssec-dnssec05>

selects all DNSSEC test cases except DNSSEC05.

=item --test=B<all-nameserver+nameserver03>

selects all test cases, excludes the Nameserver test cases, and adds back
Nameserver03.

=item --test=B<-dnssec>

removes all DNSSEC test cases from the selection.

=item --test=B<+syntax01+syntax02>

adds the Syntax01 and Syntax02 test cases to the selection.

=item --test=

empty value is valid, but does not change the selection of test cases
Specifying C<--test=> without an argument is a no-op.
Having this possibility may come in handy when invoking zonemaster-cli from scripts.

=back

=end :man

=item B<--level>=LEVEL

Specify the minimum level of a message to be printed.
(default: NOTICE)

=for :man Messages with this level
(or higher) will be printed. The levels are, from highest to lowest:
CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, DEBUG2 and DEBUG3.
The lowest three levels (DEBUG) add a significant amount of messages to be shown.
They reveal some of the internal workings of the test engine, and are probably
not useful for most users.

=begin :man

=item B<--stop-level>=LEVEL

Specify the minimum severity level after which the testing suite is terminated.
(default: the empty string)

=for :man When set to the empty string, testing is allowed to complete normally
no matter what messages are emitted.

=for :man The levels are, from highest to lowest: CRITICAL, ERROR, WARNING, NOTICE,
INFO, DEBUG, DEBUG2 and DEBUG3.

=end :man

=item B<--[no-]progress>

Print an activity indicator ("spinner").
(default: enabled if the process' standard output is a TTY)

=for :man Useful to know that something is happening during a run.

=item B<--[no-]ipv4>, B<--[no-]ipv6>

Enable or disable queries over IPv4 or IPv6.
(default: both enabled)

=begin :man

=item B<--sourceaddr4>=IPADDR, B<--sourceaddr6>=IPADDR

Set IPv4 or IPv6 source address for DNS queries.

=for :man Setting an address not correctly configured on a local network interface
fails silently.

=end :man

=item B<--profile>=FILE

Override the Zonemaster Engine default profile data with values from
the given profile JSON file.

=back

=head2 Formatting Options

=over 4

=item B<--[no-]json>

Print results as JSON instead of human language.
(default: disabled)

=begin :man

=item B<--[no-]json-stream>

Stream the results as JSON.
(default: disabled)

=for :man Useful to follow the progress in a machine-readable way.

=item B<--[no-]raw>

Print messages as raw dumps (message identifiers) instead of translating them
to human language.

=end :man

=item B<--locale>=LOCALE

Specify which locale to be used by the translation system.
(default: system locale or English)

=for :man If not given, the
translation system itself will look at environment variables to try and guess.
If the requested translation does not exist, it will fall back to the local
locale, and if that does not exist either, to English.

=begin :man

=item B<--[no-]time>

Print the timestamp for each message.
(default: enabled)

=item B<--[no-]show-level>

Print the severity level for each message.
(default: enabled)

=item B<--[no-]show-module>

Print the name of the module which produced the message.
(default: disabled)

=item B<--[no-]show-testcase>

Print the name of the test case (test case identifier) which produced the message.
(default: disabled)

=end :man

=back

=begin :man

=head2 Summary Options

=over 4

=item B<--[no-]count>

Print a summary, at the end of a run, of the numbers of messages for each severity
level that were logged during the run, as well as a count of each message tag.
(default: disabled)

=item B<--[no-]nstimes>

Print a summary, at the end of a run, of various metrics related to the times
(in milliseconds) each name server took to answer, as well as a count of the
number of sent queries per name server.
(default: disabled)

=item B<--[no-]elapsed>

Print elapsed time (in seconds) at end of a run.
(default: disabled)

=back

=head2 Undelegated Test Options

=over 4

=item B<--ns>=DOMAINNAME, B<--ns>=DOMAINNAME/IPADDR

Provide information about a nameserver, for undelegated tests.

=for :man The argument
must be either: (i) a domain name and an IP address, separated by a single
slash character (/), or (ii) only a domain name, in which case a A and AAAA
records lookup for that name is done in the live global DNS tree (unless
overridden by --hints) and from which the results of that lookup will be used.

=for :man This switch can be given multiple times. As long as any of these switches
are present, their aggregated content will be used as the
entirety of the parent-side delegation information.

=item B<--ds>=KEYTAG,ALGORITHM,TYPE,DIGEST

Provide a DS record for undelegated testing (that is, a test where the
delegating nameserver information is given via --ns switches).

=for :man The four pieces
of data (keytag, algorithm, type, digest) should be in the same format they would
have in a zone file.

=item B<--hints>=FILE

Name of a root hints file to override the defaults.

=back

=head2 Cache Options

=over 4

=item B<--save>=FILE

Write the contents of the accumulated DNS packet cache to a file with the given name
after the testing suite has finished running.

=item B<--restore>=FILE

Prime the DNS packet cache with the contents from the file with the given name
before starting the testing suite.

=for :man The format of the file should be from one produced by the --save
switch.

=back

=head2 Deprecated Options

=over 4

=item B<--encoding>=ENCODING

Deprecated: Simply remove it from your usage. It is ignored.

=item B<--[no-]json-translate>

Deprecated since v2023.1, use --no-raw instead.

=for :man For streaming JSON output, include the translated message of the tag.

=back

=head2 Option Aliases

These options are provided for compatibility with older scripts.
The first two are aliases for C<--help>.
The rest are aliases for their namesakes spelled with dash C<-> instead of
underscore C<_>.

=over 4

=item B<-?>

=item B<--usage>

=item B<--dump_profile>

=item B<--[no-]json_stream>

=item B<--[no-]json_translate>

=item B<--list_tests>

=item B<--[no-]show_level>

=item B<--[no-]show_module>

=item B<--[no-]show_testcase>

=item B<--stop_level>=LEVEL

=back

=end :man

=head1 EXAMPLES

    zonemaster-cli zonemaster.net

    zonemaster-cli --test=delegation --level=info --no-time zonemaster.net

    zonemaster-cli --test=delegation01 --level=debug zonemaster.net

    zonemaster-cli --list-tests

=head1 PROFILES

The testing and result analysis performed by Zonemaster Engine is always
guided by a profile.
Zonemaster Engine has a default profile with sensible defaults.
Zonemaster CLI allows users to override the default profile data with
values from a profile JSON file with the C<--profile> option.
For details on profiles and how they are represented in files, see
L<Zonemaster::Engine::Profile>.

=head1 CONFIGURATION

If there is a readable file F</etc/zonemaster/cli.args> (Linux style), each line
in that file will be prepended as an argument on the command line. If no
F</etc/zonemaster/cli.args> is found (or is not readable) but
F</usr/local/etc/zonemaster/cli.args> (FreeBSD style) is found and readable then
that file will be used instead. Only one global file is loaded.

If there is a readable file F<.zonemaster/cli.args> in the user's home
directory, it will be used in the same way even when a global file has been
loaded. Any argument in user's F<cli.args> will override the same argument in the
global config file.

For example, if one would like to by default run with the log
level set to DEBUG and with translation to human-readable messages turned off,
one could put this in the config file:

   --raw
   --level=DEBUG

Only one argument per line. If the argument has a value there must be a "="
between argument and value. A line starting with "#" is a comment. Comments
cannot be added on lines with arguments.

Any arguments actually given on the command line will override what is in any of
the loaded config files.

=head1 SEE ALSO

More complete documentation on Zonemaster and its tests can be found on
L<https://doc.zonemaster.net>.

=head1 AUTHOR

Calle Dybedahl <calle@init.se> and others from the Zonemaster project
