replace the not-so-useful tcal format by a plain text output - ics2txt - convert icalendar .ics file to plain text HTML git clone git://bitreich.org/ics2txt git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/ics2txt DIR Log DIR Files DIR Refs DIR Tags DIR README --- DIR commit 742516775b1d9b12e4c8893114b7cc5a363884ad DIR parent 5a6d05cc7d0f248c84b7f22bd1262bd9fdc9e750 HTML Author: Josuah Demangeon <me@josuah.net> Date: Sun, 20 Jun 2021 12:12:53 +0200 replace the not-so-useful tcal format by a plain text output The input format will be an email open by a text editor, spawned by some script. Diffstat: M .gitignore | 1 + M Makefile | 4 ++-- M README | 6 ++---- M base64.c | 3 --- D bin/tcal2tsv | 85 ------------------------------- D bin/tsv2tcal | 91 ------------------------------- M ical.c | 3 --- M ical.h | 8 +++----- M ics2tree.c | 11 +++++++---- M ics2tsv.c | 19 +++++++++++++------ A strtonum.c | 66 +++++++++++++++++++++++++++++++ D tcal.5 | 61 ------------------------------- A tsv2agenda.c | 193 +++++++++++++++++++++++++++++++ M util.c | 8 +++----- M util.h | 3 ++- 15 files changed, 292 insertions(+), 270 deletions(-) --- DIR diff --git a/.gitignore b/.gitignore @@ -1,4 +1,5 @@ *.o /ics2tsv /ics2tree +/tsv2agenda /ics2txt-[0-9]* DIR diff --git a/Makefile b/Makefile @@ -2,7 +2,7 @@ NAME = ics2txt VERSION = 0.2 W = -Wall -Wextra -std=c99 --pedantic -D = -D_POSIX_C_SOURCE=200811L -DVERSION='"${VERSION}"' +D = -D_POSIX_C_SOURCE=200811L -D_BSD_SOURCE -DVERSION='"${VERSION}"' CFLAGS = $D $W -g PREFIX = /usr/local MANPREFIX = ${PREFIX}/man @@ -10,7 +10,7 @@ MANPREFIX = ${PREFIX}/man SRC = ical.c base64.c util.c HDR = ical.h base64.h util.h OBJ = ${SRC:.c=.o} -BIN = ics2tree ics2tsv +BIN = ics2tree ics2tsv tsv2agenda MAN1 = ics2txt.1 ics2tsv.1 MAN5 = tcal.5 DIR diff --git a/README b/README @@ -7,11 +7,9 @@ The current implementation uses [awk](//josuah.net/wiki/awk/) scripts, but a rather complete implementation of iCalendar, without memory leak or crash, is already there, and used for the `ics2tree` linting tool. -Plans include to have an `ics2json` tool for a 1:1 mapping of iCalendar, and -have `ics2tsv` a more general-purpose tool with user-chosen column fields. +`ics2tsv` converts the iCalendar data to an easier-to-parse TSV format. -So far, Awk-based parsing have been tested with the following input formats -(sample account created for testing): +So far, Awk-based parsing have been tested with the following inputs: * Zoom meetings generated events * FOSDEM events, like <https://fosdem.org/2020/schedule/ical> DIR diff --git a/base64.c b/base64.c @@ -1,12 +1,9 @@ #include "base64.h" - #include <assert.h> #include <stddef.h> #include <stdint.h> #include <string.h> -#include <stdio.h> - static char encode_map[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; DIR diff --git a/bin/tcal2tsv b/bin/tcal2tsv @@ -1,85 +0,0 @@ -#!/usr/bin/awk -f - -function isleap(year) -{ - return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0) -} - -function mdays(mon, year) -{ - return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2) -} - -function maketime(tm, - sec, mon, day) -{ - sec = tm["sec"] + tm["min"] * 60 + tm["hour"] * 3600 - - day = tm["mday"] - 1 - - for (mon = tm["mon"] - 1; mon > 0; mon--) - day = day + mdays(mon, tm["year"]) - - # constants: x * 365 + x / 400 - x / 100 + x / 4 - day = day + int(tm["year"] / 400) * 146097 - day = day + int(tm["year"] % 400 / 100) * 36524 - day = day + int(tm["year"] % 100 / 4) * 1461 - day = day + int(tm["year"] % 4 / 1) * 365 - - return sec + (day - 719527) * 86400 -} - -function text_to_epoch(str, tz, - tm) -{ - tm["year"] = substr(str, 1, 4) - tm["mon"] = substr(str, 6, 2) - tm["mday"] = substr(str, 9, 2) - tm["hour"] = substr(str, 12, 2) - tm["min"] = substr(str, 15, 2) - return maketime(tm) - tz -} - -BEGIN { - FIELDS = "beg end cat loc sum des" - split(FIELDS, fields, " ") - - for (i = 1; i in fields; i++) { - pos[fields[i]] = i - printf("%s%s", (i > 1 ? "\t" : ""), fields[i]) - } - printf("\n") -} - -{ - gsub(/\t/, " ") -} - -/^TZ[+-]/ { - TZ = substr($1, 3, 1) substr($0, 4, 2)*3600 + substr($0, 6, 2)*60 - while (getline && $0 ~ /^$/) - continue -} - -/^[0-9]+-[0-9]+-[0-9]+/ { - if ("beg" in ev) - ev["end"] = text_to_epoch($0, TZ) - else - ev["beg"] = text_to_epoch($0, TZ) - next -} - -/^ / { - tag = $1 - sub("^ *[^ :]+: *", "") - sub(":$", "", tag) - ev[tag] = $0 - next -} - -/^$/ { - for (i = 1; i in fields; i++) - printf("%s%s", (i > 1 ? "\t" : ""), ev[fields[i]]) - printf("\n") - delete ev -} DIR diff --git a/bin/tsv2tcal b/bin/tsv2tcal @@ -1,91 +0,0 @@ -#!/usr/bin/awk -f - -function isleap(year) -{ - return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0) -} - -function mdays(mon, year) -{ - return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2) -} - -function gmtime(sec, tm) -{ - tm["year"] = 1970 - while (sec >= (s = 86400 * (365 + isleap(tm["year"])))) { - tm["year"]++ - sec -= s - } - tm["mon"] = 1 - while (sec >= (s = 86400 * mdays(tm["mon"], tm["year"]))) { - tm["mon"]++ - sec -= s - } - tm["mday"] = 1 - while (sec >= (s = 86400)) { - tm["mday"]++ - sec -= s - } - tm["hour"] = 0 - while (sec >= 3600) { - tm["hour"]++ - sec -= 3600 - } - tm["min"] = 0 - while (sec >= 60) { - tm["min"]++ - sec -= 60 - } - tm["sec"] = sec -} - -function localtime(sec, tm, - tz, h, m) -{ - return gmtime(sec + TZ, tm) -} - -BEGIN { - "exec date +%z" | getline tz - close("exec date +%z") - TZ = substr(tz, 1, 1) substr(tz, 2, 2)*3600 + substr(tz, 4, 2)*60 - - print("TZ" tz) - - FS = "\t" -} - -NR == 1 { - for (i = 1; i <= NF; i++) - name[i] = $i - next -} - -{ - for (i = 1; i <= NF; i++) - ev[name[i]] = $i - - print("") - - localtime(ev["beg"] + offset, tm) - printf("%04d-%02d-%02d %02d:%02d\n", - tm["year"], tm["mon"], tm["mday"], tm["hour"], tm["min"]) - delete ev["beg"] - - localtime(ev["end"] + offset, tm) - printf("%04d-%02d-%02d %02d:%02d\n", - tm["year"], tm["mon"], tm["mday"], tm["hour"], tm["min"]) - delete ev["end"] - - for (i = 1; i <= NF; i++) { - if (name[i] in ev && ev[name[i]]) - printf(" %s: %s\n", name[i], ev[name[i]]) - } - - delete ev -} - -END { - print("") -} DIR diff --git a/ical.c b/ical.c @@ -1,5 +1,4 @@ #include "ical.h" - #include <assert.h> #include <ctype.h> #include <errno.h> @@ -7,7 +6,6 @@ #include <stdlib.h> #include <string.h> #include <strings.h> - #include "util.h" #include "base64.h" @@ -329,7 +327,6 @@ ical_parse(IcalParser *p, FILE *fp) } while (l > 0 && (err = ical_parse_contentline(p, contentline)) == 0); free(contentline); - free(line); if (err == 0 && p->current != p->stack) return ical_err(p, "more BEGIN: than END:"); DIR diff --git a/ical.h b/ical.h @@ -6,9 +6,6 @@ #define ICAL_STACK_SIZE 10 -typedef struct IcalParser IcalParser; -typedef struct IcalStack IcalStack; - typedef enum { ICAL_BLOCK_VEVENT, ICAL_BLOCK_VTODO, @@ -18,11 +15,12 @@ typedef enum { ICAL_BLOCK_OTHER, } IcalBlock; -struct IcalStack { +typedef struct { char name[32]; char tzid[32]; -}; +} IcalStack; +typedef struct IcalParser IcalParser; struct IcalParser { /* function called while parsing in this order */ int (*fn_field_name)(IcalParser *, char *); DIR diff --git a/ics2tree.c b/ics2tree.c @@ -2,10 +2,13 @@ #include <stdlib.h> #include <string.h> #include <strings.h> - #include "ical.h" #include "util.h" +#ifndef __OpenBSD__ +#define pledge(...) 0 +#endif + static void print_ruler(int level) { @@ -76,7 +79,7 @@ main(int argc, char **argv) if (*argv == NULL) { if (ical_parse(&p, stdin) < 0) - err("parsing stdin:%d: %s", p.linenum, p.errmsg); + err(1, "parsing stdin:%d: %s", p.linenum, p.errmsg); } for (; *argv != NULL; argv++, argc--) { @@ -84,9 +87,9 @@ main(int argc, char **argv) debug("converting \"%s\"", *argv); if ((fp = fopen(*argv, "r")) == NULL) - err("opening %s", *argv); + err(1, "opening %s", *argv); if (ical_parse(&p, fp) < 0) - err("parsing %s:%d: %s", *argv, p.linenum, p.errmsg); + err(1, "parsing %s:%d: %s", *argv, p.linenum, p.errmsg); fclose(fp); } return 0; DIR diff --git a/ics2tsv.c b/ics2tsv.c @@ -5,10 +5,13 @@ #include <strings.h> #include <time.h> #include <unistd.h> - #include "ical.h" #include "util.h" +#ifndef __OpenBSD__ +#define pledge(...) 0 +#endif + #define FIELDS_MAX 128 typedef struct Field Field; @@ -155,6 +158,9 @@ main(int argc, char **argv) arg0 = *argv; + if (pledge("stdio rpath", "") < 0) + err(1, "pledge: %s", strerror(errno)); + p.fn_field_name = fn_field_name; p.fn_block_begin = fn_block_begin; p.fn_block_end = fn_block_end; @@ -186,12 +192,12 @@ main(int argc, char **argv) i = 0; do { if (i >= sizeof fields / sizeof *fields - 1) - err("too many fields specified with -o flag"); + err(1, "too many fields specified with -o flag"); } while ((fields[i++] = strsep(&flag_f, ",")) != NULL); fields[i] = NULL; if (flag_1) { - printf("%s\t%s\t%s", "TYPE", "BEG", "END"); + printf("%s\t%s\t%s\t%s", "TYPE", "BEG", "END", "RECUR"); for (i = 0; fields[i] != NULL; i++) printf("\t%s", fields[i]); fputc('\n', stdout); @@ -200,16 +206,17 @@ main(int argc, char **argv) if (*argv == NULL || strcmp(*argv, "-") == 0) { debug("converting *stdin*"); if (ical_parse(&p, stdin) < 0) - err("parsing *stdin*:%d: %s", p.linenum, p.errmsg); + err(1, "parsing *stdin*:%d: %s", p.linenum, p.errmsg); } for (; *argv != NULL; argv++, argc--) { FILE *fp; debug("converting \"%s\"", *argv); if ((fp = fopen(*argv, "r")) == NULL) - err("opening %s: %s", *argv, strerror(errno)); + err(1, "opening %s: %s", *argv, strerror(errno)); if (ical_parse(&p, fp) < 0) - err("parsing %s:%d: %s", *argv, p.linenum, p.errmsg); + err(1, "parsing %s:%d: %s", *argv, p.linenum, p.errmsg); fclose(fp); } + return 0; } DIR diff --git a/strtonum.c b/strtonum.c @@ -0,0 +1,66 @@ +/* $OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $ */ + +/* + * Copyright (c) 2004 Ted Unangst and Todd Miller + * All rights reserved. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <errno.h> +#include <limits.h> +#include <stdlib.h> + +#define INVALID 1 +#define TOOSMALL 2 +#define TOOLARGE 3 + +long long +strtonum(const char *numstr, long long minval, long long maxval, + const char **errstrp) +{ + long long ll = 0; + int error = 0; + char *ep; + struct errval { + const char *errstr; + int err; + } ev[4] = { + { NULL, 0 }, + { "invalid", EINVAL }, + { "too small", ERANGE }, + { "too large", ERANGE }, + }; + + ev[0].err = errno; + errno = 0; + if (minval > maxval) { + error = INVALID; + } else { + ll = strtoll(numstr, &ep, 10); + if (numstr == ep || *ep != '\0') + error = INVALID; + else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval) + error = TOOSMALL; + else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval) + error = TOOLARGE; + } + if (errstrp != NULL) + *errstrp = ev[error].errstr; + errno = ev[error].err; + if (error) + ll = 0; + + return (ll); +} +DEF_WEAK(strtonum); DIR diff --git a/tcal.5 b/tcal.5 @@ -1,61 +0,0 @@ -.Dd $Mdocdate: March 05 2020$ -.Dt TCAL 5 -.Os -. -. -.Sh NAME -. -.Nm tcal -.Nd plaintext calendar for editing by hand on the go -. -. -.Sh DESCRIPTION -. -The first line contain -.Dq TZ+HHMM -with -.Dq +HHMM -as returned by -.D1 $ date +%z . -. -.Pp -Then empty line delimited event entries follow, with for each: -One line with the start date, one line with the end date, -formatted like: -.Dq %Y-%m-%d %H:%M -. -.Pp -Then one line per attribute, each formatted with: -optional space, attribute name, colon, -optional space, and attribute content, -end of line. -. -. -.Sh EXAMPLES -. -.Bd -literal -TZ+0200 - -2021-06-28 00:00 -2021-06-05 00:00 - loc: 950-0994, Chuo Ward, Niigata, Japan - sum: summer holidays - -2021-06-29 13:30 -2021-06-29 15:00 - loc: online, irc.bitreich.org, #bitreich-en - sum: bitreich irc invitation - des: at this moment like all other moment, everyone invited on IRC -.Ed -. -. -.Sh SEE ALSO -. -.Xr cal 1 , -.Xr calendar 1 -. -. -.Sh AUTHORS -. -.An Josuah Demangeon -.Aq Mt me@josuah.net DIR diff --git a/tsv2agenda.c b/tsv2agenda.c @@ -0,0 +1,193 @@ +#include <assert.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <string.h> +#include <unistd.h> +#include "util.h" + +#ifndef __OpenBSD__ +#define pledge(...) 0 +#endif + +#define FIELDS_MAX 128 + +enum { + FIELD_TYPE, + FIELD_BEG, + FIELD_END, + FIELD_RECUR, + FIELD_OTHER, +}; + +typedef struct { + struct tm beg, end; +} AgendaCtx; + +static size_t field_categories = 0; +static size_t field_location = 0; +static size_t field_summary = 0; + +void +print_date(struct tm *tm) +{ + if (tm == NULL) { + fprintf(stdout, "%11s", ""); + } else { + char buf[128]; + if (strftime(buf, sizeof buf, "%Y-%m-%d", tm) == 0) + err(1, "strftime: %s", strerror(errno)); + fprintf(stdout, "%s ", buf); + } +} + +void +print_time(struct tm *tm) +{ + if (tm == NULL) { + fprintf(stdout, "%5s ", ""); + } else { + char buf[128]; + if (strftime(buf, sizeof buf, "%H:%M", tm) == 0) + err(1, "strftime: %s", strerror(errno)); + fprintf(stdout, "%5s ", buf); + } +} + +void +print(AgendaCtx *ctx, char **fields, size_t n) +{ + struct tm beg = {0}, end = {0}; + time_t t; + char const *e; + int rows, samedate; + + t = strtonum(fields[FIELD_BEG], 0, UINT32_MAX, &e); + if (e != NULL) + err(1, "start time %s is %s", fields[FIELD_BEG], e); + localtime_r(&t, &beg); + + t = strtonum(fields[FIELD_END], 0, UINT32_MAX, &e); + if (e != NULL) + err(1, "end time %s is %s", fields[FIELD_END], e); + localtime_r(&t, &end); + + fputc('\n', stdout); + + samedate = (ctx->beg.tm_year != beg.tm_year || ctx->beg.tm_mon != beg.tm_mon || + ctx->beg.tm_mday != beg.tm_mday); + print_date(samedate ? &beg : NULL); + print_time(&beg); + + assert(field_summary < n); + assert(field_summary > FIELD_OTHER); + fprintf(stdout, "%s\n", fields[field_summary]); + + samedate = (beg.tm_year != end.tm_year || beg.tm_mon != end.tm_mon || + beg.tm_mday != end.tm_mday); + print_date(samedate ? &end : NULL); + print_time(&end); + + rows = 0; + + assert(field_location < n); + if (field_location > 0 && fields[field_location][0] != '\0') { + assert(field_summary > FIELD_OTHER); + fprintf(stdout, "%s\n", fields[field_location]); + rows++; + } + + assert(field_categories < n); + if (field_categories > 0 && fields[field_categories][0] != '\0') { + assert(field_summary > FIELD_OTHER); + if (rows > 0) { + print_date(NULL); + print_time(NULL); + } + fprintf(stdout, "%s\n", fields[field_categories]); + } + + ctx->beg = beg; + ctx->end = end; +} + +void +set_fields_num(char **fields, size_t n) +{ + struct { char *name; size_t *var; } map[] = { + { "CATEGORIES", &field_categories }, + { "LOCATION", &field_location }, + { "SUMMARY", &field_summary }, + { NULL, NULL } + }; + + debug("n=%zd", n); + for (size_t i1 = FIELD_OTHER; i1 < n; i1++) + for (size_t i2 = 0; map[i2].name != NULL; i2++) + if (strcasecmp(fields[i1], map[i2].name) == 0) + *map[i2].var = i1; + if (field_summary < FIELD_OTHER) + err(1, "missing column SUMMARY"); +} + +ssize_t +tsv_getline(char **fields, size_t max, char **line, size_t *sz, FILE *fp) +{ + char *s; + size_t n = 0; + + if (getline(line, sz, fp) <= 0) + return ferror(fp) ? -1 : 0; + s = *line; + strchomp(s); + + do { + if (n >= max) + return errno=E2BIG, -1; + } while ((fields[n++] = strsep(&s, "\t")) != NULL); + + return n - 1; +} + +int +main(int argc, char **argv) +{ + AgendaCtx ctx = {0}; + ssize_t nfield, n; + size_t sz = 0; + char *line = NULL, *fields[FIELDS_MAX]; + + arg0 = *argv; + + if (pledge("stdio", "") < 0) + err(1, "pledge: %s", strerror(errno)); + + nfield = tsv_getline(fields, FIELDS_MAX, &line, &sz, stdin); + if (nfield == -1) + err(1, "reading stdin: %s", strerror(errno)); + if (nfield == 0) + err(1, "empty input"); + if (nfield < FIELD_OTHER) + err(1, "not enough input columns"); + + set_fields_num(fields, nfield); + + for (size_t num = 1;; num++) { + n = tsv_getline(fields, FIELDS_MAX, &line, &sz, stdin); + if (n < 0) + err(1, "line %zd: reading stdin: %s", num, strerror(errno)); + if (n == 0) + break; + if (n != nfield) + err(1, "line %zd: had %lld columns, wanted %lld", + num, n, nfield); + + print(&ctx, fields, n); + } + fputc('\n', stdout); + + free(line); + + return 0; +} DIR diff --git a/util.c b/util.c @@ -1,5 +1,4 @@ #include "util.h" - #include <errno.h> #include <stdint.h> #include <stdlib.h> @@ -22,13 +21,13 @@ _log(char const *fmt, va_list va) } void -err(char const *fmt, ...) +err(int e, char const *fmt, ...) { va_list va; va_start(va, fmt); _log( fmt, va); - exit(1); + exit(e); } void @@ -87,8 +86,7 @@ strsep(char **sp, char const *sep) if (*sp == NULL) return NULL; prev = *sp; - for (s = *sp; strchr(sep, *s) == NULL; s++) - continue; + for (s = *sp; strchr(sep, *s) == NULL; s++); if (*s == '\0') { *sp = NULL; } else { DIR diff --git a/util.h b/util.h @@ -7,7 +7,7 @@ /** logging **/ extern char *arg0; -void err(char const *fmt, ...); +void err(int, char const *fmt, ...); void warn(char const *fmt, ...); void debug(char const *fmt, ...); @@ -17,6 +17,7 @@ char *strsep(char **, char const *); void strchomp(char *); char *strappend(char **, char const *); size_t strlcat(char *, char const *, size_t); +long long strtonum(const char *, long long, long long, const char **); /** memory **/ void *reallocarray(void *, size_t, size_t);