#!/usr/bin/perl -w
##------------------------------------------------------------------------##
##  File:
##      $Id: logcmd,v 1.1 2002/10/01 22:49:46 ehood Exp $
##  Author:
##      Earl Hood       earl@earlhood.com
##  Description:
##	Program to log the output of a program.
##	POD at __END__.
##------------------------------------------------------------------------##
##    Copyright (C) 2002   Earl Hood, earl@earlhood.com
##
##    This program 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; if not, write to the Free Software
##    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
##    02111-1307, USA
##------------------------------------------------------------------------##

package Devtools::locmd;

use strict;
use Getopt::Long;
use POSIX;

my $our_name = 'logcmd';
my @cmd = ( );
my $cmd_name = "";
my $time_fmt = '%Y-%m-%d %H:%M:%S';

my $first_time = 1;
my $log_file = '-';
my $log_fh;
my $child_pid;

MAIN: {
  my @mailto   = ( );
  my ($help, $man);

  GetOptions(
    'log=s'	  => \$log_file,
    'mailto=s@'	  => \@mailto,	# not implemented, yet
    'time-fmt=s'  => \$time_fmt,

    'help'	  => \$help,
    'man'	  => \$man,
  ) || die qq/ERROR: Use -help for usage information\n/;
  usage(1, 0)  if ($help);
  usage(2, 0)  if ($man);

  if (!@ARGV) {
    die qq/ERROR: No command specified\n/;
  }
  ($cmd_name = $ARGV[0]) =~ s/.*\///;
  @cmd = @ARGV;

  $child_pid = open(CMD, "-|");
  if ($child_pid) {   # parent
    {
      local @SIG{'PIPE','TERM','INT'} = (sub {
	log_line($our_name, $$, "Caught signal: SIG$_[0]\n");
      }) x 3;

      while (<CMD>) {
	log_line($cmd_name, $child_pid, $_);
      }
    }
    if (!close(CMD)) {
      log_line($our_name, $$, qq/Non-zero exit for [$child_pid]: $?\n/);
    }
    log_close();

  } else {      # child
    open(STDERR, '>&STDOUT');
    exec(@cmd) || die qq/ERROR: Cannot exec "@cmd": $!\n/;
  }

} # End: MAIN

##=========================================================================##

sub format_time {
  POSIX::strftime($time_fmt, localtime);
}

sub log_line  {
  my $label = shift;
  my $pid   = shift;
  my $line  = shift;
  my $have_newline = $line =~ /\n\Z/;
  my $eol = ($have_newline) ? "" : "\n";
  if ($first_time) {
    $first_time = 0;
    log_open($log_file);
    print $log_fh join('', '[',format_time(),']',' [',$$,'] ',$our_name,': ',
			   "Command: [$child_pid] @cmd\n");
  }
  print $log_fh join('', '[',format_time(),']',' [',$pid,'] ',$label,': ',
			  $line,$eol);
}

sub log_open {
  my $file = shift;
  if (!defined($file) || $file eq '-') {
    $log_fh = \*STDOUT;
  } else {
    open(LOG, ">>$file") ||
	die qq/ERROR: Unable to open "$file" for appending: $!\n/;
    $log_fh = \*LOG;
  }
}

sub log_close {
  if (!$first_time) {
    if ($log_fh != \*STDOUT) {
      close($log_fh) ||
	  warn qq/Warning: Problem closing log file: $!\n/;
    }
  }
}

sub usage {
  require Pod::Usage;
  my $verbose   = shift || 0;
  my $exit_code	= shift;

  if ($verbose == 0) {
    Pod::Usage::pod2usage(-verbose => $verbose);
  } else {
    my $pager = $ENV{'PAGER'} || 'more';
    local(*PAGER);
    my $fh = (-t STDOUT && open(PAGER, "|$pager")) ? \*PAGER : \*STDOUT;
    Pod::Usage::pod2usage(-verbose => $verbose,
                          -output  => $fh);
    close(PAGER)  if ($fh == \*PAGER);
  }
  defined($exit_code) && exit($exit_code);
}

##=========================================================================##
__END__

=head1 NAME

logcmd - Log the output of a program.

=head1 SYNOPSIS

  logcmd [options] -- <command> [command-args ...]

=head1 DESCRIPTION

B<logcmd> logs the output of a program.  This program is useful
for logging program output for programs that do not have a built-in
logging facility.

Typical usage is to use B<logcmd> in crontabs to log any command
output to a file instead of mail automatically being sent to the
crontab owner.

When invoking B<logcmd>, you B<must> use "C<-->" to separate the
B<logcmd> and its options from the command you are invoking.  For
example,

  logcmd -log out.log -- some-cmd arg1 arg2 arg3

This tells B<logcmd> to run "C<some-cmd arg1 arg2 arg3>" and have
any output goto C<out.log>.

Each line logged will be preceded by a timestamp, the process ID, and
the name of the command.  Also, before the first log message for a command
is printed, B<logcmd> will print out a line giving the full command-line
of the command invoked.  Example:

  [2002-07-30 14:52:13] [25392] logcmd: Command: [25393] mhonarc -quiet -out out /var/archiver/mail
  [2002-07-30 14:52:13] [25393] mhonarc: 
  [2002-07-30 14:52:13] [25393] mhonarc: Warning: Unrecognized character set: utf-8
  [2002-07-30 14:52:13] [25393] mhonarc:          Message-Id: <1022714308.2028.1.camel@xxx.xxx.xxx>
  [2002-07-30 14:52:13] [25393] mhonarc:          Message Number: 00367
  [2002-07-30 14:52:13] [25393] mhonarc: 
  [2002-07-30 14:52:13] [25393] mhonarc: Warning: Unrecognized character set: utf-8
  [2002-07-30 14:52:13] [25393] mhonarc:          Message-Id: <1022716087.2028.3.camel@xxx.xxx.xxx>
  [2002-07-30 14:52:13] [25393] mhonarc:          Message Number: 00368
  [2002-07-30 14:52:14] [25393] mhonarc: 
  [2002-07-30 14:52:14] [25393] mhonarc: Warning: Unrecognized character set: utf-8
  [2002-07-30 14:52:14] [25393] mhonarc:          Message-Id: <1024365245.9394.0.camel@xxx.xxx.xxx>
  [2002-07-30 14:52:14] [25393] mhonarc:          Message Number: 00398


=head1 OPTIONS

=over

=item C<-help>

Display SYNOPSIS and OPTIONS sections.

=item C<-log> I<pathname>

Command output will be logged to I<pathname>.  If this option is
not specified, then standard out is used.

=item C<-man>

Display manpage.

=item C<-time-fmt> I<fmt>

Date/time format to use when printing time before each logged line.
The format string should be in C<strftime>(3) format.  If this
option is not specified, then "C<%Y-%m-%d %H:%M:%S>" is used.

=item C<-->

Terminate option processing for B<logcmd>.  Any arguments after
C<--> comprise the command that B<logcmd> should execute.

=back

=head1 EXAMPLES

The following is an example crontab entries of a user that has
any output of commands goto a personal log file:

  0 2 * * * logcmd -log $HOME/log/cron.log -- $HOME/bin/clean_account
  0 3 * * * logcmd -log $HOME/log/cron.log -- $HOME/bin/pack_mail

=head1 LIMITATIONS

=over

=item *

B<logcmd> performs no file locking on the log file specified.  To help
minimize that a single output line to the log does not get broken up
due to multiple writers, the entire line is dumped in a single call
to Perl's C<print> operator.

=back

=head1 DEPENDENCIES

C<Getopt::Long>, C<POSIX>.

=head1 AUTHOR

Earl Hood, earl@earlhood.com

This program comes with ABSOLUTELY NO WARRANTY and may be copied only
under the terms of the GNU General Public License.

=cut

