/* SPDX-License-Identifier: GPL-3.0-only */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(const 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; struct stat s; basedir = get_basedir(); xasprintf(&epidir, "%s/epigrams", basedir); free(basedir); if (!OPT_GIVEN(TFORTUNE, BASEDIR) && stat(epidir, &s) < 0) { free(epidir); epidir = xstrdup(DATADIR "/tfortunes/epigrams"); INFO_LOG("falling back to system-wide epidir %s\n", epidir); } 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 = xrealloc(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; struct regfile_iter *riter; 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) ) { free((char *)itemp->key); num_tags += (long unsigned)itemp->object; } num_unique_tags = linhash_num_items(tagtab); linhash_iterator_free(liter); 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", num_epi_files > 0? (float)num_epis / num_epi_files : 0); printf("average number of tags per epigram. %8.02f\n", num_epis > 0? (float)num_tags / num_epis : 0); printf("average number of tag recurrence... %8.02f\n", num_unique_tags > 0? (float)num_tags / num_unique_tags : 0); if (OPT_GIVEN(STATS, VERBOSE)) { char *lh_stats = linhash_statistics(tagtab); printf("\nlinear hashing statistics:\n%s\n", lh_stats); free(lh_stats); } linhash_free(tagtab); 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(", "); if (n > 70) { printf("\n\t"); n = 8; } n += printf("%s", subcommand_names[i]); } 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); const char *GET_VERSION(void); static void handle_help_and_version(void) { int i; char *help; const struct lls_command *cmd; if (OPT_GIVEN(TFORTUNE, VERSION)) { printf(PACKAGE " %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" "\n" "Web page: " PACKAGE_HOMEPAGE "\n" "Clone URL: " CLONE_URL "\n" "Gitweb: " GITWEB_URL "\n" "Author's Home Page: " HOME_URL "\n" "Send feedback to: " AUTHOR " <" EMAIL ">\n" , GET_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, "--basedir") == 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); }