One of the most challenging validation tasks , but one that's crucial for shell scripts that work with dates, is to ensure that a specific date is actually possible. If we ignore leap years, this task is not too bad, because the calendar is well behaved and consistent each year. All we need in that case is a table with the days of each month against which to compare a specified date. To take leap years into account, you have to add some additional logic to the script. One set of rules for calculating a leap year is as follows :
Years not divisible by 4 are not leap years.
Years divisible by 4 and by 400 are leap years.
Years divisible by 4, not divisible by 400, and divisible by 100, are not leap years.
All other years divisible by 4 are leap years.
Notice how this script utilizes normdate (Script #3) to ensure a consistent date format before proceeding.
#!/bin/sh # valid-date -- Validates a date, taking into account leap year rules. exceedsDaysInMonth() { # Given a month name, return 0 if the specified day value is # less than or equal to the max days in the month; 1 otherwise case $(echo tr '[:upper:]' '[:lower:]') in jan* ) days=31 ;; feb* ) days=28 ;; mar* ) days=31 ;; apr* ) days=30 ;; may* ) days=31 ;; jun* ) days=30 ;; jul* ) days=31 ;; aug* ) days=31 ;; sep* ) days=30 ;; oct* ) days=31 ;; nov* ) days=30 ;; dec* ) days=31 ;; * ) echo "#!/bin/sh # valid-date -- Validates a date, taking into account leap year rules. exceedsDaysInMonth() { # Given a month name, return 0 if the specified day value is # less than or equal to the max days in the month; 1 otherwise case $(echo $1tr '[:upper:]' '[:lower:]') in jan* ) days=31 ;; feb* ) days=28 ;; mar* ) days=31 ;; apr* ) days=30 ;; may* ) days=31 ;; jun* ) days=30 ;; jul* ) days=31 ;; aug* ) days=31 ;; sep* ) days=30 ;; oct* ) days=31 ;; nov* ) days=30 ;; dec* ) days=31 ;; * ) echo "$0: Unknown month name $1" >&2; exit 1 esac if [ $2 -lt 1 -o $2 -gt $days ] ; then return 1 else return 0 # the day number is valid fi } isLeapYear() { # This function returns 0 if a leap year; 1 otherwise . # The formula for checking whether a year is a leap year is: # 1. Years not divisible by 4 are not leap years. # 2. Years divisible by 4 and by 400 are leap years. # 3. Years divisible by 4, not divisible by 400, and divisible by 100, # are not leap years. # 4. All other years divisible by 4 are leap years. year=$1 if [ "$((year % 4))" -ne 0 ] ; then return 1 # nope, not a leap year elif [ "$((year % 400))" -eq 0 ] ; then return 0 # yes, it's a leap year elif [ "$((year % 100))" -eq 0 ] ; then return 1 else return 0 fi } ## Begin main script if [ $# -ne 3 ] ; then echo "Usage: $0 month day year" >&2 echo "Typical input formats are August 3 1962 and 8 3 2002" >&2 exit 1 fi # Normalize date and split back out returned values newdate="$(normdate "$@")" if [ $? -eq 1 ] ; then exit 1 # error condition already reported by normdate fi month="$(echo $newdate cut -d\ -f1)" day="$(echo $newdate cut -d\ -f2)" year="$(echo $newdate cut -d\ -f3)" # Now that we have a normalized date, let's check to see if the # day value is logical if ! exceedsDaysInMonth $month "$2" ; then if [ "$month" = "Feb" -a "$2" -eq "29" ] ; then if ! isLeapYear $3 ; then echo "$0: $3 is not a leap year, so Feb doesn't have 29 days" >&2 exit 1 fi else echo "$0: bad day value: $month doesn't have $2 days" >&2 exit 1 fi fi echo "Valid date: $newdate" exit 0: Unknown month name " >&2; exit 1 esac if [ -lt 1 -o -gt $days ] ; then return 1 else return 0 # the day number is valid fi } isLeapYear() { # This function returns 0 if a leap year; 1 otherwise. # The formula for checking whether a year is a leap year is: # 1. Years not divisible by 4 are not leap years. # 2. Years divisible by 4 and by 400 are leap years. # 3. Years divisible by 4, not divisible by 400, and divisible by 100, # are not leap years. # 4. All other years divisible by 4 are leap years. year= if [ "$((year % 4))" -ne 0 ] ; then return 1 # nope, not a leap year elif [ "$((year % 400))" -eq 0 ] ; then return 0 # yes, it's a leap year elif [ "$((year % 100))" -eq 0 ] ; then return 1 else return 0 fi } ## Begin main script if [ $# -ne 3 ] ; then echo "Usage:#!/bin/sh # valid-date -- Validates a date, taking into account leap year rules. exceedsDaysInMonth() { # Given a month name, return 0 if the specified day value is # less than or equal to the max days in the month; 1 otherwise case $(echo $1tr '[:upper:]' '[:lower:]') in jan* ) days=31 ;; feb* ) days=28 ;; mar* ) days=31 ;; apr* ) days=30 ;; may* ) days=31 ;; jun* ) days=30 ;; jul* ) days=31 ;; aug* ) days=31 ;; sep* ) days=30 ;; oct* ) days=31 ;; nov* ) days=30 ;; dec* ) days=31 ;; * ) echo "$0: Unknown month name $1" >&2; exit 1 esac if [ $2 -lt 1 -o $2 -gt $days ] ; then return 1 else return 0 # the day number is valid fi } isLeapYear() { # This function returns 0 if a leap year; 1 otherwise . # The formula for checking whether a year is a leap year is: # 1. Years not divisible by 4 are not leap years. # 2. Years divisible by 4 and by 400 are leap years. # 3. Years divisible by 4, not divisible by 400, and divisible by 100, # are not leap years. # 4. All other years divisible by 4 are leap years. year=$1 if [ "$((year % 4))" -ne 0 ] ; then return 1 # nope, not a leap year elif [ "$((year % 400))" -eq 0 ] ; then return 0 # yes, it's a leap year elif [ "$((year % 100))" -eq 0 ] ; then return 1 else return 0 fi } ## Begin main script if [ $# -ne 3 ] ; then echo "Usage: $0 month day year" >&2 echo "Typical input formats are August 3 1962 and 8 3 2002" >&2 exit 1 fi # Normalize date and split back out returned values newdate="$(normdate "$@")" if [ $? -eq 1 ] ; then exit 1 # error condition already reported by normdate fi month="$(echo $newdate cut -d\ -f1)" day="$(echo $newdate cut -d\ -f2)" year="$(echo $newdate cut -d\ -f3)" # Now that we have a normalized date, let's check to see if the # day value is logical if ! exceedsDaysInMonth $month "$2" ; then if [ "$month" = "Feb" -a "$2" -eq "29" ] ; then if ! isLeapYear $3 ; then echo "$0: $3 is not a leap year, so Feb doesn't have 29 days" >&2 exit 1 fi else echo "$0: bad day value: $month doesn't have $2 days" >&2 exit 1 fi fi echo "Valid date: $newdate" exit 0month day year" >&2 echo "Typical input formats are August 3 1962 and 8 3 2002" >&2 exit 1 fi # Normalize date and split back out returned values newdate="$(normdate "$@")" if [ $? -eq 1 ] ; then exit 1 # error condition already reported by normdate fi month="$(echo $newdate cut -d\ -f1)" day="$(echo $newdate cut -d\ -f2)" year="$(echo $newdate cut -d\ -f3)" # Now that we have a normalized date, let's check to see if the # day value is logical if ! exceedsDaysInMonth $month "" ; then if [ "$month" = "Feb" -a "" -eq "29" ] ; then if ! isLeapYear ; then echo "#!/bin/sh # valid-date -- Validates a date, taking into account leap year rules. exceedsDaysInMonth() { # Given a month name, return 0 if the specified day value is # less than or equal to the max days in the month; 1 otherwise case $(echo $1tr '[:upper:]' '[:lower:]') in jan* ) days=31 ;; feb* ) days=28 ;; mar* ) days=31 ;; apr* ) days=30 ;; may* ) days=31 ;; jun* ) days=30 ;; jul* ) days=31 ;; aug* ) days=31 ;; sep* ) days=30 ;; oct* ) days=31 ;; nov* ) days=30 ;; dec* ) days=31 ;; * ) echo "$0: Unknown month name $1" >&2; exit 1 esac if [ $2 -lt 1 -o $2 -gt $days ] ; then return 1 else return 0 # the day number is valid fi } isLeapYear() { # This function returns 0 if a leap year; 1 otherwise . # The formula for checking whether a year is a leap year is: # 1. Years not divisible by 4 are not leap years. # 2. Years divisible by 4 and by 400 are leap years. # 3. Years divisible by 4, not divisible by 400, and divisible by 100, # are not leap years. # 4. All other years divisible by 4 are leap years. year=$1 if [ "$((year % 4))" -ne 0 ] ; then return 1 # nope, not a leap year elif [ "$((year % 400))" -eq 0 ] ; then return 0 # yes, it's a leap year elif [ "$((year % 100))" -eq 0 ] ; then return 1 else return 0 fi } ## Begin main script if [ $# -ne 3 ] ; then echo "Usage: $0 month day year" >&2 echo "Typical input formats are August 3 1962 and 8 3 2002" >&2 exit 1 fi # Normalize date and split back out returned values newdate="$(normdate "$@")" if [ $? -eq 1 ] ; then exit 1 # error condition already reported by normdate fi month="$(echo $newdate cut -d\ -f1)" day="$(echo $newdate cut -d\ -f2)" year="$(echo $newdate cut -d\ -f3)" # Now that we have a normalized date, let's check to see if the # day value is logical if ! exceedsDaysInMonth $month "$2" ; then if [ "$month" = "Feb" -a "$2" -eq "29" ] ; then if ! isLeapYear $3 ; then echo "$0: $3 is not a leap year, so Feb doesn't have 29 days" >&2 exit 1 fi else echo "$0: bad day value: $month doesn't have $2 days" >&2 exit 1 fi fi echo "Valid date: $newdate" exit 0: is not a leap year, so Feb doesn't have 29 days" >&2 exit 1 fi else echo "#!/bin/sh # valid-date -- Validates a date, taking into account leap year rules. exceedsDaysInMonth() { # Given a month name, return 0 if the specified day value is # less than or equal to the max days in the month; 1 otherwise case $(echo $1tr '[:upper:]' '[:lower:]') in jan* ) days=31 ;; feb* ) days=28 ;; mar* ) days=31 ;; apr* ) days=30 ;; may* ) days=31 ;; jun* ) days=30 ;; jul* ) days=31 ;; aug* ) days=31 ;; sep* ) days=30 ;; oct* ) days=31 ;; nov* ) days=30 ;; dec* ) days=31 ;; * ) echo "$0: Unknown month name $1" >&2; exit 1 esac if [ $2 -lt 1 -o $2 -gt $days ] ; then return 1 else return 0 # the day number is valid fi } isLeapYear() { # This function returns 0 if a leap year; 1 otherwise . # The formula for checking whether a year is a leap year is: # 1. Years not divisible by 4 are not leap years. # 2. Years divisible by 4 and by 400 are leap years. # 3. Years divisible by 4, not divisible by 400, and divisible by 100, # are not leap years. # 4. All other years divisible by 4 are leap years. year=$1 if [ "$((year % 4))" -ne 0 ] ; then return 1 # nope, not a leap year elif [ "$((year % 400))" -eq 0 ] ; then return 0 # yes, it's a leap year elif [ "$((year % 100))" -eq 0 ] ; then return 1 else return 0 fi } ## Begin main script if [ $# -ne 3 ] ; then echo "Usage: $0 month day year" >&2 echo "Typical input formats are August 3 1962 and 8 3 2002" >&2 exit 1 fi # Normalize date and split back out returned values newdate="$(normdate "$@")" if [ $? -eq 1 ] ; then exit 1 # error condition already reported by normdate fi month="$(echo $newdate cut -d\ -f1)" day="$(echo $newdate cut -d\ -f2)" year="$(echo $newdate cut -d\ -f3)" # Now that we have a normalized date, let's check to see if the # day value is logical if ! exceedsDaysInMonth $month "$2" ; then if [ "$month" = "Feb" -a "$2" -eq "29" ] ; then if ! isLeapYear $3 ; then echo "$0: $3 is not a leap year, so Feb doesn't have 29 days" >&2 exit 1 fi else echo "$0: bad day value: $month doesn't have $2 days" >&2 exit 1 fi fi echo "Valid date: $newdate" exit 0: bad day value: $month doesn't have days" >&2 exit 1 fi fi echo "Valid date: $newdate" exit 0
To run the script, simply specify a date on the command line, in "month day year" format. The month can be a three-letter abbreviation, a full word, or a numeric value; the year must be four digits.
$ valid-date august 3 1960 Valid date: Aug 3 1960 $ valid-date 9 31 2001 valid-date: bad day value: Sep doesn't have 31 days $ valid-date feb 29 2004 Valid date: Feb 29 2004 $ valid-date feb 29 2006 valid-date: 2006 is not a leap year, so Feb doesn't have 29 days
A roughly similar approach to this script could validate time specifications, either using a 24-hour clock or with an ante meridiem/post meridiem (am/pm) suffix. Split the value at the colon , ensure that the minutes and seconds (if specified) are between 0 and 60, and then check that the first value is between 0 and 12 if allowing am/pm, or between 0 and 24 if you prefer a 24- hour clock. (Fortunately, while there are leap seconds and other tiny variations in time to help keep the calendar balanced, we can safely ignore them on a day-to-day basis.)