bash-tasksched/tasksched

782 lines
27 KiB
Bash
Executable file

#!/bin/bash
# Bash task scheduler v0.0.1.
# Copyright (C) 2020 Darren 'Tadgy' Austin <darren (at) afterdark.org.uk>.
# 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: <http://gnu.org/licenses/gpl.html>. 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] '<scheduler spec>' <job ID>
Parse a crontab style <scheduler spec> and return true or false as to whether
the current date/time means the <job ID> should be run.
Options:
-d <date spec>, --date <date spec>
Instead of using the current date/time for the comparison against the
<scheduler spec>, 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 <job ID> 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 <filename>, --statefile <filename>
Use the given <filename> as the state file for maintaining a record of
the last execution time of the job IDs.
The default statefile is $FIXME.
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 <filename>
(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):
<scheduler spec>
This is the 'crontab' style specification of the schedule on which the
given <job ID> should be executed. This schedule is compared against the
current (or 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 <scheduler spec> accepted.
<job ID>
FIXME
<job ID> 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 <darren (at) afterdark.org.uk>.
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: <http://gnu.org/licenses/gpl.html>. 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] '<scheduler spec>' <job ID>"
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 "$@"