Home
       remove unused utilities and flatten the source some more - 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 24ae7d2759496b7907cce29f0c26697950453ff5
   DIR parent 742516775b1d9b12e4c8893114b7cc5a363884ad
  HTML Author: Josuah Demangeon <me@josuah.net>
       Date:   Sun, 20 Jun 2021 18:37:15 +0200
       
       remove unused utilities and flatten the source some more
       
       Diffstat:
         M .gitignore                          |       1 +
         M Makefile                            |      19 ++++++++++++-------
         D bin/ics2tsv                         |     141 -------------------------------
         D bin/ics2txt                         |       2 --
         D bin/tsv2ics                         |     104 -------------------------------
         M ics2tsv.c                           |      57 ++++++++++++++++---------------
         M tsv2agenda.c                        |     249 ++++++++++++++++++-------------
         A tsv2ics.awk                         |     106 ++++++++++++++++++++++++++++++
         M util.c                              |      14 ++++++++++++++
         M util.h                              |       4 ++++
       
       10 files changed, 310 insertions(+), 387 deletions(-)
       ---
   DIR diff --git a/.gitignore b/.gitignore
       @@ -1,5 +1,6 @@
        *.o
        /ics2tsv
        /ics2tree
       +/tsv2ics
        /tsv2agenda
        /ics2txt-[0-9]*
   DIR diff --git a/Makefile b/Makefile
       @@ -10,31 +10,36 @@ MANPREFIX = ${PREFIX}/man
        SRC = ical.c base64.c util.c
        HDR = ical.h base64.h util.h
        OBJ = ${SRC:.c=.o}
       +AWK = tsv2ics.awk
        BIN = ics2tree ics2tsv tsv2agenda
        MAN1 = ics2txt.1 ics2tsv.1
       -MAN5 = tcal.5
        
        all: ${BIN}
        
        .c.o:
                ${CC} -c ${CFLAGS} -o $@ $<
        
       +${AWK:.awk=}:
       +        cp $@.awk $@
       +        chmod +x $@
       +
        ${OBJ}: ${HDR}
        ${BIN}: ${OBJ} ${BIN:=.o}
                ${CC} ${LDFLAGS} -o $@ $@.o ${OBJ}
        
        clean:
       -        rm -rf *.o ${BIN} ${NAME}-${VERSION} *.gz
       +        rm -rf *.o ${BIN} ${AWK:.awk} ${NAME}-${VERSION} *.gz
        
       -install:
       +install: ${BIN} ${AWK:.awk=}
                mkdir -p ${DESTDIR}$(PREFIX)/bin
       -        cp bin/* $(BIN) ${DESTDIR}$(PREFIX)/bin
       +        cp $(BIN) ${AWK:.awk=} ${DESTDIR}$(PREFIX)/bin
                mkdir -p ${DESTDIR}$(MANPREFIX)/man1
                cp ${MAN1} ${DESTDIR}$(MANPREFIX)/man1
       -        mkdir -p ${DESTDIR}$(MANPREFIX)/man5
       -        cp ${MAN5} ${DESTDIR}$(MANPREFIX)/man5
        
        dist: clean
                mkdir -p ${NAME}-${VERSION}
       -        cp -r README Makefile bin ${MAN1} ${MAN5} ${SRC} ${NAME}-${VERSION}
       +        cp -r README Makefile ${AWK} ${MAN1} ${SRC} ${NAME}-${VERSION}
                tar -cf - ${NAME}-${VERSION} | gzip -c >${NAME}-${VERSION}.tar.gz
       +
       +.SUFFIXES: .awk
       +.PHONY: ${AWK}
   DIR diff --git a/bin/ics2tsv b/bin/ics2tsv
       @@ -1,141 +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 timegm(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 print_vevent(ev, fields,
       -        i)
       -{
       -        for (i = 1; i in fields; i++)
       -                printf("%s%s", (i > 1 ? "\t" : ""), ev[fields[i]])
       -        printf("\n")
       -}
       -
       -function ical_parse_line(str, content, params,
       -        i, eq)
       -{
       -        if ((i = index(str, ":")) == 0)
       -                return -1
       -        content["value"] = substr(str, i + 1)
       -        str = substr(str, 1, i - 1)
       -
       -        if ((i = index(str, ";")) == 0) {
       -                content["name"] = str
       -                return 0
       -        }
       -        content["name"] = substr(str, 1, i - 1)
       -        str = substr(str, i + 1)
       -
       -        while ((i = index(str, ";")) > 0) {
       -                if ((eq = index(str, "=")) == 0)
       -                        return -1
       -                param[substr(str, 1, eq - 1)] = substr(str, eq + 1, i - 1)
       -                str = substr(str, eq + 1)
       -        }
       -        if ((eq = index(str, "=")) == 0)
       -                return -1
       -        params[substr(str, 1, eq - 1)] = substr(str, eq + 1)
       -        return 0
       -}
       -
       -function ical_set_tz(tzid)
       -{
       -        gsub("'", "", tzid)
       -        cmd = "TZ='" tzid "' exec date +%z" 
       -        cmd | getline tzid
       -        close(cmd)
       -        TZ = substr(tzid, 1, 1) substr(tzid, 2, 2)*3600 + substr(tzid, 4, 2)*60
       -}
       -
       -function ical_to_epoch(content, param,
       -        tz, cmd)
       -{
       -        if (param["TZID"])
       -                ical_set_tz(param["TZID"])
       -
       -        tm["year"] = substr(content["value"], 1, 4)
       -        tm["mon"] = substr(content["value"], 5, 2)
       -        tm["mday"] = substr(content["value"], 7, 2)
       -        tm["hour"] = substr(content["value"], 10, 2)
       -        tm["min"] = substr(content["value"], 12, 2)
       -        tm["sec"] = substr(content["value"], 14, 2)
       -
       -        return timegm(tm) + TZ
       -}
       -
       -BEGIN {
       -        split("DTSTART DTEND CATEGORIES LOCATION SUMMARY DESCRIPTION URL",
       -          FIELDS, " ")
       -        DT["DTSTART"] = DT["DTEND"] = DT["DUE"] = 1
       -
       -        # by default: "CATEGORIES" -> "cat", "LOCATION" -> "loc"...
       -        translate["DTSTART"] = "beg"
       -        translate["DTEND"] = "end"
       -
       -        for (i = 1; i in FIELDS; i++) {
       -                if (!(s = translate[FIELDS[i]]))
       -                        s = tolower(substr(FIELDS[i], 1, 3))
       -                printf("%s%s", (i > 1 ? "\t" : ""), s)
       -        }
       -        printf("\n")
       -
       -        FS = "[:;]"
       -}
       -
       -{
       -        gsub("\r", "")
       -        gsub("\t", "\\\\t")
       -}
       -
       -sub("^ ", "") {
       -        content["value"] = content["value"] $0
       -        next
       -}
       -
       -{
       -        delete content
       -        delete param
       -
       -        if (ical_parse_line($0, content, params) < 0)
       -                next
       -
       -        if (content["name"] == "TZID") {
       -                ical_set_tz(content["value"])
       -        } else if (DT[content["name"]]) {
       -                vevent[content["name"]] = ical_to_epoch(content, params)
       -        } else {
       -                vevent[content["name"]] = content["value"]
       -        }
       -}
       -
       -/^END:VEVENT/ {
       -        print_vevent(vevent, FIELDS)
       -        delete vevent
       -        next
       -}
   DIR diff --git a/bin/ics2txt b/bin/ics2txt
       @@ -1,2 +0,0 @@
       -#!/bin/sh -e
       -exec ics2tsv "$@" | exec tsv2tcal
   DIR diff --git a/bin/tsv2ics b/bin/tsv2ics
       @@ -1,104 +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)
       -}
       -
       -# Split the time in seconds since epoch into a table, with fields
       -# named as with gmtime(3): tm["year"], tm["mon"], tm["mday"],
       -# tm["hour"], tm["min"], tm["sec"]
       -function gmtime(sec, tm,
       -        s)
       -{
       -        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 print_fold(prefix, s, n)
       -{
       -        while (s != "") {
       -                line = substr(s, 1, n)
       -                if (length(s) > n) sub(" +[^ \t\r\n]*$", "", line)
       -                print prefix line
       -                s = substr(s, length(line) + 2)
       -        }
       -}
       -
       -BEGIN {
       -        FS = "\t"
       -
       -        print "BEGIN:VCALENDAR"
       -        print "VERSION:2.0"
       -        print "CALSCALE:GREGORIAN"
       -        print "METHOD:PUBLISH"
       -}
       -
       -NR == 1 {
       -        for (i = 1; i <= NF; i++)
       -                name[i] = $i
       -        next
       -}
       -
       -{
       -        for (i = 1; i <= NF; i++)
       -                ev[name[i]] = $i
       -
       -        print ""
       -        print "BEGIN:VEVENT"
       -
       -        gmtime(ev["beg"] + offset, ev)
       -        printf "DTSTART:%04d%02d%02dT%02d%02d00Z\n",
       -          ev["year"], ev["mon"], ev["mday"], ev["hour"], ev["min"]
       -
       -        gmtime(ev["end"] + offset, ev)
       -        printf "DTEND:%04d%02d%02dT%02d%02d00Z\n",
       -          ev["year"], ev["mon"], ev["mday"], ev["hour"], ev["min"]
       -
       -        print "SUMMARY:" ev["sum"]
       -        print "DESCRIPTION:" ev["des"]
       -        print "CATEGORIES:" ev["cat"]
       -        print "LOCATION:" ev["loc"]
       -        print "END:VEVENT"
       -
       -        delete ev
       -}
       -
       -END {
       -        print ""
       -        print "END:VCALENDAR"
       -}
   DIR diff --git a/ics2tsv.c b/ics2tsv.c
       @@ -27,11 +27,11 @@ struct Block {
                char *fields[FIELDS_MAX];
        };
        
       -static int flag_1 = 0;
       -static char default_fields[] = "CATEGORIES,LOCATION,SUMMARY,DESCRIPTION";
       -static char *flag_s = ",";
       -static char *flag_t = NULL;
       -static char *flag_f = default_fields;
       +static int flag_header = 1;
       +static char default_fields[] = "SUMMARY,DESCRIPTION,CATEGORIES,LOCATION";
       +static char *flag_sep = ",";
       +static char *flag_timefmt = NULL;
       +static char *flag_fields = default_fields;
        static char *fields[FIELDS_MAX];
        static Block block;
        
       @@ -60,9 +60,6 @@ fn_block_begin(IcalParser *p, char *name)
        static int
        fn_block_end(IcalParser *p, char *name)
        {
       -        char buf[128];
       -        struct tm tm = {0};
       -
                (void)name;
        
                if (p->blocktype == ICAL_BLOCK_OTHER)
       @@ -70,12 +67,18 @@ fn_block_end(IcalParser *p, char *name)
                fputs(p->current->name, stdout);
        
                /* printing dates with %s is much much slower than %lld */
       -        if (flag_t == NULL) {
       +        if (flag_timefmt == NULL) {
                        printf("\t%lld\t%lld", block.beg, block.end);
                } else {
       -                strftime(buf, sizeof buf, flag_t, localtime_r(&block.beg, &tm));
       +                char buf[128];
       +                struct tm tm = {0};
       +
       +                localtime_r(&block.beg, &tm);
       +                strftime(buf, sizeof buf, flag_timefmt, &tm);
                        printf("\t%s", buf);
       -                strftime(buf, sizeof buf, flag_t, localtime_r(&block.end, &tm));
       +
       +                localtime_r(&block.end, &tm);
       +                strftime(buf, sizeof buf, flag_timefmt, &tm);
                        printf("\t%s", buf);
                }
        
       @@ -131,7 +134,7 @@ fn_field_value(IcalParser *p, char *name, char *value)
                                        if ((block.fields[i] = strdup(value)) == NULL)
                                                return ical_err(p, strerror(errno));
                                } else {
       -                                if (strappend(&block.fields[i], flag_s) == NULL ||
       +                                if (strappend(&block.fields[i], flag_sep) == NULL ||
                                            strappend(&block.fields[i], value) == NULL)
                                                return ical_err(p, strerror(errno));
                                }
       @@ -144,7 +147,7 @@ fn_field_value(IcalParser *p, char *name, char *value)
        static void
        usage(void)
        {
       -        fprintf(stderr,"usage: %s [-1] [-f fields] [-s subsep] [-t timefmt]"
       +        fprintf(stderr,"usage: %s [-1] [-f fields] [-s separator] [-t timefmt]"
                    " [file...]\n", arg0);
                exit(1);
        }
       @@ -153,7 +156,6 @@ int
        main(int argc, char **argv)
        {
                IcalParser p = {0};
       -        size_t i;
                int c;
        
                arg0 = *argv;
       @@ -167,19 +169,22 @@ main(int argc, char **argv)
                p.fn_param_value = fn_param_value;
                p.fn_field_value = fn_field_value;
        
       -        while ((c = getopt(argc, argv, "1f:s:t:")) != -1) {
       +        while ((c = getopt(argc, argv, "01f:s:t:")) != -1) {
                        switch (c) {
       +                case '0':
       +                        flag_header = 0;
       +                        break;
                        case '1':
       -                        flag_1 = 1;
       +                        flag_header = 1;
                                break;
                        case 'f':
       -                        flag_f = optarg;
       +                        flag_fields = optarg;
                                break;
                        case 's':
       -                        flag_s = optarg;
       +                        flag_sep = optarg;
                                break;
                        case 't':
       -                        flag_t = optarg;
       +                        flag_timefmt = optarg;
                                break;
                        case '?':
                                usage();
       @@ -189,16 +194,12 @@ main(int argc, char **argv)
                argv += optind;
                argc -= optind;
        
       -        i = 0;
       -        do {
       -                if (i >= sizeof fields / sizeof *fields - 1)
       -                        err(1, "too many fields specified with -o flag");
       -        } while ((fields[i++] = strsep(&flag_f, ",")) != NULL);
       -        fields[i] = NULL;
       +        if (strsplit(flag_fields, fields, LEN(fields), ",") < 0)
       +                err(1, "too many fields specified with -f flag");
        
       -        if (flag_1) {
       -                printf("%s\t%s\t%s\t%s", "TYPE", "BEG", "END", "RECUR");
       -                for (i = 0; fields[i] != NULL; i++)
       +        if (flag_header) {
       +                printf("%s\t%s\t%s\t%s", "TYPE", "START", "END", "RECUR");
       +                for (size_t i = 0; fields[i] != NULL; i++)
                                printf("\t%s", fields[i]);
                        fputc('\n', stdout);
                }
   DIR diff --git a/tsv2agenda.c b/tsv2agenda.c
       @@ -1,35 +1,38 @@
        #include <assert.h>
       +#include <ctype.h>
        #include <errno.h>
        #include <stdio.h>
        #include <stdlib.h>
        #include <stdint.h>
        #include <string.h>
        #include <unistd.h>
       +#include <time.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,
       +        FIELD_MAX = 128,
        };
        
        typedef struct {
                struct tm beg, end;
       +        char *fieldnames[FIELD_MAX];
       +        size_t fieldnum;
       +        size_t linenum;
        } AgendaCtx;
        
       -static size_t field_categories = 0;
       -static size_t field_location = 0;
       -static size_t field_summary = 0;
       +static time_t flag_from = INT64_MIN;
       +static time_t flag_to = INT64_MAX;
        
       -void
       +static void
        print_date(struct tm *tm)
        {
                 if (tm == NULL) {
       @@ -42,7 +45,7 @@ print_date(struct tm *tm)
                }
        }
        
       -void
       +static void
        print_time(struct tm *tm)
        {
                if (tm == NULL) {
       @@ -55,139 +58,175 @@ print_time(struct tm *tm)
                }
        }
        
       -void
       -print(AgendaCtx *ctx, char **fields, size_t n)
       +static void
       +print_header1(struct tm *old, struct tm *new)
       +{
       +        int same;
       +
       +        same = (old->tm_year == new->tm_year && old->tm_mon == new->tm_mon &&
       +            old->tm_mday == new->tm_mday);
       +        print_date(same ? NULL : new);
       +        print_time(new);
       +}
       +
       +static void
       +print_header2(struct tm *beg, struct tm *end)
       +{
       +        int same;
       +
       +        same = (beg->tm_year == end->tm_year && beg->tm_mon == end->tm_mon &&
       +            beg->tm_mday == end->tm_mday);
       +        print_date(same ? NULL : end);
       +
       +        same = (beg->tm_hour == end->tm_hour && beg->tm_min == end->tm_min);
       +        print_time(same ? NULL : end);
       +}
       +
       +static void
       +print_header3(void)
       +{
       +        print_date(NULL);
       +        print_time(NULL);
       +}
       +
       +static void
       +print_row(AgendaCtx *ctx, char **fields, size_t i)
       +{
       +        if (i > ctx->fieldnum || *fields[i] == '\0')
       +                return;
       +        fprintf(stdout, "%s\n", fields[i]);
       +}
       +
       +static void
       +print(AgendaCtx *ctx, char **fields)
        {
                struct tm beg = {0}, end = {0};
                time_t t;
       +        size_t i = FIELD_OTHER;
                char const *e;
       -        int rows, samedate;
        
       -        t = strtonum(fields[FIELD_BEG], 0, UINT32_MAX, &e);
       +        t = strtonum(fields[FIELD_BEG], INT64_MIN, INT64_MAX, &e);
                if (e != NULL)
                        err(1, "start time %s is %s", fields[FIELD_BEG], e);
       +        if (t > flag_to)
       +                return;
                localtime_r(&t, &beg);
        
       -        t = strtonum(fields[FIELD_END], 0, UINT32_MAX, &e);
       +        t = strtonum(fields[FIELD_END], INT64_MIN, INT64_MAX, &e);
                if (e != NULL)
                        err(1, "end time %s is %s", fields[FIELD_END], e);
       +        if (t < flag_from)
       +                return;
                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]);
       +        print_header1(&ctx->beg, &beg);
       +        print_row(ctx, fields, i++);
       +        print_header2(&beg, &end);
       +        print_row(ctx, fields, i++);
       +        while (i < ctx->fieldnum) {
       +                print_header3();
       +                print_row(ctx, fields, i++);
                }
        
                ctx->beg = beg;
                ctx->end = end;
        }
        
       -void
       -set_fields_num(char **fields, size_t n)
       +static void
       +tsv_to_agenda(AgendaCtx *ctx, FILE *fp)
        {
       -        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");
       -}
       +        char *ln1 = NULL, *ln2 = NULL;
       +        size_t sz1 = 0, sz2 = 0;
       +
       +        if (ctx->linenum == 0) {
       +                char *fields[FIELD_MAX];
       +
       +                ctx->linenum++;
       +                if (getline(&ln1, &sz1, fp) < 0)
       +                        err(1, "reading stdin: %s", strerror(errno));
       +                if (feof(fp))
       +                        err(1, "empty input");
       +
       +                strchomp(ln1);
       +                ctx->fieldnum = strsplit(ln1, fields, FIELD_MAX, "\t");
       +                if (ctx->fieldnum == FIELD_MAX)
       +                        err(1, "line 1: too many fields");
       +                if (ctx->fieldnum < FIELD_OTHER)
       +                        err(1, "line 1: not enough input columns");
       +                if (strcasecmp(fields[0], "TYPE") != 0)
       +                        err(1, "line 1: 1st column is not \"TYPE\"");
       +                if (strcasecmp(fields[1], "START") != 0)
       +                        err(1, "line 1: 2nd column is not \"START\"");
       +                if (strcasecmp(fields[2], "END") != 0)
       +                        err(1, "line 1: 3rd column is not \"END\"");
       +                if (strcasecmp(fields[3], "RECUR") != 0)
       +                        err(1, "line 1: 4th column is not \"RECUR\"");
       +        }
        
       -ssize_t
       -tsv_getline(char **fields, size_t max, char **line, size_t *sz, FILE *fp)
       -{
       -        char *s;
       -        size_t n = 0;
       +        for (;;) {
       +                char *fields[FIELD_MAX];
       +
       +                ctx->linenum++;
       +                if (getline(&ln2, &sz2, fp) < 0)
       +                        err(1, "reading stdin: %s", strerror(errno));
       +                if (feof(fp))
       +                        break;
       +
       +                strchomp(ln2);
       +                if (strsplit(ln2, fields, FIELD_MAX, "\t") != ctx->fieldnum)
       +                        err(1, "line %zd: bad number of columns",
       +                            ctx->linenum, strerror(errno));
        
       -        if (getline(line, sz, fp) <= 0)
       -                return ferror(fp) ? -1 : 0;
       -        s = *line;
       -        strchomp(s);
       +                fputc('\n', stdout);
       +                print(ctx, fields);
       +        }
       +        fputc('\n', stdout);
        
       -        do {
       -                if (n >= max)
       -                        return errno=E2BIG, -1;
       -        } while ((fields[n++] = strsep(&s, "\t")) != NULL);
       +        free(ln1);
       +        free(ln2);
       +}
        
       -        return n - 1;
       +static void
       +usage(void)
       +{
       +        fprintf(stderr, "usage: %s [-f fromdate] [-t todate]\n", arg0);
       +        exit(1);
        }
        
        int
        main(int argc, char **argv)
        {
                AgendaCtx ctx = {0};
       -        ssize_t nfield, n;
       -        size_t sz = 0;
       -        char *line = NULL, *fields[FIELDS_MAX];
       +        char c;
        
       -        arg0 = *argv;
       -
       -        if (pledge("stdio", "") < 0)
       -                err(1, "pledge: %s", strerror(errno));
       +        if ((flag_from = time(NULL)) == (time_t)-1)
       +                err(1, "time: %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)
       +        arg0 = *argv;
       +        while ((c = getopt(argc, argv, "f:t:")) > 0) {
       +                char const *e;
       +
       +                switch (c) {
       +                case 'f':
       +                        flag_from = strtonum(optarg, INT64_MIN, INT64_MAX, &e);
       +                        if (e != NULL)
       +                                err(1, "fromdate value %s is %s", optarg, e);
                                break;
       -                if (n != nfield)
       -                        err(1, "line %zd: had %lld columns, wanted %lld",
       -                            num, n, nfield);
       -
       -                print(&ctx, fields, n);
       +                case 't':
       +                        flag_to = strtonum(optarg, INT64_MIN, INT64_MAX, &e);
       +                        if (e != NULL)
       +                                err(1, "todate value %s is %s", optarg, e);
       +                        break;
       +                default:
       +                        usage();
       +                }
                }
       -        fputc('\n', stdout);
       +        argc -= optind;
       +        argv += optind;
        
       -        free(line);
       +        if (pledge("stdio", "") < 0)
       +                err(1, "pledge: %s", strerror(errno));
        
       +        tsv_to_agenda(&ctx, stdin);
                return 0;
        }
   DIR diff --git a/tsv2ics.awk b/tsv2ics.awk
       @@ -0,0 +1,106 @@
       +#!/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)
       +}
       +
       +# Split the time in seconds since epoch into a table, with fields
       +# named as with gmtime(3): tm["year"], tm["mon"], tm["mday"],
       +# tm["hour"], tm["min"], tm["sec"]
       +function gmtime(sec, tm,
       +        s)
       +{
       +        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
       +}
       +
       +BEGIN {
       +        FS = "\t"
       +
       +        DTSTART["VEVENT"] = "DTSTART"
       +        DTEND["VEVENT"] = "DTEND"
       +
       +        DTEND["VTODO"] = "DUE"
       +
       +        DTSTART["VJOURNAL"] = "DTSTAMP"
       +
       +        DTSTART["VFREEBUSY"] = "DTSTART"
       +        DTEND["VFREEBUSY"] = "DTEND"
       +
       +        DTSTART["VALARM"] = "DTSTART"
       +
       +        print "BEGIN:VCALENDAR"
       +        print "VERSION:2.0"
       +        print "CALSCALE:GREGORIAN"
       +        print "METHOD:PUBLISH"
       +}
       +
       +NR == 1 {
       +        if ($1 != "TYPE" || $2 != "START" || $3 != "END" || $4 != "RECUR") {
       +                print "tsv2ics: invalid column names on first line" >/dev/stderr
       +                exit(EXIT = 1)
       +        }
       +        for (i = 1; i <= NF; i++) {
       +                FIELD[$i] = i
       +                NAME[i] = $i
       +        }
       +        next
       +}
       +
       +{
       +        type = $FIELD["TYPE"]
       +        print "BEGIN:"type
       +
       +        if (type in DTSTART) {
       +                gmtime($FIELD["START"] + offset, tm)
       +                printf "%s:%04d%02d%02dT%02d%02d00Z\n", DTSTART[type],
       +                  tm["year"], tm["mon"], tm["mday"], tm["hour"], tm["min"]
       +        }
       +
       +        if (type in DTEND) {
       +                gmtime($FIELD["END"] + offset, tm)
       +                printf "%s:%04d%02d%02dT%02d%02d00Z\n", DTEND[type],
       +                  tm["year"], tm["mon"], tm["mday"], tm["hour"], tm["min"]
       +        }
       +
       +        for (i = 5; i in NAME; i++)
       +                print$NAME[i]":"$i
       +
       +        print "END:"type
       +}
       +
       +END {
       +        if (EXIT) exit(EXIT)
       +        print ""
       +        print "END:VCALENDAR"
       +}
   DIR diff --git a/util.c b/util.c
       @@ -1,4 +1,5 @@
        #include "util.h"
       +#include <assert.h>
        #include <errno.h>
        #include <stdint.h>
        #include <stdlib.h>
       @@ -123,6 +124,19 @@ strappend(char **dp, char const *s)
                return *dp;
        }
        
       +size_t
       +strsplit(char *s, char **array, size_t len, char const *sep)
       +{
       +        size_t i;
       +
       +        assert(len > 0);
       +        for (i = 0; i < len; i++)
       +                if ((array[i] = strsep(&s, sep)) == NULL)
       +                        break;
       +        array[len - 1] = NULL;
       +        return i;
       +}
       +
        /** memory **/
        
        void *
   DIR diff --git a/util.h b/util.h
       @@ -2,9 +2,12 @@
        #define UTIL_H
        
        #include <stddef.h>
       +#include <stdio.h>
        #include <stdarg.h>
        #include <time.h>
        
       +#define LEN(x) (sizeof (x) / sizeof *(x))
       +
        /** logging **/
        extern char *arg0;
        void         err(int, char const *fmt, ...);
       @@ -18,6 +21,7 @@ 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 **);
       +size_t         strsplit(char *, char **, size_t, char const *);
        
        /** memory **/
        void        *reallocarray(void *, size_t, size_t);