783 lines
27 KiB
Bash
Executable file
783 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 \$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 <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 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 <scheduler spec> accepted.
|
|
<job ID>
|
|
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. <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 "$@"
|