370 lines
8.7 KiB
Diff
370 lines
8.7 KiB
Diff
From 0f1704a0f8c5fd2a4da6f530694bdd93a7ca3226 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Ond=C5=99ej=20Poho=C5=99elsk=C3=BD?=
|
|
<35430604+opohorel@users.noreply.github.com>
|
|
Date: Mon, 8 Nov 2021 16:20:09 +0100
|
|
Subject: [PATCH] Add random within range '~' operator
|
|
|
|
With the operator one can specify for a job a random time or date within
|
|
a specified range for a field.
|
|
The random value is generated when the crontab where the job is
|
|
specified, is loaded.
|
|
---
|
|
man/crontab.5 | 9 ++
|
|
src/entry.c | 262 ++++++++++++++++++++++++++++++++------------------
|
|
2 files changed, 176 insertions(+), 95 deletions(-)
|
|
|
|
diff --git a/man/crontab.5 b/man/crontab.5
|
|
index a011c89..ba8f0c3 100644
|
|
--- a/man/crontab.5
|
|
+++ b/man/crontab.5
|
|
@@ -199,6 +199,15 @@ hyphen. The specified range is inclusive. For example, 8-11 for
|
|
an 'hours' entry specifies execution at hours 8, 9, 10, and 11. The first
|
|
number must be less than or equal to the second one.
|
|
.PP
|
|
+Randomization of the execution time within a range can be used.
|
|
+A random number within a range specified as two numbers separated with
|
|
+a tilde is picked. The specified range is inclusive.
|
|
+For example, 6~15 for a 'minutes' entry picks a random minute
|
|
+within 6 to 15 range. The random number is picked when crontab file is parsed.
|
|
+The first number must be less than or equal to the second one. You might omit
|
|
+one or both of the numbers specifying the range. For example, ~ for a 'minutes'
|
|
+entry picks a random minute within 0 to 59 range.
|
|
+.PP
|
|
Lists are allowed. A list is a set of numbers (or ranges) separated by
|
|
commas. Examples: "1,2,5,9", "0-4,8-12".
|
|
.PP
|
|
diff --git a/src/entry.c b/src/entry.c
|
|
index 92b55f5..9276f47 100644
|
|
--- a/src/entry.c
|
|
+++ b/src/entry.c
|
|
@@ -62,9 +62,22 @@ static const char *ecodes[] = {
|
|
"out of memory"
|
|
};
|
|
|
|
+typedef enum {
|
|
+ R_START,
|
|
+ R_AST,
|
|
+ R_STEP,
|
|
+ R_TERMS,
|
|
+ R_NUM1,
|
|
+ R_RANGE,
|
|
+ R_RANGE_NUM2,
|
|
+ R_RANDOM,
|
|
+ R_RANDOM_NUM2,
|
|
+ R_FINISH,
|
|
+} range_state_t;
|
|
+
|
|
static int get_list(bitstr_t *, int, int, const char *[], int, FILE *),
|
|
-get_range(bitstr_t *, int, int, const char *[], int, FILE *),
|
|
-get_number(int *, int, const char *[], int, FILE *, const char *),
|
|
+get_range(bitstr_t *, int, int, const char *[], FILE *),
|
|
+get_number(int *, int, const char *[], FILE *),
|
|
set_element(bitstr_t *, int, int, int);
|
|
|
|
void free_entry(entry * e) {
|
|
@@ -449,11 +462,14 @@ get_list(bitstr_t * bits, int low, int high, const char *names[],
|
|
/* process all ranges
|
|
*/
|
|
done = FALSE;
|
|
+ /* unget ch to allow get_range() to process it properly
|
|
+ */
|
|
+ unget_char(ch, file);
|
|
while (!done) {
|
|
- if (EOF == (ch = get_range(bits, low, high, names, ch, file)))
|
|
+ if (EOF == (ch = get_range(bits, low, high, names, file)))
|
|
return (EOF);
|
|
if (ch == ',')
|
|
- ch = get_char(file);
|
|
+ continue;
|
|
else
|
|
done = TRUE;
|
|
}
|
|
@@ -468,137 +484,193 @@ get_list(bitstr_t * bits, int low, int high, const char *names[],
|
|
return (ch);
|
|
}
|
|
|
|
+inline static int is_separator(int ch) {
|
|
+ switch (ch) {
|
|
+ case '\t':
|
|
+ case '\n':
|
|
+ case ' ':
|
|
+ case ',':
|
|
+ return 1;
|
|
+ default:
|
|
+ return 0;
|
|
+ }
|
|
+}
|
|
+
|
|
+
|
|
|
|
static int
|
|
get_range(bitstr_t * bits, int low, int high, const char *names[],
|
|
- int ch, FILE * file) {
|
|
+ FILE * file) {
|
|
/* range = number | number "-" number [ "/" number ]
|
|
+ * | [number] "~" [number]
|
|
*/
|
|
+
|
|
+ int ch, i, num1, num2, num3;
|
|
|
|
- int i, num1, num2, num3;
|
|
+ /* default value for step
|
|
+ */
|
|
+ num3 = 1;
|
|
+ range_state_t state = R_START;
|
|
+
|
|
+ while (state != R_FINISH && ((ch = get_char(file)) != EOF)) {
|
|
+ switch (state) {
|
|
+ case R_START:
|
|
+ if (ch == '*') {
|
|
+ num1 = low;
|
|
+ num2 = high;
|
|
+ state = R_AST;
|
|
+ break;
|
|
+ }
|
|
+ if (ch == '~') {
|
|
+ num1 = low;
|
|
+ state = R_RANDOM;
|
|
+ break;
|
|
+ }
|
|
+ unget_char(ch, file);
|
|
+ if (get_number(&num1, low, names, file) != EOF) {
|
|
+ state = R_NUM1;
|
|
+ break;
|
|
+ }
|
|
+ return (EOF);
|
|
|
|
- Debug(DPARS | DEXT, ("get_range()...entering, exit won't show\n"));
|
|
+ case R_AST:
|
|
+ if (ch == '/') {
|
|
+ state = R_STEP;
|
|
+ break;
|
|
+ }
|
|
+ if (is_separator(ch)) {
|
|
+ state = R_FINISH;
|
|
+ break;
|
|
+ }
|
|
+ return (EOF);
|
|
|
|
- if (ch == '*') {
|
|
- /* '*' means "first-last" but can still be modified by /step
|
|
- */
|
|
- num1 = low;
|
|
- num2 = high;
|
|
- ch = get_char(file);
|
|
- if (ch == EOF)
|
|
- return (EOF);
|
|
- }
|
|
- else {
|
|
- ch = get_number(&num1, low, names, ch, file, ",- \t\n");
|
|
- if (ch == EOF)
|
|
- return (EOF);
|
|
+ case R_STEP:
|
|
+ if (get_number(&num3, 0, PPC_NULL, file) != EOF) {
|
|
+ state = R_TERMS;
|
|
+ break;
|
|
+ }
|
|
+ return (EOF);
|
|
|
|
- if (ch != '-') {
|
|
- /* not a range, it's a single number.
|
|
- */
|
|
- if (EOF == set_element(bits, low, high, num1)) {
|
|
- unget_char(ch, file);
|
|
+ case R_TERMS:
|
|
+ if (is_separator(ch)) {
|
|
+ state = R_FINISH;
|
|
+ break;
|
|
+ }
|
|
return (EOF);
|
|
- }
|
|
- return (ch);
|
|
- }
|
|
- else {
|
|
- /* eat the dash
|
|
- */
|
|
- ch = get_char(file);
|
|
- if (ch == EOF)
|
|
+
|
|
+ case R_NUM1:
|
|
+ if (ch == '-') {
|
|
+ state = R_RANGE;
|
|
+ break;
|
|
+ }
|
|
+ if (ch == '~') {
|
|
+ state = R_RANDOM;
|
|
+ break;
|
|
+ }
|
|
+ if (is_separator(ch)) {
|
|
+ num2 = num1;
|
|
+ state = R_FINISH;
|
|
+ break;
|
|
+ }
|
|
return (EOF);
|
|
|
|
- /* get the number following the dash
|
|
- */
|
|
- ch = get_number(&num2, low, names, ch, file, "/, \t\n");
|
|
- if (ch == EOF || num1 > num2)
|
|
+ case R_RANGE:
|
|
+ if (get_number(&num2, low, names, file) != EOF) {
|
|
+ state = R_RANGE_NUM2;
|
|
+ break;
|
|
+ }
|
|
return (EOF);
|
|
- }
|
|
- }
|
|
|
|
- /* check for step size
|
|
- */
|
|
- if (ch == '/') {
|
|
- /* eat the slash
|
|
- */
|
|
- ch = get_char(file);
|
|
- if (ch == EOF)
|
|
- return (EOF);
|
|
+ case R_RANGE_NUM2:
|
|
+ if (ch == '/') {
|
|
+ state = R_STEP;
|
|
+ break;
|
|
+ }
|
|
+ if (is_separator(ch)) {
|
|
+ state = R_FINISH;
|
|
+ break;
|
|
+ }
|
|
+ return (EOF);
|
|
|
|
- /* get the step size -- note: we don't pass the
|
|
- * names here, because the number is not an
|
|
- * element id, it's a step size. 'low' is
|
|
- * sent as a 0 since there is no offset either.
|
|
- */
|
|
- ch = get_number(&num3, 0, PPC_NULL, ch, file, ", \t\n");
|
|
- if (ch == EOF || num3 == 0)
|
|
- return (EOF);
|
|
- }
|
|
- else {
|
|
- /* no step. default==1.
|
|
- */
|
|
- num3 = 1;
|
|
+ case R_RANDOM:
|
|
+ if (is_separator(ch)) {
|
|
+ num2 = high;
|
|
+ state = R_FINISH;
|
|
+ }
|
|
+ else if (unget_char(ch, file),
|
|
+ get_number(&num2, low, names, file) != EOF) {
|
|
+ state = R_TERMS;
|
|
+ }
|
|
+ /* fail if couldn't find match on previous term
|
|
+ */
|
|
+ else
|
|
+ return (EOF);
|
|
+
|
|
+ /* if invalid random range was selected */
|
|
+ if (num1 > num2)
|
|
+ return (EOF);
|
|
+
|
|
+ /* select random number in range <num1, num2>
|
|
+ */
|
|
+ num1 = num2 = random() % (num2 - num1 + 1) + num1;
|
|
+ break;
|
|
+
|
|
+
|
|
+ default:
|
|
+ /* We should never get here
|
|
+ */
|
|
+ return (EOF);
|
|
+ }
|
|
}
|
|
+ if (state != R_FINISH || ch == EOF)
|
|
+ return (EOF);
|
|
|
|
- /* range. set all elements from num1 to num2, stepping
|
|
- * by num3. (the step is a downward-compatible extension
|
|
- * proposed conceptually by bob@acornrc, syntactically
|
|
- * designed then implemented by paul vixie).
|
|
- */
|
|
for (i = num1; i <= num2; i += num3)
|
|
if (EOF == set_element(bits, low, high, i)) {
|
|
unget_char(ch, file);
|
|
return (EOF);
|
|
}
|
|
-
|
|
- return (ch);
|
|
+ return ch;
|
|
}
|
|
|
|
static int
|
|
-get_number(int *numptr, int low, const char *names[], int ch, FILE * file,
|
|
- const char *terms) {
|
|
+get_number(int *numptr, int low, const char *names[], FILE * file) {
|
|
char temp[MAX_TEMPSTR], *pc;
|
|
- int len, i;
|
|
+ int len, i, ch;
|
|
+ char *endptr;
|
|
|
|
pc = temp;
|
|
len = 0;
|
|
|
|
- /* first look for a number */
|
|
- while (isdigit((unsigned char) ch)) {
|
|
+ /* get all alnum characters available */
|
|
+ while (isalnum((ch = get_char(file)))) {
|
|
if (++len >= MAX_TEMPSTR)
|
|
goto bad;
|
|
*pc++ = (char)ch;
|
|
- ch = get_char(file);
|
|
}
|
|
- *pc = '\0';
|
|
- if (len != 0) {
|
|
- /* got a number, check for valid terminator */
|
|
- if (!strchr(terms, ch))
|
|
- goto bad;
|
|
- *numptr = atoi(temp);
|
|
- return (ch);
|
|
+ if (len == 0)
|
|
+ goto bad;
|
|
+
|
|
+ unget_char(ch, file);
|
|
+
|
|
+ /* try to get number */
|
|
+ *numptr = (int) strtol(temp, &endptr, 10);
|
|
+ if (*endptr == '\0' && temp != endptr) {
|
|
+ /* We have a number */
|
|
+ return 0;
|
|
}
|
|
|
|
/* no numbers, look for a string if we have any */
|
|
if (names) {
|
|
- while (isalpha((unsigned char) ch)) {
|
|
- if (++len >= MAX_TEMPSTR)
|
|
- goto bad;
|
|
- *pc++ = (char)ch;
|
|
- ch = get_char(file);
|
|
- }
|
|
- *pc = '\0';
|
|
- if (len != 0 && strchr(terms, ch)) {
|
|
- for (i = 0; names[i] != NULL; i++) {
|
|
- Debug(DPARS | DEXT,
|
|
- ("get_num, compare(%s,%s)\n", names[i], temp));
|
|
- if (!strcasecmp(names[i], temp)) {
|
|
- *numptr = i + low;
|
|
- return (ch);
|
|
- }
|
|
+ for (i = 0; names[i] != NULL; i++) {
|
|
+ Debug(DPARS | DEXT, ("get_num, compare(%s,%s)\n", names[i], temp));
|
|
+ if (strcasecmp(names[i], temp) == 0) {
|
|
+ *numptr = i + low;
|
|
+ return 0;
|
|
}
|
|
}
|
|
+ } else {
|
|
+ goto bad;
|
|
}
|
|
|
|
bad:
|
|
--
|
|
2.35.1
|
|
|