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 =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