Copyright Notice
This text is copyright by CMP Media, LLC, and is used with their permission. Further distribution or use is not permitted.This text has appeared in an edited form in SysAdmin/PerformanceComputing/UnixReview magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.
Please read all the information in the table of contents before using this article.
Unix Review Column 71 (Jul 2007)
[Suggested title: ``Updating a public calendar automatically'']
I maintain a ``public'' calendar, so that my friends and associates can
see where I'll be. The calendar started as part of my ~/.plan
file
in my home directory on my first internet-connected host at Teleport
over a decade ago. For those of you too young to remember, the
dot-plan file was revealed when you execute finger on the
person, specifying both the username and the host. By placing my
schedule in my plan file, you could find out where I'd be.
Over the passing years, the use of finger became more worrisome,
and eventually fell out of favor, especially since the Web was a bit
more secure and a lot more familiar. To accomodate, I simply
symlinked my dot-plan file into a corresponding URL on my web server,
yielding the somewhat awkward and anachronistically named URL of
www.stonehenge.com/merlyn/dot-plan.txt
. I still edit it by
updating ~/.plan
with my favorite text editor.
Inside my ~/.plan
file, my upcoming schedule occupies the last
half, and currently looks something like:
Future plans:
07 to 19 May 2007: Buffalo (NY) working for buffalo.edu 21 May 2007: Portland (OR) speaking at Advanced PLUG about PostgreSQL 23 to 26 May 2007: Portland (OR) working for geekcruises.com 26 May to 03 Jun 2007: MacMania 6 out of Seattle for Alaska (www.geekcruises.com) 24 Jun to 01 Jul 2007: Houston (TX) for YAPC::NA::2007
Now, up until yesterday, I was manually editing that portion of the file, trying to keep it in sync with my laptop's calendar (using Mac OS X's iCal application). As I went through the sporadic recognition of ``oh my gosh, it's probably out of sync again'', followed by a painful comparison of each of my personal calendar items to see if they should be published, followed by cutting and pasting those items into the editor window, I thought, you know, there must be a better way.
But I couldn't just publish my entire calendar. I have private items in there: you really don't need to know when I'm getting a haircut, for example. And it's not enough to separate it by category, because some of my items have private aspects and public aspects. For example, you're certainly welcome to know that I'm in Houston for those days, but not my hotel arrangements or flight times.
So I came up with an interesting compromise. Within the description
of each calendar item, I insert as the first line a string beginning
with DOTPLAN:
. If that item is present, then the corresponding
dates should be published to my public calendar, together with the
remainder of the string as the entire identification of the event.
With this strategy, I could give the summary information for the
public calendar right alongside the private details in the same
description field. For example, my YAPC::NA description begins:
DOTPLAN: Houston (TX) for YAPC::NA::2007
To transfer this information automatically, I next wrote some code
that would go through the text files created by iCal. They're
theoretically in a standard iCalendar
format, but the tools on the
CPAN didn't parse them nicely. So I just went for brute force. I
added DOTPLAN:
to the description of a couple of events in a couple
of my calendars, including an event that has a start time, and then
figured out how it would look in the resulting calendar files.
Because I made some wild guesses on that, and I'm not particularly
proud of the code, and it might even be somewhat buggy, I'm not going
to show that here, except to represent it symbolically:
my %events;
while (<>) { ## magic here push @{$events{$start}}, [$end => $description]; }
Presuming that magic here
means we're skipping over lines that are
uninteresting, and somehow we come up with a start date, end date, and
description, I'm building a hash of arrayrefs to hold all items that
begin on the same date. $start
and $end
here look like
YYYYMMDD, with a 4-digit year, 2-digit month, and 2-digit day of
month.
The next step is to show these events sorted by start date:
for my $start (sort keys %events) { for my $event (@{$events{$start}}) { my ($end, $desc) = @$event; print range($start, $end), ": $desc\n"; } }
The date strings sort naturally in the proper order. For each start
date, I'll loop over all events, displaying each event. The call to
range
here is to turn two dates into a meaningful range, removing
any redundancy. If the years differ, both dates are shown in their
entirety. However, if only the days differ, then I don't want to
repeat the month, and if the months differ, I don't want to repeat the
year. For this, I came up with the following code:
sub range { my $start = shift; my $end = shift; $_ = ymd2dmy($_) for $start, $end; my $range = "$start to $end"; $range =~ s/^(.*?)(.*) to (.*)\2$/$1 to $3$2/; $range =~ s/^ to //; # single day $range; }
The call to ymd2dmy
turns the 20070714
into ``14 Jul 2007'', using
the following subroutine:
sub ymd2dmy { my $ymd = shift; my ($y, $m, $d) = unpack "A4 A2 A2", $ymd; $m = (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec))[$m - 1]; "$d $m $y"; }
Next, the $range
string is set to include both strings in full.
The first substitution on $range
tries to note common items from
both the start and end dates, ripping them out of the start date. If
the start date is the same as the end date, the resulting string
begins with to
, which the next replacement cleans up. Yes, I'm
sure there were less clever and more clear ways of doing this, but
this works.
Running this code produces the text that I want in my dot-plan
, but I still
had two problems to solve. First, I want to edit only a portion of the
dot-plan file, leaving the rest of the text intact. Second, the code that
generates these lines runs on my laptop, but I really wanted to update the
~/.plan
file on my server.
Again, it's just a small matter of programming. To update just a portion
of the file, I simply use in-place editing mode. First, I set $^I
:
$^I = "~";
And then I establish the list of files to edit (just one, here):
@ARGV = glob '~/.plan';
The loop prints anything that I'm not changing, but replaces the stuff I am changing:
while (<>) { if (my $line = /^Future plans/..eof()) { print "$_\n", <DATA> if $line == 1; } else { print; } }
Note the use of the ..
operator here. When the loop first sees
Future plans
, $line
will start being 1, 2, 3, and so on. If
$line
is true, then we're in the tail part of the file, and
shouldn't copy anything. Otherwise, the lone print
copies the
existing data.
For the first line only (the line beginning with Future plans
), I
copy the line, and append a blank line, and then add the result of
reading the DATA
filehandle. And that's where I'll put my updated
data:
__END__ 07 to 19 May 2007: Buffalo (NY) working for buffalo.edu 21 May 2007: Portland (OR) speaking at Advanced PLUG about PostgreSQL 23 to 26 May 2007: Portland (OR) working for geekcruises.com 26 May to 03 Jun 2007: MacMania 6 out of Seattle for Alaska (www.geekcruises.com) 24 Jun to 01 Jul 2007: Houston (TX) for YAPC::NA::2007
I now have a Perl script that when executed, updates my dot-plan file
as needed. How do I get this Perl script over to the server? This is
the final cool step: I execute an ssh command that launches a
remote perl
invocation:
open SSHPIPE, "|ssh my.server.example.com perl" or die "$!"; print SSHPIPE <<'END_PROG'; $^I = "~"; @ARGV = glob '~/.plan'; while (<>) { if (my $line = /^Future plans/..eof()) { print "$_\n", <DATA> if $line == 1; } else { print; } } __END__ END_PROG
So, I launch the remote Perl, and feed it the first part of the program. I then execute the remainder of my local calendar scraper:
my %events;
while (<>) { ## magic here push @{$events{$start}}, [$end => $description]; }
for my $start (sort keys %events) { for my $event (@{$events{$start}}) { my ($end, $desc) = @$event; print SSHPIPE range($start, $end), ": $desc\n"; } }
Note the change to the print
to print it to the ssh pipe. And finally,
I close the pipe, and the remote Perl code can begin:
close SSHPIPE;
And there you have it: a local calendar scraper that pushes the results to a remote calendar, using a pipe to a remote Perl and some cool in-place editing. And amazingly enough, I got this running within an hour or so, which means it'll take me only, well, three years to earn that time back! Oh well. Until next time, enjoy!