#!/bin/bash # Bash task scheduler v0.0.1. # Copyright (C) 2020 Darren 'Tadgy' Austin . # Licensed under the terms of the GNU General Public Licence version 3. # # This program comes with ABSOLUTELY NO WARRANTY. For details and a full copy of # the license terms, see: . This is free # software - you can modify and redistribute it under the terms of the GPL v3. check_day() { # $1 = The job ID. # $2 = The 'crontab' style schedule entry to match. # Variables. # shellcheck disable=SC2155 local DATESECS="$(date --date="$(printf "%(%Y-%m-%d)T %s" "$DATESECS" "0:0:0")" +%s)" # Round to the top of the day. # shellcheck disable=SC2155 local TODAY="$(printf "%(%d)T" "$DATESECS")" local ITEM LHS RHS TEMP debug " - Processing 'day' expressions" while read -r -d , ITEM; do debug " - Found expression: $ITEM" case "${ITEM,,}" in 01|02|03|04|05|06|07|08|09|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31) # Matched a single day by number. (( 10#$TODAY == 10#$ITEM )) && { debug " - Match: $DATE with $ITEM" return 0 } ;; '*') # Matched a single *. debug " - Match: $DATE with $ITEM" return 0 ;; *-*) # Matched a range of days by name or number. LHS="${ITEM%%-*}" RHS="${ITEM##*-}" debug " - *-* expression: LHS=$LHS RHS=$RHS" # Check the LHS and RHS for validity. for TEMP in LHS RHS; do if [[ "${!TEMP}" =~ ^($(printf "%s|" {01..09} {1..30}; printf "%s" "31"))$ ]]; then local $TEMP="$(( 10#${!TEMP} ))" else die "invalid $TEMP in expression: $ITEM" fi done # The RHS can't come before the LHS. (( LHS > RHS )) && die "invalid expression - RHS cannot preceed LHS: $ITEM" # Test expression. (( RHS <= 10#$TODAY && 10#$(printf "%(%d)T" "${STATES["$1"]}") <= RHS )) || (( LHS <= 10#$TODAY && 10#$TODAY <= RHS )) && { debug " - Match: $DATE with $ITEM" return 0 } ;; */*) # Matched a step value of days. LHS="${ITEM%%/*}" RHS="${ITEM##*/}" debug " - */* expression: LHS=$LHS RHS=$RHS" # Must be numbers. [[ ! "$LHS" =~ ^([[:digit:]]+|\*)$ ]] && die "invalid LHS in expression: $ITEM" [[ ! "$RHS" =~ ^[[:digit:]]+$ ]] && die "invalid LHS in expression: $ITEM" # The RHS must be >0. (( RHS <= 0 )) && die "invalid RHS in expression: $ITEM" if [[ "$LHS" == "*" ]]; then # Matched * as the LHS. (( DATESECS == $($DADD -f %s "$(printf "%(%Y-%m-%d)T %s" "${STATES["$1"]}" "0:0:0")" "+$(( RHS - 1 ))mo") )) && { debug " - Match: $DATE with $ITEM" return 0 } elif [[ "${LHS}" =~ ^($(printf "%s|" {01..09} {1..30}; printf "%s" "31"))$ ]]; then # Matched a day by number as the LHS. (( DATESECS == $($DROUND -f %s "$($DADD -f "%Y-%m-%d %H:%M:%S" "$(printf "%(%Y-%m-%d)T %s" "${STATES["$1"]}" "0:0:0")" "+$(( RHS - 1 ))mo")" "$LHS") )) && { debug " - Match: $DATE with $ITEM" return 0 } else die "invalid LHS in expression: $ITEM" fi ;; *) die "invalid expression: ${ITEM:-(empty item)}" ;; esac done <<<"${2/%,}," # $DATE is not a match for the items spec. return 1 } check_dayofweek() { # $1 = The job ID. # $2 = The 'crontab' style schedule entry to match. # Variables # shellcheck disable=SC2155 local DATESECS="$(date --date="$(printf "%(%Y-%m-%d)T %s" "$DATESECS" "0:0:0")" +%s)" # Round to the top of the day. # shellcheck disable=SC2155 local TODAY="$(printf "%(%u)T" "$DATESECS")" local ITEM LHS RHS DAY TEMP debug " - Processing 'dayofweek' expressions" while read -r -d , ITEM; do debug " - found expression: $ITEM" case "${ITEM,,}" in mon|tue|wed|thu|fri|sat|sun) # Matched a single day by name. (( TODAY == ${DAYS["${ITEM,,}"]} )) && { debug " - Match: $DATE ($(printf "%(%a)T" "$DATESECS") #$(printf "%(%u)T" "$DATESECS")) with $ITEM" return 0 } ;; 01|02|03|04|05|06|07|1|2|3|4|5|6|7) # Matched a single day by number. (( TODAY == 10#$ITEM )) && { debug " - Match: $DATE ($(printf "%(%a)T" "$DATESECS") #$(printf "%(%u)T" "$DATESECS")) with $ITEM" return 0 } ;; '*') # Matched a single *. debug " - Match: $DATE ($(printf "%(%a)T" "$DATESECS") #$(printf "%(%u)T" "$DATESECS")) with $ITEM" return 0 ;; *-*) # Matched a range of days by name or number. LHS="${ITEM%%-*}" RHS="${ITEM##*-}" debug " - *-* expression: LHS=$LHS RHS=$RHS" # Validate the LHS and RHS, and convert day names to numbers. for TEMP in LHS RHS; do if [[ "${!TEMP,,}" =~ ^($(IFS='|'; printf "%s" "${!DAYS[*]}" "${DAYS[*]:+|${DAYS[*]}}"; printf "|0%s" "${DAYS[@]}"))$ ]]; then if [[ ! "${!TEMP}" =~ ^[[:digit:]]+$ ]]; then local $TEMP="${DAYS[${!TEMP,,}]}" fi local $TEMP="$(( 10#${!TEMP} ))" else die "invalid $TEMP in expression: $ITEM" fi done # The RHS can't come before the LHS. (( LHS > RHS )) && die "invalid expression - RHS cannot preceed LHS: $ITEM" # Test expression. (( LHS <= $(printf "%(%u)T" "$DATESECS") && $(printf "%(%u)T" "$DATESECS") <= RHS )) && { debug " - Match: $DATE ($(printf "%(%a)T" "$DATESECS") #$(printf "%(%u)T" "$DATESECS")) with $ITEM" return 0 } ;; */*) # Matched a step value of days. LHS="${ITEM%%/*}" RHS="${ITEM##*/}" debug " - */* expression: LHS=$LHS RHS=$RHS" # The RHS must be a number >0. (( RHS <= 0 )) && die "invalid RHS in expression: $ITEM" if [[ "$LHS" == "*" ]]; then # Matched * as the LHS. # Test expression. for (( TEMP=$(date --date="$(printf "%(%Y-%m-%d)T %s" "${STATES["$1"]}" "0:0:0")" +%s); TEMP <= DATESECS; TEMP+=(86400 * RHS) )); do (( DATESECS == TEMP )) && { debug " - Match: $DATE with $ITEM" return 0 } done elif [[ "${LHS,,}" =~ ^(mon|tue|wed|thu|fri|sat|sun|01|02|03|04|05|06|07|1|2|3|4|5|6|7)$ ]]; then # Matched a day by name or number as the LHS. # We need to work with the name of the day, so convert day numbers. if [[ "$LHS" =~ ^[[:digit:]]+$ ]]; then while read -r -d ' ' DAY; do (( ${DAYS[$DAY]} == 10#$LHS )) && break done <<<"${!DAYS[*]} " else DAY="$LHS" fi # Test expression. (( DATESECS == $($DROUND -f %s "$($DADD "$(printf "%(%Y-%m-%d)T %s" "${STATES["$1"]}" "0:0:0")" "+$(( RHS - 1 ))w")" "$DAY") )) && { debug " - Match: $DATE with $ITEM" return 0 } else die "invalid LHS in expression: $ITEM" fi ;; *) die "invalid expression: ${ITEM:-(empty item)}" ;; esac done <<<"${2/%,}," # $DATE not a match for the items spec. return 1 } check_hour() { # $1 = The job ID. # $2 = The 'crontab' style schedule entry to match. # Variables # shellcheck disable=SC2155 local DATESECS="$(date --date="$(printf "%(%Y-%m-%d %H)T%s" "$DATESECS" ":0:0")" +%s)" # Round to the top of the hour. local ITEM LHS RHS TEMP debug " - Processing 'hour' expressions" while read -r -d , ITEM; do debug " - Found expression: $ITEM" case "$ITEM" in 00|01|02|03|04|05|06|07|08|09|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23) # Matched an hour by number. (( 10#$ITEM == $(printf "%(%H)T" "$DATESECS") )) && { debug " - Match: $DATE with $ITEM" return 0 } ;; '*') # Matched a single *. debug " - Match: $DATE with $ITEM" return 0 ;; *-*) # Matched a range of hours by name or number. LHS="${ITEM%%-*}" RHS="${ITEM##*-}" debug " - *-* expression: LHS=$LHS RHS=$RHS" # Check the LHS and RHS for validity. for TEMP in LHS RHS; do if [[ "${!TEMP}" =~ ^($(printf "%s|" {0..22}; printf "%s" "23"))$ ]]; then local $TEMP="$(( 10#${!TEMP} ))" else die "invalid $TEMP in expression: $ITEM" fi done # The LHS can't come before the RHS. (( LHS > RHS )) && die "invalid expression - RHS cannot preceed LHS: $ITEM" # Test expression. (( LHS <= $(printf "%(%H)T" "$DATESECS") && $(printf "%(%H)T" "$DATESECS") <= RHS )) && { debug " - Match: $DATE with $ITEM" return 0 } ;; */*) # Matched a step value of hours. LHS="${ITEM%%/*}" RHS="${ITEM##*/}" debug " - */* expression: LHS=$LHS RHS=$RHS" # The RHS must be >0. (( RHS <= 0 )) && die "invalid RHS in expression: $ITEM" if [[ "$LHS" == "*" ]]; then # Matched * as the LHS. # Test expression. for (( TEMP=$(date --date="$(printf "%(%Y-%m-%d %H)T%s" "${STATES["$1"]}" ":0:0")" +%s); TEMP <= DATESECS; TEMP+=(3600 * RHS) )); do (( DATESECS == TEMP )) && { debug " - Match: $DATE with $ITEM" return 0 } done elif [[ "${LHS}" =~ ^($(printf "%s|" {00..09} {0..22}; printf "%s" "23"))$ ]]; then # Matched an hour as the LHS. # Test expression. for (( TEMP=$($DROUND -f %s "$($DADD -f "%Y-%m-%d %H:00:00" "$(printf "%(%Y-%m-%d %H)T%s" "${STATES["$1"]}" ":0:0")" "+${RHS}d")" "+${LHS}h"); TEMP <= DATESECS; TEMP+=(3600 * RHS) )); do (( DATESECS == TEMP )) && { debug " - Match: $DATE with $ITEM" return 0 } done else die "invalid LHS in expression: $ITEM" fi ;; *) die "invalid expression: ${ITEM:-(empty item)}" ;; esac done <<<"${2/%,}," # $DATE is not a match for the items spec. return 1 } check_minute() { # $1 = The job ID. # $2 = The 'crontab' style schedule entry to match. # Variables. # shellcheck disable=SC2155 local DATESECS="$(date --date="$(printf "%(%Y-%m-%d %H:%M)T%s" "$DATESECS" ":0")" +%s)" # Round to the top of the minute. local ITEM LHS RHS TEMP debug " - Processing 'minute' expressions" while read -r -d , ITEM; do debug " - found expression: $ITEM" case "${ITEM,,}" in 01|02|03|04|05|06|07|08|09|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60) # Matched a single minute by number. (( 10#$ITEM == $(printf "%(%M)T" "$DATESECS") )) && { debug " - Match: $DATE with $ITEM" return 0 } ;; '*') # Matched a single *. debug " - Match: $DATE with $ITEM" return 0 ;; *-*) # Matched a range of minutes by name or number. LHS="${ITEM%%-*}" RHS="${ITEM##*-}" debug " - *-* expression: LHS=$LHS RHS=$RHS" # Check the LHS and RHS for validity. for TEMP in LHS RHS; do if [[ "${!TEMP}" =~ ^($(printf "%s|" {00..09} {0..58}; printf "%s" "59"))$ ]]; then local $TEMP="$(( 10#${!TEMP} ))" else die "invalid $TEMP in expression: $ITEM" fi done # The LHS can't come before the RHS. (( LHS > RHS )) && die "invalid expression - RHS cannot preceed LHS: $ITEM" # Test expression. (( LHS <= $(printf "%(%M)T" "$DATESECS") && $(printf "%(%M)T" "$DATESECS") <= RHS )) && { debug " - Match: $DATE with $ITEM" return 0 } ;; */*) # Matched a step value of minutes. LHS="${ITEM%%/*}" RHS="${ITEM##*/}" debug " - */* expression: LHS=$LHS RHS=$RHS" # The RHS must be >0. (( RHS <= 0 )) && die "invalid RHS in expression: $ITEM" if [[ "$LHS" == "*" ]]; then # Matched * as the LHS. # Test expression. for (( TEMP=$(date --date="$(printf "%(%Y-%m-%d %H:%M)T%s" "${STATES["$1"]}" ":0")" +%s); TEMP <= DATESECS; TEMP+=(60 * RHS) )); do (( DATESECS == TEMP )) && { debug " - Match: $DATE with $ITEM" return 0 } done elif [[ "${LHS}" =~ ^($(printf "%s|" {00..09} {0..58}; printf "%s" "59"))$ ]]; then # Matched an hour as the LHS. # Test expression. for (( TEMP=$($DROUND -f %s "$($DADD -f "%Y-%m-%d %H:00:00" "$(printf "%(%Y-%m-%d %H)T%s" "${STATES["$1"]}" ":0:0")" "+${RHS}h")" "+${LHS}m"); TEMP <= DATESECS; TEMP+=(60 * RHS) )); do (( DATESECS == TEMP )) && { debug " - Match: $DATE with $ITEM" return 0 } done else die "invalid LHS in expression: $ITEM" fi ;; *) die "invalid expression: ${ITEM:-(empty item)}" ;; esac done <<<"${2/%,}," # $DATE is not a match for the items spec. return 1 } check_month() { # $1 = The job ID. # $2 = The 'crontab' style schedule entry to match. # Variables. # shellcheck disable=SC2155 local DATESECS="$(date --date="$(printf "%(%Y-%m)T%s" "$DATESECS" "-1 0:0:0")" +%s)" # Round to the beginning of the month. local ITEM LHS RHS MONTH TEMP debug " - begin processing of 'month' expressions" while read -r -d , ITEM; do debug " - found expression: $ITEM" case "${ITEM,,}" in jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|01|02|03|04|05|06|07|08|09|1|2|3|4|5|6|7|8|9|10|11|12) # Matched a single month by name or number. if [[ "${ITEM,,}" =~ ^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)$ ]]; then TEMP="${MONTHS[${ITEM,,}]}" else TEMP="$(( 10#$ITEM ))" fi # Test expression. (( TEMP == $(printf "%(%m)T" "$DATESECS") )) && { debug " - Match: $DATE with $ITEM" return 0 } ;; '*') # Matched a single *. debug " - Match: $DATE with $ITEM" return 0 ;; *-*) # Matched a range of months by name or number. LHS="${ITEM%%-*}" RHS="${ITEM##*-}" debug " - *-* expression: LHS=$LHS RHS=$RHS" # Validate the LHS and RHS, and convert month names to numbers. for TEMP in LHS RHS; do if [[ "${!TEMP,,}" =~ ^($(IFS='|'; printf "%s" "${!MONTHS[*]}" "${MONTHS[*]:+|${MONTHS[*]}}"; printf "|0%s" "${MONTHS[@]}"))$ ]]; then if [[ ! "${!TEMP}" =~ ^[[:digit:]]+$ ]]; then local $TEMP="${MONTHS[${!TEMP,,}]}" fi local $TEMP="$(( 10#${!TEMP} ))" else die "invalid $TEMP in expression: $ITEM" fi done # The RHS can't come before the LHS. (( LHS > RHS )) && die "invalid expression - RHS cannot preceed LHS: $ITEM" # Test expression. (( LHS <= $(printf "%(%m)T" "$DATESECS") && $(printf "%(%m)T" "$DATESECS") <= RHS )) && { debug " - Match: $DATE ($(printf "%(%b)T" "$DATESECS") #$(printf "%(%m)T" "$DATESECS")) with $ITEM" return 0 } ;; */*) # Matched a step iteration of months. LHS="${ITEM%%/*}" RHS="${ITEM##*/}" debug " - */* expression: LHS=$LHS RHS=$RHS" # The RHS must be >0. (( RHS <= 0 )) && die "invalid RHS in expression: $ITEM" if [[ "$LHS" == "*" ]]; then # Matched * as the LHS. # Test expression. while read -r TEMP; do (( TEMP == DATESECS )) && { debug " Match: $DATE with $ITEM" return 0 } done < <($DSEQ -f %s "$(printf "%(%Y-%m)T%s" "${STATES["$1"]}" "-1 0:0:0")" "${RHS}mo" "$(printf "%(%Y-%m)T%s" "$DATESECS" "-1 0:0:0")") elif [[ "${LHS,,}" =~ ^($(printf "%s|" jan feb mar apr may jun jul aug sep oct nov dec {01..09} {1..11}; printf "%s" "12"))$ ]]; then # Marched a month name or number as the LHS. # The RHS must be >0. (( RHS <= 0 )) && die "invalid RHS in expression: $ITEM" # We need to work with the name of the month, so convert month numbers. if [[ "$LHS" =~ ^[[:digit:]]+$ ]]; then while read -r -d ' ' MONTH; do (( ${MONTHS[$MONTH]} == 10#$LHS )) && break done <<<"${!MONTHS[*]} " else MONTH="$LHS" fi # Test expression. while read -r TEMP; do (( TEMP == DATESECS )) && { debug " - Match: $DATE with $ITEM" return 0 } done < <($DSEQ -f %s "$($DADD -f "%Y-%m-1 0:0:0" "$($DROUND -f "%Y-%m-1 0:0:0" "$(printf "%(%Y-%m)T%s" "${STATES["$1"]}" "-1 0:0:0")" "$MONTH")" "$(( RHS -1 ))y")" "${RHS}y" "$(printf "%(%Y-%m)T%s" "$DATESECS" "-1 0:0:0")") else die "invalid LHS in expression: $ITEM" fi ;; *) die "invalid expression: ${ITEM:-(empty item)}" ;; esac done <<<"${2/%,}," # $DATE is not a match for the items spec. return 1 } debug() { (( DEBUG == 1 )) && error "debug: $*" } die() { error "fatal: $*" exit 1 } display_help() { #........1.........2.........3.........4.........5.........6.........7.........8 cat <<-EOF Usage: ${0##*/} [options] '' Parse a crontab style and return true or false as to whether the current date/time means the should be run. Options: -d , --date Instead of using the current date/time for the comparison against the , use this date and time specification instead. This must be given as a single argument, enclosed in single (') or double (") quotes if it contains spaces, and must be a valid date accepted by 'date --date'. Note: by default UTC is expected, but a specifically given date/time + TZ will be converted to UTC. -q, --quiet Normally, when the scheduler spec> indicates the job should be run, the name of the is output to stdout. With this option, ${0##*/} operates quietly and will only output errors to stderr. The return code issued to indicate whether the job would be run is unaffected. -s , --statefile Use the given as the state file for maintaining a record of the last execution time of the job IDs. The default statefile is \$HOME/.config/${0##*/}.state. Using this option allows non-root users to use the scheduler's job tracking functionallity. If the file provided does not exist, it will be created if possible, or an error will be emitted if the (or default system wide file) cannot be written. -t, --test Parse the schedule and return the result, but do not update the statefile. This allows for testing of schedules and dates. -z, --debug Enable debugging output, which will be spamey. -- Cease option processing and begin argument parsing. Option processing ceases with the first non-option argument or --. Arguments (all of which are mandatory): This is the 'crontab' style specification of the schedule on which the given should be executed. This schedule is compared against the current (or that provided by option --date/-d) date/time. The schedule is given as a single argument enclosed in single (') or double (") quotes, with each component separated by spaces. Be mindful of shell globbing while using a '*' on the command line. See documentation for a full rundown of the accepted. A free form identifier used for state tracking between runs. This is also output to stdout if the schedule is matched against the date/time. may not contain the character ':'. This is only restriction on job identifications. EOF } display_version() { #........1.........2.........3.........4.........5.........6.........7.........8 cat <<-EOF Bash task scheduler v0.0.1. Copyright (C) 2020 Darren 'Tadgy' Austin . Licensed under the terms of the GNU General Public Licence version 3. This program comes with ABSOLUTELY NO WARRANTY. For details and a full copy of the license terms, see: . This is free software - you can modify and redistribute it under the terms of the GPL v3. EOF } error() { printf "%s: %s\\n" "${0##*/}" "$*" >&2 } parse_options() { # Parse the command line options. while [[ -n "$1" ]]; do case "$1" in -d|-date|--date) [[ -z "$2" ]] && die "date cannot be an empty value" DATE="$(date --date="$2" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)" || die "invalid date specification: $2" shift ;; -q|-quiet|--quiet) (( DEBUG == 1 )) && die "cannot use -q/--quiet with -z/--debug" QUIET=1 ;; -s|-statefile|--statefile) [[ -z "$2" ]] && die "state file cannot be an empty value" STATEFILE="$2" shift ;; -t|-test|--test) TEST=1 ;; -h|-\?|-help|--help) display_help exit 0 ;; -v|-version|--version) display_version exit 0 ;; -z|-debug|--debug) (( QUIET == 1 )) && die "cannot use -z/--debug with -q/--quiet" DEBUG=1 ;; --) shift break ;; --*|-*) die "invalid option: $1" return 1 ;; *) break ;; esac shift done # Make sure we have a schedule spec and job ID after all the options are removed. if (( $# < 2 )) || (( $# >= 3 )) || [[ -z "$1" ]] || [[ -z "$2" ]]; then error "Usage: ${0##*/} [options] '' " error "Try: ${0##*/} --help" return 1 else (( $(printf "%s" "$1" | awk '{print NF}') != 5 )) && die "wrong number of components in scheduler spec: $1" SCHEDSPEC="$1" debug "Found valid schedule spec: $SCHEDSPEC" [[ "$2" =~ : ]] && die "invalid character (:) in job ID: $1" JOBID="$2" debug "Found job ID: $JOBID" fi } readstate() { local LINE while read -r LINE; do STATES["${LINE%%:*}"]="${LINE##*:}" debug "Read statefile entry: ${LINE%%:*} = ${LINE##*:}" done <"$STATEFILE" } writestate() { local FD TEMP # Lock the statefile for writing. debug "Opening and locking statefile" exec {FD}>"$STATEFILE" flock -w 1 -E 10 $FD TEMP="$?" if (( TEMP == 10 )); then die "failed to obtain lock on statefile" elif (( TEMP != 0 )); then die "flock error on statefile" fi # Output the data in the STATES array to the statefile. for JOBID in "${!STATES[@]}"; do printf "%s:%s\\n" "$JOBID" "${STATES["$JOBID"]}" >&$FD debug "Wrote statefile entry: $JOBID = ${STATES["$JOBID"]}" done # Close statefile and release lock. debug "Releasing statefile lock and closing" exec {FD}>&- } tasksched() { # Bash v4.0+ is required. [[ -z "${BASH_VERSINFO[0]}" ]] || ((BASH_VERSINFO[0] < 4)) && die "minimum of bash v4 required" # Make life easier and stick to a single TZ. export TZ=UTC # Set defaults. # shellcheck disable=SC2155 local DATE="$(date "+%Y-%m-%d %H:%M:%S")" # The date/time to compare the scheduler spec to. Use current date/time by default. local DEBUG=0 # Whether to emit debugging messages. local STATEFILE="$HOME/.config/${0##*/}.state" # The default state file. local QUIET=0 # Whether to print the job ID if the schedule spec matches. 0 = print, 1 = don't print. local TEST=0 # Whether this execution is a test run. 0 = no, 1 = yes. # Variables. local DAY DAYOFWEEK HOUR JOBID MINUTE MONTH SCHEDSPEC TEMP local -A STATES local -A DAYS=([mon]=1 [tue]=2 [wed]=3 [thu]=4 [fri]=5 [sat]=6 [sun]=7) local -A MONTHS=([jan]=1 [feb]=2 [mar]=3 [apr]=4 [may]=5 [jun]=6 [jul]=7 [aug]=8 [sep]=9 [oct]=10 [nov]=11 [dec]=12) # Parse options. parse_options "$@" # Convert the supplied/default date to seconds from the epoch. # shellcheck disable=SC2155 local DATESECS="$(date --date="$DATE" +%s)" # Figure out the naming of the dateutils binaries. # Fucking Debian/Ubuntu being different, of course. for TEMP in dadd dround dseq; do if hash "$TEMP" 2>/dev/null; then local ${TEMP^^}="$(hash -t "$TEMP")" elif hash "dateutils.$TEMP" 2>/dev/null; then local ${TEMP^^}="$(hash -t "dateutils.$TEMP")" else die "cannot locate dateutils' $TEMP binary" fi done # Check statefile. if [[ -e "$STATEFILE" ]]; then [[ ! -r "$STATEFILE" ]] && die "cannot read statefile: $STATEFILE" [[ ! -w "$STATEFILE" ]] && die "cannot write statefile: $STATEFILE" else mkdir -p "${STATEFILE%/*}" touch "$STATEFILE" 2>/dev/null || die "cannot create statefile: $STATEFILE" fi # Confirm what date we're using. debug "Using date: $DATE" # Read the states from the statefile. readstate # Initialise the STATES array with the JOBID if necessary. [[ -z "${STATES["$JOBID"]}" ]] && STATES["$JOBID"]="$DATESECS" # Print the last time the job went off. debug "Timestamp for job ID '$JOBID': $(printf "%(%Y-%m-%d %H:%M:%S)T\\n" "${STATES["$JOBID"]}")" # Extract components from schedule spec. MINUTE="${SCHEDSPEC%% *}"; SCHEDSPEC="${SCHEDSPEC#* }" HOUR="${SCHEDSPEC%% *}"; SCHEDSPEC="${SCHEDSPEC#* }" DAY="${SCHEDSPEC%% *}"; SCHEDSPEC="${SCHEDSPEC#* }" MONTH="${SCHEDSPEC%% *}"; SCHEDSPEC="${SCHEDSPEC#* }" DAYOFWEEK="${SCHEDSPEC%% *}" # Verify that the spec was parsed correctly. [[ -z "$MINUTE" ]] || [[ -z "$HOUR" ]] || [[ -z "$DAY" ]] || [[ -z "$MONTH" ]] || [[ -z "$DAYOFWEEK" ]] && die "failed to parse schedule spec" # Check the specification against $DATE. debug "Testing expressions:" check_month "$JOBID" "$MONTH" && \ check_dayofweek "$JOBID" "$DAYOFWEEK" && \ check_day "$JOBID" "$DAY" && \ check_hour "$JOBID" "$HOUR" && \ check_minute "$JOBID" "$MINUTE" ERRNO="$?" (( ERRNO == 0 )) && { debug "Matched specification to $DATE" debug "Updating status date" STATES["$JOBID"]="$DATESECS" (( TEST == 0 )) && writestate (( QUIET == 0 )) && printf "%s\n" "$JOBID" } return $ERRNO } # If using the above functions in a script, adjust the call to 'tasksched' below to use your chosen options. tasksched "$@"