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!


Randal L. Schwartz is a renowned expert on the Perl programming language (the lifeblood of the Internet), having contributed to a dozen top-selling books on the subject, and over 200 magazine articles. Schwartz runs a Perl training and consulting company (Stonehenge Consulting Services, Inc of Portland, Oregon), and is a highly sought-after speaker for his masterful stage combination of technical skill, comedic timing, and crowd rapport. And he's a pretty good Karaoke singer, winning contests regularly.

Schwartz can be reached for comment at merlyn@stonehenge.com or +1 503 777-0095, and welcomes questions on Perl and other related topics.