#!/usr/bin/perl
#
# ADDENDUM by Kyle Yetter (9/11/11)
# This module is required by the imap-archiver tool
#
############################################################
############################################################
##
## Scott Wiersdorf
## Created: Fri May 9 14:03:01 MDT 2003
## Updated: $Id: Crontab.pm,v 1.7 2011/04/11 22:55:03 scott Exp $
##
## Config::Crontab - a crontab(5) parser
##
## This file contains the following classes:
##
## - Config::Crontab - the top level crontab object
## - Config::Crontab::Block - crontab block (paragraph) handling
## - Config::Crontab::Event - "5 0 * * * /bin/command"
## - Config::Crontab::Env - "VAR=value"
## - Config::Crontab::Comment - "## a comment"
## - Config::Crontab::Base - base class from which all other
## Config::Crontab classes inherit
## - Config::Crontab::Container - base class from which Crontab and
## Block classes inherit
##
############################################################
############################################################
## to do: if -file = /etc/crontab, set system => 1
## to do: if adding a non-block to a $ct file, make a block for us automatically
## a crontab object is a list of Block objects (see below) This class
## (Config::Crontab) is for working with crontab files as a whole.
package Config::Crontab;
use strict;
use warnings;
use Carp;
use 5.006_001;
our @ISA = qw(Config::Crontab::Base Config::Crontab::Container);
## these two are for the 'write' method
use Fcntl;
use File::Temp qw(:POSIX);
our $VERSION = '1.33';
sub init {
my $self = shift;
my %args = @_;
$self->file('');
$self->mode('block');
$self->squeeze(1); ## only in block mode
$self->strict(0);
$self->blocks([]);
$self->error('');
$self->system(0);
$self->owner('');
$self->owner_re( '[^a-zA-Z0-9\._-]' );
$self->file( $args{'-file'}) if exists $args{'-file'};
$self->mode( $args{'-mode'}) if exists $args{'-mode'};
$self->squeeze( $args{'-squeeze'}) if exists $args{'-squeeze'};
$self->strict( $args{'-strict'}) if exists $args{'-strict'};
$self->system( $args{'-system'}) if exists $args{'-system'};
$self->owner( $args{'-owner'}) if exists $args{'-owner'};
$self->owner_re( $args{'-owner_re'}) if exists $args{'-owner_re'};
## auto-parse if file is specified
$self->read if $self->file;
return 1;
}
sub read {
my $self = shift;
my %args = @_;
$self->file( $args{'-file'}) if exists $args{'-file'};
$self->mode( $args{'-mode'}) if exists $args{'-mode'};
$self->squeeze( $args{'-squeeze'}) if exists $args{'-squeeze'};
$self->strict( $args{'-strict'}) if exists $args{'-strict'};
$self->system( $args{'-system'}) if exists $args{'-system'};
$self->owner( $args{'-owner'}) if exists $args{'-owner'};
$self->owner_re( $args{'-owner_re'}) if exists $args{'-owner_re'};
## set default system crontab
if( $self->system && ! $self->file ) {
$self->file('/etc/crontab');
}
## parse the file accordingly
if( $self->file ) {
open FILE, $self->file
or do {
$self->error($!);
if( $self->strict ) {
croak "Could not open " . $self->file . ": " . $self->error . "\n";
}
return;
}
}
else {
my $crontab_cmd = "crontab -l 2>/dev/null|";
if( $self->owner ) {
if( $^O eq 'SunOS' ) {
$crontab_cmd = "crontab -l " . $self->owner . " 2>/dev/null|";
}
else {
$crontab_cmd = "crontab -u " . $self->owner . " -l 2>/dev/null|";
}
}
open FILE, $crontab_cmd
or do {
$self->error($!);
if( $self->strict ) {
croak "Could not open pipe from crontab: " . $self->error . "\n";
}
return;
}
}
## reset internal block list and errors
$self->blocks([]);
$self->error('');
PARSE: {
local $/;
## each line is a block
if( $self->mode eq 'line' ) {
$/ = "\n";
}
## whole file is a block
elsif( $self->mode eq 'file' ) {
$/ = undef;
}
## each paragraph (\n\n+) is a block
else {
$/ = ( $self->squeeze ? '' : "\n\n" );
}
local $_;
while( ) {
chomp;
$self->last( new Config::Crontab::Block( -system => $self->system,
-data => $_ ) );
}
}
close FILE;
}
## this is needed for Config::Crontab::Container class methods
*elements = \&blocks;
sub blocks {
my $self = shift;
my $blocks = shift;
if( ref($blocks) eq 'ARRAY' ) {
$self->{'_blocks'} = $blocks;
}
## return only blocks (in case of accidental non-block pushing)
return grep { UNIVERSAL::isa($_, 'Config::Crontab::Block') }
grep { ref($_) } @{$self->{'_blocks'}};
}
sub select {
my $self = shift;
my @results = ();
push @results, $_->select(@_) for $self->blocks;
@results;
}
sub select_blocks {
my $self = shift;
my %crit = @_;
my @results = ();
unless( keys %crit ) {
@results = $self->blocks;
}
while( my($key, $value) = each %crit ) {
$key =~ s/^\-//; ## strip leading hyphen
if( $key eq 'index' ) {
unless( defined $value ) {
if( $self->strict ) {
carp "index value undefined\n";
}
next;
}
## a list ref of integers
if( ref($value) eq 'ARRAY' ) {
push @results, @{$self->{'_blocks'}}[@$value];
}
## an integer
elsif( $value =~ /^\d+$/ ) {
push @results, @{$self->{'_blocks'}}[$value];
}
else {
if( $self->strict ) {
carp "index value not recognized\n";
}
}
}
else {
if( $self->strict ) {
carp "Unknown block selection type '$key'\n";
}
}
}
@results;
}
sub block {
my $self = shift;
my $obj = shift
or return;
my $rblock;
BLOCK: for my $block ( $self->blocks ) {
for my $line ( $block->lines ) {
if( $line == $obj ) {
$rblock = $block;
last BLOCK;
}
}
}
return $rblock;
}
sub remove {
my $self = shift;
my @objs = @_;
if( @objs ) {
for my $obj ( @objs ) {
next unless defined $obj && ref($obj);
unless( UNIVERSAL::isa($obj, 'Config::Crontab::Block') ) {
if( $self->block($obj) ) {
$self->block($obj)->remove($obj);
}
## a non-block object in our crontab file!
else {
undef $obj;
}
next;
}
for my $block ( @{$self->{'_blocks'}} ) {
next unless defined $block && ref($block);
if( $block == $obj ) {
undef $block;
}
}
}
## strip out undefined objects
$self->blocks([ grep { defined } $self->elements ]);
}
return $self->elements;
}
## same as 'crontab -u user file'
sub write {
my $self = shift;
my $file = shift;
## see if a file is present, allow for ''
if( defined $file ) {
$self->file($file);
}
if( $self->file ) {
open CT, ">" . $self->file
or croak "Could not open " . $self->file . ": $!\n";
print CT $self->dump;
close CT;
}
## use a temporary filename
else {
my $tmpfile;
do { $tmpfile = tmpnam() }
until sysopen(CT, $tmpfile, O_RDWR|O_CREAT|O_EXCL);
print CT $self->dump;
close CT;
my $crontab;
if( my $owner = $self->owner ) {
$crontab = `crontab -u $owner $tmpfile 2>&1`;
}
else {
$crontab = `crontab $tmpfile 2>&1`;
}
chomp $crontab;
unlink $tmpfile;
if( $crontab || $? ) {
$self->error($crontab);
if( $self->strict ) {
carp "Error writing crontab (crontab exited with status " .
($? >> 8) . "): " . $self->error;
}
return;
}
}
return 1;
}
sub remove_tab {
my $self = shift;
my $file = shift;
## see if a file is present, allow for ''
if( defined $file ) {
$self->file($file);
}
if( $self->file ) {
unlink $self->file;
}
else {
my $output = '';
if( my $owner = $self->owner ) {
$output = `crontab -u $owner -r 2>&1`;
}
else {
$output = `yes | crontab -r 2>&1`;
}
chomp $output;
## FIXME: what if no $output, but only '$?' ?
if( $output || $? ) {
$self->error($output);
if( $self->strict ) {
carp "Error removing crontab (crontab exited with status " .
($? >> 8) ."): " . $self->error;
}
return;
}
}
return 1;
}
sub dump {
my $self = shift;
my $ret = '';
for my $block ( $self->blocks ) {
$ret .= "\n" if $ret && $block->dump; ## empty blocks should not invoke a newline
$ret .= $block->dump;
}
return $ret;
}
sub owner {
my $self = shift;
if( @_ ) {
my $owner = shift;
if( $owner ) {
unless( defined( getpwnam($owner) ) ) {
$self->error("Unknown user: $owner");
if( $self->strict ) {
croak $self->error;
}
return;
}
if( $owner =~ $self->owner_re ) {
$self->error("Illegal username: $owner");
if( $self->strict ) {
croak $self->error;
}
return;
}
}
$self->{_owner} = $owner;
}
return ( defined $self->{_owner} ? $self->{_owner} : '' );
}
sub owner_re {
my $self = shift;
if( @_ ) {
my $re = shift;
$self->{_owner_re} = qr($re);
}
return ( defined $self->{_owner_re} ? $self->{_owner_re} : qr() );
}
############################################################
############################################################
=head1 NAME
Config::Crontab - Read/Write Vixie compatible crontab(5) files
=head1 SYNOPSIS
use Config::Crontab;
####################################
## making a new crontab from scratch
####################################
my $ct = new Config::Crontab;
## make a new Block object
my $block = new Config::Crontab::Block( -data => <<_BLOCK_ );
## mail something to joe at 5 after midnight on Fridays
MAILTO=joe
5 0 * * Fri /bin/someprogram 2>&1
_BLOCK_
## add this block to the crontab object
$ct->last($block);
## make another block using Block methods
$block = new Config::Crontab::Block;
$block->last( new Config::Crontab::Comment( -data => '## do backups' ) );
$block->last( new Config::Crontab::Env( -name => 'MAILTO', -value => 'bob' ) );
$block->last( new Config::Crontab::Event( -minute => 40,
-hour => 3,
-command => '/sbin/backup --partition=all' ) );
## add this block to crontab file
$ct->last($block);
## write out crontab file
$ct->write;
###############################
## changing an existing crontab
###############################
my $ct = new Config::Crontab; $ct->read;
## comment out the command that runs our backup
$_->active(0) for $ct->select(-command_re => '/sbin/backup');
## save our crontab again
$ct->write;
###############################
## read joe's crontab (must have root permissions)
###############################
## same as "crontab -u joe -l"
my $ct = new Config::Crontab( -owner => 'joe' );
$ct->read;
=head1 DESCRIPTION
B provides an object-oriented interface to
Vixie-style crontab(5) files for Perl.
A B object allows you to manipulate an ordered set
of B, B, or B objects (also included with this
package). Descriptions of these packages may be found below.
In short, B reads and writes crontab(5) files (and
does a little pretty-printing too) using objects. The general idea is
that you create a B object and associate it with a
file (if unassociated, it will work over a pipe to C). From
there, you can add lines to your crontab object, change existing line
attributes, and write everything back to file.
=over 4
=item
NOTE: B does I (currently) do validity checks
on your data (i.e., dates out of range, etc.). However, if the call
to B fails when you invoke B, B will return
I and set B with the error message returned from the
B command. Future development may tend toward more validity
checks.
=back
Now, to successfully navigate the module's ins and outs, we'll need a
little terminology lesson.
=head2 Terminology
B (hereafter simply B) sees a C
file in terms of I. A block is simply an ordered set of one
or more lines. Blocks are separated by two or more newlines. For
example, here is a crontab file with two blocks:
## a comment
30 4 * * * /bin/some_command
## another comment
ENV=some_value
50 9 * * 1-5 /bin/reminder --meeting=friday
The first block contains two B objects: a
B object and an B object. The second block contains
an B object in addition to a B object and an B
object. The B class, then, consists of zero or more
B objects. B objects have these three
basic elements:
=over 4
=item B
Any lines in a crontab that look like these are B objects:
5 10 * * * /some/command
@reboot /bin/mystartup.sh
## 0 0 * * Fri /disabled/command
Notice that commented out event lines are still considered B
objects.
B objects are described below in the B package
description. Please refer to it for details on manipulating B
objects.
=item B
Any lines in a crontab that look like these are B objects:
MAILTO=joe
SOMEVAR = some_value
#DISABLED=env_setting
Notice that commented out environment lines are still considered
B objects.
B objects are described below in the B package description.
Please refer to it for details on manipulating B objects.
=item B
Any lines containing only whitespace or lines beginning with a pound
sign (but are not B or B objects) are B objects:
## this is a comment
(imagine somewhitespace here)
B objects are described below in the B package
description. Please refer to it for details on manipulating B
objects.
=back
=head2 Illustration
Here is a simple crontab file:
MAILTO=joe@schmoe.org
## send reminder in April
3 10 * Apr Fri joe echo "Friday a.m. in April"
The file consists of an environment variable setting (MAILTO), a
comment, and a command to run. After parsing the above file,
B would break it up into the following objects:
+---------------------------------------------------------+
| Config::Crontab object |
| |
| +---------------------------------------------------+ |
| | Config::Crontab::Block object | |
| | | |
| | +---------------------------------------------+ | |
| | | Config::Crontab::Env object | | |
| | | | | |
| | | -name => MAILTO | | |
| | | -value => joe@schmoe.org | | |
| | | -data => MAILTO=joe@schmoe.org | | |
| | +---------------------------------------------+ | |
| +---------------------------------------------------+ |
| |
| +---------------------------------------------------+ |
| | Config::Crontab::Block object | |
| | | |
| | +---------------------------------------------+ | |
| | | Config::Crontab::Comment object | | |
| | | | | |
| | | -data => ## send reminder in April | | |
| | +---------------------------------------------+ | |
| | | |
| | +---------------------------------------------+ | |
| | | Config::Crontab::Event Object | | |
| | | | | |
| | | -datetime => 3 10 * Apr Fri | | |
| | | -special => (empty) | | |
| | | -minute => 3 | | |
| | | -hour => 10 | | |
| | | -dom => * | | |
| | | -month => Apr | | |
| | | -dow => Fri | | |
| | | -user => joe | | |
| | | -command => echo "Friday a.m. in April" | | |
| | +---------------------------------------------+ | |
| +---------------------------------------------------+ |
+---------------------------------------------------------+
You'll notice the main Config::Crontab object encapsulates the entire
file. The parser found two B objects: the lone MAILTO variable
setting, and the comment and command (together). Two or more newlines
together in a crontab file constitute a block separator. This allows
you to logically group commands (as most people do anyway) in the
crontab file, and work with them as a Config::Crontab::Block objects.
The second block consists of a B object and an B
object, shown are some of the data methods you can use to get or set
data in those objects.
=head2 Practical Usage: A Brief Tutorial
Now that we know what B objects look like and what
they're called, let's play around a little.
Let's say we have an existing crontab on many machines that we want
to manage. The crontab contains some machine-dependent information
(e.g., timezone, etc.), so we can't just copy a file out everywhere
and replace the existing crontab. We need to edit each crontab
individually, specifically, we need to change the time when a
particular job runs:
30 2 * * * /usr/local/sbin/pirate --arg=matey
to 3:30 am because of daylight saving time (i.e., we don't want this
job to run twice).
We can do something like this:
use Config::Crontab;
my $ct = new Config::Crontab;
$ct->read;
my ($event) = $ct->select(-command_re => 'pirate --arg=matey');
$event->hour(3);
$ct->write;
All done! This shows us a couple of subtle but important points:
=over 4
=item *
The B object must have its B method invoked
for it to read the crontab file.
=item *
The B