package Lire::LrCommand;
use strict;
use Lire::Config;
use Lire::Config::Build qw/ac_info/;
use Lire::ReportConfig;
use Lire::PluginManager;
use Lire::DlfStore;
use Lire::ImportJob;
use Lire::OutputJob;
use Lire::DlfConverterProcess;
use Lire::Utils qw/check_param check_object_param tempdir/;
use Lire::Error qw/file_not_writable file_not_readable/;
use File::Path qw/rmtree/;
use POSIX qw/:locale_h/;
use File::Basename;
use File::Copy qw/copy/;
use Locale::TextDomain 'lire';
use Text::Wrap qw/ wrap /;
use Carp;
use Getopt::Long;
=pod
=head1 NAME
Lire::LrCommand - Base class for command line programs.
=head1 SYNOPSIS
my $prog = new Lire::LrCommand();
$prog->init();
$prog->add_section( 'myprog', __( 'My Program Options' ) );
$prog->add_option( 'name' => 'myoption', 'type' => '=s',
'help' => __( 'My option help.' ) );
$prog->add_common_options();
$prog->parse_options();
=head1 DESCRIPTION
Class which is used to create command line program which shares
functionaliy.
=head1 METHODS
=head2 new( [$usage], [$help] )
Creates a new Lire::LrCommon instance. Initializes the usage string
and the help message to $usage and $help.
=cut
sub new {
my ( $class, $usage, $help ) = @_;
my $self = bless {}, $class;
$self->{'_prog'} = basename( $0 );
$self->{'_options'} = {};
$self->{'_sections'} = [];
$self->{'_section_labels'} = {};
$self->{'_cfg'} = {};
$self->{'_usage'} = $usage || '';
$self->{'_help'} = $help || '';
return $self;
}
sub init {
my $self = $_[0];
setlocale( LC_ALL, '' );
$SIG{'__WARN__'} = sub { $self->warning( @_ ); };
foreach my $name ( qw|.lire .lire/converters .lire/plugins .lire/reports
.lire/filters .lire/schemas .lire/templates| )
{
my $dir = "$ENV{'HOME'}/$name";
unless ( -d $dir ) {
mkdir $dir
or $self->error( __x( 'Cannot create directory {directory}.',
'directory' => $dir ) );
$self->info( __x( 'Created {directory}.',
'directory' => $dir ) );
}
}
Lire::Config->init();
Lire::PluginManager->register_default_plugins();
$self->{'_tmpdir'} = tempdir( $self->{'_prog'} . '_XXXXXX' );
$self->{'_keepdir'} = Lire::Config->get( 'lr_keep' );
$SIG{'PIPE'} = 'IGNORE';
return;
}
sub DESTROY {
my $self = $_[0];
if ( $self->{'_keepdir'} ) {
$self->info( __x( 'Keeping temporary directory {tmpdir}',
'tmpdir' => $self->{'_tmpdir'} ) );
} else {
rmtree( $self->{'_tmpdir'} );
}
return;
}
=pod
=head2 set_usage( [$usage] )
=cut
sub set_usage {
my ( $self, $usage ) = @_;
$self->{'_usage'} = $usage;
return;
}
=pod
=head2 set_help( [$help] )
=cut
sub set_help {
my ( $self, $help ) = @_;
$self->{'_help'} = $help;
return;
}
=pod
=head2 set_section( [$name], [$label] )
=cut
sub add_section {
my ( $self, $name, $label ) = @_;
check_param( $name, 'name' );
check_param( $label, 'label' );
push @{$self->{'_sections'}}, $name;
$self->{'_section_labels'}{$name} = $label;
return;
}
=pod
=head2 add_option( [%args] )
=cut
sub add_option {
my ( $self, %args ) = @_;
check_param( $args{'name'}, 'name' );
check_param( $args{'section'}, 'section' );
my $section = $args{'section'};
croak "invalid section: $section"
unless exists $self->{'_section_labels'}{$section};
check_param( $args{'help'}, 'help' );
check_object_param( $args{'aliases'}, 'aliases', 'ARRAY' )
if defined $args{'aliases'};
$args{'aliases'} = [] unless defined $args{'aliases'};
$args{'type'} = '' unless defined $args{'type'};
$args{'action'} = 'store' unless defined $args{'action'};
$self->{'_options'} ||= [];
push @{$self->{'_options'}{$section}}, \%args;
return;
}
=pod
=head2 add_common_options()
=cut
sub add_common_options {
my $self = $_[0];
$self->add_section( 'common', __( 'Common options' ) );
$self->add_option( 'name' => 'help',
'type' => ':s',
'section' => 'common',
'action' => sub { $self->do_help( $_[1] ) },
'help' => __( 'Print usage information and exit. If "report-templates", "output-formats" or "dlf-converters" is given as argument, the command will print an information list for that object type.' ),
);
$self->add_option( 'name' => 'version',
'section' => 'common',
'action' => sub { $self->do_version() },
'help' => __( 'Print Lire version and exit.' ),
);
$self->add_option( 'name' => 'quiet',
'type' => '!',
'section' => 'common',
'help' => __( 'Only print errors.' ),
);
return;
}
sub add_email_options {
my $self = $_[0];
$self->add_section( 'email', __( 'Email related options' ) );
$self->add_option( 'name' => 'subject',
'section' => 'email',
'help' => __( "Sets the email's subject." ),
'type' => '=s' );
$self->add_option( 'name' => 'extra-file',
'type' => '=s',
'section' => 'email',
'help' => __( 'A text file that will be included with the report.' ),
);
return;
}
sub add_output_format_options {
my $self = $_[0];
$self->add_section( 'output', __( 'Output format options' ) );
$self->add_option( 'name' => 'image',
'section' => 'output',
'help' => __( 'Backward compatibility.' ),
'obsolete' => 1 );
$self->add_option( 'name' => 'output-format',
'type' => '=s',
'section' => 'output',
'help' => __( 'Select the output format. Default to "txt"' ),
);
return;
}
sub add_merging_options {
my $self = $_[0];
$self->add_section( 'merging', __( 'Merging options' ) );
$self->add_option( 'name' => 'template',
'section' => 'merging',
'type' => '=s',
'help' => __( 'Select the report configuration template that should be used for merging the reports. This option is required if you use the --merge option.' ),
);
$self->add_option( 'name' => 'merge',
'section' => 'merging',
'aliases' => [ 'x' ],
'type' => '=s@',
'help' => __( 'Merge an additional report before formatting the report. This option can be used multipe times.' ),
);
$self->add_option( 'name' => 'report-cfg',
'type' => '=s',
'section' => 'merging',
'aliases' => [ 'c' ],
'help' => __( 'Pre Lire 2.0 report configuration file.' ),
'obsolete' => 1 );
$self->add_option( 'name' => 'U',
'type' => '=s',
'section' => 'merging',
'help' => __( 'Selects the superservice for which the report configuration file is made.' ),
'obsolete' => 1 );
return;
}
sub add_report_options {
my $self = $_[0];
$self->add_section( 'report', __( 'Report generation options' ) );
$self->add_option( 'name' => 'report-cfg',
'type' => '=s',
'section' => 'report',
'aliases' => [ 'c' ],
'help' => __( 'Pre Lire 2.0 report configuration file.' ),
'obsolete' => 1 );
$self->add_option( 'name' => 'template',
'section' => 'report',
'type' => '=s',
'help' => __( 'Select the report configuration template that should be used for generating the report.' ),
);
return;
}
sub get_option {
my ( $self, $name, $default ) = @_;
check_param( $name, 'name' );
return defined $self->{'_cfg'}{$name} ? $self->{'_cfg'}{$name} : $default;
}
sub parse_options {
my $self = $_[0];
my @options = ();
foreach my $section ( @{$self->{'_sections'}} ) {
foreach my $opt ( @{$self->{'_options'}{$section}} ) {
my $spec = join ( "|", $opt->{'name'}, @{$opt->{'aliases'}} )
. $opt->{'type'};
$self->{'_cfg'}{$opt->{'name'}} = $opt->{'action'}
if ref $opt->{'action'};
push @options, $spec;
}
}
GetOptions( $self->{'_cfg'}, @options )
or $self->usage();
return;
}
sub error {
my ( $self, @msgs ) = @_;
print STDERR $self->{'_prog'} . ': ' . __( 'ERROR ' ),
join( "\n", @msgs ), "\n";
exit(1);
}
sub warning {
my ( $self, @msgs ) = @_;
return if $self->{'_cfg'}{'quiet'};
print STDERR $self->{'_prog'} . ': ' . __( 'WARNING ' ),
join( "\n", @msgs ), "\n";
return;
}
sub info {
my ( $self, @msgs ) = @_;
return if $self->{'_cfg'}{'quiet'};
print STDERR join( "\n", @msgs ), "\n";
return;
}
sub do_version {
my $self = $_[0];
print STDERR __x( '{program} as shipped with Lire version {version}',
'program' => $self->{'_prog'},
'version' => ac_info( 'VERSION' ) ), "\n";
exit(0);
}
sub usage {
my ( $self, $msg ) = @_;
if ( $msg ) {
chomp $msg;
print STDERR $msg, "\n"
if $msg;
}
$self->print_usage();
exit(1);
}
sub print_usage {
my $self = $_[0];
print STDERR __x( 'Usage: {program} {usage}',
'program' => $self->{'_prog'},
'usage' => $self->{'_usage'} ), "\n";
return;
}
sub do_help {
my ( $self, $section ) = @_;
$section ||= '';
if ( $section eq 'output-formats' ) {
$self->do_help_output_formats();
} elsif ( $section eq 'report-templates' ) {
$self->do_help_report_templates();
} elsif ( $section eq 'dlf-converters' ) {
$self->do_help_dlf_converters();
} elsif ( $section ) {
print STDERR __x( 'Unknown help section: {section}',
'section' => $section ), "\n";
}
$self->print_usage();
print STDERR "\n", wrap( '', '', $self->{'_help'} ), "\n", "\n";
foreach my $section ( @{$self->{'_sections'} } ) {
print STDERR uc $self->{'_section_labels'}{$section}, "\n";
foreach my $option ( @{$self->{'_options'}{$section}} ) {
my $obsolete = $option->{'obsolete'} ? ' ' .
__( 'This option is obsolete.' ) : '';
print wrap( sprintf( ' --%-17s', $option->{'name'} ) . ' ',
' ' x 22, $option->{'help'} . $obsolete ), "\n";
}
print "\n";
}
exit(0);
}
sub do_help_report_templates {
my $self = $_[0];
print STDERR __( 'Available report templates:' ), "\n";
foreach my $template ( sort @{Lire::ReportConfig->templates()} ) {
print STDERR $template, "\n";
}
exit(0);
}
sub do_help_dlf_converters {
my $self = $_[0];
print STDERR __( 'Available DLF converters:' ), "\n";
my @plugins = sort { $a->name() cmp $b->name() }
@{Lire::PluginManager->plugins( 'dlf_converter' )};
foreach my $converter ( @plugins ) {
printf STDERR "%-16s %s\n", $converter->name(), $converter->title();
}
exit(0);
}
sub do_help_output_formats {
my $self = $_[0];
print STDERR __( 'Available output formats:' ), "\n";
my @plugins = sort { $a->name() cmp $b->name() }
@{Lire::PluginManager->plugins( 'output_format' )};
foreach my $format ( @plugins ) {
printf STDERR "%-16s %s\n", $format->name(), $format->title();
}
exit(0);
}
sub init_temp_store {
my $self = $_[0];
$self->{'_store'} = eval {
Lire::DlfStore->open( "$self->{'_tmpdir'}/store", 1 );
};
$self->error( $@ ) if $@;
return;
}
sub is_compressed {
my ( $self, $magic ) = @_;
return unless length $magic;
if ( substr($magic,0,2) eq "\037\235" ) {
return 'compress';
} elsif ( substr($magic,0,2) eq "\037\213" ) {
return 'gzip';
} elsif ($magic eq "PK\003\004" ) {
return 'PKZIP';
}
return;
}
sub handle_logfile {
my ( $self, $logfile ) = @_;
$self->{'_logfile'} = $logfile;
open my $fh, $logfile
or $self->error( file_not_readable( $logfile ) );
my $magic;
read $fh, $magic, 4;
$self->error( __x( 'read failed: {error}', 'error' => $! ) )
unless defined $magic;
my $algo = $self->is_compressed( $magic );
my $tmplogfile = "$self->{'_tmpdir'}/logfile";
my $out_fh;
if ( $algo ) {
$self->info( __x( 'Log file was compressed using {algo}.',
'algo' => $algo ) );
$self->error( __( "gzip(1) isn't available" ) )
unless Lire::Config->get_var( 'gzip_path' )->is_valid();
my $gzip = Lire::Config->get( 'gzip_path' );
$self->info( __x( 'Decompressing log file using {gzip}...',
'gzip' => $gzip ) );
local %ENV = ();
open $out_fh, "|$gzip -d > $tmplogfile"
or $self->error( __x( 'error forking: {error}', 'error' => $! ) );
} elsif ( $logfile eq '-' or $logfile =~ /\|\s*$/ ) {
open $out_fh, "> $tmplogfile"
or $self->error( file_not_writable( $tmplogfile ) );
}
if ( $out_fh ) {
$self->{'_logfile'} = $tmplogfile;
print $out_fh $magic;
my $buffer;
while ( read $fh, $buffer, 1024**2 ) {
print $out_fh $buffer;
}
$self->error( __x( 'read failed: {error}', 'error' => $! ) )
unless defined $buffer;
close $out_fh
or $self->error( __x( "gzip exited with non zero status: {status}",
'status' => $? ) );
}
close $fh;
return;
}
=pod
=head2 import_log( [$converter], [$logfile] )
import_log invokes Lire::ImportJob and Lire::DlfConverterProcess.
It uses &handle_logfile.
This means that in case the $logfile is compressed, it is decompressed to a
file in $TMPDIR. In case the $logfile is read from a pipe or from STDIN, it is
written to a file in $TMPDIR. So, if you're short on discspace, pass the
logfile as an uncompressed file.
=cut
sub import_log {
my ( $self, $converter, $logfile ) = @_;
$self->check_converter( $converter );
$self->handle_logfile( $logfile );
$self->info( __x( 'Parsing log file using {converter} DLF Converter...',
'converter' => $converter ) );
eval {
my $src = new Lire::ImportJob( "lr_store",
pattern => $self->{'_logfile'},
period => 'unique',
converter => $converter );
my $process = new Lire::DlfConverterProcess( $src, $self->{'_store'} );
$process->run_import_job();
$self->info( __x( 'Extracted {dlf_count} DLF records on {line_count} lines.',
'dlf_count' => $process->dlf_count(),
'line_count' => $process->line_count() ) );
$self->info( __x( 'Encountered {error_count} errors and ignored {ignored_count} lines.',
'error_count' => $process->error_count(),
'ignored_count' => $process->ignored_count() ) );
};
$self->error( $@ ) if $@;
return;
}
sub init_report_config {
my ( $self, $converter ) = @_;
if ( $self->{'_cfg'}{'template'} ) {
$self->{'_report_cfg'} = $self->load_report_config_from_template();
} elsif ( $self->{'_cfg'}{'report-cfg'} ) {
$self->{'_report_cfg'} =
eval { new_from_file Lire::ReportConfig( $self->find_superservice( $converter ), $self->{'_cfg'}{'report-cfg'} ) };
$self->error( $@ ) if $@;
} else {
my $template = $self->find_superservice( $converter) . '_default';
$self->error( __x( 'No default report configuration template defined for DLF converter "{converter}.".', 'converter' => $converter,
'template' => $template ),
__( 'Use the --template option.' ) )
unless Lire::ReportConfig->has_template( $template );
$self->{'_report_cfg'} = Lire::ReportConfig->template( $template )->as_value();
}
return;
}
sub check_converter {
my ( $self, $converter ) = @_;
$self->error( __x( 'No such DLF converter: {converter}.',
'converter' => $converter ),
__( 'Use --help dlf-converters to list available converters' ) )
unless Lire::PluginManager->has_plugin( 'dlf_converter', $converter );
return;
}
sub find_superservice {
my ( $self, $converter ) = @_;
$self->check_converter( $converter );
my $plugin = Lire::PluginManager->get_plugin( 'dlf_converter', $converter);
return ($plugin->schemas())[0];
}
=pod
=head2 generate_report( [$converter], [$logfile] )
Generate a report from a $logfile, using $converter. Returns $reportfile.
Uses Lire::ReportConfig::generate_report to do the real work.
=cut
sub generate_report {
my ( $self, $converter, $logfile ) = @_;
check_param( $converter, 'converter' );
check_param( $logfile, 'logfile' );
$self->init_temp_store();
$self->init_report_config( $converter );
$self->import_log( $converter, $logfile );
$self->info( __( 'Running analysers...' ) );
my $cfg = $self->{'_report_cfg'};
eval { $cfg->create_analyser_streams( $self->{'_store'} ) };
$self->error( $@ ) if $@;
$self->info( __( 'Generating XML report...' ) );
my $report = eval { $cfg->generate_report( $self->{'_store'} ) };
$self->error( $@ ) if $@;
my $reportfile = "$self->{'_tmpdir'}/report.xml";
open my $fh, "> $reportfile"
or $self->error( file_not_writable( $reportfile ) );
eval {
$report->write_report( $fh );
$fh->close();
};
$self->error( $@ ) if $@;
return $reportfile;
}
sub load_report_config_from_template {
my $self = $_[0];
my $name = $self->{'_cfg'}{'template'};
$self->error( __x( 'No report configuration template: {template}.',
'template' => $name ),
__( 'Use --help report-templates to list available templates.', ) )
unless Lire::ReportConfig->has_template( $name );
return Lire::ReportConfig->template( $name )->as_value();
}
sub init_merging_report_config {
my $self = $_[0];
if ( $self->{'_cfg'}{'template'} ) {
return $self->load_report_config_from_template();
} elsif ( $self->{'_cfg'}{'report-cfg'} ) {
$self->error( __( 'The -U option is required with --report-cfg' ) )
unless $self->{'_cfg'}{'U'};
return eval { new_from_file Lire::ReportConfig( $self->{'_cfg'}{'U'}, $self->{'_cfg'}{'report-cfg'} ) };
$self->error( $@ ) if $@;
} else {
$self->error( __( 'You need to use the --template option to merge reports.' ) );
}
}
sub merge_reports {
my ( $self, $report ) = @_;
return $report
unless $self->{'_cfg'}{'merge'};
my @reports = ( $report, @{$self->{'_cfg'}{'merge'}} );
my $cfg = $self->init_merging_report_config();
my $tmpreport = "$self->{'_tmpdir'}/merged-report.xml";
$self->info( __x( "Merging reports '{reports}'...",
'reports' => join "', '", @reports ) );
eval {
my $report = $cfg->merge_report_files( @reports );
open my $fh, "> $tmpreport"
or $self->error( file_not_writable( $tmpreport ) );
$report->write_report( $fh );
close $fh;
};
$self->error( $@ ) if $@;
return $tmpreport;
}
sub check_output_format {
my $self = $_[0];
$self->{'_cfg'}{'output-format'} ||= 'txt';
my $format = $self->{'_cfg'}{'output-format'} ||= 'txt';
$self->error( __x( 'No such output format: {format}.',
'format' => $format ),
__( 'Use --help output-formats to list available formats.') )
unless Lire::PluginManager->has_plugin( 'output_format', $format );
my $plugin = Lire::PluginManager->get_plugin( 'output_format', $format );
my $missings = join "\n", $plugin->missing_requirements();
$self->error( __( 'This output format has missing requirements:' ),
$missings )
if $missings;
return;
}
sub format_report {
my ( $self, $report_file, $output_file ) = @_;
check_param( $report_file, 'report_file' );
check_param( $output_file, 'output_file' );
eval {
my $format = $self->{'_cfg'}{'output-format'};
$self->info( __x( 'Formatting report as {format} in {file}...',
'format' => $format,
'file' => $output_file ) );
my $stdout = 0;
if ( $output_file eq '-' or $output_file =~ /\|\s*/ ) {
$output_file = "$self->{'_tmpdir'}/formatted-report.$format";
$stdout = 1;
}
my $job = new Lire::OutputJob( 'file', Lire::PluginManager->get_plugin( 'output_format', $format ), {}, 'file' => $output_file );
$job->run( $report_file );
if ( $format eq 'html' && $stdout ) {
my $tar = Lire::Config->get( 'tar_path' );
system ( "$tar cC $self->{'_tmpdir'} report" );
} elsif ( $stdout ) {
copy( $output_file, \*STDOUT );
}
};
$self->error( $@ ) if $@;
return;
}
sub check_email_requirements {
my $self = $_[0];
eval "use MIME::Entity;";
$self->error( __( 'This command requires the MIME::Tools CPAN module.' ) )
if $@;
return;
}
sub email_report {
my ( $self, $report_file, @emails ) = @_;
check_param( $report_file, 'report_file' );
eval {
my $format = $self->{'_cfg'}{'output-format'};
my $subject = $self->{'_cfg'}{'subject'}
|| __x( 'Your {format} report.', 'format' => $format );
$self->info( __x( 'Formatting report as {format} and emailing it to {emails}...',
'format' => $format,
'emails' => join( ", ", @emails ) ), );
my $plugin =
Lire::PluginManager->get_plugin( 'output_format', $format );
my $job = new Lire::OutputJob( 'email', $plugin, {},
'subject' => $subject,
'extra_file' => $self->{'_cfg'}{'extra-file'},
'emails' => \@emails );
$job->run( $report_file );
};
$self->error( $@ ) if $@;
return;
}
1;
__END__
=pod
=head1 SEE ALSO
lr_log2report(1), lr_log2mail(1), lr_xml2mail(1), lr_xml2report(1),
Lire::Config(3pm), Lire::ReportConfig(3pm), Lire::PluginManager(3pm),
Lire::DlfStore(3pm), Lire::ImportJob(3pm), Lire::OutputJob(3pm),
Lire::DlfConverterProcess(3pm)
=head1 AUTHOR
Francis J. Lacoste <flacoste@logreport.org>
=head1 VERSION
$Id: LrCommand.pm,v 1.9 2006/07/23 13:16:29 vanbaal Exp $
=head1 COPYRIGHT
Copyright (C) 2004 Stichting LogReport Foundation LogReport@LogRepor
t.org
This file is part of Lire.
Lire is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program (see COPYING); if not, check with
http://www.gnu.org/copyleft/gpl.html.
=cut
syntax highlighted by Code2HTML, v. 0.9.1