+/* SPDX-License-Identifier: GPL-3.0-only */
+
+#include <stdlib.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <assert.h>
+#include <sys/mman.h>
+#include <stdbool.h>
+#include <string.h>
+#include <time.h>
+#include <limits.h>
+#include <sys/time.h>
+#include <lopsub.h>
+
+#include "tf.h"
+#include "tfortune.lsg.h"
+
+#define TF_SEP "---- "
+
+static struct lls_parse_result *lpr, *sublpr;
+#define CMD_PTR(_cname) lls_cmd(LSG_TFORTUNE_CMD_ ## _cname, tfortune_suite)
+#define OPT_RESULT(_cname, _oname) (lls_opt_result(\
+ LSG_TFORTUNE_ ## _cname ## _OPT_ ## _oname, \
+ (CMD_PTR(_cname) == CMD_PTR(TFORTUNE))? lpr : sublpr))
+#define OPT_GIVEN(_cname, _oname) (lls_opt_given(OPT_RESULT(_cname, _oname)))
+#define OPT_STRING_VAL(_cname, _oname) (lls_string_val(0, \
+ OPT_RESULT(_cname, _oname)))
+#define OPT_UINT32_VAL(_cname, _oname) (lls_uint32_val(0, \
+ OPT_RESULT(_cname, _oname)))
+
+int loglevel_arg_val;
+
+struct tf_user_data {int (*handler)(void);};
+#define EXPORT_CMD_HANDLER(_cmd) const struct tf_user_data \
+ lsg_tfortune_com_ ## _cmd ## _user_data = { \
+ .handler = com_ ## _cmd \
+ };
+
+struct map_chunk {
+ const char *base;
+ size_t len;
+};
+
+struct epigram {
+ struct map_chunk epi, tags;
+};
+
+static int lopsub_error(int lopsub_ret, char **errctx)
+{
+ const char *msg = lls_strerror(-lopsub_ret);
+ if (*errctx)
+ ERROR_LOG("%s: %s\n", *errctx, msg);
+ else
+ ERROR_LOG("%s\n", msg);
+ free(*errctx);
+ *errctx = NULL;
+ return -E_LOPSUB;
+}
+
+/* per epigram context for the tag expression parser */
+struct epi_properties {
+ const struct map_chunk *chunk; /* only the epigram */
+ struct linhash_table *tagtab;
+ unsigned num_tags;
+};
+
+unsigned epi_len(const struct epi_properties *props)
+{
+ return props->chunk->len;
+}
+
+char *epi_text(const struct epi_properties *props)
+{
+ const char *txt = props->chunk->base;
+ char *result = malloc(props->chunk->len + 1);
+ memcpy(result, txt, props->chunk->len);
+ result[props->chunk->len] = '\0';
+ return result;
+}
+
+bool epi_has_tag(const char *tag, const struct epi_properties *props)
+{
+ return linhash_lookup(tag, props->tagtab);
+}
+
+struct tag_iter {
+ char *str;
+ char *saveptr; /* for strtok_r(3) */
+ char *token;
+};
+
+static struct tag_iter *tag_iter_new(const struct map_chunk *tags)
+{
+ struct tag_iter *titer = xmalloc(sizeof(*titer));
+
+ titer->str = xmalloc(tags->len + 1);
+ memcpy(titer->str, tags->base, tags->len);
+ titer->str[tags->len] = '\0';
+ titer->token = strtok_r(titer->str, ",", &titer->saveptr);
+ return titer;
+}
+
+static const char *tag_iter_get(const struct tag_iter *titer)
+{
+ return titer->token;
+}
+
+static void tag_iter_next(struct tag_iter *titer)
+{
+ titer->token = strtok_r(NULL, ",", &titer->saveptr);
+}
+
+static void tag_iter_free(struct tag_iter *titer)
+{
+ if (!titer)
+ return;
+ free(titer->str);
+ free(titer);
+}
+
+static bool epi_admissible(const struct epigram *epi,
+ const struct txp_context *ast)
+{
+ bool admissible;
+ const char *p;
+ struct epi_properties props = {
+ .chunk = &epi->epi,
+ .tagtab = linhash_new(3),
+ .num_tags = 0,
+ };
+ struct tag_iter *titer;
+
+
+ for (
+ titer = tag_iter_new(&epi->tags);
+ (p = tag_iter_get(titer));
+ tag_iter_next(titer)
+ ) {
+ struct linhash_item item = {.key = p};
+ linhash_insert(&item, props.tagtab, NULL);
+ props.num_tags++;
+ }
+
+ admissible = txp_eval_ast(ast, &props);
+ linhash_free(props.tagtab);
+ tag_iter_free(titer);
+ return admissible;
+}
+
+static void print_epigram(const struct epigram *epi, bool print_tags)
+{
+ printf("%.*s", (int)epi->epi.len, epi->epi.base);
+ if (print_tags)
+ printf(TF_SEP "%.*s\n", (int)epi->tags.len, epi->tags.base);
+}
+
+static void print_admissible_epigrams(const struct epigram *epis,
+ unsigned num_epis)
+{
+ unsigned n;
+
+ for (n = 0; n < num_epis; n++)
+ print_epigram(epis + n, OPT_GIVEN(PRINT, TAGS));
+}
+
+static void print_random_epigram(const struct epigram *epis, unsigned num_epis)
+{
+ long unsigned r;
+ const struct epigram *epi;
+ struct timeval tv;
+
+ if (num_epis == 0) {
+ ERROR_LOG("no matching epigram\n");
+ return;
+ }
+ gettimeofday(&tv, NULL);
+ srandom((unsigned)tv.tv_usec);
+ r = (num_epis + 0.0) * (random() / (RAND_MAX + 1.0));
+ assert(r < num_epis);
+ epi = epis + r;
+ print_epigram(epi, OPT_GIVEN(PRINT, TAGS));
+}
+
+static int read_tag_expression(struct iovec *result)
+{
+ const char *tx = OPT_STRING_VAL(PRINT, EXPRESSION);
+ int ret, fd;
+
+ assert(tx);
+ if (strcmp(tx, "-")) {
+ char *filename;
+ if (tx[0] != '/') {
+ char *home = get_homedir();
+ xasprintf(&filename, "%s/.tfortune/expressions/%s",
+ home, tx);
+ free(home);
+ } else
+ filename = xstrdup(tx);
+ ret = open(filename, O_RDONLY);
+ if (ret < 0) {
+ ret = -ERRNO_TO_TF_ERROR(errno);
+ ERROR_LOG("could not open %s\n", filename);
+ }
+ free(filename);
+ if (ret < 0)
+ return ret;
+ fd = ret;
+ } else
+ fd = STDIN_FILENO;
+ ret = fd2buf(fd, result);
+ close(fd);
+ return ret;
+}
+
+static int tx2ast(struct iovec *tx, struct txp_context **ast)
+{
+ int ret;
+ char *errmsg;
+
+ ret = txp_init(tx, ast, &errmsg);
+ if (ret < 0) {
+ ERROR_LOG("could not parse tag expression: %s\n", errmsg);
+ free(errmsg);
+ return ret;
+ }
+ return 1;
+}
+
+struct epi_iter {
+ struct iovec *maps;
+ unsigned num_maps;
+ unsigned map_num;
+ struct epigram epi;
+ unsigned num_epis;
+};
+
+static bool get_next_epi(struct epi_iter *eiter)
+{
+ const char *epi_start = NULL;
+
+ for (; eiter->map_num < eiter->num_maps; eiter->map_num++) {
+ struct iovec *iov = eiter->maps + eiter->map_num;
+ const char *buf, *end = iov->iov_base + iov->iov_len;
+
+ if (!epi_start && eiter->epi.tags.base)
+ epi_start = eiter->epi.tags.base
+ + eiter->epi.tags.len + 1;
+ else
+ epi_start = iov->iov_base;
+ buf = epi_start;
+ while (buf < end) {
+ const size_t sep_len = strlen(TF_SEP);
+ const char *p, *cr, *tags;
+ size_t tag_len;
+
+ cr = memchr(buf, '\n', end - buf);
+ if (!cr)
+ break;
+ p = cr + 1;
+ if (p + sep_len >= end)
+ break;
+ if (strncmp(p, TF_SEP, sep_len) != 0) {
+ buf = p;
+ continue;
+ }
+ tags = p + sep_len;
+ cr = memchr(tags, '\n', end - tags);
+ if (cr)
+ tag_len = cr - tags;
+ else
+ tag_len = end - tags;
+ eiter->epi.epi.base = epi_start;
+ eiter->epi.epi.len = p - epi_start;
+ eiter->epi.tags.base = tags;
+ eiter->epi.tags.len = tag_len;
+ eiter->num_epis++;
+ return true;
+ }
+ }
+ eiter->epi.epi.base = NULL;
+ eiter->epi.epi.len = 0;
+ eiter->epi.tags.base = NULL;
+ eiter->epi.tags.len = 0;
+ return false;
+}
+
+static char *get_basedir(void)
+{
+ char *home, *basedir;
+ if (OPT_GIVEN(TFORTUNE, BASEDIR))
+ return xstrdup(OPT_STRING_VAL(TFORTUNE, BASEDIR));
+ home = get_homedir();
+ xasprintf(&basedir, "%s/.tfortune", home);
+ free(home);
+ return basedir;
+}
+
+static char *get_epidir(void)
+{
+ char *basedir, *epidir;
+ basedir = get_basedir();
+ xasprintf(&epidir, "%s/epigrams", basedir);
+ free(basedir);
+ return epidir;
+}
+
+static char *get_xdir(void)
+{
+ char *basedir = get_basedir(), *xdir;
+ xasprintf(&xdir, "%s/expressions", basedir);
+ free(basedir);
+ return xdir;
+}
+
+static struct epi_iter *epi_iter_new(void)
+{
+ struct epi_iter *eiter = xmalloc(sizeof(*eiter));
+ unsigned num_inputs = lls_num_inputs(sublpr);
+
+ if (num_inputs == 0) {
+ struct regfile_iter *riter;
+ struct iovec iov;
+ char *epidir = get_epidir();
+
+ regfile_iter_new(epidir, &riter);
+ free(epidir);
+ eiter->maps = NULL;
+ eiter->num_maps = 0;
+ for (;
+ regfile_iter_map(riter, &iov);
+ regfile_iter_next(riter)
+ ) {
+ eiter->num_maps++;
+ eiter->maps = realloc(eiter->maps,
+ eiter->num_maps * sizeof(*eiter->maps));
+ eiter->maps[eiter->num_maps - 1] = iov;
+ }
+ regfile_iter_free(riter);
+ } else {
+ unsigned n;
+ eiter->maps = xmalloc(num_inputs * sizeof(*eiter->maps));
+ for (n = 0; n < num_inputs; n++)
+ mmap_file(lls_input(n, sublpr), eiter->maps + n);
+ eiter->num_maps = num_inputs;
+ }
+ eiter->map_num = 0;
+ eiter->epi.epi.base = NULL;
+ eiter->epi.epi.len = 0;
+ eiter->epi.tags.base = NULL;
+ eiter->epi.tags.len = 0;
+ eiter->num_epis = 0;
+ get_next_epi(eiter);
+ return eiter;
+}
+
+static const struct epigram *epi_iter_get(const struct epi_iter *eiter)
+{
+ return (eiter->epi.epi.base && eiter->epi.tags.base)?
+ &eiter->epi : NULL;
+}
+
+static unsigned epi_iter_num_maps(const struct epi_iter *eiter)
+{
+ return eiter->num_maps;
+}
+
+static unsigned epi_iter_num_epis(const struct epi_iter *eiter)
+{
+ return eiter->num_epis;
+}
+
+static void epi_iter_next(struct epi_iter *eiter)
+{
+ get_next_epi(eiter);
+}
+
+static void epi_iter_free(struct epi_iter *eiter)
+{
+ unsigned n;
+
+ if (!eiter)
+ return;
+ for (n = 0; n < eiter->num_maps; n++)
+ munmap(eiter->maps[n].iov_base, eiter->maps[n].iov_len);
+ free(eiter->maps);
+ free(eiter);
+}
+
+static int com_print(void)
+{
+ int ret;
+ struct epigram *epis = NULL;
+ unsigned epis_sz = 0, nae = 0; /* number of admissible epis */
+ struct iovec tx;
+ struct txp_context *ast;
+ struct epi_iter *eiter;
+ const struct epigram *epi;
+
+ ret = read_tag_expression(&tx);
+ if (ret < 0)
+ return ret;
+ ret = tx2ast(&tx, &ast);
+ if (ret < 0)
+ goto free_tx;
+ for (
+ eiter = epi_iter_new();
+ (epi = epi_iter_get(eiter));
+ epi_iter_next(eiter)
+ ) {
+ if (!epi_admissible(epi, ast))
+ continue;
+ if (nae >= epis_sz) {
+ epis_sz = 2 * epis_sz + 1;
+ epis = xrealloc(epis, epis_sz * sizeof(*epis));
+ }
+ epis[nae++] = *epi;
+ }
+ if (OPT_GIVEN(PRINT, ALL))
+ print_admissible_epigrams(epis, nae);
+ else
+ print_random_epigram(epis, nae);
+ epi_iter_free(eiter);
+ free(epis);
+ txp_free(ast);
+ ret = 1;
+free_tx:
+ free(tx.iov_base);
+ return ret;
+}
+EXPORT_CMD_HANDLER(print);
+
+static char *get_editor(void)
+{
+ char *val = getenv("TFORTUNE_EDITOR");
+
+ if (val && val[0])
+ return xstrdup(val);
+ val = getenv("EDITOR");
+ if (val && val[0])
+ return xstrdup(val);
+ return xstrdup("vi");
+}
+
+static void open_editor(const char *dir)
+{
+ char *editor;
+ char **argv;
+ pid_t pid;
+ unsigned n, num_inputs = lls_num_inputs(sublpr);
+
+ if ((pid = fork()) < 0) {
+ EMERG_LOG("fork error: %s\n", strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+ if (pid) { /* parent */
+ wait(NULL);
+ return;
+ }
+ editor = get_editor();
+ argv = xmalloc((num_inputs + 2) * sizeof(*argv));
+ argv[0] = editor;
+ for (n = 0; n < num_inputs; n++)
+ xasprintf(&argv[n + 1], "%s/%s", dir, lls_input(n, sublpr));
+ argv[num_inputs + 1] = NULL;
+ execvp(editor, argv);
+ EMERG_LOG("execvp error: %s\n", strerror(errno));
+ _exit(EXIT_FAILURE);
+}
+
+static int create_dir(const char *path)
+{
+ int ret;
+
+ ret = mkdir(path, 0777); /* rely on umask */
+ if (ret < 0) {
+ if (errno == EEXIST)
+ return 0;
+ ERROR_LOG("could not create %s\n", path);
+ return -ERRNO_TO_TF_ERROR(errno);
+ }
+ NOTICE_LOG("created directory %s\n", path);
+ return 1;
+}
+
+static int create_basedir(void)
+{
+ char *basedir;
+ int ret;
+
+ basedir = get_basedir();
+ ret = create_dir(basedir);
+ free(basedir);
+ return ret;
+}
+
+static int generic_edit(const char *dir)
+{
+ char *errctx;
+ int ret;
+ bool basedir_given = OPT_GIVEN(TFORTUNE, BASEDIR);
+
+ ret = lls_check_arg_count(sublpr, 1, INT_MAX, &errctx);
+ if (ret < 0) {
+ ret = lopsub_error(ret, &errctx);
+ return ret;
+ }
+ if (!basedir_given) {
+ ret = create_basedir();
+ if (ret < 0)
+ return ret;
+ ret = create_dir(dir);
+ if (ret < 0)
+ return ret;
+ }
+ open_editor(dir);
+ ret = 1;
+ return ret;
+}
+
+static int com_ede(void)
+{
+ int ret;
+ char *epidir = get_epidir();
+ ret = generic_edit(epidir);
+ free(epidir);
+ return ret;
+}
+EXPORT_CMD_HANDLER(ede);
+
+static int com_edx(void)
+{
+ int ret;
+ char *xdir = get_xdir();
+ ret = generic_edit(xdir);
+ free(xdir);
+ return ret;
+}
+EXPORT_CMD_HANDLER(edx);
+
+static int item_alpha_compare(const struct linhash_item **i1,
+ const struct linhash_item **i2)
+{
+ return strcmp((*i1)->key, (*i2)->key);
+}
+
+static int item_num_compare(const struct linhash_item **i1,
+ const struct linhash_item **i2)
+{
+ long unsigned v1 = (long unsigned)(*i1)->object;
+ long unsigned v2 = (long unsigned)(*i2)->object;
+
+ return v1 < v2? -1 : (v1 == v2? 0 : 1);
+}
+
+struct dentry {
+ char mode[11];
+ nlink_t nlink;
+ char *user, *group;
+ off_t size;
+ uint64_t mtime;
+ char *name;
+};
+
+static void make_dentry(const char *name, const struct stat *stat,
+ struct dentry *d)
+{
+ mode_t m = stat->st_mode;
+ struct group *g;
+ struct passwd *pwentry;
+
+ sprintf(d->mode, "----------");
+ if (S_ISREG(m))
+ d->mode[0] = '-';
+ else if (S_ISDIR(m))
+ d->mode[0] = 'd';
+ else if (S_ISCHR(m))
+ d->mode[0] = 'c';
+ else if ((S_ISBLK(m)))
+ d->mode[0] = 'b';
+ else if (S_ISLNK(m))
+ d->mode[0] = 'l';
+ else if (S_ISFIFO(m))
+ d->mode[0] = 'p';
+ else if ((S_ISSOCK(m)))
+ d->mode[0] = 's';
+ else
+ d->mode[0] = '?';
+
+ if (m & S_IRUSR)
+ d->mode[1] = 'r';
+ if (m & S_IWUSR)
+ d->mode[2] = 'w';
+ if (m & S_IXUSR) {
+ if (m & S_ISUID)
+ d->mode[3] = 's';
+ else
+ d->mode[3] = 'x';
+ } else if (m & S_ISUID)
+ d->mode[3] = 'S';
+
+ if (m & S_IRGRP)
+ d->mode[4] = 'r';
+ if (m & S_IWGRP)
+ d->mode[5] = 'w';
+ if (m & S_IXGRP) {
+ if (m & S_ISGID)
+ d->mode[6] = 's';
+ else
+ d->mode[6] = 'x';
+ } else if (m & S_ISGID)
+ d->mode[6] = 'S';
+
+ if (m & S_IROTH)
+ d->mode[7] = 'r';
+ if (m & S_IWOTH)
+ d->mode[8] = 'w';
+ if (m & S_IXOTH) {
+ if (m & S_ISVTX)
+ d->mode[9] = 't';
+ else
+ d->mode[9] = 'x';
+ } else if (m & S_ISVTX)
+ d->mode[9] = 'T';
+
+ d->nlink = stat->st_nlink;
+
+ pwentry = getpwuid(stat->st_uid);
+ if (pwentry && pwentry->pw_name)
+ d->user = xstrdup(pwentry->pw_name);
+ else
+ xasprintf(&d->user, "%u", stat->st_uid);
+
+ g = getgrgid(stat->st_gid);
+ if (g && g->gr_name)
+ d->group = xstrdup(g->gr_name);
+ else
+ xasprintf(&d->group, "%u", stat->st_gid);
+ d->size = stat->st_size;
+ d->mtime = stat->st_mtime;
+ d->name = xstrdup(name);
+}
+
+static int num_digits(uint64_t x)
+{
+ unsigned n = 1;
+
+ if (x != 0)
+ while (x > 9) {
+ x /= 10;
+ n++;
+ }
+ return n;
+}
+
+enum var_length_dentry_fields {
+ VLDF_NLINKS,
+ VLDF_USER,
+ VLDF_GROUP,
+ VLDF_SIZE,
+ NUM_VLDF
+};
+
+static void update_field_field_widths(int field_widths[NUM_VLDF],
+ const struct dentry *d)
+{
+ int *w, n;
+
+ w = field_widths + VLDF_NLINKS;
+ n = num_digits(d->nlink);
+ *w = MAX(*w, n);
+
+ w = field_widths + VLDF_USER;
+ n = strlen(d->user);
+ *w = MAX(*w, n);
+
+ w = field_widths + VLDF_GROUP;
+ n = strlen(d->group);
+ *w = MAX(*w, n);
+
+ w = field_widths + VLDF_SIZE;
+ n = num_digits(d->size);
+ *w = MAX(*w, n);
+}
+
+static void format_time(uint64_t seconds, uint64_t now, struct iovec *result)
+{
+ struct tm *tm;
+ const uint64_t m = 6 * 30 * 24 * 3600; /* six months */
+ size_t nbytes;
+
+ tm = localtime((time_t *)&seconds);
+ assert(tm);
+
+ if (seconds > now - m && seconds < now + m) {
+ nbytes = strftime(result->iov_base, result->iov_len,
+ "%b %e %k:%M", tm);
+ assert(nbytes > 0);
+ } else {
+ nbytes = strftime(result->iov_base, result->iov_len,
+ "%b %e %Y", tm);
+ assert(nbytes > 0);
+ }
+}
+
+static int list_directory(const char *dir, bool long_listing)
+{
+ struct regfile_iter *riter;
+ const char *basename;
+ int ret, field_widths[NUM_VLDF] = {0};
+ struct dentry *dentries = NULL;
+ unsigned n, num_dentries = 0, dentries_size = 0;
+ struct timespec now;
+
+ for (
+ regfile_iter_new(dir, &riter);
+ (basename = regfile_iter_basename(riter));
+ regfile_iter_next(riter)
+ ) {
+ const struct stat *stat;
+ struct dentry *dentry;
+ if (!long_listing) {
+ printf("%s\n", basename);
+ continue;
+ }
+ num_dentries++;
+ if (num_dentries > dentries_size) {
+ dentries_size = 2 * dentries_size + 1;
+ dentries = xrealloc(dentries,
+ dentries_size * sizeof(*dentries));
+ }
+ dentry = dentries + num_dentries - 1;
+ stat = regfile_iter_stat(riter);
+ make_dentry(basename, stat, dentry);
+ update_field_field_widths(field_widths, dentry);
+ }
+ regfile_iter_free(riter);
+ if (!long_listing)
+ return 0;
+ ret = clock_gettime(CLOCK_REALTIME, &now);
+ assert(ret == 0);
+ for (n = 0; n < num_dentries; n++) {
+ struct dentry *d = dentries + n;
+ char buf[30];
+ struct iovec iov = {.iov_base = buf, .iov_len = sizeof(buf)};
+
+ format_time(d->mtime, now.tv_sec, &iov);
+ printf("%s %*lu %*s %*s %*" PRIu64 " %s %s\n",
+ d->mode,
+ field_widths[VLDF_NLINKS], (long unsigned)d->nlink,
+ field_widths[VLDF_USER], d->user,
+ field_widths[VLDF_GROUP], d->group,
+ field_widths[VLDF_SIZE], (uint64_t)d->size,
+ buf,
+ d->name
+ );
+ free(d->user);
+ free(d->group);
+ free(d->name);
+ }
+ free(dentries);
+ return 1;
+}
+
+static int com_lse(void)
+{
+ int ret;
+ char *dir = get_epidir();
+
+ ret = list_directory(dir, OPT_GIVEN(LSE, LONG));
+ free(dir);
+ return ret;
+}
+EXPORT_CMD_HANDLER(lse);
+
+static int com_lsx(void)
+{
+ int ret;
+ char *dir = get_xdir();
+
+ ret = list_directory(dir, OPT_GIVEN(LSE, LONG));
+ free(dir);
+ return ret;
+}
+EXPORT_CMD_HANDLER(lsx);
+
+static struct linhash_table *hash_tags(unsigned *num_epi_files,
+ unsigned *num_epis)
+{
+ struct linhash_table *tagtab = linhash_new(3);
+ struct epi_iter *eiter;
+ const struct epigram *epi;
+
+ for (
+ eiter = epi_iter_new();
+ (epi = epi_iter_get(eiter));
+ epi_iter_next(eiter)
+ ) {
+ struct tag_iter *titer;
+ const char *tag;
+ for (
+ titer = tag_iter_new(&epi->tags);
+ (tag = tag_iter_get(titer));
+ tag_iter_next(titer)
+ ) {
+ struct linhash_item item = {
+ .key = xstrdup(tag),
+ .object = (void *)1LU
+ };
+ void **object;
+ if (linhash_insert(&item, tagtab, &object) < 0) {
+ long unsigned val = (long unsigned)*object;
+ val++;
+ *object = (void *)val;
+ free((char *)item.key);
+ }
+ }
+ tag_iter_free(titer);
+ }
+ if (num_epi_files)
+ *num_epi_files = epi_iter_num_maps(eiter);
+ if (num_epis)
+ *num_epis = epi_iter_num_epis(eiter);
+ epi_iter_free(eiter);
+ return tagtab;
+}
+
+static int com_lst(void)
+{
+ struct linhash_table *tagtab;
+ struct linhash_iterator *liter;
+ struct linhash_item *itemp;
+ linhash_comparator *comp = OPT_GIVEN(LST, SORT_BY_COUNT)?
+ item_num_compare : item_alpha_compare;
+ bool reverse = OPT_GIVEN(LST, REVERSE);
+
+ tagtab = hash_tags(NULL, NULL);
+ for (
+ liter = linhash_iterator_new(tagtab, comp, reverse);
+ (itemp = linhash_iterator_item(liter));
+ linhash_iterator_next(liter)
+ ) {
+ if (OPT_GIVEN(LST, LONG))
+ printf("%lu\t%s\n", (long unsigned)itemp->object,
+ itemp->key);
+ else
+ printf("%s\n", itemp->key);
+ free((char *)itemp->key);
+ }
+ linhash_iterator_free(liter);
+ linhash_free(tagtab);
+ return 0;
+}
+EXPORT_CMD_HANDLER(lst);
+
+static int com_stats(void)
+{
+ struct linhash_table *tagtab;
+ struct linhash_iterator *liter;
+ struct linhash_item *itemp;
+ unsigned num_epi_files, num_epis, num_unique_tags, num_x = 0;
+ long unsigned num_tags = 0;
+ char *xdir, *lh_stats;
+ struct regfile_iter *riter;
+ bool verbose = OPT_GIVEN(STATS, VERBOSE);
+
+ tagtab = hash_tags(&num_epi_files, &num_epis);
+ for (
+ liter = linhash_iterator_new(tagtab, NULL, false);
+ (itemp = linhash_iterator_item(liter));
+ linhash_iterator_next(liter)
+ )
+ num_tags += (long unsigned)itemp->object;
+ num_unique_tags = linhash_num_items(tagtab);
+ linhash_iterator_free(liter);
+ if (verbose)
+ lh_stats = linhash_statistics(tagtab);
+ linhash_free(tagtab);
+
+ xdir = get_xdir();
+ for (
+ regfile_iter_new(xdir, &riter);
+ regfile_iter_basename(riter);
+ regfile_iter_next(riter)
+ )
+ num_x++;
+ regfile_iter_free(riter);
+ free(xdir);
+ printf("number of tag expressions.......... %5u\n", num_x);
+ printf("number of epigram files............ %5u\n", num_epi_files);
+ printf("number of epigrams................. %5u\n", num_epis);
+ printf("number of tags..................... %5lu\n", num_tags);
+ printf("number of unique tags.............. %5u\n", num_unique_tags);
+ printf("average number of epigrams per file %8.02f\n",
+ (float)num_epis / num_epi_files);
+ printf("average number of tags per epigram. %8.02f\n",
+ (float)num_tags / num_epis);
+ printf("average number of tag recurrence... %8.02f\n",
+ (float)num_tags / num_unique_tags);
+ if (verbose) {
+ printf("\nlinear hashing statistics:\n%s\n", lh_stats);
+ free(lh_stats);
+ }
+ return 1;
+}
+EXPORT_CMD_HANDLER(stats);
+
+#define LSG_TFORTUNE_CMD(_name) #_name
+static const char * const subcommand_names[] = {LSG_TFORTUNE_SUBCOMMANDS NULL};
+#undef LSG_TFORTUNE_CMD
+
+static void show_subcommand_summary(bool verbose)
+{
+ int i;
+
+ printf("Available subcommands:\n");
+ if (verbose) {
+ const struct lls_command *cmd;
+ for (i = 1; (cmd = lls_cmd(i, tfortune_suite)); i++) {
+ const char *purpose = lls_purpose(cmd);
+ const char *name = lls_command_name(cmd);
+ printf("%-11s%s\n", name, purpose);
+ }
+ } else {
+ unsigned n = 8;
+ printf("\t");
+ for (i = 0; i < LSG_NUM_TFORTUNE_SUBCOMMANDS; i++) {
+ if (i > 0)
+ n += printf(", ");
+ n += printf("%s", subcommand_names[i]);
+ if (n > 70) {
+ printf("\n\t");
+ n = 8;
+ }
+ }
+ printf("\n");
+ }
+}
+
+static int com_help(void)
+{
+ int ret;
+ char *errctx, *help;
+ const char *arg;
+ const struct lls_command *cmd;
+
+ ret = lls_check_arg_count(sublpr, 0, 1, &errctx);
+ if (ret < 0)
+ return lopsub_error(ret, &errctx);
+ if (lls_num_inputs(sublpr) == 0) {
+ show_subcommand_summary(OPT_GIVEN(HELP, LONG));
+ return 0;
+ }
+ arg = lls_input(0, sublpr);
+ ret = lls_lookup_subcmd(arg, tfortune_suite, &errctx);
+ if (ret < 0)
+ return lopsub_error(ret, &errctx);
+ cmd = lls_cmd(ret, tfortune_suite);
+ if (OPT_GIVEN(HELP, LONG))
+ help = lls_long_help(cmd);
+ else
+ help = lls_short_help(cmd);
+ printf("%s\n", help);
+ free(help);
+ return 1;
+}
+EXPORT_CMD_HANDLER(help);
+
+static void handle_help_and_version(void)
+{
+ int i;
+ char *help;
+ const struct lls_command *cmd;
+
+ if (OPT_GIVEN(TFORTUNE, VERSION)) {
+ printf("tfortune %s\n"
+ "Copyright (C) " COPYRIGHT_YEAR " " AUTHOR ".\n"
+ "License " LICENSE ": <" LICENSE_URL ">.\n"
+ "This is free software: you are free to change and redistribute it.\n"
+ "There is NO WARRANTY, to the extent permitted by law.\n"
+ "Report bugs to " AUTHOR " <" PACKAGE_BUGREPORT ">.\n"
+ ,
+ tf_version()
+ );
+ exit(EXIT_SUCCESS);
+ }
+ cmd = CMD_PTR(TFORTUNE);
+ if (OPT_GIVEN(TFORTUNE, DETAILED_HELP))
+ help = lls_long_help(cmd);
+ else if (OPT_GIVEN(TFORTUNE, HELP))
+ help = lls_short_help(cmd);
+ else
+ return;
+ printf("%s\n", help);
+ free(help);
+ if (OPT_GIVEN(TFORTUNE, DETAILED_HELP))
+ for (i = 1; (cmd = lls_cmd(i, tfortune_suite)); i++) {
+ help = lls_short_help(cmd);
+ printf("%s\n---\n", help);
+ free(help);
+ }
+ else
+ show_subcommand_summary(true /* verbose */);
+ exit(EXIT_SUCCESS);
+}
+
+enum tf_word_type {
+ WT_COMMAND_NAME,
+ WT_DOUBLE_DASH, /* -- */
+ WT_SHORT_OPT_WITH_ARG, /* -l */
+ WT_SHORT_OPT_WITHOUT_ARG, /* -V, -abc=d */
+ WT_LONG_OPT_WITH_ARG, /* --loglevel */
+ WT_LONG_OPT_WITHOUT_ARG, /* --foo=bar --help */
+ WT_OPTION_ARG,
+ WT_NON_OPTION_ARG,
+ WT_DUNNO,
+};
+
+static bool is_short_opt(const char *word)
+{
+ if (word[0] != '-')
+ return false;
+ if (word[1] == '-')
+ return false;
+ if (word[1] == '\0')
+ return false;
+ return true;
+}
+
+static bool is_long_opt(const char *word)
+{
+ if (word[0] != '-')
+ return false;
+ if (word[1] != '-')
+ return false;
+ if (word[2] == '\0')
+ return false;
+ return true;
+}
+
+/* whether the next word will be an arg to this short opt */
+static int short_opt_needs_arg(const char *word,
+ const char * const *short_opts)
+{
+ size_t n, len;
+
+ if (strchr(word, '='))
+ return false;
+ len = strlen(word);
+ for (n = 0; short_opts[n]; n++) {
+ const char *opt = short_opts[n];
+ if (word[len - 1] != opt[1])
+ continue;
+ if (opt[2] == '=')
+ return true;
+ else
+ return false;
+ }
+ return -1;
+}
+
+/* whether the next word will be an arg to this long opt */
+static int long_opt_needs_arg(const char *word,
+ const char * const *long_opts)
+{
+ size_t n;
+
+ if (strchr(word, '='))
+ return false;
+ for (n = 0; long_opts[n]; n++) {
+ const char *opt = long_opts[n];
+ size_t len = strlen(opt);
+
+ if (opt[len - 1] == '=')
+ len--;
+ if (strncmp(word + 2, opt + 2, len - 2))
+ continue;
+ if (opt[len] == '=')
+ return true;
+ else
+ return false;
+ }
+ return -1;
+}
+
+static bool get_word_types(unsigned cword, unsigned arg0,
+ const char * const *short_opts, const char * const *long_opts,
+ enum tf_word_type *result)
+{
+ const char *word;
+ unsigned n;
+ bool have_dd = false;
+ int ret;
+
+ /* index zero is always the command name */
+ assert(cword > arg0);
+ result[arg0] = WT_COMMAND_NAME;
+ for (n = arg0 + 1; n < cword; n++) {
+ enum tf_word_type prev_type = result[n - 1];
+
+ if (have_dd) {
+ result[n] = WT_NON_OPTION_ARG;
+ continue;
+ }
+ if (prev_type == WT_SHORT_OPT_WITH_ARG) {
+ result[n] = WT_OPTION_ARG;
+ continue;
+ }
+ if (prev_type == WT_LONG_OPT_WITH_ARG) {
+ result[n] = WT_OPTION_ARG;
+ continue;
+ }
+ word = lls_input(n, sublpr);
+ if (strcmp(word, "--") == 0) {
+ result[n] = WT_DOUBLE_DASH;
+ have_dd = true;
+ continue;
+ }
+ if (is_short_opt(word)) {
+ ret = short_opt_needs_arg(word, short_opts);
+ if (ret < 0)
+ goto dunno;
+ if (ret > 0)
+ result[n] = WT_SHORT_OPT_WITH_ARG;
+ else
+ result[n] = WT_SHORT_OPT_WITHOUT_ARG;
+ continue;
+ }
+ if (is_long_opt(word)) {
+ ret = long_opt_needs_arg(word, long_opts);
+ if (ret < 0)
+ goto dunno;
+ if (ret > 0)
+ result[n] = WT_LONG_OPT_WITH_ARG;
+ else
+ result[n] = WT_LONG_OPT_WITHOUT_ARG;
+ continue;
+ }
+ result[n] = WT_NON_OPTION_ARG;
+ }
+ return have_dd;
+dunno:
+ for (; n <= cword; n++)
+ result[n] = WT_DUNNO;
+ return false;
+}
+
+#define DUMMY_COMPLETER(_name) static char **complete_ ## _name( \
+ __attribute__ ((unused)) uint32_t cword, \
+ __attribute__ ((unused)) unsigned arg0, \
+ __attribute__ ((unused)) bool have_dd \
+ ) {return NULL;}
+
+DUMMY_COMPLETER(tfortune)
+DUMMY_COMPLETER(compgen)
+DUMMY_COMPLETER(completer)
+
+static const char * const supercmd_opts[] = {LSG_TFORTUNE_TFORTUNE_OPTS, NULL};
+
+static void print_zero_terminated_list(const char * const *list)
+{
+ const char * const *c;
+ for (c = list; *c; c++)
+ printf("%s%c", *c, '\0');
+}
+
+static void print_option_list(const char * const *opts)
+{
+ const char * const *c;
+
+ for (c = opts; *c; c++) {
+ int len = strlen(*c);
+ assert(len > 0);
+ if ((*c)[len - 1] == '=')
+ len--;
+ printf("%.*s%c", len, *c, '\0');
+ }
+}
+
+static void activate_dirname_completion(void)
+{
+ printf("%c", '\0');
+ printf("-o dirnames%c", '\0');
+}
+
+static void complete_loglevels(void)
+{
+ unsigned n;
+ const struct lls_option *opt = lls_opt(LSG_TFORTUNE_TFORTUNE_OPT_LOGLEVEL,
+ CMD_PTR(TFORTUNE));
+
+ for (n = 0; n < LSG_NUM_TFORTUNE_TFORTUNE_LOGLEVEL_VALUES; n++) {
+ const char *v = lls_enum_string_val(n, opt);
+ printf("%s%c", v, '\0');
+ }
+}
+
+static char **complete_dentries(const char *dir)
+{
+ const char *bn;
+ struct regfile_iter *riter;
+ unsigned n;
+ char **result = NULL;
+
+ regfile_iter_new(dir, &riter);
+ for (
+ n = 0;
+ (bn = regfile_iter_basename(riter));
+ regfile_iter_next(riter), n++
+ ) {
+ result = xrealloc(result, (n + 2) * sizeof(*result));
+ result[n] = xstrdup(bn);
+ result[n + 1] = NULL;
+ }
+ return result;
+}
+
+static char **complete_ede(__attribute__ ((unused)) uint32_t cword,
+ __attribute__ ((unused)) unsigned arg0,
+ __attribute__ ((unused)) bool have_dd)
+{
+ char **result, *epidir = get_epidir();
+
+ result = complete_dentries(epidir);
+ free(epidir);
+ return result;
+}
+
+static char **complete_edx(__attribute__ ((unused)) uint32_t cword,
+ __attribute__ ((unused)) unsigned arg0,
+ __attribute__ ((unused)) bool have_dd)
+{
+ char **result, *xdir = get_xdir();
+
+ result = complete_dentries(xdir);
+ free(xdir);
+ return result;
+}
+
+static char **complete_std_opts(bool have_dd, const char * const *opts)
+{
+ if (have_dd)
+ print_option_list(opts);
+ else
+ print_option_list(supercmd_opts);
+ return NULL;
+}
+
+static char **complete_stats(__attribute__ ((unused)) uint32_t cword,
+ __attribute__ ((unused)) unsigned arg0, bool have_dd)
+{
+ const char * const opts[] = {LSG_TFORTUNE_STATS_OPTS, NULL};
+ return complete_std_opts(have_dd, opts);
+}
+
+static char **complete_lse(__attribute__ ((unused)) uint32_t cword,
+ __attribute__ ((unused)) unsigned arg0, bool have_dd)
+{
+ const char * const opts[] = {LSG_TFORTUNE_LSE_OPTS, NULL};
+ return complete_std_opts(have_dd, opts);
+}
+
+static char **complete_lst(__attribute__ ((unused)) uint32_t cword,
+ __attribute__ ((unused)) unsigned arg0, bool have_dd)
+{
+ const char * const opts[] = {LSG_TFORTUNE_LST_OPTS, NULL};
+ return complete_std_opts(have_dd, opts);
+}
+
+static char **complete_lsx(__attribute__ ((unused)) uint32_t cword,
+ __attribute__ ((unused)) unsigned arg0, bool have_dd)
+{
+ const char * const opts[] = {LSG_TFORTUNE_LSX_OPTS, NULL};
+ return complete_std_opts(have_dd, opts);
+}
+
+static char **complete_help(__attribute__ ((unused)) uint32_t cword,
+ __attribute__ ((unused)) unsigned arg0, bool have_dd)
+{
+ const char * const opts[] = {LSG_TFORTUNE_HELP_OPTS, NULL};
+
+ if (!have_dd)
+ print_option_list(supercmd_opts);
+ else
+ print_option_list(opts);
+ print_zero_terminated_list(subcommand_names);
+ return NULL;
+}
+
+static char **complete_print(uint32_t cword, unsigned arg0, bool have_dd)
+{
+ const char * const short_opts[] = {LSG_TFORTUNE_PRINT_SHORT_OPTS, NULL};
+ const char * const long_opts[] = {LSG_TFORTUNE_PRINT_LONG_OPTS, NULL};
+ const char * const opts[] = {LSG_TFORTUNE_PRINT_OPTS, NULL};
+ enum tf_word_type *word_types, prev_type;
+ const char *prev;
+ char **result, *xdir;
+
+ word_types = xmalloc(cword * sizeof(*word_types));
+ get_word_types(cword, arg0, short_opts, long_opts, word_types);
+ prev = lls_input(cword - 1, sublpr);
+ prev_type = word_types[cword - 1];
+ free(word_types);
+ switch (prev_type) {
+ case WT_COMMAND_NAME:
+ case WT_SHORT_OPT_WITHOUT_ARG:
+ case WT_OPTION_ARG:
+ case WT_LONG_OPT_WITHOUT_ARG:
+ case WT_DOUBLE_DASH:
+ if (!have_dd)
+ print_option_list(supercmd_opts);
+ else
+ print_option_list(opts);
+ return NULL;
+ case WT_SHORT_OPT_WITH_ARG:
+ if (strcmp(prev, "-x") == 0)
+ goto complete_expression;
+ break;
+ case WT_LONG_OPT_WITH_ARG:
+ if (strcmp(prev, "--expression") == 0)
+ goto complete_expression;
+ break;
+ default:
+ return NULL;
+ }
+complete_expression:
+ xdir = get_xdir();
+ result = complete_dentries(xdir);
+ free(xdir);
+ return result;
+}
+
+typedef char **(*completer)(uint32_t cword, unsigned arg0, bool have_dd);
+
+#define LSG_TFORTUNE_CMD(_name) complete_ ## _name
+static const completer completers[] = {LSG_TFORTUNE_COMMANDS};
+#undef LSG_TFORTUNE_CMD
+
+static int call_subcmd_completer(unsigned cmd_num, int arg0, uint32_t cword,
+ bool have_dd)
+{
+ char **c, **candidates = completers[cmd_num](cword, arg0, have_dd);
+
+ if (!candidates)
+ return 0;
+ for (c = candidates; *c; c++) {
+ printf("%s%c", *c, '\0');
+ free(*c);
+ }
+ free(candidates);
+ return 1;
+}
+
+static bool need_subcommand_completer(uint32_t cword, unsigned subcmd_idx,
+ const enum tf_word_type *word_types, bool have_dd)
+{
+ enum tf_word_type prev_type;
+ const char *word;
+
+ if (subcmd_idx == 0)
+ return false;
+ if (have_dd)
+ return true;
+ prev_type = word_types[cword - 1];
+ assert(prev_type != WT_COMMAND_NAME);
+ switch (prev_type) {
+ case WT_SHORT_OPT_WITH_ARG:
+ case WT_LONG_OPT_WITH_ARG:
+ case WT_DUNNO:
+ return false;
+ default:
+ break;
+ }
+ word = lls_input(cword, sublpr);
+ if (is_short_opt(word))
+ return false;
+ if (is_long_opt(word))
+ return false;
+ return true;
+}
+
+static int com_compgen(void)
+{
+ unsigned n;
+ uint32_t cword = OPT_UINT32_VAL(COMPGEN, CURRENT_WORD_INDEX);
+ int ret;
+ unsigned subcmd_idx;
+ const char *word, *prev;
+ const char * const short_opts[] = {LSG_TFORTUNE_TFORTUNE_SHORT_OPTS, NULL};
+ const char * const long_opts[] = {LSG_TFORTUNE_TFORTUNE_LONG_OPTS, NULL};
+ enum tf_word_type *word_types, prev_type;
+ bool have_dd;
+
+ if (cword == 0 || cword > lls_num_inputs(sublpr)) {
+ ERROR_LOG("current word index == %u!?\n", cword);
+ return -ERRNO_TO_TF_ERROR(EINVAL);
+ }
+ word_types = xmalloc(cword * sizeof(*word_types));
+ have_dd = get_word_types(cword, 0, short_opts, long_opts, word_types);
+ /*
+ * Locate the subcommand argument, if present. It is always the first
+ * non-option argument.
+ */
+ subcmd_idx = 0;
+ for (n = 1; n < cword; n++) {
+ if (word_types[n] != WT_NON_OPTION_ARG)
+ continue;
+ subcmd_idx = n;
+ break;
+ }
+ if (need_subcommand_completer(cword, subcmd_idx, word_types, have_dd)) {
+ free(word_types);
+ word = lls_input(subcmd_idx, sublpr);
+ ret = lls_lookup_subcmd(word, tfortune_suite, NULL);
+ if (ret < 0) /* invalid subcommand */
+ return 0;
+ return call_subcmd_completer(ret, subcmd_idx, cword, have_dd);
+ }
+ /* no subcommand */
+ prev_type = word_types[cword - 1];
+ prev = lls_input(cword - 1, sublpr);
+ free(word_types);
+ switch (prev_type) {
+ case WT_DUNNO:
+ return 0;
+ case WT_COMMAND_NAME:
+ case WT_SHORT_OPT_WITHOUT_ARG:
+ case WT_OPTION_ARG:
+ case WT_NON_OPTION_ARG:
+ case WT_LONG_OPT_WITHOUT_ARG:
+ if (!have_dd)
+ print_option_list(supercmd_opts);
+ /* fall through */
+ case WT_DOUBLE_DASH:
+ print_zero_terminated_list(subcommand_names);
+ break;
+ case WT_SHORT_OPT_WITH_ARG:
+ if (strcmp(prev, "-b") == 0) {
+ activate_dirname_completion();
+ return 1;
+ }
+ if (strcmp(prev, "-l") == 0) {
+ complete_loglevels();
+ return 1;
+ }
+ break;
+ case WT_LONG_OPT_WITH_ARG:
+ if (strcmp(prev, "--basename") == 0) {
+ activate_dirname_completion();
+ return 1;
+ }
+ if (strcmp(prev, "--loglevel") == 0) {
+ complete_loglevels();
+ return 1;
+ }
+ break;
+ }
+ return 0;
+}
+EXPORT_CMD_HANDLER(compgen);
+
+static int com_completer(void)
+{
+ printf("%s\n",
+ "_tfortune() \n"
+ "{ \n"
+ "local -i i offset=${TF_OFFSET:-0} \n"
+ "local w compopts= have_empty=false\n"
+ "local cur=\"${COMP_WORDS[$COMP_CWORD]}\" \n"
+
+ "i=0 \n"
+ "COMPREPLY=() \n"
+ "while read -d '' w; do \n"
+ "[[ -z \"$w\" ]] && { have_empty=true; continue; }\n"
+ "if [[ $have_empty == true ]]; then\n"
+ "compopt $w\n"
+ "else \n"
+ "[[ \"$w\" != \"$cur\"* ]] && continue \n"
+ "COMPREPLY[i]=\"$w\" \n"
+ "let i++ \n"
+ "fi \n"
+ "done < <(tfortune -- compgen --current-word-index \\\n"
+ "\"$((COMP_CWORD + offset))\" -- $TF_EXTRA \"${COMP_WORDS[@]}\")\n"
+ "} \n"
+ "complete -F _tfortune tfortune \n"
+ );
+ if (OPT_GIVEN(COMPLETER, ALIAS)) {
+ const char *ali = OPT_STRING_VAL(PRINT, EXPRESSION);
+ printf("alias %s=\"tfortune --\"\n", ali);
+ printf("_%s() { \n"
+ "COMP_WORDS[0]='--'\n"
+ "TF_EXTRA='tf' \n"
+ "TF_OFFSET=1 \n"
+ "_tfortune \"$@\" \n"
+ "unset TF_EXTRA TF_OFFSET\n"
+ "}\n",
+ ali
+ );
+ printf("complete -F _%s %s \n", ali, ali);
+ }
+ return 1;
+}
+EXPORT_CMD_HANDLER(completer);
+
+int main(int argc, char **argv)
+{
+ char *errctx;
+ int ret;
+ const struct lls_command *cmd = CMD_PTR(TFORTUNE), *subcmd;
+ const struct tf_user_data *ud;
+ unsigned num_inputs;
+
+ ret = lls_parse(argc, argv, cmd, &lpr, &errctx);
+ if (ret < 0) {
+ lopsub_error(ret, &errctx);
+ exit(EXIT_FAILURE);
+ }
+ loglevel_arg_val = OPT_UINT32_VAL(TFORTUNE, LOGLEVEL);
+ handle_help_and_version();
+ num_inputs = lls_num_inputs(lpr);
+ if (num_inputs == 0) {
+ show_subcommand_summary(true /* verbose */);
+ ret = 0;
+ goto free_lpr;
+ }
+ ret = lls_lookup_subcmd(argv[argc - num_inputs], tfortune_suite, &errctx);
+ if (ret < 0) {
+ ret = lopsub_error(ret, &errctx);
+ goto free_lpr;
+ }
+ subcmd = lls_cmd(ret, tfortune_suite);
+ ret = lls_parse(num_inputs, argv + argc - num_inputs, subcmd,
+ &sublpr, &errctx);
+ if (ret < 0) {
+ ret = lopsub_error(ret, &errctx);
+ goto free_lpr;
+ }
+ ud = lls_user_data(subcmd);
+ ret = ud->handler();
+ lls_free_parse_result(sublpr, subcmd);
+ if (ret < 0)
+ ERROR_LOG("%s\n", tf_strerror(-ret));
+free_lpr:
+ lls_free_parse_result(lpr, cmd);
+ exit(ret >= 0? EXIT_SUCCESS : EXIT_FAILURE);
+}