Hailo::Command - Class for the L<hailo> command-line interface to L<Hailo>


Hailo documentation Contained in the Hailo distribution.

Index


Code Index:

NAME

Top

Hailo::Command - Class for the hailo command-line interface to Hailo

DESCRIPTION

Top

This is an internal class hailo uses for its command-line interface. See Hailo for the public interface.

PRIVATE METHODS

Top

run

Run Hailo in accordance with the the attributes that were passed to it, this method is called by the hailo command-line utility and the Hailo test suite, its behavior is subject to change.

AUTHOR

Top

Ævar Arnfjörð Bjarmason <avar@cpan.org>

LICENSE AND COPYRIGHT

Top


Hailo documentation Contained in the Hailo distribution.

package Hailo::Command;
BEGIN {
  $Hailo::Command::AUTHORITY = 'cpan:AVAR';
}
BEGIN {
  $Hailo::Command::VERSION = '0.69';
}

use 5.010;
use Any::Moose;
use Any::Moose 'X::Getopt';
use Any::Moose 'X::StrictConstructor';
use namespace::clean -except => 'meta';

extends 'Hailo';

with any_moose('X::Getopt::Dashes');

## Our internal Getopts method that Hailo.pm doesn't care about.

has help_flag => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => 'h',
    cmd_flag      => 'help',
    isa           => 'Bool',
    is            => 'ro',
    default       => 0,
    documentation => "You're soaking it in",
);

has _go_version => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => 'v',
    cmd_flag      => 'version',
    documentation => 'Print version and exit',
    isa           => 'Bool',
    is            => 'ro',
);

has _go_examples => (
    traits        => [ qw/ Getopt / ],
    cmd_flag      => 'examples',
    documentation => 'Print examples along with the help message',
    isa           => 'Bool',
    is            => 'ro',
);

has _go_progress => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => 'p',
    cmd_flag      => 'progress',
    documentation => 'Display progress during the import',
    isa           => 'Bool',
    is            => 'ro',
    default       => sub {
        my ($self) = @_;
        $self->_is_interactive();
    },
);

has _go_learn => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "l",
    cmd_flag      => "learn",
    documentation => "Learn from STRING",
    isa           => 'Str',
    is            => "ro",
);

has _go_learn_reply => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "L",
    cmd_flag      => "learn-reply",
    documentation => "Learn from STRING and reply to it",
    isa           => 'Str',
    is            => "ro",
);

has _go_train => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "t",
    cmd_flag      => "train",
    documentation => "Learn from all the lines in FILE, use - for STDIN",
    isa           => 'Str',
    is            => "ro",
);

has _go_train_fast => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "f",
    cmd_flag      => "train-fast",
    documentation => "Train with aggressive caching (memory-hungry!)",
    isa           => 'Str',
    is            => "ro",
);

has _go_reply => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "r",
    cmd_flag      => "reply",
    documentation => "Reply to STRING",
    isa           => 'Str',
    is            => "ro",
);

has _go_random_reply => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "R",
    cmd_flag      => "random-reply",
    documentation => "Like --reply but takes no STRING; Babble at random",
    isa           => 'Bool',
    is            => "ro",
);

has _go_stats => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "s",
    cmd_flag      => "stats",
    documentation => "Print statistics about the brain",
    isa           => 'Bool',
    is            => "ro",
);

## Things we have to pass to Hailo.pm via triggers when they're set

has _go_autosave => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => 'a',
    cmd_flag      => 'autosave',
    documentation => 'Save the brain on exit (on by default)',
    isa           => 'Bool',
    is            => 'rw',
    trigger       => sub {
        my ($self, $bool) = @_;
        $self->save_on_exit($bool);
    },
);

has _go_order => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "o",
    cmd_flag      => "order",
    documentation => "Markov order; How deep the rabbit hole goes",
    isa           => 'Int',
    is            => "rw",
    trigger       => sub {
        my ($self, $order) = @_;
        $self->order($order);
    },
);

has _go_brain => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "b",
    cmd_flag      => "brain",
    documentation => "Load/save brain to/from FILE",
    isa           => 'Str',
    is            => "ro",
    trigger       => sub {
        my ($self, $brain) = @_;
        $self->brain($brain);
    },
);

# working classes
has _go_engine_class => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "E",
    cmd_flag      => "engine",
    isa           => 'Str',
    is            => "rw",
    documentation => "Use engine CLASS",
    trigger       => sub {
        my ($self, $class) = @_;
        $self->engine_class($class);
    },
);

has _go_storage_class => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "S",
    cmd_flag      => "storage",
    isa           => 'Str',
    is            => "rw",
    documentation => "Use storage CLASS",
    trigger       => sub {
        my ($self, $class) = @_;
        $self->storage_class($class);
    },
);

has _go_tokenizer_class => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "T",
    cmd_flag      => "tokenizer",
    isa           => 'Str',
    is            => "rw",
    documentation => "Use tokenizer CLASS",
    trigger       => sub {
        my ($self, $class) = @_;
        $self->tokenizer_class($class);
    },
);

has _go_ui_class => (
    traits        => [ qw/ Getopt / ],
    cmd_aliases   => "u",
    cmd_flag      => "ui",
    isa           => 'Str',
    is            => "rw",
    documentation => "Use UI CLASS",
    trigger       => sub {
        my ($self, $class) = @_;
        $self->ui_class($class);
    },
);

# Stop Hailo from polluting our command-line interface
for (qw/ save_on_exit order brain /, map { qq[${_}_class] } qw/ engine storage tokenizer ui /) {
    has "+$_" => (
        traits => [ qw/ NoGetopt / ],
    );
}

# Check validity of options
before run => sub {
    my ($self) = @_;

    if (not $self->_storage->ready and
        (defined $self->_go_reply or
         defined $self->_go_train or
         defined $self->_go_train_fast or
         defined $self->_go_stats or
         defined $self->_go_learn or
         defined $self->_go_learn_reply or
         defined $self->_go_random_reply)) {
        # TODO: Make this spew out the --help reply just like hailo
        # with invalid options does usually, but only if run via
        # ->new_with_options
        die "To reply/train/learn/stat you must specify options to initialize your storage backend\n";
    }

    if (defined $self->_go_train and defined $self->_go_train_fast) {
        die "You can only specify one of --train and --train-fast\n";
    }

    return;
};

sub run {
    my ($self) = @_;

    if ($self->_go_version) {
        # Munging strictness because we don't have a version from a
        # Git checkout. Dist::Zilla provides it.
        no strict 'vars';
        my $version = $VERSION // 'dev-git';

        say "hailo $version";
        return;
    }

    if ($self->_is_interactive() and
        $self->_storage->ready and
        not defined $self->_go_train and
        not defined $self->_go_train_fast and
        not defined $self->_go_learn and
        not defined $self->_go_reply and
        not defined $self->_go_learn_reply and
        not defined $self->_go_stats and
        not defined $self->_go_random_reply) {
        $self->_ui->run($self);
    }

    $self->train($self->_go_train) if defined $self->_go_train;
    $self->train($self->_go_train_fast, 1) if defined $self->_go_train_fast;
    $self->learn($self->_go_learn) if defined $self->_go_learn;

    if (defined $self->_go_learn_reply) {
        my $answer = $self->learn_reply($self->_go_learn_reply);
        say $answer // "I don't know enough to answer you yet.";
    }

    if (defined $self->_go_random_reply) {
        my $answer = $self->reply();
        say $answer // "I don't know enough to answer you yet.";
    }
    elsif (defined $self->_go_reply) {
        my $answer = $self->reply($self->_go_reply);
        say $answer // "I don't know enough to answer you yet.";
    }

    if ($self->_go_stats) {
        my ($tok, $ex, $prev, $next) = $self->stats();
        my $order = $self->_storage->order;
        say "Tokens: $tok";
        say "Expression length: $order tokens";
        say "Expressions: $ex";
        say "Links to preceding tokens: $prev";
        say "Links to following tokens: $next";
    }

    return;
}

override _train_fh => sub {
    my ($self, $fh, $fast, $filename) = @_;

    if ($self->_go_progress and $self->_is_interactive) {
        $self->train_progress($fh, $fast, $filename);
    } else {
        super();
    }
};

before train_progress => sub {
    require Term::Sk;
    require File::CountLines;
    File::CountLines->import('count_lines');
    require Time::HiRes;
    Time::HiRes->import(qw(gettimeofday tv_interval));
    return;
};

sub train_progress {
    my ($self, $fh, $fast, $filename) = @_;
    my $lines = count_lines($filename);
    my $progress = Term::Sk->new('%d Elapsed: %8t %21b %4p %2d (%c lines of %m)', {
        # Start at line 1, not 0
        base => 1,
        target => $lines,
        # Every 0.1 seconds for long files
        freq => ($lines < 10_000 ? 10 : 'd'),
        # Override Term::Sk's default 100_100 to 100,000
        commify => sub {
            my $int = shift;
            $int = reverse $int;
            $int =~ s/(\d{3})(?=\d)(?!\d*\.)/$1,/g;
            $int = reverse $int;
            return $int;
        },
    }) or die "Error in Term::Sk->new: (code $Term::Sk::errcode) $Term::Sk::errmsg";

    my $next_update = 0;
    my $start_time = [gettimeofday()];

    my $i = 0; while (my $line = <$fh>) {
        $i++;
        chomp $line;
        $self->_learn_one($line, $fast);
        $self->_engine->flush_cache if !$fast;
        $progress->up;
    }

    $progress->close;

    if ($fast) {
        print "Flushing cache (this may take a while for large inputs)\n";
        $self->_engine->flush_cache;
    }

    my $elapsed = tv_interval($start_time);
    say sprintf "Trained from %d lines in %.2f seconds; %.2f lines/s", $i, $elapsed, ($i / $elapsed);

    return;
}

# --i--do-not-exist
sub _getopt_spec_exception { goto &_getopt_full_usage }

# --help
sub _getopt_full_usage {
    my ($self, $usage, $plain_str) = @_;

    # If called from _getopt_spec_exception we get "Unknown option: foo"
    my $warning = ref $usage eq 'ARRAY' ? $usage->[0] : undef;

    my ($use, $options) = do {
        # $plain_str under _getopt_spec_exception
        my $out = $plain_str // $usage->text;

        # The default getopt order sucks, use reverse sort order
        chomp(my @out = split /^/, $out);
        my $opt = join "\n", sort { $b cmp $a } @out[1 .. $#out];
        ($out[0], $opt);
    };
    my $synopsis = do {
        require Pod::Usage;
        my $out;
        open my $fh, '>', \$out;

        no warnings 'once';

        my $hailo = File::Spec->catfile($Hailo::Command::HERE_MOMMY, 'hailo');
        # Try not to fail on Win32 or other odd systems which might have hailo.pl not hailo
        $hailo = ((glob("$hailo*"))[0]) unless -f $hailo;
        Pod::Usage::pod2usage(
            -input => $hailo,
            -sections => 'SYNOPSIS',
            -output   => $fh,
            -exitval  => 'noexit',
        );
        close $fh;

        $out =~ s/\n+$//s;
        $out =~ s/^Usage:/examples:/;

        $out;
    };

    # Unknown option provided
    print $warning if $warning;

    print <<"USAGE";
$use
$options
\n\tNote: All input/output and files are assumed to be UTF-8 encoded.
USAGE

    # Hack: We can't get at our object from here so we have to inspect
    # @ARGV directly.
    say "\n", $synopsis if "@ARGV" ~~ /--examples/;

    exit 1;
}

__PACKAGE__->meta->make_immutable;