Home
       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);