replace the not-so-useful tcal format by a plain text output - ics2txt - convert icalendar .ics file to plain text
(HTM) 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
(HTM) 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);