Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

I'm trying to set up a prompt that will allow the time portion of a unix crontab to be entered, but cannot seem to get the pattern match right
Can anybody spot what I'm doing wrong ?
while ($newtime !~ /[0-59\*]\s+[0-23\*]\s+[1-31\*]\s+[1-12\*]\s+[0-6\* +]/) { print qq (\n Enter the time: ); $newtime = <STDIN>; chomp $newtime; }
Where a unix crontab should match this
minute (0-59), hour (0-23), day of the month (1-31), month of the year (1-12), day of the week (0-6 with 0=Sunday).
and each position can also be replaced with an asteris (*)
e.g. I should be able to enter
0 10 * * *
But the code above isn't allowing this

Replies are listed 'Best First'.
Re: simple pattern match ?
by Corion (Patriarch) on Mar 15, 2006 at 15:32 UTC

    A regular expression only works character by character, not token by token. So, if you want to match a two-digit number, you have to specify two characters that can match 0 through 9.

    Your expression  [0-59\*] will allow the characters 0,1,2,3,4,5,9 and *, which, likely, isn't what you wanted. To match a digit between 0 and 59, or a *, you could use the following regular expression:

    ([012345]?\d|\*)

    That is, you allow the first digit (if any) to be between 0 and 5, and then require a second digit after it, you just don't care what it is.

    For matching digits between 0 and 23, it gets trickier:

    ([01]?\d|2[0123]|\*)

    The approach is to match (optionally) a zero or one, and then any digit. If the first digit is a 2, we explicitly require a 0, 1, 2 or 3.

    Is there any reason you're forced to parsing the crontab yourself? There is Schedule::Cron and some other Crontab parsers, and your regular expression does not cover the weirder (Vixie) cron extensions like */3, which allows to trigger an event every 3 units, or 1,5,7,9,11, which would trigger the event at 1 minute, 5 minutes, 7 minutes and so on.

    Update: ikegami pointed out that I forgot to allow for *.

      Thanks. I realised after I'd posted this that it didn't handle the plethra of other extensions. I'll take a look at the CPAN modules to avoid re-inventing the wheel !
      To match a digit between 0 and 59, or a *, you could use the following regular expression:
      ([012345]?\d)
      Actually, that would fail on single digit entries between 0 and 5 - because the first (optional) part of the expression would match, and then there would be no 2nd digit to match the second (required) part (\d). For the sake of the exercise, I had a play around with it and the following seems to work (for 0-59):
      /^(?:[0-9*]|[12345]\d)$/
      (But I also echo your sentiments about not re-inventing the wheel)

      Cheers,
      Darren :)

        This is a quick, nasty way of matching a range without spending a lot of effort on crafting a clever regular expression.

        @range = (0 .. 59); { local $" = '|'; $rxMatchIt = qr{(@range)}; }

        You end up with a regular expression which says match 0 or 1 or ... 59. This falls over if there are leading zeros but could easily be adapted to cope. I like nifty regular expressions but sometimes quick and dirty gets the job done.

        Cheers,

        JohnGG

        On Perl 5.8.5, the pattern works for me:

        #!perl -w for (qw( 00 01 02 03 04 05 06 07 08 09 0 1 2 3 4 5 6 7 8 9 10 20 30 40 50 58 59 )) { die "Ooops: $_ didn't match the regular expression" unless /([012345]?\d)/; }; print "All numbers passed.";
Re: simple pattern match ?
by ides (Deacon) on Mar 15, 2006 at 16:08 UTC

    You're not using a regex range properly. It should be:

    $newtime !~ /((\d{1,2}|\*)\s+){5,}/

    This looks for one or two digits OR an asterisk followed by whitespace and looks for 5 of those groups.

    Frank Wiles <frank@revsys.com>
    www.revsys.com

Re: simple pattern match ?
by smokemachine (Hermit) on Mar 15, 2006 at 16:38 UTC
    $newtime !~ m#^((\d{1,2}(-\d{1,2})?|\*)(/\d{1,2})?\s?){5}$#
Re: simple pattern match ?
by eric256 (Parson) on Mar 15, 2006 at 22:22 UTC

    In perl6.....

    #!/usr/bin/pugs use v6; rule minute { <[1..5]>?\d | <[*]> }; rule hour { <[1..2]>?\d | <[*]> }; rule day { 31 | 30 | <[1..2]>?\d | <[*]> } rule month { 1?\d | <[*]> } rule day_of_week { <[0..6]> | <[*]> } rule crontab { <minute> \s+ <hour> \s+ <day> \s+ <month> \s+ <day_of_w +eek> } my $crontab = "0 10 * * *" ~~ /<crontab>/; my $match = $crontab<crontab>; say "Minute: $match<minute>\nHour: $match<hour>\n" ~ "Day: $match<day>\nMonth: $match<month>\n" ~ "Day of Week: $match<day_of_week>";
    1;0 eric256@feather:~/tests$ pugs crontab.p6 Minute: 0 Hour: 10 Day: * Month: * Day of Week: *

    ___________
    Eric Hodges

      rule hour { [1\d | 2<[0..4]>] | 0?\d | <[*]> } rather, assuming that the range for hours is 1..24 and not 0..23.

      similarly,

      rule day { 31 | 30 | <[0..2]>?\d | <[*]> } rule month { 1<[210]> | 0?\d | <[*]> }

Re: simple pattern match ?
by Anonymous Monk on Mar 15, 2006 at 15:33 UTC
    I forgot to mention that each position can also have ranges e.g.
    10,20,30 10-12 * * 1-5
    is a legal input !

      As well as

      23 0-23/2 * * * command

      which runs 'command' 23 minutes after midn, 2am, 4am ..., everyday.

      Cheers, Flo

Re: simple pattern match ?
by Krambambuli (Curate) on May 11, 2007 at 16:32 UTC
    I'm not sure you'll find a regexp-way to solve this in an accurate way.
    You might want to process/validate the entry you get with DateTime::Event::Cron, somehow similar to the following:
    #!/usr/bin/perl use strict; use warnings; use DateTime::Event::Cron; my @crontab = ( '*/3 15 1-10 3,4,5 */2', '* * * * 1-4,5-7,11,20', '* * * 1-4,5-7,11,20 *', '* * 1-4,5-7,11,20 * *', '* 1-4,5-7,11,20 * * *', '1-4,5-7,11,20 * * * *', ); my $set; foreach my $line (@crontab) { eval { $set = DateTime::Event::Cron->from_cron( $line ); }; print "$line: ", ($@ ? $@ : 'OK'), "\n"; }
    The output of which is
    */3 15 1-10 3,4,5 */2: OK * * * * 1-4,5-7,11,20: Field value (20) out of range (1-7) * * * 1-4,5-7,11,20 *: Field value (20) out of range (1-12) * * 1-4,5-7,11,20 * *: OK * 1-4,5-7,11,20 * * *: OK 1-4,5-7,11,20 * * * *: OK
    which might be easier to handle/debug then the output of a regexp check.

    Hth.
Re: simple pattern match ?
by Anonymous Monk on May 11, 2007 at 11:14 UTC
    So I find myself with a similar task, a front end to cron. I looked at several of the Crontab related packages on CPAN, but I can't find in them where they do the hairy pattern matching. So, how does one match a pattern line 1-4,5-7,11,20 ?