tabular

Flexible input tabulator
git clone https://git.sinitax.com/sinitax/tabular
Log | Files | Refs | Submodules | sfeed.txt

tabular.c (12708B)


      1#include "tabular.h"
      2#include "hmap.h"
      3#include "dvec.h"
      4#include "allocator.h"
      5
      6#include <sys/ioctl.h>
      7#include <unistd.h>
      8#include <err.h>
      9#include <string.h>
     10#include <stdbool.h>
     11#include <stdarg.h>
     12#include <stdio.h>
     13#include <stdint.h>
     14
     15#define ARRLEN(x) (sizeof(x)/sizeof(*(x)))
     16
     17struct col {
     18	struct tabular_col tabular;
     19	const char *hide_out;
     20};
     21
     22static bool print_style(FILE *file, const struct tabular_cfg *cfg,
     23	const struct tabular_row *row, const struct tabular_col *col);
     24
     25static struct tabular_row *row_gen(const struct tabular_user *user);
     26static char *col_str(const struct tabular_user *user_row,
     27	const struct tabular_user *user_col);
     28static bool col_hidden(const struct tabular_user *user_row,
     29	const struct tabular_user *user_col);
     30
     31static const struct allocator *ga = &stdlib_strict_heap_allocator;
     32
     33static bool hide_empty = true;
     34static bool skip_empty_lines = true;
     35static bool skip_empty_entries = false;
     36static bool header_underline = false;
     37
     38static char entry_sep = '\t';
     39static char line_sep = '\n';
     40
     41static struct hmap colmap;
     42static struct dvec cols;
     43static size_t colcnt = 0;
     44
     45static struct dvec input;
     46static size_t input_off = 0;
     47static bool input_done = false;
     48
     49static struct dvec line;
     50static struct dvec entries;
     51static size_t linecnt = 0;
     52
     53static struct tabular_cfg cfg = {
     54	.colors = 256,
     55
     56	.columns = NULL,
     57	.column_count = 0,
     58
     59	.fit_rows = false,
     60
     61	.hsep = "│",
     62	.vsep = "─",
     63	.xsep = "┼",
     64
     65	.outw = 0,
     66	.outh = 0,
     67
     68	.lpad = 0,
     69	.rpad = 0,
     70
     71	.user.ptr = NULL,
     72	.row_gen = row_gen,
     73	.print_style = print_style,
     74
     75	.skip_lines = 4,
     76
     77	.allocator = &stdlib_heap_allocator
     78};
     79
     80static void __attribute__((noreturn))
     81die(const char *fmt, ...)
     82{
     83	va_list ap;
     84
     85	fputs("tabular: ", stderr);
     86	va_start(ap, fmt);
     87	vfprintf(stderr, fmt, ap);
     88	va_end(ap);
     89	if (*fmt && fmt[strlen(fmt)-1] == ':') {
     90		fputc(' ', stderr);
     91		perror(NULL);
     92	} else {
     93		fputc('\n', stderr);
     94	}
     95
     96	exit(1);
     97}
     98
     99static bool
    100print_style(FILE *file, const struct tabular_cfg *cfg,
    101	const struct tabular_row *row, const struct tabular_col *col)
    102{
    103	if (cfg->colors == 256) {
    104		if (!col) { /* separators */
    105			fprintf(file, "\x1b[90m");
    106			return true;
    107		} else if (!row) { /* header */
    108			fprintf(file, "\x1b[1m");
    109			if (header_underline)
    110				fprintf(file, "\x1b[4m");
    111			return true;
    112		}
    113	}
    114
    115	return false;
    116}
    117
    118static bool
    119read_line(void)
    120{
    121	uint8_t *tok, *sep, *end, *line_end;
    122	ssize_t n;
    123
    124	if (input_done) return false;
    125
    126	while (1) {
    127		while (!(line_end = memchr(input.data + input_off,
    128				line_sep, input.len - input_off))) {
    129			dvec_rm(&input, 0, input_off);
    130			input_off = 0;
    131			dvec_reserve(&input, input.len + BUFSIZ + 1);
    132			n = read(0, input.data + input.len, BUFSIZ);
    133			if (n <= 0) {
    134				input_done = true;
    135				line_end = input.data + input.len;
    136				break;
    137			}
    138			input.len += (size_t) n;
    139		}
    140		if (input_done && !input.len) return false;
    141
    142		if (line_end != input.data || !skip_empty_lines)
    143			break;
    144		input_off += 1;
    145	}
    146	*(char *)line_end = '\0';
    147
    148	dvec_clear(&entries);
    149	tok = input.data + input_off;
    150	while (tok) {
    151		sep = memchr(tok, entry_sep,
    152			(size_t) (input.data + input.len - tok));
    153		end = (sep && sep < line_end) ? sep : line_end;
    154		*(char *)end = '\0';
    155		if (tok != end || !skip_empty_entries) {
    156			dvec_add_back(&entries, 1);
    157			*(size_t *)dvec_back(&entries) =
    158				(size_t) (tok - input.data) - input_off;
    159		}
    160		tok = (sep && sep < line_end) ? sep + 1 : NULL;
    161	}
    162
    163	dvec_clear(&line);
    164	dvec_add_back(&line, (size_t) (line_end - input.data) - input_off + 1);
    165	memcpy(line.data, input.data + input_off, line.len);
    166	input_off += line.len;
    167
    168	return true;
    169}
    170
    171static struct tabular_row *
    172row_gen(const struct tabular_user *user_cfg)
    173{
    174	struct tabular_row *row;
    175	size_t i;
    176	int rc;
    177
    178	if (!read_line()) return NULL;
    179
    180	row = tabular_alloc_row(&cfg, &rc,
    181		(struct tabular_user) { .id = linecnt++ });
    182	if (!row) errx(1, "tabular_append_row %i", rc);
    183
    184	for (i = 0; i < cols.len; i++) {
    185		tabular_load_row_entry_hidden(&cfg, row, i);
    186		tabular_load_row_entry_str(&cfg, row, i);
    187	}
    188
    189	return row;
    190}
    191
    192static char *
    193col_str(const struct tabular_user *user_row, const struct tabular_user *user_col)
    194{
    195	struct col *col = user_col->ptr;
    196	char *str;
    197	size_t *off;
    198
    199	if (col->tabular.user.id >= dvec_len(&entries))
    200		return NULL;
    201
    202	off = dvec_at(&entries, col->tabular.user.id);
    203	str = strdup(line.data + *off);
    204	if (!str) die("strdup:");
    205
    206	return str;
    207}
    208
    209static bool
    210col_hidden(const struct tabular_user *user, const struct tabular_user *user_col)
    211{
    212	struct col *col = user_col->ptr;
    213	size_t *off;
    214
    215	if (col->tabular.user.id >= dvec_len(&entries))
    216		return hide_empty;
    217
    218	off = dvec_at(&entries, col->tabular.user.id);
    219
    220	if (col->hide_out && !strcmp(line.data + *off, col->hide_out))
    221		return true;
    222
    223	return hide_empty && !strcmp(line.data + *off, "");
    224}
    225
    226static bool
    227bool_arg(const char *arg, const char *name)
    228{
    229	if (!strcmp(arg, "1") || !strcmp(arg, "true"))
    230		return true;
    231	else if (!strcmp(arg, "0") || !strcmp(arg, "false"))
    232		return false;
    233	else
    234		die("bad %s", name);
    235}
    236
    237static void
    238parse(int argc, const char **argv)
    239{
    240	struct col *col;
    241	struct tabular_col *tcol;
    242	const char **arg, **dst;
    243	struct hmap_link *link;
    244	struct hmap_iter iter;
    245	struct winsize ws;
    246	char namebuf[64];
    247	char *end, *c, *upper;
    248	size_t len;
    249	int rc, n;
    250
    251	hmap_init(&colmap, 16, hmap_str_hash, hmap_str_keycmp, ga);
    252	dvec_init(&cols, sizeof(struct tabular_col), 0, ga);
    253
    254	/* get general flags */
    255	for (dst = arg = argv + 1; *arg; arg++) {
    256		if (!strcmp(*arg, "-h") || !strcmp(*arg, "--help")) {
    257			fprintf(stderr, "Usage: tabular "
    258				"[--col NAME].. [--NAME-OPT VAL]..\n");
    259			exit(1);
    260		} else if (!strcmp(*arg, "--hsep")) {
    261			if (!*++arg) die("missing args");
    262			cfg.hsep = *arg;
    263		} else if (!strcmp(*arg, "--vsep")) {
    264			if (!*++arg) die("missing args");
    265			cfg.vsep = *arg;
    266		} else if (!strcmp(*arg, "--xsep")) {
    267			if (!*++arg) die("missing args");
    268			cfg.xsep = *arg;
    269		} else if (!strcmp(*arg, "--lpad")) {
    270			if (!*++arg) die("missing args");
    271			cfg.lpad = strtoul(*arg, &end, 10);
    272			if (end && *end) die("bad %s", arg[-1]);
    273		} else if (!strcmp(*arg, "--rpad")) {
    274			if (!*++arg) die("missing args");
    275			cfg.rpad = strtoul(*arg, &end, 10);
    276			if (end && *end) die("bad %s", arg[-1]);
    277		} else if (!strcmp(*arg, "--skip-lines")) {
    278			if (!*++arg) die("missing args");
    279			cfg.skip_lines = strtoul(*arg, &end, 10);
    280			if (end && *end) die("bad %s", arg[-1]);
    281		} else if (!strcmp(*arg, "--fit-rows")) {
    282			if (!*++arg) die("missing args");
    283			cfg.fit_rows = bool_arg(*arg, "--fit-rows");
    284		} else if (!strcmp(*arg, "--outw")) {
    285			if (!*++arg) die("missing args");
    286			cfg.outw = strtoul(*arg, &end, 10);
    287			if (end && *end) die("bad %s", arg[-1]);
    288		} else if (!strcmp(*arg, "--outh")) {
    289			if (!*++arg) die("missing args");
    290			cfg.outh = strtoul(*arg, &end, 10);
    291			if (end && *end) die("bad %s", arg[-1]);
    292		} else if (!strcmp(*arg, "--skip-lines")) {
    293			if (!*++arg) die("missing args");
    294			cfg.skip_lines = strtoul(*arg, &end, 10);
    295			if (end && *end) die("bad %s", arg[-1]);
    296		} else if (!strcmp(*arg, "--hide-empty")) {
    297			if (!*++arg) die("missing args");
    298			hide_empty = bool_arg(*arg, arg[-1]);
    299		} else if (!strcmp(*arg, "--line-sep")) {
    300			if (!*++arg || !**arg || *(*arg+1))
    301				die("missing args");
    302			line_sep = **arg;
    303		} else if (!strcmp(*arg, "--entry-sep")) {
    304			if (!*++arg || !**arg || *(*arg+1))
    305				die("missing args");
    306			entry_sep = **arg;
    307		} else if (!strcmp(*arg, "--header-underline")) {
    308			if (!*++arg) die("missing args");
    309			header_underline = bool_arg(*arg, arg[-1]);
    310		} else if (!strcmp(*arg, "--skip-empty-entries")) {
    311			if (!*++arg) die("missing args");
    312			skip_empty_entries = bool_arg(*arg, arg[-1]);
    313		} else if (!strcmp(*arg, "--skip-empty-lines")) {
    314			if (!*++arg) die("missing args");
    315			skip_empty_lines = bool_arg(*arg, arg[-1]);
    316			if (!*++arg) die("missing args");
    317		} else if (!strcmp(*arg, "--colors")) {
    318			if (!*++arg) die("missing args");
    319			cfg.colors = (int) strtol(*arg, &end, 10);
    320			if (end && *end) die("bad %s", arg[-1]);
    321		} else {
    322			*dst++ = *arg;
    323		}
    324	}
    325	*dst = NULL;
    326
    327	if (!cfg.outw || !cfg.outh) {
    328		rc = ioctl(1, TIOCGWINSZ, &ws);
    329		if (!rc) {
    330			cfg.outw = ws.ws_col;
    331			cfg.outh = ws.ws_row;
    332		} else {
    333			cfg.outw = 80;
    334			cfg.outh = 26;
    335		}
    336	}
    337
    338	/* get columns */
    339	for (dst = arg = argv + 1; *arg; arg++) {
    340		if (!strcmp(*arg, "--col")) {
    341			if (!*++arg) die("missing args");
    342			col = ga->alloc(ga, sizeof(struct col), NULL);
    343			tcol = &col->tabular;
    344
    345			if (strlen(*arg) > 63) die("col name too long");
    346			strncpy(namebuf, *arg, 64);
    347			for (c = namebuf; *c; c++)
    348				*c = (*c >= 'a' && *c <= 'z') ? *c - 32 : *c;
    349			upper = strdup(namebuf);
    350			if (!upper) die("strdup:");
    351
    352			col->hide_out = NULL;
    353			tcol->name = *arg;
    354			tcol->align = TABULAR_ALIGN_LEFT;
    355			tcol->essential = true;
    356			tcol->is_hidden = col_hidden;
    357			tcol->to_str = col_str;
    358			tcol->lpad = 0;
    359			tcol->rpad = 0;
    360			tcol->minwidth = strlen(tcol->name);
    361			tcol->maxwidth = cfg.outw;
    362			tcol->strategy = TABULAR_WRAP_WORDAWARE;
    363			tcol->squashable = false;
    364			tcol->user.id = colcnt++;
    365			rc = hmap_add(&colmap,
    366				(struct hmap_key) { .p = upper },
    367				(struct hmap_val) { .p = col });
    368			if (rc) die("duplicate col %s", tcol->name);
    369		} else {
    370			*dst++ = *arg;
    371		}
    372	}
    373	*dst = NULL;
    374
    375	/* set column attrs */
    376	for (dst = arg = argv + 1; *arg; arg++) {
    377		if (!strncmp(*arg, "--", 2)) {
    378			n = 0;
    379			rc = sscanf(*arg, "--%63[^-]-%n", namebuf, &n);
    380			if (rc != 1 || *(*arg+n-1) != '-') goto skip;
    381			link = hmap_get(&colmap,
    382				(struct hmap_key) { .p = namebuf });
    383			if (!link) goto skip;
    384			col = link->value._p;
    385			tcol = &col->tabular;
    386
    387			if (!strcmp(*arg + n, "lpad")) {
    388				if (!*++arg) die("missing args");
    389				tcol->lpad = strtoul(*arg, &end, 10);
    390				if (end && *end) die("bad %s", arg[-1]);
    391				len = tcol->lpad + strlen(tcol->name) + tcol->rpad;
    392				if (tcol->minwidth < len)
    393					tcol->minwidth = len;
    394			} else if (!strcmp(*arg + n, "rpad")) {
    395				if (!*++arg) die("missing args");
    396				tcol->rpad = strtoul(*arg, &end, 10);
    397				if (end && *end) die("bad %s", arg[-1]);
    398				len = tcol->lpad + strlen(tcol->name) + tcol->rpad;
    399				if (tcol->minwidth < len)
    400					tcol->minwidth = len;
    401			} else if (!strcmp(*arg + n, "align")) {
    402				if (!*++arg) die("missing args");
    403				if (!strcmp(*arg, "left")) {
    404					tcol->align = TABULAR_ALIGN_LEFT;
    405				} else if (!strcmp(*arg, "right")) {
    406					tcol->align = TABULAR_ALIGN_RIGHT;
    407				} else if (!strcmp(*arg, "center")) {
    408					tcol->align = TABULAR_ALIGN_CENTER;
    409				} else {
    410					die("bad %s", arg[-1]);
    411				}
    412			} else if (!strcmp(*arg + n, "squashable")) {
    413				if (!*++arg) die("missing args");
    414				tcol->squashable = bool_arg(*arg, arg[-1]);
    415			} else if (!strcmp(*arg + n, "essential")) {
    416				if (!*++arg) die("missing args");
    417				tcol->essential = bool_arg(*arg, arg[-1]);
    418			} else if (!strcmp(*arg + n, "minwidth")) {
    419				if (!*++arg) die("missing args");
    420				tcol->minwidth = strtoul(*arg, &end, 10);
    421				if (tcol->minwidth > tcol->maxwidth)
    422					tcol->maxwidth = tcol->minwidth;
    423				if (end && *end) die("bad %s", arg[-1]);
    424			} else if (!strcmp(*arg + n, "maxwidth")) {
    425				if (!*++arg) die("missing args");
    426				tcol->maxwidth = strtoul(*arg, &end, 10);
    427				if (end && *end) die("bad %s", arg[-1]);
    428			} else if (!strcmp(*arg + n, "strategy")) {
    429				if (!*++arg) die("missing args");
    430				if (!strcmp(*arg, "word-aware")) {
    431					tcol->strategy = TABULAR_WRAP_WORDAWARE;
    432				} else if (!strcmp(*arg, "wrap")) {
    433					tcol->strategy = TABULAR_WRAP;
    434				} else if (!strcmp(*arg, "trunc")) {
    435					tcol->strategy = TABULAR_TRUNC;
    436				} else {
    437					die("bad %s", arg[-1]);
    438				}
    439			} else if (!strcmp(*arg + n, "hide")) {
    440				if (!*++arg) die("missing args");
    441				col->hide_out = *arg;
    442			} else {
    443				goto skip;
    444			}
    445		} else {
    446skip:
    447			*dst++ = *arg;
    448		}
    449	}
    450	*dst = NULL;
    451
    452	if (argv[1]) die("unused argument '%s'", argv[1]);
    453
    454	dvec_add_back(&cols, colcnt);
    455	for (HMAP_ITER(&colmap, iter)) {
    456		col = iter.link->value._p;
    457		tcol = dvec_at(&cols, col->tabular.user.id);
    458		memcpy(tcol, &col->tabular, sizeof(struct tabular_col));
    459		tcol->user.ptr = col;
    460	}
    461	cfg.columns = dvec_front(&cols);
    462	cfg.column_count = cols.len;
    463}
    464
    465int
    466main(int argc, const char **argv)
    467{
    468	struct tabular_row *rows;
    469	struct tabular_stats stats;
    470	int rc;
    471
    472	parse(argc, argv);
    473
    474	dvec_init(&line, 1, 1024, ga);
    475	dvec_init(&input, 1, 1024, ga);
    476	dvec_init(&entries, sizeof(size_t), 1, ga);
    477
    478	rows = NULL;
    479	rc = tabular_format(stdout, &cfg, &stats, &rows);
    480	if (rc) errx(1, "tabular_format (%i)", rc);
    481
    482	printf("\n%lu lines, %lu rows",
    483		stats.lines_used, stats.rows_displayed);
    484	if (stats.rows_truncated) printf(" (rows truncated)");
    485	if (stats.cols_truncated) printf(" (cols truncated)");
    486	printf("\n");
    487
    488	tabular_free_rows(&cfg, rows);
    489}