diff options
-rw-r--r-- | apropos.1 | 144 | ||||
-rw-r--r-- | apropos.c | 50 | ||||
-rw-r--r-- | apropos_db.c | 432 | ||||
-rw-r--r-- | apropos_db.h | 21 |
4 files changed, 450 insertions, 197 deletions
@@ -22,15 +22,17 @@ .Nd search the manual page database .Sh SYNOPSIS .Nm -.Op Fl I .Op Fl S Ar arch .Op Fl s Ar section -.Op Fl t Ar types -.Ar pattern +.Ar expression... .Sh DESCRIPTION The .Nm -utility queries the manual page database. +utility queries a manual page database generated by +.Xr mandocdb 8 , +evaluating on +.Ar expression +for each file in the database. Its arguments are as follows: .Bl -tag -width Ds .It Fl S Ar arch @@ -40,52 +42,76 @@ Search only for a manual section. See .Xr man 1 for a listing of manual sections. -.It Fl I -Case-insensitive pattern matching. -.It Fl t Ar types -Consider only types of keyword match, where -.Ar types -is a comma-separated list consisting of the following: -.Ar name , -manual names; -.Ar func , -function names; -.Ar utility , -utility names; -.Ar incl , -include files; -.Ar var , -variable names; -.Ar stand , -standards ; -.Ar auth , -authors; -.Ar conf , -configuration strings; -.Ar desc , -descriptive text; -.Ar xref , -cross-references. -.Ar path , -file pathnames; -.Ar env , -environment variables; or -.Ar err , -error codes. -Specifying -.Ar all -will search for all types. -.It Ar key -The search key. +.El +.Pp +An +.Ar expression +consists of search terms joined by logical operators +.Fl a +.Pq and +and +.Fl o +.Pq or . +The +.Fl a +operator has precedence over +.Fl o +and both are evaluated left-to-right. +.Pp +.Bl -tag -width Ds +.It \&( Ar expr No \&) +True if the subexpression +.Ar expr +is true. +.It Ar expr1 Fl a Ar expr2 +True if both +.Ar expr1 +and +.Ar expr2 +are true (logical +.Qq and ) . +.It Ar expr1 Oo Fl o Oc Ar expr2 +True if +.Ar expr1 +and/or +.Ar expr2 +evaluate to true (logical +.Qq or ) . +.It Ar term +True if +.Ar term +is satisfied. +This has syntax +.Li [key[,key]*(=~)]?val , +where operand +.Li key +is an +.Xr mdoc 7 +macro to query and +.Li val +is its value. +Operator +.Li \&= +evaluates a substring, while +.Li \&~ +evaluates a regular expression. +.It Fl i Ar term +Same as +.Ar term , +but +.Ar term +is evaluated case-insensitively. .El .Pp By default, .Nm -searches for pattern matches of -.Ar pattern -in manual names and descriptions -and displays results sorted by manual title. -Output is formatted as +parses terms as case-sensitive regular expressions +.Pq the Li \&~ operator +over manual names and descriptions +.Pq the Li \&Nm No and Li \&Nd No macros . +Multiple terms imply pairwise +.Fl o . +Results are sorted by manual title, with output formatted as .Pp .D1 title(sec) \- description .Pp @@ -119,16 +145,34 @@ If an architecture is specified in the output, use .Ex -std .Sh EXAMPLES Search for +.Qq mdoc +within the manual name and description: +.Pp +.Dl $ apropos mdoc +.Pp +Two variants of searching for +.Qq mdoc , +.Qq roff , +or +.Qq man +within manual names and descriptions: +.Pp +.Dl $ apropos mdoc roff man +.Dl $ apropos mdoc \-o roff \-o man +.Pp +Search for .Qq optind -as a variable name in the library category: +and +.Qq optarg +as variable names in the library category: .Pp -.Dl $ apropos \-tvar \-s 3 optind +.Dl $ apropos \-s 3 \(dqVa~^optind$\(dq -a \(dqVa~^optarg$\(dq .Pp -Search for all manuals referencing the term +Search for all manuals referencing .Qq POSIX in any letter case: .Pp -.Dl $ apropos \-tall \-I posix +.Dl $ apropos \-\- \-i posix .\" .Sh DIAGNOSTICS .\" For sections 1, 4, 6, 7, & 8 only. .\" .Sh ERRORS @@ -29,7 +29,7 @@ #include "mandoc.h" static int cmp(const void *, const void *); -static void list(struct rec *, size_t, void *); +static void list(struct res *, size_t, void *); static void usage(void); static char *progname; @@ -38,8 +38,7 @@ int main(int argc, char *argv[]) { int ch; - size_t sz; - char *buf; + size_t terms; struct opts opts; struct expr *e; extern int optind; @@ -72,32 +71,11 @@ main(int argc, char *argv[]) if (0 == argc) return(EXIT_SUCCESS); - /* - * Collapse expressions into a single string. - * First count up the contained strings, adding a space at the - * end of each (plus nil-terminator). Then merge. - */ - - for (sz = 0, ch = 0; ch < argc; ch++) - sz += strlen(argv[ch]) + 1; - - buf = mandoc_malloc(++sz); - - for (*buf = '\0', ch = 0; ch < argc; ch++) { - strlcat(buf, argv[ch], sz); - strlcat(buf, " ", sz); - } - - buf[sz - 2] = '\0'; - - if (NULL == (e = exprcomp(buf))) { + if (NULL == (e = exprcomp(argc, argv, &terms))) { fprintf(stderr, "Bad expression\n"); - free(buf); return(EXIT_FAILURE); } - free(buf); - /* * Configure databases. * The keyword database is a btree that allows for duplicate @@ -105,18 +83,20 @@ main(int argc, char *argv[]) * The index database is a recno. */ - apropos_search(&opts, e, NULL, list); + ch = apropos_search(&opts, e, terms, NULL, list); exprfree(e); - return(EXIT_SUCCESS); + if (0 == ch) + fprintf(stderr, "%s: Database error\n", progname); + return(ch ? EXIT_SUCCESS : EXIT_FAILURE); } /* ARGSUSED */ static void -list(struct rec *res, size_t sz, void *arg) +list(struct res *res, size_t sz, void *arg) { int i; - qsort(res, sz, sizeof(struct rec), cmp); + qsort(res, sz, sizeof(struct res), cmp); for (i = 0; i < (int)sz; i++) printf("%s(%s%s%s) - %s\n", res[i].title, @@ -130,18 +110,14 @@ static int cmp(const void *p1, const void *p2) { - return(strcmp(((const struct rec *)p1)->title, - ((const struct rec *)p2)->title)); + return(strcmp(((const struct res *)p1)->title, + ((const struct res *)p2)->title)); } static void usage(void) { - fprintf(stderr, "usage: %s " - "[-I] " - "[-S arch] " - "[-s section] " - "EXPR\n", - progname); + fprintf(stderr, "usage: %s [-S arch] [-s section] " + "expression...\n", progname); } diff --git a/apropos_db.c b/apropos_db.c index 94985c9b..e0e48f33 100644 --- a/apropos_db.c +++ b/apropos_db.c @@ -32,11 +32,30 @@ #include "apropos_db.h" #include "mandoc.h" +struct rec { + struct res res; /* resulting record info */ + /* + * Maintain a binary tree for checking the uniqueness of `rec' + * when adding elements to the results array. + * Since the results array is dynamic, use offset in the array + * instead of a pointer to the structure. + */ + int lhs; + int rhs; + int matched; /* expression is true */ + int *matches; /* partial truth evaluations */ +}; + struct expr { - int regex; - int mask; - char *v; - regex_t re; + int regex; /* is regex? */ + int index; /* index in match array */ + int mask; /* type-mask */ + int cs; /* is case-sensitive? */ + int and; /* is rhs of logical AND? */ + char *v; /* search value */ + regex_t re; /* compiled re, if regex */ + struct expr *next; /* next in sequence */ + struct expr *subexpr; }; struct type { @@ -64,14 +83,22 @@ static const struct type types[] = { }; static DB *btree_open(void); -static int btree_read(const DBT *, const struct mchars *, char **); -static int exprexec(const struct expr *, char *, int); +static int btree_read(const DBT *, + const struct mchars *, char **); +static int expreval(const struct expr *, int *); +static void exprexec(const struct expr *, + const char *, int, struct rec *); +static int exprmark(const struct expr *, + const char *, int, int *); +static struct expr *exprexpr(int, char *[], int *, int *, size_t *); +static struct expr *exprterm(char *, int); static DB *index_open(void); static int index_read(const DBT *, const DBT *, const struct mchars *, struct rec *); static void norm_string(const char *, const struct mchars *, char **); static size_t norm_utf8(unsigned int, char[7]); +static void recfree(struct rec *); /* * Open the keyword mandoc-db database. @@ -300,13 +327,13 @@ index_read(const DBT *key, const DBT *val, left = val->size; cp = (char *)val->data; - rec->rec = *(recno_t *)key->data; + rec->res.rec = *(recno_t *)key->data; - INDEX_BREAD(rec->file); - INDEX_BREAD(rec->cat); - INDEX_BREAD(rec->title); - INDEX_BREAD(rec->arch); - INDEX_BREAD(rec->desc); + INDEX_BREAD(rec->res.file); + INDEX_BREAD(rec->res.cat); + INDEX_BREAD(rec->res.title); + INDEX_BREAD(rec->res.arch); + INDEX_BREAD(rec->res.desc); return(1); } @@ -314,38 +341,37 @@ index_read(const DBT *key, const DBT *val, * Search the mandocdb database for the expression "expr". * Filter out by "opts". * Call "res" with the results, which may be zero. + * Return 0 if there was a database error, else return 1. */ -void +int apropos_search(const struct opts *opts, const struct expr *expr, - void *arg, void (*res)(struct rec *, size_t, void *)) + size_t terms, void *arg, + void (*res)(struct res *, size_t, void *)) { - int i, len, root, leaf; + int i, rsz, root, leaf, mask, mlen, rc, ch; DBT key, val; DB *btree, *idx; struct mchars *mc; - int ch; char *buf; recno_t rec; - struct rec *recs; - struct rec srec; + struct rec *rs; + struct res *ress; + struct rec r; + rc = 0; root = -1; leaf = -1; btree = NULL; idx = NULL; mc = NULL; buf = NULL; - recs = NULL; - len = 0; - - memset(&srec, 0, sizeof(struct rec)); + rs = NULL; + rsz = 0; - /* XXX: error out with bad regexp? */ + memset(&r, 0, sizeof(struct rec)); mc = mchars_alloc(); - /* XXX: return fact that we've errored? */ - if (NULL == (btree = btree_open())) goto out; if (NULL == (idx = index_open())) @@ -362,7 +388,13 @@ apropos_search(const struct opts *opts, const struct expr *expr, if ( ! btree_read(&key, mc, &buf)) break; - if ( ! exprexec(expr, buf, *(int *)val.data)) + mask = *(int *)val.data; + + /* + * See if this keyword record matches any of the + * expressions we have stored. + */ + if ( ! exprmark(expr, buf, mask, NULL)) continue; memcpy(&rec, val.data + 4, sizeof(recno_t)); @@ -374,19 +406,31 @@ apropos_search(const struct opts *opts, const struct expr *expr, */ for (leaf = root; leaf >= 0; ) - if (rec > recs[leaf].rec && recs[leaf].rhs >= 0) - leaf = recs[leaf].rhs; - else if (rec < recs[leaf].rec && recs[leaf].lhs >= 0) - leaf = recs[leaf].lhs; + if (rec > rs[leaf].res.rec && + rs[leaf].rhs >= 0) + leaf = rs[leaf].rhs; + else if (rec < rs[leaf].res.rec && + rs[leaf].lhs >= 0) + leaf = rs[leaf].lhs; else break; - if (leaf >= 0 && recs[leaf].rec == rec) + /* + * If we find a record, see if it has already evaluated + * to true. If it has, great, just keep going. If not, + * try to evaluate it now and continue anyway. + */ + + if (leaf >= 0 && rs[leaf].res.rec == rec) { + if (0 == rs[leaf].matched) + exprexec(expr, buf, mask, &rs[leaf]); continue; + } /* - * Now we actually extract the manpage's metadata from - * the index database. + * We have a new file to examine. + * Extract the manpage's metadata from the index + * database, then begin partial evaluation. */ key.data = &rec; @@ -395,52 +439,62 @@ apropos_search(const struct opts *opts, const struct expr *expr, if (0 != (*idx->get)(idx, &key, &val, 0)) break; - srec.lhs = srec.rhs = -1; - if ( ! index_read(&key, &val, mc, &srec)) + r.lhs = r.rhs = -1; + if ( ! index_read(&key, &val, mc, &r)) break; - if (opts->cat && strcasecmp(opts->cat, srec.cat)) + /* XXX: this should be elsewhere, I guess? */ + + if (opts->cat && strcasecmp(opts->cat, r.res.cat)) continue; - if (opts->arch && strcasecmp(opts->arch, srec.arch)) + if (opts->arch && strcasecmp(opts->arch, r.res.arch)) continue; - recs = mandoc_realloc - (recs, (len + 1) * sizeof(struct rec)); + rs = mandoc_realloc + (rs, (rsz + 1) * sizeof(struct rec)); - memcpy(&recs[len], &srec, sizeof(struct rec)); + memcpy(&rs[rsz], &r, sizeof(struct rec)); + rs[rsz].matches = mandoc_calloc(terms, sizeof(int)); + exprexec(expr, buf, mask, &rs[rsz]); /* Append to our tree. */ if (leaf >= 0) { - if (rec > recs[leaf].rec) - recs[leaf].rhs = len; + if (rec > rs[leaf].res.rec) + rs[leaf].rhs = rsz; else - recs[leaf].lhs = len; + rs[leaf].lhs = rsz; } else - root = len; + root = rsz; - memset(&srec, 0, sizeof(struct rec)); - len++; + memset(&r, 0, sizeof(struct rec)); + rsz++; } + + /* + * If we haven't encountered any database errors, then construct + * an array of results and push them to the caller. + */ - if (1 == ch) - (*res)(recs, len, arg); + if (1 == ch) { + for (mlen = i = 0; i < rsz; i++) + if (rs[i].matched) + mlen++; + ress = mandoc_malloc(mlen * sizeof(struct res)); + for (mlen = i = 0; i < rsz; i++) + if (rs[i].matched) + memcpy(&ress[mlen++], &rs[i].res, + sizeof(struct res)); + (*res)(ress, mlen, arg); + free(ress); + rc = 1; + } - /* XXX: else? corrupt database error? */ out: - for (i = 0; i < len; i++) { - free(recs[i].file); - free(recs[i].cat); - free(recs[i].title); - free(recs[i].arch); - free(recs[i].desc); - } + for (i = 0; i < rsz; i++) + recfree(&rs[i]); - free(srec.file); - free(srec.cat); - free(srec.title); - free(srec.arch); - free(srec.desc); + recfree(&r); if (mc) mchars_free(mc); @@ -450,23 +504,148 @@ out: (*idx->close)(idx); free(buf); - free(recs); + free(rs); + return(rc); +} + +static void +recfree(struct rec *rec) +{ + + free(rec->res.file); + free(rec->res.cat); + free(rec->res.title); + free(rec->res.arch); + free(rec->res.desc); + + free(rec->matches); } struct expr * -exprcomp(char *buf) +exprcomp(int argc, char *argv[], size_t *tt) +{ + int pos, lvl; + struct expr *e; + + pos = lvl = 0; + *tt = 0; + + e = exprexpr(argc, argv, &pos, &lvl, tt); + + if (0 == lvl && pos >= argc) + return(e); + + exprfree(e); + return(NULL); +} + +/* + * Compile an array of tokens into an expression. + * An informal expression grammar is defined in apropos(1). + * Return NULL if we fail doing so. All memory will be cleaned up. + * Return the root of the expression sequence if alright. + */ +static struct expr * +exprexpr(int argc, char *argv[], int *pos, int *lvl, size_t *tt) +{ + struct expr *e, *first, *next; + int log; + + first = next = NULL; + + for ( ; *pos < argc; (*pos)++) { + e = next; + + /* + * Close out a subexpression. + */ + + if (NULL != e && 0 == strcmp(")", argv[*pos])) { + if (--(*lvl) < 0) + goto err; + break; + } + + /* + * Small note: if we're just starting, don't let "-a" + * and "-o" be considered logical operators: they're + * just tokens unless pairwise joining, in which case we + * record their existence (or assume "OR"). + */ + log = 0; + + if (NULL != e && 0 == strcmp("-a", argv[*pos])) + log = 1; + else if (NULL != e && 0 == strcmp("-o", argv[*pos])) + log = 2; + + if (log > 0 && ++(*pos) >= argc) + goto err; + + /* + * Now we parse the term part. This can begin with + * "-i", in which case the expression is case + * insensitive. + */ + + if (0 == strcmp("(", argv[*pos])) { + ++(*pos); + ++(*lvl); + next = mandoc_calloc(1, sizeof(struct expr)); + next->cs = 1; + next->subexpr = exprexpr(argc, argv, pos, lvl, tt); + if (NULL == next->subexpr) { + free(next); + next = NULL; + } + } else if (0 == strcmp("-i", argv[*pos])) { + if (++(*pos) >= argc) + goto err; + next = exprterm(argv[*pos], 0); + } else + next = exprterm(argv[*pos], 1); + + if (NULL == next) + goto err; + + next->and = log == 1; + next->index = (int)(*tt)++; + + /* Append to our chain of expressions. */ + + if (NULL == first) { + assert(NULL == e); + first = next; + } else { + assert(NULL != e); + e->next = next; + } + } + + return(first); +err: + exprfree(first); + return(NULL); +} + +/* + * Parse a terminal expression with the grammar as defined in + * apropos(1). + * Return NULL if we fail the parse. + */ +static struct expr * +exprterm(char *buf, int cs) { - struct expr *p; struct expr e; + struct expr *p; char *key; - int i, icase; + int i; - if ('\0' == *buf) - return(NULL); + memset(&e, 0, sizeof(struct expr)); - /* - * Choose regex or substring match. - */ + e.cs = cs; + + /* Choose regex or substring match. */ if (NULL == (e.v = strpbrk(buf, "=~"))) { e.regex = 0; @@ -476,18 +655,11 @@ exprcomp(char *buf) *e.v++ = '\0'; } - /* - * Determine the record types to search for. - */ + /* Determine the record types to search for. */ - icase = 0; e.mask = 0; if (buf < e.v) { while (NULL != (key = strsep(&buf, ","))) { - if ('i' == key[0] && '\0' == key[1]) { - icase = REG_ICASE; - continue; - } i = 0; while (types[i].mask && strcmp(types[i].name, key)) @@ -498,9 +670,11 @@ exprcomp(char *buf) if (0 == e.mask) e.mask = TYPE_Nm | TYPE_Nd; - if (e.regex && - regcomp(&e.re, e.v, REG_EXTENDED | REG_NOSUB | icase)) - return(NULL); + if (e.regex) { + i = REG_EXTENDED | REG_NOSUB | cs ? 0 : REG_ICASE; + if (regcomp(&e.re, e.v, i)) + return(NULL); + } e.v = mandoc_strdup(e.v); @@ -512,26 +686,96 @@ exprcomp(char *buf) void exprfree(struct expr *p) { + struct expr *pp; + + while (NULL != p) { + if (p->subexpr) + exprfree(p->subexpr); + if (p->regex) + regfree(&p->re); + free(p->v); + pp = p->next; + free(p); + p = pp; + } +} - if (NULL == p) - return; +static int +exprmark(const struct expr *p, const char *cp, int mask, int *ms) +{ + + for ( ; p; p = p->next) { + if (p->subexpr) { + if (exprmark(p->subexpr, cp, mask, ms)) + return(1); + continue; + } else if ( ! (mask & p->mask)) + continue; - if (p->regex) - regfree(&p->re); + if (p->regex) { + if (regexec(&p->re, cp, 0, NULL, 0)) + continue; + } else if (p->cs) { + if (NULL == strstr(cp, p->v)) + continue; + } else { + if (NULL == strcasestr(cp, p->v)) + continue; + } + + if (NULL == ms) + return(1); + else + ms[p->index] = 1; + } - free(p->v); - free(p); + return(0); } static int -exprexec(const struct expr *p, char *cp, int mask) +expreval(const struct expr *p, int *ms) { + int match; - if ( ! (mask & p->mask)) - return(0); + /* + * AND has precedence over OR. Analysis is left-right, though + * it doesn't matter because there are no side-effects. + * Thus, step through pairwise ANDs and accumulate their Boolean + * evaluation. If we encounter a single true AND collection or + * standalone term, the whole expression is true (by definition + * of OR). + */ + + for (match = 0; p && ! match; p = p->next) { + /* Evaluate a subexpression, if applicable. */ + if (p->subexpr && ! ms[p->index]) + ms[p->index] = expreval(p->subexpr, ms); + + match = ms[p->index]; + for ( ; p->next && p->next->and; p = p->next) { + /* Evaluate a subexpression, if applicable. */ + if (p->next->subexpr && ! ms[p->next->index]) + ms[p->next->index] = + expreval(p->next->subexpr, ms); + match = match && ms[p->next->index]; + } + } + + return(match); +} + +/* + * First, update the array of terms for which this expression evaluates + * to true. + * Second, logically evaluate all terms over the updated array of truth + * values. + * If this evaluates to true, mark the expression as satisfied. + */ +static void +exprexec(const struct expr *p, const char *cp, int mask, struct rec *r) +{ - if (p->regex) - return(0 == regexec(&p->re, cp, 0, NULL, 0)); - else - return(NULL != strcasestr(cp, p->v)); + assert(0 == r->matched); + exprmark(p, cp, mask, r->matches); + r->matched = expreval(p, r->matches); } diff --git a/apropos_db.h b/apropos_db.h index cc6e41d9..95ac1567 100644 --- a/apropos_db.h +++ b/apropos_db.h @@ -17,23 +17,13 @@ #ifndef APROPOS_H #define APROPOS_H -struct rec { +struct res { char *file; /* file in file-system */ char *cat; /* category (3p, 3, etc.) */ char *title; /* title (FOO, etc.) */ char *arch; /* arch (or empty string) */ char *desc; /* description (from Nd) */ unsigned int rec; /* record in index */ - /* - * By the time the apropos_search() callback is called, these - * are superfluous. - * Maintain a binary tree for checking the uniqueness of `rec' - * when adding elements to the results array. - * Since the results array is dynamic, use offset in the array - * instead of a pointer to the structure. - */ - int lhs; - int rhs; }; struct opts { @@ -45,11 +35,10 @@ __BEGIN_DECLS struct expr; -void apropos_search(const struct opts *, - const struct expr *, void *, - void (*)(struct rec *, size_t, void *)); - -struct expr *exprcomp(char *); +int apropos_search(const struct opts *, + const struct expr *, size_t, void *, + void (*)(struct res *, size_t, void *)); +struct expr *exprcomp(int, char *[], size_t *); void exprfree(struct expr *); __END_DECLS |