53 Validating User crontab Entries


#53 Validating User crontab Entries

One of the most helpful facilities in Unix is cron , with its ability to schedule jobs at arbitrary times in the future, recurring every minute, every few hours, monthly, or annually. Every good system administrator has a Swiss army knife of scripts running from the crontab file.

However, the format for entering cron specifications is a bit tricky, and the cron fields have numeric values, ranges, sets, and even mnemonic names for days of the week or months. What's worse is that the crontab program generates insufficient error messages when scanning in a cron file that might be incorrectly structured.

For example, specify a day of the week with a typo, and crontab reports

 "/tmp/crontab.Dj7Tr4vw6R":9: bad day-of-week crontab: errors in crontab file, can't install 

In fact, there's a second error in the sample input file, on line 12, but crontab is going to force us to take the long way around to find it in the script because of its poor error-checking code.

Instead of doing it crontab 's way, a somewhat lengthy shell script can step through the crontab files, checking the syntax and ensuring that values are within reasonable ranges. One of the reasons that this validation is possible in a shell script is that sets and ranges can be treated as individual values. So to test whether 3 “11 or 4,6,9 are acceptable values for a field, simply test 3 and 11 in the former case, and 4, 6, and 9 in the latter.

The Code

 #!/bin/sh # verifycron - Checks a crontab file to ensure that it's #    formatted properly.  Expects standard cron notation of #       min hr dom mon dow CMD #    where min is 0-59, hr is 0-23, dom is 1-31, mon is 1-12 (or names) #    and dow is 0-7 (or names).  Fields can be ranges (a-e), lists #    separated by commas (a,c,z), or an asterisk. Note that the step #    value notation of Vixie cron (e.g., 2-6/2) is not supported by this script. validNum() {   # Return 0 if valid, 1 if not. Specify number and maxvalue as args   num=   max=   if [ "$num" = "X" ] ; then     return 0   elif [ ! -s $(echo $num  sed 's/[[:digit:]]//g') ] ; then     return 1   elif [ $num -lt 0 -o $num -gt $max ] ; then     return 1   else     return 0   fi } validDay() {   # Return 0 if a valid dayname, 1 otherwise   case $(echo   tr '[:upper:]' '[:lower:]') in     sun*mon*tue*wed*thu*fri*sat*) return 0 ;;     X) return 0 ;; # special case - it's an "*"     *) return 1   esac } validMon() {   # Return 0 if a valid month name, 1 otherwise    case $(echo   tr '[:upper:]' '[:lower:]') in      jan*feb*mar*apr*mayjun*jul*aug*) return 0           ;;      sep*oct*nov*dec*)                    return 0           ;;      X) return 0 ;; # special case, it's an "*"      *) return 1        ;;    esac } fixvars() {   # Translate all '*' into 'X' to bypass shell expansion hassles   # Save original input as "sourceline" for error messages   sourceline="$min $hour $dom $mon $dow $command"    min=$(echo "$min"  tr '*' 'X')   hour=$(echo "$hour"  tr '*' 'X')    dom=$(echo "$dom"  tr '*' 'X')    mon=$(echo "$mon"  tr '*' 'X')    dow=$(echo "$dow"  tr '*' 'X') } if [ $# -ne 1 ]  [ ! -r  ] ; then   echo "Usage: 
 #!/bin/sh # verifycron - Checks a crontab file to ensure that it's # formatted properly. Expects standard cron notation of # min hr dom mon dow CMD # where min is 0-59, hr is 0-23, dom is 1-31, mon is 1-12 (or names) # and dow is 0-7 (or names). Fields can be ranges (a-e), lists # separated by commas (a,c,z), or an asterisk. Note that the step # value notation of Vixie cron (e.g., 2-6/2) is not supported by this script. validNum() { # Return 0 if valid, 1 if not. Specify number and maxvalue as args num=$1 max=$2 if [ "$num" = "X" ] ; then return 0 elif [ ! -s $(echo $num  sed 's/[[:digit:]]//g') ] ; then return 1 elif [ $num -lt 0 -o $num -gt $max ] ; then return 1 else return 0 fi } validDay() { # Return 0 if a valid dayname, 1 otherwise case $(echo $1  tr '[:upper:]' '[:lower:]') in sun*mon*tue*wed*thu*fri*sat*) return 0 ;; X) return 0 ;; # special case - it's an "*" *) return 1 esac } validMon() { # Return 0 if a valid month name , 1 otherwise case $(echo $1  tr '[:upper:]' '[:lower:]') in jan*feb*mar*apr*mayjun*jul*aug*) return 0 ;; sep*oct*nov*dec*) return 0 ;; X) return 0 ;; # special case, it's an "*" *) return 1 ;; esac } fixvars() { # Translate all '*' into 'X' to bypass shell expansion hassles # Save original input as "sourceline" for error messages sourceline="$min $hour $dom $mon $dow $command" min=$(echo "$min"  tr '*' 'X') hour=$(echo "$hour"  tr '*' 'X') dom=$(echo "$dom"  tr '*' 'X') mon=$(echo "$mon"  tr '*' 'X') dow=$(echo "$dow"  tr '*' 'X') } if [ $# -ne 1 ]  [ ! -r $1 ] ; then echo "Usage: $0 usercrontabfile" >&2; exit 1 fi lines=0 entries=0 totalerrors=0 while read min hour dom mon dow command do lines="$(($lines + 1))" errors=0 if [ -z "$min" -o "${min%${min#?}}" = "#" ] ; then continue # nothing to check elif [ ! -z $(echo ${min%${min#?}}  sed 's/[[:digit:]]//') ] ; then continue # first char not digit: skip! fi entries="$(($entries + 1))" fixvars #### Broken into fields, all '*' replaced with 'X' # Minute check for minslice in $(echo "$min"  sed 's/[,-]/ /g') ; do if ! validNum $minslice 60 ; then echo "Line ${lines}: Invalid minute value \"$minslice\"" errors=1 fi done # Hour check for hrslice in $(echo "$hour"  sed 's/[,-]/ /g') ; do if ! validNum $hrslice 24 ; then echo "Line ${lines}: Invalid hour value \"$hrslice\"" errors=1 fi done # Day of month check for domslice in $(echo $dom  sed 's/[,-]/ /g') ; do if ! validNum $domslice 31 ; then echo "Line ${lines}: Invalid day of month value \"$domslice\"" errors=1 fi done # Month check for monslice in $(echo "$mon"  sed 's/[,-]/ /g') ; do if ! validNum $monslice 12 ; then if ! validMon "$monslice" ; then echo "Line ${lines}: Invalid month value \"$monslice\"" errors=1 fi fi done # Day of week check for dowslice in $(echo "$dow"  sed 's/[,-]/ /g') ; do if ! validNum $dowslice 31 ; then if ! validDay $dowslice ; then echo "Line ${lines}: Invalid day of week value \"$dowslice\"" errors=1 fi fi done if [ $errors -gt 0 ] ; then echo ">>>> ${lines}: $sourceline" echo "" totalerrors="$(( $totalerrors + 1 ))" fi done < $1 echo "Done. Found $totalerrors errors in $entries crontab entries." exit 0 
usercrontabfile" >&2; exit 1 fi lines=0 entries=0 totalerrors=0 while read min hour dom mon dow command do lines="$(($lines + 1))" errors=0 if [ -z "$min" -o "${min%${min#?}}" = "#" ] ; then continue # nothing to check elif [ ! -z $(echo ${min%${min#?}} sed 's/[[:digit:]]//') ] ; then continue # first char not digit: skip! fi entries="$(($entries + 1))" fixvars #### Broken into fields, all '*' replaced with 'X' # Minute check for minslice in $(echo "$min" sed 's/[,-]/ /g') ; do if ! validNum $minslice 60 ; then echo "Line ${lines}: Invalid minute value \"$minslice\"" errors=1 fi done # Hour check for hrslice in $(echo "$hour" sed 's/[,-]/ /g') ; do if ! validNum $hrslice 24 ; then echo "Line ${lines}: Invalid hour value \"$hrslice\"" errors=1 fi done # Day of month check for domslice in $(echo $dom sed 's/[,-]/ /g') ; do if ! validNum $domslice 31 ; then echo "Line ${lines}: Invalid day of month value \"$domslice\"" errors=1 fi done # Month check for monslice in $(echo "$mon" sed 's/[,-]/ /g') ; do if ! validNum $monslice 12 ; then if ! validMon "$monslice" ; then echo "Line ${lines}: Invalid month value \"$monslice\"" errors=1 fi fi done # Day of week check for dowslice in $(echo "$dow" sed 's/[,-]/ /g') ; do if ! validNum $dowslice 31 ; then if ! validDay $dowslice ; then echo "Line ${lines}: Invalid day of week value \"$dowslice\"" errors=1 fi fi done if [ $errors -gt 0 ] ; then echo ">>>> ${lines}: $sourceline" echo "" totalerrors="$(( $totalerrors + 1 ))" fi done < echo "Done. Found $totalerrors errors in $entries crontab entries." exit 0

How It Works

The greatest challenge in getting this script to work is sidestepping problems with the shell wanting to expand the field value * . An asterisk is perfectly acceptable in a cron entry, and indeed is quite common, but give one to a backtick command and it'll expand to the files in the current directory ” definitely not a desired result. Rather than puzzle through the combination of single and double quotes necessary to solve this problem, it proves quite a bit simpler to replace each asterisk with an X, which is what the fixvars function accomplishes.

Also worthy of note is the simple solution to processing comma-and dash-separated lists of values. The punctuation is simply replaced with spaces, and each value is tested as if it were a stand-alone numeric value. That's what the $() sequence does in the for loops :

 $(echo "$dow"  sed 's/[,-]/ /g') 

With this in the code, it's then simple to step through all numeric values, ensuring that each and every one is valid and within the range for that specific crontab field parameter.

Running the Script

This script is easy to run: Just specify the name of a crontab file as its only argument. To work with an existing crontab file, do this:

 $  crontab -l > my.crontab  $  verifycron my.crontab  $  rm my.crontab  

The Results

Using a sample crontab file that has two errors and lots of comments, the script produced these results:

 $  verifycron sample.crontab  Line 10: Invalid day of week value "Mou" >>>> 10: 06 22 * * Mou /home/ACeSystem/bin/del_old_ACinventories.pl Line 12: Invalid minute value "99" >>>> 12: 99 22 * * 1-3,6 /home/ACeSystem/bin/dump_cust_part_no.pl Done. Found 2 errors in 17 crontab entries. 

The sample crontab file with the two errors, along with all the shell scripts explored in this book, are available at the official Wicked Cool Shell Scripts website, at http://www.intuitive.com/wicked/

Hacking the Script

Two enhancements would be potentially worth adding to this script. Validating the compatibility of month and day combinations would ensure that users don't schedule a cron job to run on, for example, 31 February, which will never happen. It could also be useful to check that the command being invoked can be found, but that would entail parsing and processing a PATH variable (i.e., a list of directories within which to look for commands specified in the script), which can be set explicitly within a crontab file. That could be quite tricky. . . .




Wicked Cool Shell Scripts. 101 Scripts for Linux, Mac OS X, and Unix Systems
Wicked Cool Shell Scripts
ISBN: 1593270127
EAN: 2147483647
Year: 2004
Pages: 150
Authors: Dave Taylor

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net