root/usr.sbin/makemandb/apropos.c
/*      $NetBSD: apropos.c,v 1.27 2023/08/03 07:49:23 rin Exp $ */
/*-
 * Copyright (c) 2011 Abhinav Upadhyay <er.abhinav.upadhyay@gmail.com>
 * All rights reserved.
 *
 * This code was developed as part of Google's Summer of Code 2011 program.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__RCSID("$NetBSD: apropos.c,v 1.27 2023/08/03 07:49:23 rin Exp $");

#include <err.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <util.h>

#include "apropos-utils.h"

typedef struct apropos_flags {
        char **sections;
        int nresults;
        int pager;
        int no_context;
        query_format format;
        int legacy;
        const char *machine;
        const char *manconf;
} apropos_flags;

typedef struct callback_data {
        int count;
        FILE *out;
        apropos_flags *aflags;
} callback_data;

static char *remove_stopwords(const char *);
static int query_callback(query_callback_args *);
__dead static void usage(void);

#define _PATH_PAGER     "/usr/bin/more -s"
#define SECTIONS_ARGS_LENGTH 4;

static void
parseargs(int argc, char **argv, struct apropos_flags *aflags)
{
        int ch;
        size_t sections_offset = 0;
        size_t sections_size = 0;
        char **sections = NULL;
        char *section;
        aflags->manconf = MANCONF;

#define RESIZE_SECTIONS(newsize) \
        if (sections == NULL || sections_offset > sections_size - 1) { \
                sections_size += newsize; \
                sections = erealloc(sections, sections_size * sizeof(*sections)); \
        }

        while ((ch = getopt(argc, argv, "123456789C:hilMmn:PprS:s:")) != -1) {
                switch (ch) {
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                        section = emalloc(2);
                        section[0] = ch;
                        section[1] = 0;
                        RESIZE_SECTIONS(SECTIONS_ARGS_LENGTH)
                        sections[sections_offset++] = section;
                        break;
                case 'C':
                        aflags->manconf = optarg;
                        break;
                case 'h':
                        aflags->format = APROPOS_HTML;
                        break;
                case 'i':
                        aflags->format = APROPOS_TERM;
                        break;
                case 'l':
                        aflags->legacy = 1;
                        aflags->no_context = 1;
                        aflags->format = APROPOS_NONE;
                        break;
                case 'M':
                        aflags->no_context = 1;
                        break;
                case 'm':
                        aflags->no_context = 0;
                        break;
                case 'n':
                        aflags->nresults = atoi(optarg);
                        break;
                case 'p':       // user wants a pager
                        aflags->pager = 1;
                        /*FALLTHROUGH*/
                case 'P':
                        aflags->format = APROPOS_PAGER;
                        break;
                case 'r':
                        aflags->format = APROPOS_NONE;
                        break;
                case 'S':
                        aflags->machine = optarg;
                        break;
                case 's':
                        RESIZE_SECTIONS(SECTIONS_ARGS_LENGTH)
                        sections[sections_offset++] = estrdup(optarg);
                        break;
                case '?':
                default:
                        usage();
                }
        }
        if (sections) {
                RESIZE_SECTIONS(1)
                sections[sections_offset] = NULL;
        }
        aflags->sections = sections;
}

int
main(int argc, char *argv[])
{
        query_args args;
        char *query = NULL;     // the user query
        char *errmsg = NULL;
        char *str;
        int pc = 0;
        int rc = 0;
        size_t i;
        int s;
        callback_data cbdata;
        cbdata.out = stdout;            // the default output stream
        cbdata.count = 0;
        apropos_flags aflags;
        aflags.sections = NULL;
        cbdata.aflags = &aflags;
        sqlite3 *db;
        setprogname(argv[0]);
        if (argc < 2)
                usage();

        memset(&aflags, 0, sizeof(aflags));

        if (!isatty(STDOUT_FILENO))
                aflags.format = APROPOS_NONE;
        else
                aflags.format = APROPOS_TERM;

        if ((str = getenv("APROPOS")) != NULL) {
                char **ptr = emalloc((strlen(str) + 2) * sizeof(*ptr));
#define WS "\t\n\r "
                ptr[0] = __UNCONST(getprogname());
                for (s = 1, str = strtok(str, WS); str;
                    str = strtok(NULL, WS), s++)
                        ptr[s] = str;
                ptr[s] = NULL;
                parseargs(s, ptr, &aflags);
                free(ptr);
                optreset = 1;
                optind = 1;
        }

        parseargs(argc, argv, &aflags);

        argc -= optind;
        argv += optind;

        if (!argc)
                usage();

        str = NULL;
        while (argc--)
                concat(&str, *argv++);
        query = remove_stopwords(lower(str));

        /*
         * If the query consisted only of stopwords and we removed all of
         * them, use the original query.
         */
        if (query == NULL)
                query = str;
        else
                free(str);

        if ((db = init_db(MANDB_READONLY, aflags.manconf)) == NULL)
                exit(EXIT_FAILURE);

        /* If user wants to page the output, then set some settings */
        if (aflags.pager) {
                const char *pager = getenv("PAGER");
                if (pager == NULL)
                        pager = _PATH_PAGER;

                /* Don't get killed by a broken pipe */
                signal(SIGPIPE, SIG_IGN);

                /* Open a pipe to the pager */
                if ((cbdata.out = popen(pager, "w")) == NULL) {
                        close_db(db);
                        err(EXIT_FAILURE, "pipe failed");
                }
        }

        args.search_str = query;
        args.sections = aflags.sections;
        args.legacy = aflags.legacy;
        args.nrec = aflags.nresults ? aflags.nresults : -1;
        args.offset = 0;
        args.machine = aflags.machine;
        args.callback = &query_callback;
        args.callback_data = &cbdata;
        args.errmsg = &errmsg;

        if (aflags.format == APROPOS_HTML) {
                fprintf(cbdata.out, "<html>\n<header>\n<title>apropos results "
                    "for %s</title></header>\n<body>\n<table cellpadding=\"4\""
                    "style=\"border: 1px solid #000000; border-collapse:"
                    "collapse;\" border=\"1\">\n", query);
        }
        rc = run_query(db, aflags.format, &args);
        if (aflags.format == APROPOS_HTML)
                fprintf(cbdata.out, "</table>\n</body>\n</html>\n");

        if (aflags.pager)
                pc = pclose(cbdata.out);
        free(query);

        if (aflags.sections) {
                for(i = 0; aflags.sections[i]; i++)
                        free(aflags.sections[i]);
                free(aflags.sections);
        }

        close_db(db);
        if (errmsg) {
                warnx("%s", errmsg);
                free(errmsg);
                exit(EXIT_FAILURE);
        }

        if (pc == -1)
                err(EXIT_FAILURE, "pclose error");

        /*
         * Something wrong with the database, writing output, or a non-existent
         * pager.
         */
        if (rc < 0)
                exit(EXIT_FAILURE);

        if (cbdata.count == 0) {
                warnx("No relevant results obtained.\n"
                    "Please make sure that you spelled all the terms correctly "
                    "or try using different keywords.");
        }
        return 0;
}

/*
 * query_callback --
 *  Callback function for run_query.
 *  It simply outputs the results from run_query. If the user specified the -p
 *  option, then the output is sent to a pager, otherwise stdout is the default
 *  output stream.
 */
static int
query_callback(query_callback_args *qargs)
{
        callback_data *cbdata = (callback_data *) qargs->other_data;
        FILE *out = cbdata->out;
        cbdata->count++;
        if (cbdata->aflags->format != APROPOS_HTML) {
            fprintf(out, cbdata->aflags->legacy ? "%s(%s) - %s\n" :
                "%s (%s)\t%s\n", qargs->name, qargs->section, qargs->name_desc);
            if (cbdata->aflags->no_context == 0)
                    fprintf(out, "%s\n\n", qargs->snippet);
        } else {
            fprintf(out, "<tr><td>%s(%s)</td><td>%s</td></tr>\n", qargs->name,
                qargs->section, qargs->name_desc);
            if (cbdata->aflags->no_context == 0)
                    fprintf(out, "<tr><td colspan=2>%s</td></tr>\n", qargs->snippet);
        }

        return fflush(out);
}

#include "stopwords.c"

/*
 * remove_stopwords--
 *  Scans the query and removes any stop words from it.
 *  Returns the modified query or NULL, if it contained only stop words.
 */

static char *
remove_stopwords(const char *query)
{
        size_t len, idx;
        char *output, *buf;
        const char *sep, *next;

        output = buf = emalloc(strlen(query) + 1);

        for (; query[0] != '\0'; query = next) {
                sep = strchr(query, ' ');
                if (sep == NULL) {
                        len = strlen(query);
                        next = query + len;
                } else {
                        len = sep - query;
                        next = sep + 1;
                }
                if (len == 0)
                        continue;
                idx = stopwords_hash(query, len);
                if (memcmp(stopwords[idx], query, len) == 0 &&
                    stopwords[idx][len] == '\0')
                        continue;
                memcpy(buf, query, len);
                buf += len;
                *buf++ = ' ';
        }

        if (output == buf) {
                free(output);
                return NULL;
        }
        buf[-1] = '\0';
        return output;
}

/*
 * usage --
 *      print usage message and die
 */
static void
usage(void)
{
        fprintf(stderr, "Usage: %s [-123456789ilMmpr] [-C path] [-n results] "
            "[-S machine] [-s section] query\n",
            getprogname());
        exit(1);
}