#!/usr/bin/perl -w our $ID = q$Id: weekly,v 1.5 2009-07-11 17:40:34 eagle Exp $; # # weekly -- Manage a weekly status report. # # Written by Russ Allbery # Copyright 1998, 1999, 2008, 2009 # Board of Trustees, Leland Stanford Jr. University # # This program is free software; you may redistribute it and/or modify it # under the same terms as Perl itself. ############################################################################## # Initialization ############################################################################## require 5.003; use strict; use Getopt::Long qw(GetOptions); use News::FormArticle (); use POSIX qw(strftime); # The directory containing all of the log files and the form letter. our $BASE = "$ENV{HOME}/data/reports"; # The abbreviated names of the months (for building a subject header). our @MONTHS = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'); # The endings for ordinal numbers. our @ORDINALS = ('th', 'st', 'nd', 'rd', ('th') x 17, 'st', 'nd', 'rd', ('th') x 7, 'st'); ############################################################################## # Implementation ############################################################################## # Check for command line options. Any non-option arguments are taken to be a # date. my ($end, $force, $help, $nomail, $nocheck, $version); Getopt::Long::config ('bundling', 'no_ignore_case'); GetOptions ('f|force' => \$force, 'h|help' => \$help, 'n|dry-run|just-print' => \$nomail, 'v|version' => \$version) or exit 1; if ($help) { print "Feeding myself to perldoc, please wait....\n"; exec ('perldoc', '-t', $0); } elsif ($version) { my $version = join (' ', (split (' ', $ID))[1..3]); $version =~ s/,v\b//; $version =~ s/(\S+)$/($1)/; $version =~ tr%/%-%; print $version, "\n"; exit; } if (@ARGV) { require Date::Parse; my $date = join (' ', @ARGV); $end = Date::Parse::str2time ($date) or die "Can't parse $date\n"; $nocheck = 1; } else { $end = time; } # Perform our checks. die "Report file missing\n" unless -r "$BASE/current"; unless ($nocheck or $force) { die "You didn't do anything today?\n" if (-M "$BASE/current" > 1); } if ($force and -r "$BASE/todo" and not -r "$BASE/next") { rename ("$BASE/next", "$BASE/todo") or die "Can't rename future plans file: $!\n"; } die "Stray todo file left around\n" if -r "$BASE/todo"; die "No future plans?\n" unless -r "$BASE/next"; # Now, build our start and end dates. The start date is always assumed to be # four days earlier than the end date. We use this little sub to build dates # rather than strftime so that we can get the right letters after the date. sub date { my ($time) = @_; my ($month, $day) = (localtime $time)[4,3]; return $MONTHS[$month] . ' ' . $day . $ORDINALS[$day]; } my $start = $end - 4 * 24 * 3600; my $range = (date $start) . ' - ' . (date $end); # Read in our two files (what we did and what we're going to do). my (@current, @next); open (CURRENT, "< $BASE/current") or die "Can't open report file: $!\n"; chomp (@current = ); close CURRENT; open (NEXT, "< $BASE/next") or die "Can't open future plans file: $!\n"; chomp (@next = ); close NEXT; # Generate and mail (or print to the screen) our form letter, and then archive # our report files. my $source = { CURRENT => \@current, NEXT => \@next, DATE => $range }; my $letter = News::FormArticle->new ("$BASE/report", $source) or die "Can't create form letter\n"; if ($nomail) { $letter->write (\*STDOUT); } else { $letter->mail; my $archivedir = $BASE . strftime ('/%Y', localtime $end); my $archive = $BASE . strftime ('/%Y/%Y%m%d', localtime $end); unless (-d $archivedir) { mkdir ($archivedir, 0755) or die "Can't create $archivedir: $!\n"; } rename ("$BASE/current", $archive) or die "Can't rename report file: $!\n"; rename ("$BASE/next", "$BASE/todo") or die "Can't rename future plans file: $!\n"; } ############################################################################## # Documentation ############################################################################## =head1 NAME weekly - Manage a weekly status report =head1 SYNOPSIS B [B<-fhnv>] [I] =head1 REQUIREMENTS Perl 5.003 or later and News::Article. Date::Parse (part of the timedate distribution) is required if a date will be given on the command line. =head1 DESCRIPTION B is part of a status reporting system that periodically sends e-mail updates about what one has accomplished. It's intended to run as a weekly cron job to send status reports at the end of each week. It performs a few checks before sending the status report to see if it looks ready for sending and complains if it isn't, thereby acting as a reminder to finish the report. Inside its report directory, set at the top of the script, should be three files: F, which is the status report for the current week; F, which is the projected to-do list for upcoming weeks; and F, which is a News::FormArticle template used to generate the e-mail message containing the weekly report. When B runs, it generates a report using the F template and including the contents of F and F, archives F, and then renames F to F. F must therefore be renamed back to F, indicating the next report is ready for sending, before B will send another report. The report template may contain the following variables: =over 4 =item $DATE Replaced by a string giving the dates of the weekly report. This will be two dates, separated by C< - >, with each date given as a three-character English month followed by the day of the month as an ordinal. The current day is used as the end date of the report unless a date was specified on the command line, in which case that date is used. The start date is always four days earlier than the end date (so Monday if the end date is a Friday). =item @CURRENT Replaced by the contents of the F file. =item @NEXT Replaced by the contents of the F file. =back For B to be happy sending a report (if run without any options), F must be modified within the past twelve hours and F must exist. Old reports are archived after being sent. Those archived reports are saved in directories named after the year. Each old report will be stored as a file named I in the archive directory for that year. The date will be the date of the last day of the report. B can also take a date argument on the command line (in any form parsed by Date::Parse), which will be used as the last date of the report instead of the current day if present. If a date is specified, no checks are made for whether F is recently modified. =head1 OPTIONS =over 4 =item B<-f>, B<--force> If F exists in the report directory but F does not, rename F to F and then continue as normal. This flag can be used if you don't want to have to manually rename F to F to signal that the report is ready each week, or if you're sending the report by hand. =item B<-h>, B<--help> Print out this documentation (which is done simply by feeding the script to C). =item B<-n>, B<--dry-run>, B<--just-print> Rather than sending the mail report, print what would be sent to standard output. If this option is given, the report also won't be archived and F will not be renamed to F. =item B<-v>, B<--version> Print out the version of B and exit. =back =head1 FILES =over 4 =item F<$HOME/data/reports> The default report directory, set at the top of this script. It is expected to contain the F, F, and F files as described above. =back =head1 BUGS The report period isn't configurable, nor is the language for encoding the dates (which since they use ordinals isn't quite as easy as it sounds). =head1 SEE ALSO The current version of this script is available from Russ Allbery's script page at L. =head1 AUTHOR Russ Allbery =head1 COPYRIGHT AND LICENSE Copyright 1998, 1999, 2008, 2009 Board of Trustees, Leland Stanford Jr. University. This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself. =cut