initial C implementation - ics2txt - convert icalendar .ics file to plain text | |
git clone git://bitreich.org/ics2txt git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws… | |
Log | |
Files | |
Refs | |
Tags | |
README | |
--- | |
commit 94bccd0b9ea7049ebeec4fcf2416f6f0b7d221b5 | |
parent 78e0184b4deb29669bfde9a66fc945968845ced8 | |
Author: Josuah Demangeon <[email protected]> | |
Date: Sat, 27 Jun 2020 20:31:09 +0200 | |
initial C implementation | |
Diffstat: | |
A .gitignore | 2 ++ | |
M Makefile | 24 +++++++++++++++++++----- | |
A bin/ics2tsv | 141 +++++++++++++++++++++++++++++… | |
R ics2txt -> bin/ics2txt | 0 | |
R tcal2tsv -> bin/tcal2tsv | 0 | |
R tsv2ics -> bin/tsv2ics | 0 | |
A bin/tsv2tcal | 91 +++++++++++++++++++++++++++++… | |
A doc/index.md | 11 +++++++++++ | |
D ics2tsv | 138 ------------------------------ | |
A ics2tsv.c | 62 +++++++++++++++++++++++++++++… | |
A src/ical.c | 108 +++++++++++++++++++++++++++++… | |
A src/ical.h | 25 +++++++++++++++++++++++++ | |
A src/log.c | 89 +++++++++++++++++++++++++++++… | |
A src/log.h | 15 +++++++++++++++ | |
A src/map.c | 102 +++++++++++++++++++++++++++++… | |
A src/map.h | 23 +++++++++++++++++++++++ | |
A src/util.c | 74 +++++++++++++++++++++++++++++… | |
A src/util.h | 13 +++++++++++++ | |
D tsv2tcal | 91 -----------------------------… | |
19 files changed, 775 insertions(+), 234 deletions(-) | |
--- | |
diff --git a/.gitignore b/.gitignore | |
@@ -0,0 +1,2 @@ | |
+*.o | |
+ics2tsv | |
diff --git a/Makefile b/Makefile | |
@@ -1,23 +1,37 @@ | |
NAME = ics2txt | |
VERSION = 0.1 | |
-BIN = ics2tsv tsv2tcal tcal2tsv tsv2ics ics2txt | |
- | |
+W = -Wall -Wextra -std=c99 --pedantic | |
+I = -Isrc | |
+D = -D_POSIX_C_SOURCE=200811L -DVERSION='"${VERSION}"' | |
+CFLAGS = $I $D $W -g | |
PREFIX = /usr/local | |
MANPREFIX = ${PREFIX}/man | |
+SRC = src/ical.c src/map.c src/util.c src/log.c | |
+HDR = src/ical.h src/map.h src/util.h src/log.h | |
+OBJ = ${SRC:.c=.o} | |
+BIN = ics2tsv | |
+ | |
all: ${BIN} | |
+.c.o: | |
+ ${CC} -c ${CFLAGS} -o $@ $< | |
+ | |
+${OBJ}: ${HDR} | |
+${BIN}: ${OBJ} ${BIN:=.o} | |
+ ${CC} ${LDFLAGS} -o $@ [email protected] ${OBJ} | |
+ | |
clean: | |
- rm -rf ${NAME}-${VERSION} *.gz | |
+ rm -rf *.o */*.o ${BIN} ${NAME}-${VERSION} *.gz | |
install: | |
mkdir -p ${DESTDIR}$(PREFIX)/bin | |
- cp $(BIN) ${DESTDIR}$(PREFIX)/bin | |
+ cp bin/* $(BIN) ${DESTDIR}$(PREFIX)/bin | |
mkdir -p ${DESTDIR}$(MANPREFIX)/man1 | |
cp doc/*.1 ${DESTDIR}$(MANPREFIX)/man1 | |
dist: clean | |
mkdir -p ${NAME}-${VERSION} | |
- cp -r README Makefile doc ${BIN} ${NAME}-${VERSION} | |
+ cp -r README Makefile doc bin ${SRC} ${NAME}-${VERSION} | |
tar -cf - ${NAME}-${VERSION} | gzip -c >${NAME}-${VERSION}.tar.gz | |
diff --git a/bin/ics2tsv b/bin/ics2tsv | |
@@ -0,0 +1,141 @@ | |
+#!/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_tzid(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 | |
+} | |
diff --git a/ics2txt b/bin/ics2txt | |
diff --git a/tcal2tsv b/bin/tcal2tsv | |
diff --git a/tsv2ics b/bin/tsv2ics | |
diff --git a/bin/tsv2tcal b/bin/tsv2tcal | |
@@ -0,0 +1,91 @@ | |
+#!/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("") | |
+} | |
diff --git a/doc/index.md b/doc/index.md | |
@@ -0,0 +1,11 @@ | |
+ics2txt | |
+======= | |
+Set of tools to work with the popular iCalendar format and converting to even | |
+simpler TSV and text forms. | |
+ | |
+Parsing have been tested with the following input formats (sample account | |
+created for testing): | |
+ | |
+* Zoom meetings generated events | |
+* FOSDEM events, like <https://fosdem.org/2020/schedule/ical> | |
+* Google Calendar | |
diff --git a/ics2tsv b/ics2tsv | |
@@ -1,138 +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_to_epoch(content, param, | |
- tz, cmd) | |
-{ | |
- tz = (param["TZID"] ? param["TZID"] : vcalendar["TZID"]) | |
- gsub("'", "", tz) | |
- | |
- cmd = "TZ='"tz"' date +%z" | |
- cmd | getline tz | |
- close(cmd) | |
- | |
- tz = substr(tz, 1, 1) substr(tz, 2, 2)*3600 + substr(tz, 4, 2)*60 | |
- | |
- tm["year"] = substr(content["value"], 1, 4) | |
- tm["mon"] = substr(content["value"], 5, 2) | |
- tm["mday"] = substr(content["value"], 8, 2) | |
- tm["hour"] = substr(content["value"], 12, 2) | |
- tm["min"] = substr(content["value"], 15, 2) | |
- tm["sec"] = substr(content["value"], 18, 2) | |
- | |
- return timegm(tm) + tz | |
-} | |
- | |
-BEGIN { | |
- split("DTSTART DTEND CATEGORIES LOCATION SUMMARY DESCRIPTION", | |
- 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") { | |
- vcalendar[content["name"]] = 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 | |
-} | |
diff --git a/ics2tsv.c b/ics2tsv.c | |
@@ -0,0 +1,62 @@ | |
+#include <stdio.h> | |
+ | |
+#include "ical.h" | |
+#include "log.h" | |
+#include "util.h" | |
+ | |
+int | |
+print_ical_to_tsv(FILE *fp) | |
+{ | |
+ struct ical_contentline contentline; | |
+ char *line = NULL; | |
+ size_t sz = 0; | |
+ ssize_t r; | |
+ | |
+ ical_init_contentline(&contentline); | |
+ | |
+ while ((r = ical_read_line(&line, &sz, fp)) > 0) { | |
+ debug("readling line \"%s\"", line); | |
+ if (ical_parse_contentline(&contentline, line) < 0) | |
+ die("parsing line \"%s\"", line); | |
+ } | |
+ return r; | |
+} | |
+ | |
+void | |
+print_header(void) | |
+{ | |
+ char *fields[] = { "", NULL }; | |
+ | |
+ printf("%s\t%s", "beg", "end"); | |
+ | |
+ for (char **f = fields; *f != NULL; f++) { | |
+ fprintf(stdout, "\t%s", *f); | |
+ } | |
+ fprintf(stdout, "\n"); | |
+} | |
+ | |
+int | |
+main(int argc, char **argv) | |
+{ | |
+ print_header(); | |
+ | |
+ log_arg0 = *argv++; | |
+ | |
+ if (*argv == NULL) { | |
+ if (print_ical_to_tsv(stdin) < 0) | |
+ die("converting stdin"); | |
+ } | |
+ | |
+ for (; *argv != NULL; argv++, argc--) { | |
+ FILE *fp; | |
+ | |
+ info("converting \"%s\"", *argv); | |
+ if ((fp = fopen(*argv, "r")) == NULL) | |
+ die("opening %s", *argv); | |
+ if (print_ical_to_tsv(fp) < 0) | |
+ die("converting %s", *argv); | |
+ fclose(fp); | |
+ } | |
+ | |
+ return 0; | |
+} | |
diff --git a/src/ical.c b/src/ical.c | |
@@ -0,0 +1,108 @@ | |
+#include "ical.h" | |
+ | |
+#include <errno.h> | |
+#include <stdio.h> | |
+#include <stdlib.h> | |
+#include <string.h> | |
+ | |
+#include "util.h" | |
+ | |
+int | |
+ical_read_line(char **line, size_t *sz, FILE *fp) | |
+{ | |
+ ssize_t r; | |
+ char *tail = NULL; | |
+ size_t tail_sz = 0; | |
+ int c, ret = -1; | |
+ | |
+ if ((r = getline(line, sz, fp)) <= 0) | |
+ return r; | |
+ strchomp(*line); | |
+ | |
+ for (;;) { | |
+ if ((c = fgetc(fp)) == EOF) { | |
+ ret = ferror(fp) ? -1 : 0; | |
+ goto end; | |
+ } | |
+ if (c != ' ') | |
+ break; | |
+ if ((r = getline(&tail, &tail_sz, fp)) <= 0) { | |
+ ret = r; | |
+ goto end; | |
+ } | |
+ strchomp(tail); | |
+ if (strappend(line, tail) < 0) | |
+ goto end; | |
+ } | |
+ | |
+ ret = 1; | |
+end: | |
+ free(tail); | |
+ ungetc(c, fp); | |
+ return ret; | |
+} | |
+ | |
+int | |
+ical_parse_contentline(struct ical_contentline *contentline, char *line) | |
+{ | |
+ char *column, *equal, *param, *cp; | |
+ size_t sz; | |
+ | |
+ debug("0"); | |
+ | |
+ if ((column = strchr(line, ':')) == NULL) | |
+ return -1; | |
+ *column = '\0'; | |
+ | |
+ { | |
+ size_t len; | |
+ | |
+ debug("1.1"); | |
+ len = strlen(column + 1); | |
+ debug("1.2"); | |
+ } | |
+ | |
+ | |
+ if ((contentline->value = strdup(column + 1)) == NULL) | |
+ return -1; | |
+ | |
+ debug("2"); | |
+ | |
+ cp = strchr(line, ';'); | |
+ cp = (cp == NULL) ? (NULL) : (cp + 1); | |
+ | |
+ debug("3"); | |
+ | |
+ while ((param = strsep(&cp, ";")) != NULL) { | |
+ if ((equal = strchr(param, '=')) == NULL) | |
+ return -1; | |
+ *equal = '\0'; | |
+ | |
+ if (map_set(&contentline->param, param, equal + 1) < 0) | |
+ return -1; | |
+ } | |
+ | |
+ debug("4"); | |
+ | |
+ sz = sizeof(contentline->name); | |
+ if (strlcpy(contentline->name, line, sz) >= sz) | |
+ return errno=EMSGSIZE, -1; | |
+ | |
+ debug("5"); | |
+ | |
+ return 0; | |
+} | |
+ | |
+void | |
+ical_init_contentline(struct ical_contentline *contentline) | |
+{ | |
+ memset(contentline, 0, sizeof(*contentline)); | |
+} | |
+ | |
+ | |
+void | |
+ical_free_contentline(struct ical_contentline *contentline) | |
+{ | |
+ map_free(&contentline->param); | |
+ free(contentline->value); | |
+} | |
diff --git a/src/ical.h b/src/ical.h | |
@@ -0,0 +1,25 @@ | |
+#ifndef ICAL_H | |
+#define ICAL_H | |
+ | |
+#include <stdio.h> | |
+#include <time.h> | |
+ | |
+#include "map.h" | |
+ | |
+struct ical_vevent { | |
+ time_t beg, end; | |
+ struct map map; | |
+}; | |
+ | |
+struct ical_contentline { | |
+ char name[32], *value; | |
+ struct map param; | |
+}; | |
+ | |
+/** src/ical.c **/ | |
+int ical_read_line(char **line, size_t *sz, FILE *fp); | |
+int ical_parse_contentline(struct ical_contentline *contentline, char *line); | |
+void ical_init_contentline(struct ical_contentline *contentline); | |
+void ical_free_contentline(struct ical_contentline *contentline); | |
+ | |
+#endif | |
diff --git a/src/log.c b/src/log.c | |
@@ -0,0 +1,89 @@ | |
+#include "log.h" | |
+ | |
+#include <assert.h> | |
+#include <string.h> | |
+ | |
+/* | |
+ * log.c - log to standard error according to the log level | |
+ * | |
+ * Instead of logging to syslog, delegate logging to a separate | |
+ * tool, such as FreeBSD's daemon(8), POSIX's logger(1). | |
+ */ | |
+ | |
+#include <errno.h> | |
+#include <stdio.h> | |
+#include <stdlib.h> | |
+ | |
+#define LOG_DEFAULT 3 /* info */ | |
+ | |
+int log_level = -1; | |
+char *log_arg0 = NULL; | |
+ | |
+void | |
+vlogf(int level, char const *flag, char const *fmt, va_list va) | |
+{ | |
+ char *env; | |
+ int e = errno; | |
+ | |
+ if (log_level < 0) { | |
+ env = getenv("LOG"); | |
+ log_level = (env == NULL ? 0 : atoi(env)); | |
+ log_level = (log_level > 0 ? log_level : LOG_DEFAULT); | |
+ } | |
+ | |
+ if (log_level < level) | |
+ return; | |
+ | |
+ if (log_arg0 != NULL) | |
+ fprintf(stderr, "%s: ", log_arg0); | |
+ | |
+ fprintf(stderr, "%s: ", flag); | |
+ vfprintf(stderr, fmt, va); | |
+ | |
+ if (e != 0) | |
+ fprintf(stderr, ": %s", strerror(e)); | |
+ | |
+ fprintf(stderr, "\n"); | |
+ fflush(stderr); | |
+} | |
+ | |
+void | |
+die(char const *fmt, ...) | |
+{ | |
+ va_list va; | |
+ | |
+ va_start(va, fmt); | |
+ vlogf(1, "error", fmt, va); | |
+ va_end(va); | |
+ exit(1); | |
+} | |
+ | |
+void | |
+warn(char const *fmt, ...) | |
+{ | |
+ va_list va; | |
+ | |
+ va_start(va, fmt); | |
+ vlogf(2, "warn", fmt, va); | |
+ va_end(va); | |
+} | |
+ | |
+void | |
+info(char const *fmt, ...) | |
+{ | |
+ va_list va; | |
+ | |
+ va_start(va, fmt); | |
+ vlogf(3, "info", fmt, va); | |
+ va_end(va); | |
+} | |
+ | |
+void | |
+debug(char const *fmt, ...) | |
+{ | |
+ va_list va; | |
+ | |
+ va_start(va, fmt); | |
+ vlogf(4, "debug", fmt, va); | |
+ va_end(va); | |
+} | |
diff --git a/src/log.h b/src/log.h | |
@@ -0,0 +1,15 @@ | |
+#ifndef LOG_H | |
+#define LOG_H | |
+ | |
+#include <stdarg.h> | |
+ | |
+/** src/log.c **/ | |
+int log_level; | |
+char *log_arg0; | |
+void vlogf(int level, char const *flag, char const *fmt, va_list va); | |
+void die(char const *fmt, ...); | |
+void warn(char const *fmt, ...); | |
+void info(char const *fmt, ...); | |
+void debug(char const *fmt, ...); | |
+ | |
+#endif | |
diff --git a/src/map.c b/src/map.c | |
@@ -0,0 +1,102 @@ | |
+#include "map.h" | |
+ | |
+#include <stdlib.h> | |
+#include <string.h> | |
+ | |
+#include "util.h" | |
+ | |
+static int | |
+map_cmp(void const *v1, void const *v2) | |
+{ | |
+ struct map_entry const *e1 = v1, *e2 = v2; | |
+ | |
+ return strcmp(e1->key, e2->key); | |
+} | |
+ | |
+void * | |
+map_get(struct map *map, char *key) | |
+{ | |
+ struct map_entry *entry, k = { .key = key }; | |
+ size_t sz; | |
+ | |
+ sz = sizeof(*map->entry); | |
+ if ((entry = bsearch(&k, map->entry, map->len, sz, map_cmp)) == NULL) | |
+ return NULL; | |
+ return entry->value; | |
+} | |
+ | |
+int | |
+map_set(struct map *map, char *key, void *value) | |
+{ | |
+ struct map_entry *insert, *e; | |
+ size_t i, sz; | |
+ void *v; | |
+ | |
+ debug("%s: key=%s len=%zd", __func__, key, map->len); | |
+ | |
+ for (i = 0; i < map->len; i++) { | |
+ int cmp = strcmp(key, map->entry[i].key); | |
+ debug("cmp(%s,%s)=%d", key, map->entry[i].key, cmp); | |
+ | |
+ if (cmp == 0) { | |
+ map->entry[i].value = value; | |
+ return 0; | |
+ } | |
+ if (cmp < 0) | |
+ break; | |
+ } | |
+ | |
+ sz = sizeof(*map->entry); | |
+ if ((v = reallocarray(map->entry, map->len + 1, sz)) == NULL) | |
+ return -1; | |
+ map->entry = v; | |
+ map->len++; | |
+ | |
+ insert = map->entry + i; | |
+ e = map->entry + map->len - 1 - 1; | |
+ for (; e >= insert; e--) | |
+ e[1].key = e[0].key; | |
+ | |
+ if ((insert->key = strdup(key)) == NULL) | |
+ return -1; | |
+ insert->value = value; | |
+ | |
+ return 0; | |
+} | |
+ | |
+int | |
+map_del(struct map *map, char *key) | |
+{ | |
+ size_t i; | |
+ | |
+ for (i = 0; i < map->len; i++) { | |
+ int cmp = strcmp(key, map->entry[i].key); | |
+ | |
+ if (cmp == 0) | |
+ break; | |
+ if (cmp < 0) | |
+ return -1; | |
+ } | |
+ if (i == map->len) | |
+ return -1; | |
+ | |
+ map->len--; | |
+ for (; i < map->len; i++) | |
+ map->entry[i] = map->entry[i + 1]; | |
+ return 0; | |
+} | |
+ | |
+void | |
+map_free_values(struct map *map) | |
+{ | |
+ for (size_t i = 0; i < map->len; i++) | |
+ free(map->entry[map->len - 1].value); | |
+} | |
+ | |
+void | |
+map_free(struct map *map) | |
+{ | |
+ for (size_t i = 0; i < map->len; i++) | |
+ free(map->entry[map->len - 1].key); | |
+ free(map->entry); | |
+} | |
diff --git a/src/map.h b/src/map.h | |
@@ -0,0 +1,23 @@ | |
+#ifndef MAP_H | |
+#define MAP_H | |
+ | |
+#include <stddef.h> | |
+ | |
+struct map_entry { | |
+ char *key; | |
+ void *value; | |
+}; | |
+ | |
+struct map { | |
+ struct map_entry *entry; | |
+ size_t len; | |
+}; | |
+ | |
+/** src/map.c **/ | |
+void * map_get(struct map *map, char *key); | |
+int map_set(struct map *map, char *key, void *value); | |
+int map_del(struct map *map, char *key); | |
+void map_free_values(struct map *map); | |
+void map_free(struct map *map); | |
+ | |
+#endif | |
diff --git a/src/util.c b/src/util.c | |
@@ -0,0 +1,74 @@ | |
+#include "util.h" | |
+ | |
+#include <errno.h> | |
+#include <stdint.h> | |
+#include <stdlib.h> | |
+#include <string.h> | |
+ | |
+size_t | |
+strlcpy(char *buf, char const *str, size_t sz) | |
+{ | |
+ size_t len, cpy; | |
+ | |
+ cpy = ((len = strlen(str)) > sz) ? (sz) : (len); | |
+ memcpy(buf, str, cpy + 1); | |
+ buf[sz - 1] = '\0'; | |
+ return len; | |
+} | |
+ | |
+char * | |
+strsep(char **str_p, char const *sep) | |
+{ | |
+ char *s, *prev; | |
+ | |
+ if (*str_p == NULL) | |
+ return NULL; | |
+ | |
+ for (s = prev = *str_p; strchr(sep, *s) == NULL; s++) | |
+ continue; | |
+ | |
+ if (*s == '\0') { | |
+ *str_p = NULL; | |
+ } else { | |
+ *s = '\0'; | |
+ *str_p = s + 1; | |
+ } | |
+ return prev; | |
+} | |
+ | |
+void | |
+strchomp(char *line) | |
+{ | |
+ size_t len; | |
+ | |
+ len = strlen(line); | |
+ if (len > 0 && line[len - 1] == '\n') | |
+ line[len-- - 1] = '\0'; | |
+ if (len > 0 && line[len - 1] == '\r') | |
+ line[len-- - 1] = '\0'; | |
+} | |
+ | |
+int | |
+strappend(char **base_p, char const *s) | |
+{ | |
+ size_t base_len, s_len; | |
+ void *v; | |
+ | |
+ base_len = strlen(*base_p); | |
+ s_len = strlen(s); | |
+ | |
+ if ((v = realloc(*base_p, base_len + s_len + 1)) == NULL) | |
+ return -1; | |
+ | |
+ *base_p = v; | |
+ memcpy(*base_p + base_len, s, s_len + 1); | |
+ return 0; | |
+} | |
+ | |
+void * | |
+reallocarray(void *buf, size_t len, size_t sz) | |
+{ | |
+ if (SIZE_MAX / len < sz) | |
+ return errno=ERANGE, NULL; | |
+ return realloc(buf, len * sz); | |
+} | |
diff --git a/src/util.h b/src/util.h | |
@@ -0,0 +1,13 @@ | |
+#ifndef UTIL_H | |
+#define UTIL_H | |
+ | |
+#include <stddef.h> | |
+ | |
+/** src/util.c **/ | |
+size_t strlcpy(char *buf, char const *str, size_t sz); | |
+char * strsep(char **str_p, char const *sep); | |
+void strchomp(char *line); | |
+int strappend(char **base_p, char const *s); | |
+void * reallocarray(void *buf, size_t len, size_t sz); | |
+ | |
+#endif | |
diff --git a/tsv2tcal b/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 { | |
- "date +%z" | getline tz | |
- close("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("") | |
-} |