Try to match full words first. Otherwise it would match the beginning
[cronlist.git] / cronlist.c
blob6d4d86f18398df432856bb4505ad5935bc5b9b68
1 /*
2 This program is free software: you can redistribute it and/or modify
3 it under the terms of the GNU General Public License as published by
4 the Free Software Foundation, either version 3 of the License, or
5 (at your option) any later version.
7 This program is distributed in the hope that it will be useful,
8 but WITHOUT ANY WARRANTY; without even the implied warranty of
9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 GNU General Public License for more details.
12 You should have received a copy of the GNU General Public License
13 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <ctype.h>
20 #include <string.h>
21 #include <unistd.h>
22 #include <sys/types.h>
23 #include <pwd.h>
24 #include <getopt.h>
25 #include <stdarg.h>
26 #include <time.h>
28 #define PROGRAM_NAME "cronlist"
30 const char IGNORE_DOM = 'm';
31 const char IGNORE_DOW = 'w';
32 const char IGNORE_NOTHING = ' ';
34 struct time_entry {
35 char m[60];
36 char h[24];
37 char dom[31];
38 char mon[12];
39 char dow[7];
40 char ignore;
43 struct entry {
44 struct time_entry te;
45 char *username;
46 char *command;
47 struct entry *next;
50 struct time_entry EMPTY_TIME_ENTRY;
52 char *slurp (FILE *f)
54 const int increment = 8192;
55 int capa = increment;
56 char *buf = malloc(capa);
57 size_t res, offs = 0;
59 for (;;) {
60 res = fread(buf+offs, 1, increment, f);
61 offs += res;
62 if (res < increment) {
63 buf[offs] = '\0';
64 return buf;
66 capa += increment;
67 buf = realloc(buf, capa);
71 char *empty_string (void)
73 char *s = malloc(1);
74 s[0] = '\0';
75 return s;
78 char *slurp_command (char *command)
80 char *result;
81 FILE *p = popen(command, "r");
82 if (!p) return empty_string();
84 result = slurp(p);
85 pclose(p);
86 return result;
89 char *slurp_file (char *filename)
91 char *result;
92 FILE *f = fopen(filename, "r");
93 if (!f) return empty_string();
95 result = slurp(f);
96 fclose(f);
97 return result;
100 char *next_line (char *p)
102 while (*p && *p != '\n') p++;
103 if (*p == '\n') p++;
104 return p;
107 char *skip_spaces (char *p)
109 while (*p && isspace(*p)) p++;
110 return p;
113 char *skip_blanks (char *p)
115 while (*p && isblank(*p)) p++;
116 return p;
119 char *skip_irrelevant (char *p)
121 p = skip_spaces(p);
122 while (*p && *p == '#') {
123 p = next_line(p);
124 p = skip_spaces(p);
126 return p;
129 int eoln (char *p)
131 return !*p || *p == '\n';
134 typedef int (*fn)(char *, char **);
136 char *read_number (char *buf, fn get_number, int *res)
138 char *p = buf;
139 if (isdigit(*p)) {
140 *res = strtol(buf, &p, 10);
141 if (buf == p) return NULL;
142 return p;
144 else if (!get_number) return NULL;
145 else {
146 *res = get_number(buf, &p);
147 if (buf == p) return NULL;
148 return p;
152 int find_string (char *s, char *table[])
154 int i;
155 for (i = 0; table[i]; i++) {
156 if (!strncasecmp(table[i], s, strlen(table[i]))) return i;
158 return -1;
161 int get_from_tables (char *buf, char **tables[], char **end)
163 int i, idx;
164 for (i = 0; tables[i]; i++) {
165 idx = find_string(buf, tables[i]);
166 if (idx >= 0) {
167 *end = buf + strlen(tables[i][idx]);
168 return idx;
171 /* not found */
172 *end = buf;
173 return -1;
176 static char *mon_fullnames[] = {
177 "January", "February", "March", "April", "May", "June", "July", "August",
178 "September", "October", "November", "December", NULL };
179 static char *mon_abbrevs[] = {
180 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
181 "Sep", "Oct", "Nov", "Dec", NULL };
182 static char **months[] = { mon_fullnames, mon_abbrevs, NULL };
185 int get_month (char *buf, char **end)
187 return get_from_tables(buf, months, end) + 1;
190 static char *dow_fullnames[] = {
191 "Sunday", "Monday", "Tuesday", "Wednesday",
192 "Thursday", "Friday", "Saturday", NULL };
193 static char *dow_abbrevs[] = {
194 "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", NULL };
195 static char **dows[] = { dow_fullnames, dow_abbrevs, NULL };
198 int get_dow_1 (char *buf, char **end)
200 return get_from_tables(buf, dows, end);
203 int get_dow_2 (char *buf, char **end)
205 int res = get_dow_1(buf, end);
206 return res == 0 ? 7 : res;
210 char *read_range (char *buf, int min, int max, int offs,
211 fn get_number_1, fn get_number_2,
212 char *dest)
214 char *p = skip_blanks(buf);
215 int num1, num2, step, i;
217 for (;;) {
218 if (*p == '*') {
219 p++;
220 num1 = min; num2 = max;
222 else {
223 p = read_number(p, get_number_1, &num1); if (!p) return NULL;
224 if (*p == '-') {
225 p++;
226 p = read_number(p, get_number_2, &num2); if (!p) return NULL;
228 else num2 = num1;
230 if (*p == '/') {
231 p++;
232 p = read_number(p, NULL, &step);
233 if (!p) return NULL;
235 else step = 1;
236 if (num1 < min || num1 > max || num2 < min || num2 > max) return NULL;
238 for (i = num1; i <= num2; i += step) {
239 dest[i-offs] = 1;
242 if (*p != ',') return p;
243 else p++;
247 int all_full (char *vec, int len)
249 int i;
250 for (i = 0; i < len; i++) {
251 if (!vec[i]) return 0;
253 return 1;
256 void print_vec (char *vec, int len)
258 int i;
259 for (i = 0; i < len; i++) {
260 if (vec[i]) printf(" %2d", i);
261 else printf(" --");
263 printf("\n");
266 char *read_time_entry (char *buf, struct time_entry *res)
268 char raw_dow[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
269 char *p = skip_irrelevant(buf);
270 char *keyword;
271 int len;
272 int fulldom, fulldow;
274 if (eoln(p)) return NULL;
275 *res = EMPTY_TIME_ENTRY;
276 if (*p == '@') {
277 p++;
278 keyword = p;
279 while (*p && isalpha(*p)) p++;
280 len = p - keyword;
281 if (!strncmp(keyword, "reboot", len)) /* ignore, keep empty time_entry */ ;
282 else if (!strncmp(keyword, "yearly", len)) read_time_entry("0 0 1 1 *", res);
283 else if (!strncmp(keyword, "annually", len)) read_time_entry("0 0 1 1 *", res);
284 else if (!strncmp(keyword, "monthly", len)) read_time_entry("0 0 1 * *", res);
285 else if (!strncmp(keyword, "weekly", len)) read_time_entry("0 0 * * 0", res);
286 else if (!strncmp(keyword, "daily", len)) read_time_entry("0 0 * * *", res);
287 else if (!strncmp(keyword, "midnight", len)) read_time_entry("0 0 * * *", res);
288 else if (!strncmp(keyword, "hourly", len)) read_time_entry("0 * * * *", res);
289 else /* invalid */
290 return NULL;
291 return p;
293 /* minutes */
294 p = read_range(p, 0, 59, 0, NULL, NULL, res->m);
295 if (!p) return NULL;
296 /* hours */
297 p = read_range(p, 0, 23, 0, NULL, NULL, res->h);
298 if (!p) return NULL;
299 /* dom */
300 p = read_range(p, 1, 31, 1, NULL, NULL, res->dom);
301 if (!p) return NULL;
302 /* month */
303 p = read_range(p, 1, 12, 1, get_month, get_month, res->mon);
304 if (!p) return NULL;
305 /* dow */
306 p = read_range(p, 0, 7, 0, get_dow_1, get_dow_2, raw_dow);
307 if (!p) return NULL;
309 if (raw_dow[7]) raw_dow[0] = 1;
310 memcpy(res->dow, raw_dow, 7);
312 fulldom = all_full(res->dom, 31);
313 fulldow = all_full(res->dow, 7);
315 if (fulldom && !fulldow) res->ignore = IGNORE_DOM;
316 else if (fulldow && !fulldom) res->ignore = IGNORE_DOW;
317 else res->ignore = IGNORE_NOTHING;
319 return p;
322 struct entry *add_entries (char *buf, char *username, struct entry *link)
324 struct entry *list = link;
325 char *p = buf, *q;
327 while (*p) {
328 struct entry *entry = malloc(sizeof(struct entry));
329 q = read_time_entry(p, &entry->te);
330 if (!q) {
331 free(entry);
333 else {
334 entry->next = list;
335 list = entry;
336 p = skip_blanks(q);
337 /* username */
338 if (username) entry->username = strdup(username);
339 else {
340 q = p;
341 while (isalnum(*q)) q++;
342 entry->username = strndup(p, q-p);
343 p = skip_blanks(q);
345 /* command */
346 q = p;
347 while (!eoln(q)) q++;
348 entry->command = strndup(p, q-p);
349 p = q;
351 p = next_line(p);
353 return list;
356 void free_entry_list (struct entry *list)
358 struct entry *p;
359 while (list) {
360 p = list;
361 list = list->next;
362 free(p->username);
363 free(p->command);
364 free(p);
368 char *get_username (void)
370 uid_t uid = getuid();
371 struct passwd *pwd = getpwuid(uid);
372 char *res;
373 if (pwd) return strdup(pwd->pw_name);
374 res = malloc(20);
375 sprintf(res, "%u", uid);
376 return res;
379 void print_te_part (char *arr, int len, int offs)
381 int first = 1, i;
382 for (i = 0; i < len; i++) {
383 if (arr[i]) {
384 if (!first) putchar(',');
385 printf("%d", i+offs);
386 first = 0;
391 void print_entry (struct entry *e)
393 print_te_part(e->te.m, 60, 0); putchar(' ');
394 print_te_part(e->te.h, 24, 0); putchar(' ');
395 if (e->te.ignore == IGNORE_DOM) putchar('-');
396 print_te_part(e->te.dom, 31, 1); putchar(' ');
397 print_te_part(e->te.mon, 12, 1); putchar(' ');
398 if (e->te.ignore == IGNORE_DOW) putchar('-');
399 print_te_part(e->te.dow, 7, 0); putchar(' ');
400 printf(" %s %s\n", e->username, e->command);
403 struct entry *read_crontabs (int user, int system)
405 struct entry *list = NULL;
407 if (user) {
408 char *buf = slurp_command("crontab -l");
409 if (buf) {
410 char *username = get_username();
411 list = add_entries(buf, username, list);
412 free(username);
413 free(buf);
417 if (system) {
418 char *buf = slurp_file("/etc/crontab");
419 if (buf) {
420 list = add_entries(buf, NULL, list);
421 free(buf);
424 return list;
427 int match_time (struct time_entry *te, struct tm *tm) {
428 return
429 te->m [ tm->tm_min ] &&
430 te->h [ tm->tm_hour ] &&
431 te->mon[ tm->tm_mon ] &&
432 ((te->ignore != IGNORE_DOM && te->dom[ tm->tm_mday-1 ]) ||
433 (te->ignore != IGNORE_DOW && te->dow[ tm->tm_wday ]));
436 int tm_ge (struct tm *tm1, struct tm *tm2)
438 return
439 tm1->tm_year > tm2->tm_year ||
440 (tm1->tm_year == tm2->tm_year &&
441 (tm1->tm_mon > tm2->tm_mon ||
442 (tm1->tm_mon == tm2->tm_mon &&
443 (tm1->tm_mday > tm2->tm_mday ||
444 (tm1->tm_mday == tm2->tm_mday &&
445 (tm1->tm_hour > tm2->tm_hour ||
446 (tm1->tm_hour == tm2->tm_hour &&
447 (tm1->tm_min > tm2->tm_min ||
448 (tm1->tm_min == tm2->tm_min)))))))));
451 void die (char *s, ...) {
452 va_list ap;
454 fputs(PROGRAM_NAME ": ", stderr);
455 va_start(ap, s);
456 vfprintf(stderr, s, ap);
457 va_end(ap);
458 fputc('\n', stderr);
459 exit(1);
462 void usage (void) {
463 puts(PROGRAM_NAME " lists upcoming cron actions from /etc/crontab\n\
464 and your personal crontab.\n\
465 Options:\n\
466 -f --from=DATETIME list actions starting on or after DATETIME (default now)\n\
467 -t --to=DATETIME list actions starting on or before DATETIME\n\
468 -n --entries=NUMBER stop after NUMBER actions (default 10)\n\
469 -s --system show /etc/crontab only\n\
470 -c --crontab show your personal crontab only\n\
471 -h --help shows this help\n\
473 DATETIME should be a date expression that can be passed to date(1).\n");
474 exit(0);
477 void get_tm_from_date (char *datespec, struct tm *dest) {
478 char *cmd = malloc(strlen(datespec) + 20);
479 FILE *p;
480 char number[20];
481 long n;
482 struct tm *stm;
484 sprintf(cmd, "date -d \"%s\" '+%%s'", datespec);
485 p = popen(cmd, "r");
486 if (!p) die("command ‘%s’ failed", cmd);
487 fgets(number, 20, p);
488 pclose(p);
489 if (!isdigit(number[0])) die("command ‘%s’ didn't return a meaningful value", cmd);
490 n = atoi(number);
491 stm = localtime(&n);
492 if (!stm) die("date ‘%s’ not supported", datespec);
493 *dest = *stm;
496 int mdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
498 int leap (int year) {
499 return (year+1900)%400 == 0 || (year+1900)%100 != 0 || year%4 == 0;
503 int main (int argc, char *argv[])
505 static char *shortopts = "f:t:n:csh";
506 static struct option longopts[] = {
507 { "from", required_argument, NULL, 'f' },
508 { "to", required_argument, NULL, 't' },
509 { "entries", required_argument, NULL, 'n' },
510 { "crontab", no_argument, NULL, 'c' },
511 { "system", no_argument, NULL, 's' },
512 { "help", no_argument, NULL, 'h' }
515 int have_from = 0, have_to = 0, have_n = 0, only_system = 0, only_crontab = 0;
516 int n, outputted;
517 struct tm from, to, tm, *stm;
518 time_t t;
519 int opt;
520 char *p;
521 struct entry *entries;
523 opterr = 1;
524 while ((opt = getopt_long(argc, argv, shortopts, longopts, NULL)) != -1) {
525 switch (opt) {
526 case 'f':
527 have_from = 1;
528 get_tm_from_date(optarg, &from);
529 break;
530 case 't':
531 have_to = 1;
532 get_tm_from_date(optarg, &to);
533 break;
534 case 'n':
535 have_n = 1;
536 n = strtol(optarg, &p, 10);
537 if (*p || n < 0) die("Invalid entry count: %s", optarg);
538 break;
539 case 's':
540 only_system = 1;
541 break;
542 case 'c':
543 only_crontab = 1;
544 break;
545 case 'h':
546 usage();
547 break;
548 default:
549 /* die("Bad argument. Use ‘--help’ for a list of options"); */
550 exit(1);
553 if (only_system && only_crontab)
554 die("Can't choose both --system and --crontab");
556 if (!have_from) {
557 t = time(NULL);
558 stm = localtime(&t);
559 from = *stm;
561 if (!have_to && !have_n) {
562 have_n = 1;
563 n = 10;
566 if (n <= 0) return 0;
568 entries = read_crontabs(!only_system, !only_crontab);
569 if (!entries) return 0;
571 outputted = 0;
572 tm = from;
573 for (;;) {
574 struct entry *e = entries;
575 while (e) {
576 if (match_time(&e->te, &tm)) {
577 printf("%04d-%02d-%02d %2d:%02d %s %s\n",
578 tm.tm_year+1900,
579 tm.tm_mon+1,
580 tm.tm_mday,
581 tm.tm_hour,
582 tm.tm_min,
583 e->username,
584 e->command);
585 outputted++;
586 if (have_n && outputted >= n) return 0;
588 e = e->next;
591 if (have_to && tm_ge(&tm, &to)) return 0;
593 /* after one year, if nothing was output, nothing will be */
594 if (outputted == 0 &&
595 (tm.tm_year > from.tm_year+1 ||
596 (tm.tm_year == from.tm_year+1 && tm.tm_mon > from.tm_mon)))
597 return 0;
599 /* increment tm */
600 tm.tm_min++;
601 if (tm.tm_min > 59) {
602 tm.tm_min = 0;
603 tm.tm_hour++;
604 if (tm.tm_hour > 23) {
605 tm.tm_hour = 0;
606 tm.tm_wday = (tm.tm_wday+1)%7;
607 tm.tm_mday++;
608 if (tm.tm_mday > mdays[tm.tm_mon]) {
609 if (tm.tm_mon != 1 ||
610 tm.tm_mday == 30 ||
611 !leap(tm.tm_year)) {
612 tm.tm_mday = 1;
613 tm.tm_mon++;
614 if (tm.tm_mon > 11) {
615 tm.tm_mon = 0;
616 tm.tm_year++;
623 /* never reached */
625 return 0;