diff options
author | markm <markm@FreeBSD.org> | 2002-05-18 09:19:08 +0000 |
---|---|---|
committer | markm <markm@FreeBSD.org> | 2002-05-18 09:19:08 +0000 |
commit | 7528001bd00aabf85cdf82b67961536208ea5c9f (patch) | |
tree | 13ad193e090d73a66e9cbb476cec92afc65d0279 /usr.bin | |
parent | 768f23ab17cc7b567acc41b7e4be3b92579b77f9 (diff) | |
download | FreeBSD-src-7528001bd00aabf85cdf82b67961536208ea5c9f.zip FreeBSD-src-7528001bd00aabf85cdf82b67961536208ea5c9f.tar.gz |
Add C rewrites of catman and makewhatis. These aren't quite as
the submitter supplied them, as I did some WARNS=n fixups (mostly
const-ification).
Submitted by: John Rochester <john@jrochester.org>
Diffstat (limited to 'usr.bin')
-rw-r--r-- | usr.bin/Makefile | 2 | ||||
-rw-r--r-- | usr.bin/catman/Makefile | 5 | ||||
-rw-r--r-- | usr.bin/catman/catman.1 | 103 | ||||
-rw-r--r-- | usr.bin/catman/catman.c | 784 | ||||
-rw-r--r-- | usr.bin/makewhatis/Makefile | 8 | ||||
-rw-r--r-- | usr.bin/makewhatis/makewhatis.1 | 122 | ||||
-rw-r--r-- | usr.bin/makewhatis/makewhatis.c | 1011 |
7 files changed, 2035 insertions, 0 deletions
diff --git a/usr.bin/Makefile b/usr.bin/Makefile index 4e8fd11..332d9b8 100644 --- a/usr.bin/Makefile +++ b/usr.bin/Makefile @@ -19,6 +19,7 @@ SUBDIR= apply \ c89 \ calendar \ cap_mkdb \ + catman \ chat \ checknr \ chflags \ @@ -95,6 +96,7 @@ SUBDIR= apply \ m4 \ mail \ make \ + makewhatis \ mesg \ minigzip \ mkdep \ diff --git a/usr.bin/catman/Makefile b/usr.bin/catman/Makefile new file mode 100644 index 0000000..ab4c014 --- /dev/null +++ b/usr.bin/catman/Makefile @@ -0,0 +1,5 @@ +# $FreeBSD$ + +PROG= catman + +.include <bsd.prog.mk> diff --git a/usr.bin/catman/catman.1 b/usr.bin/catman/catman.1 new file mode 100644 index 0000000..d019387 --- /dev/null +++ b/usr.bin/catman/catman.1 @@ -0,0 +1,103 @@ +.\" Copyright (c) 2002 John Rochester +.\" All rights reserved. +.\" +.\" 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 AUTHOR 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 AUTHOR 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. +.\" +.\" $FreeBSD$ +.\" +.\" Note: The date here should be updated whenever a non-trivial +.\" change is made to the manual page. +.Dd May 11, 2002 +.Dt MAKEWHATIS 1 +.Os +.Sh NAME +.Nm catman +.Nd "preformat man pages" +.Sh SYNOPSIS +.Nm +.Op Fl fnrvL +.Op Ar directories ... +.Sh DESCRIPTION +The +.Nm +utility preformats all the man pages in +.Ar directories +using the +.Ic "nroff -man" +command. +Directories may be separated by colons instead of spaces. +If no +.Ar directories +are specified, the contents of the +.Ev MANPATH +environment variable is used or if that is not set the default directory +.Pa /usr/share/man +is processed. +.Pp +The options are as follows: +.Bl -tag -width ".Fl L" +.It Fl f +forces all man pages to be reformatted even if the corresponding cat page +is newer. +.It Fl L +Process only localized subdirectories corresponding to the locale specified +in the standard environment variables. +.It Fl n +prints out what would be done instead of performing any formatting. +.It Fl r +scans for and removes "junk" files that are neither man pages nor their +corresponding formatted cat pages. +.It Fl v +makes +.Nm +more verbose about what it is doing. +.El +.Sh ENVIRONMENT +.Bl -tag -width ".Ev MANPATH" -compact +.It Ev LC_ALL , LC_CTYPE , LANG +These variables control what subdirectories will be processed if the +.Fl L +option is used. +.It Ev MANPATH +Determines the set of directories to be processed if none are given on +the command line. +.El +.Sh FILES +.Bl -tag -width ".Pa /usr/share/man" -compact +.It Pa /usr/share/man +Default directory to process if the +.Ev MANPATH +environment variable is not set. +.El +.Sh DIAGNOSTICS +.Ex -std +.Sh SEE ALSO +.Xr man 1 , +.Xr makewhatis 1 , +.Xr nroff 1 +.Sh HISTORY +A previous version of the +.Nm +command appeared in +.Fx 2.1 . +.Sh AUTHORS +.An John Rochester . diff --git a/usr.bin/catman/catman.c b/usr.bin/catman/catman.c new file mode 100644 index 0000000..f600b3c --- /dev/null +++ b/usr.bin/catman/catman.c @@ -0,0 +1,784 @@ +/*- + * Copyright (c) 2002 John Rochester + * All rights reserved. + * + * 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, + * in this position and unchanged. + * 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. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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> +__FBSDID("$FreeBSD$"); + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/param.h> + +#include <ctype.h> +#include <dirent.h> +#include <err.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define DEFAULT_MANPATH "/usr/share/man" + +#define TOP_LEVEL_DIR 0 /* signifies a top-level man directory */ +#define MAN_SECTION_DIR 1 /* signifies a man section directory */ +#define UNKNOWN 2 /* signifies an unclassifiable directory */ + +#define TEST_EXISTS 0x01 +#define TEST_DIR 0x02 +#define TEST_FILE 0x04 +#define TEST_READABLE 0x08 +#define TEST_WRITABLE 0x10 +#define TEST_EXECUTABLE 0x20 + +static int verbose; /* -v flag: be verbose with warnings */ +static int pretend; /* -n, -p flags: print out what would be done + instead of actually doing it */ +static int force; /* -f flag: force overwriting all cat pages */ +static int rm_junk; /* -r flag: remove garbage pages */ +static char *locale; /* user's locale if -L is used */ +static char *lang_locale; /* short form of locale */ +static int exit_code; /* exit code to use when finished */ + +/* + * -T argument for nroff + */ +static const char *nroff_device = "ascii"; + +/* + * Mapping from locale to nroff device + */ +static const char *locale_device[] = { + "KOI8-R", "koi8-r", + "ISO8859-1", "latin1", + "ISO_8859-1", "latin1", + "ISO8859-15", "latin1", + "ISO_8859-15", "latin1", + NULL +}; + +static uid_t uid; +static gid_t gids[NGROUPS_MAX]; +static int ngids; +static int starting_dir; +static char tmp_file[MAXPATHLEN]; +struct stat test_st; + +/* + * A hashtable is an array of chains composed of this entry structure. + */ +struct hash_entry { + ino_t inode_number; + dev_t device_number; + const char *data; + struct hash_entry *next; +}; + +#define HASHTABLE_ALLOC 16384 /* allocation for hashtable (power of 2) */ +#define HASH_MASK (HASHTABLE_ALLOC - 1) + +static struct hash_entry *visited[HASHTABLE_ALLOC]; +static struct hash_entry *links[HASHTABLE_ALLOC]; + +/* + * Inserts a string into a hashtable keyed by inode & device number. + */ +static void +insert_hashtable(struct hash_entry **table, + ino_t inode_number, + dev_t device_number, + const char *data) +{ + struct hash_entry *new_entry; + struct hash_entry **chain; + + new_entry = (struct hash_entry *) malloc(sizeof(struct hash_entry)); + if (new_entry == NULL) + err(1, "can't insert into hashtable"); + chain = &table[inode_number & HASH_MASK]; + new_entry->inode_number = inode_number; + new_entry->device_number = device_number; + new_entry->data = data; + new_entry->next = *chain; + *chain = new_entry; +} + +/* + * Finds a string in a hashtable keyed by inode & device number. + */ +static const char * +find_hashtable(struct hash_entry **table, + ino_t inode_number, + dev_t device_number) +{ + struct hash_entry *chain; + + chain = table[inode_number & HASH_MASK]; + while (chain != NULL) { + if (chain->inode_number == inode_number && + chain->device_number == device_number) + return chain->data; + chain = chain->next; + } + return NULL; +} + +static void +trap_signal(int sig __unused) +{ + if (tmp_file[0] != '\0') + unlink(tmp_file); + exit(1); +} + +/* + * Deals with junk files in the man or cat section directories. + */ +static void +junk(const char *mandir, const char *name, const char *reason) +{ + if (verbose) + fprintf(stderr, "%s/%s: %s\n", mandir, name, reason); + if (rm_junk) { + fprintf(stderr, "rm %s/%s\n", mandir, name); + if (!pretend && unlink(name) < 0) + warn("%s/%s", mandir, name); + } +} + +/* + * Returns TOP_LEVEL_DIR for .../man, MAN_SECTION_DIR for .../manXXX, + * and UNKNOWN for everything else. + */ +static int +directory_type(char *dir) +{ + char *p; + + for (;;) { + p = strrchr(dir, '/'); + if (p == NULL || p[1] != '\0') + break; + *p = '\0'; + } + if (p == NULL) + p = dir; + else + p++; + if (strncmp(p, "man", 3) == 0) { + p += 3; + if (*p == '\0') + return TOP_LEVEL_DIR; + while (isalnum(*p) || *p == '_') { + if (*++p == '\0') + return MAN_SECTION_DIR; + } + } + return UNKNOWN; +} + +/* + * Tests whether the given file name (without a preceding path) + * is a proper man page name (like "mk-amd-map.8.gz"). + * Only alphanumerics and '_' are allowed after the last '.' and + * the last '.' can't be the first or last characters. + */ +static int +is_manpage_name(char *name) +{ + char *lastdot = NULL; + char *n = name; + + while (*n != '\0') { + if (!isalnum(*n)) { + switch (*n) { + case '_': + break; + case '-': + case '+': + case '[': + case ':': + lastdot = NULL; + break; + case '.': + lastdot = n; + break; + default: + return 0; + } + } + n++; + } + return lastdot > name && lastdot + 1 < n; +} + +static int +is_gzipped(char *name) +{ + int len = strlen(name); + return len >= 4 && strcmp(&name[len - 3], ".gz") == 0; +} + +/* + * Converts manXXX to catXXX. + */ +static char * +get_cat_section(char *section) +{ + char *cat_section; + + cat_section = strdup(section); + strncpy(cat_section, "cat", 3); + return cat_section; +} + +/* + * Converts .../man/manXXX to .../man. + */ +static char * +get_mandir(char *section) +{ + char *slash; + char *mandir; + + slash = strrchr(section, '/'); + mandir = (char *) malloc(slash - section + 1); + strncpy(mandir, section, slash - section); + mandir[slash - section] = '\0'; + return mandir; +} + +/* + * Tests to see if the given directory has already been visited. + */ +static int +already_visited(char *mandir, char *dir, int count_visit) +{ + struct stat st; + + if (stat(dir, &st) < 0) { + if (mandir != NULL) + warn("%s/%s", mandir, dir); + else + warn("%s", dir); + exit_code = 1; + return 1; + } + if (find_hashtable(visited, st.st_ino, st.st_dev) != NULL) { + if (mandir != NULL) + warnx("already visited %s/%s", mandir, dir); + else + warnx("already visited %s", dir); + return 1; + } + if (count_visit) + insert_hashtable(visited, st.st_ino, st.st_dev, ""); + return 0; +} + +/* + * Returns a set of TEST_* bits describing a file's type and permissions. + * If mod_time isn't NULL, it will contain the file's modification time. + */ +static int +test_path(char *name, time_t *mod_time) +{ + int result; + + if (stat(name, &test_st) < 0) + return 0; + result = TEST_EXISTS; + if (mod_time != NULL) + *mod_time = test_st.st_mtime; + if (S_ISDIR(test_st.st_mode)) + result |= TEST_DIR; + else if (S_ISREG(test_st.st_mode)) + result |= TEST_FILE; + if (test_st.st_uid == uid) { + test_st.st_mode >>= 6; + } else { + int i; + for (i = 0; i < ngids; i++) { + if (test_st.st_gid == gids[i]) { + test_st.st_mode >>= 3; + break; + } + } + } + if (test_st.st_mode & S_IROTH) + result |= TEST_READABLE; + if (test_st.st_mode & S_IWOTH) + result |= TEST_WRITABLE; + if (test_st.st_mode & S_IXOTH) + result |= TEST_EXECUTABLE; + return result; +} + +/* + * Checks whether a file is a symbolic link. + */ +static int +is_symlink(char *path) +{ + struct stat st; + + return lstat(path, &st) >= 0 && S_ISLNK(st.st_mode); +} + +/* + * Tests to see if the given directory can be written to. + */ +static void +check_writable(char *mandir) +{ + if (verbose && !(test_path(mandir, NULL) & TEST_WRITABLE)) + fprintf(stderr, "%s: not writable - will only be able to write to existing cat directories\n", mandir); +} + +/* + * If the directory exists, attempt to make it writable, otherwise + * attempt to create it. + */ +static int +make_writable_dir(char *mandir, char *dir) +{ + int test; + + if ((test = test_path(dir, NULL)) != 0) { + if (!(test & TEST_WRITABLE) && chmod(dir, 0755) < 0) { + warn("%s/%s: chmod", mandir, dir); + exit_code = 1; + return 0; + } + } else { + if (verbose || pretend) + fprintf(stderr, "mkdir %s\n", dir); + if (!pretend) { + unlink(dir); + if (mkdir(dir, 0755) < 0) { + warn("%s/%s: mkdir", mandir, dir); + exit_code = 1; + return 0; + } + } + } + return 1; +} + +/* + * Processes a single man page source by using nroff to create + * the preformatted cat page. + */ +static void +process_page(char *mandir, char *src, char *cat, int src_gzipped) +{ + int src_test, cat_test; + time_t src_mtime, cat_mtime; + char cmd[MAXPATHLEN]; + dev_t src_dev; + ino_t src_ino; + const char *link_name; + + src_test = test_path(src, &src_mtime); + if (!(src_test & (TEST_FILE|TEST_READABLE))) { + if (!(src_test & TEST_DIR)) { + warnx("%s/%s: unreadable", mandir, src); + exit_code = 1; + if (rm_junk && is_symlink(src)) + junk(mandir, src, "bogus symlink"); + } + return; + } + src_dev = test_st.st_dev; + src_ino = test_st.st_ino; + cat_test = test_path(cat, &cat_mtime); + if (cat_test & (TEST_FILE|TEST_READABLE)) { + if (!force && cat_mtime >= src_mtime) { + if (verbose) { + fprintf(stderr, "\t%s/%s: up to date\n", + mandir, src); + } + return; + } + } + /* + * Is the man page a link to one we've already processed? + */ + if ((link_name = find_hashtable(links, src_ino, src_dev)) != NULL) { + if (verbose || pretend) { + fprintf(stderr, "%slink %s -> %s\n", + verbose ? "\t" : "", cat, link_name); + } + if (!pretend) + link(link_name, cat); + return; + } + insert_hashtable(links, src_ino, src_dev, strdup(cat)); + if (verbose || pretend) { + fprintf(stderr, "%sformat %s -> %s\n", + verbose ? "\t" : "", src, cat); + if (pretend) + return; + } + snprintf(tmp_file, sizeof tmp_file, "%s.tmp", cat); + snprintf(cmd, sizeof cmd, + "%scat %s | tbl | nroff -T%s -man | col | gzip -cn > %s.tmp", + src_gzipped ? "z" : "", src, nroff_device, cat); + if (system(cmd) != 0) + err(1, "formatting pipeline"); + if (rename(tmp_file, cat) < 0) + warn("%s", cat); + tmp_file[0] = '\0'; +} + +/* + * Scan the man section directory for pages and process each one, + * then check for junk in the corresponding cat section. + */ +static void +scan_section(char *mandir, char *section, char *cat_section) +{ + struct dirent **entries; + char **expected = NULL; + int npages; + int nexpected = 0; + int i, e; + char *page_name; + char page_path[MAXPATHLEN]; + char cat_path[MAXPATHLEN]; + char gzip_path[MAXPATHLEN]; + + /* + * scan the man section directory for pages + */ + npages = scandir(section, &entries, NULL, alphasort); + if (npages < 0) { + warn("%s/%s", mandir, section); + exit_code = 1; + return; + } + if (verbose || rm_junk) { + /* + * Maintain a list of all cat pages that should exist, + * corresponding to existing man pages. + */ + expected = (char **) calloc(npages, sizeof(char *)); + } + for (i = 0; i < npages; free(entries[i++])) { + page_name = entries[i]->d_name; + snprintf(page_path, sizeof page_path, "%s/%s", section, + page_name); + if (!is_manpage_name(page_name)) { + if (!(test_path(page_path, NULL) & TEST_DIR)) { + junk(mandir, page_path, + "invalid man page name"); + } + continue; + } + if (is_gzipped(page_name)) { + snprintf(cat_path, sizeof cat_path, "%s/%s", + cat_section, page_name); + if (expected != NULL) + expected[nexpected++] = strdup(page_name); + process_page(mandir, page_path, cat_path, 1); + } else { + /* + * We've got an uncompressed man page, + * check to see if there's a (preferred) + * compressed one. + */ + snprintf(gzip_path, sizeof gzip_path, "%s.gz", + page_path); + if (test_path(gzip_path, NULL) != 0) { + junk(mandir, page_path, + "man page unused due to existing .gz"); + } else { + if (verbose) { + fprintf(stderr, + "warning, %s is uncompressed\n", + page_path); + } + snprintf(cat_path, sizeof cat_path, "%s/%s.gz", + cat_section, page_name); + if (expected != NULL) { + asprintf(&expected[nexpected++], + "%s.gz", page_name); + } + process_page(mandir, page_path, cat_path, 0); + } + } + } + free(entries); + if (expected == NULL) + return; + /* + * scan cat sections for junk + */ + npages = scandir(cat_section, &entries, NULL, alphasort); + e = 0; + for (i = 0; i < npages; free(entries[i++])) { + const char *junk_reason; + int cmp = 1; + + page_name = entries[i]->d_name; + if (strcmp(page_name, ".") == 0 || strcmp(page_name, "..") == 0) + continue; + /* + * Keep the index into the expected cat page list + * ahead of the name we've found. + */ + while (e < nexpected && + (cmp = strcmp(page_name, expected[e])) > 0) + free(expected[e++]); + if (cmp == 0) + continue; + /* we have an unexpected page */ + if (!is_manpage_name(page_name)) { + junk_reason = "invalid cat page name"; + } else if (!is_gzipped(page_name) && e + 1 < nexpected && + strncmp(page_name, expected[e + 1], strlen(page_name)) == 0 && + strlen(expected[e + 1]) == strlen(page_name) + 3) { + junk_reason = "cat page unused due to existing .gz"; + } else + junk_reason = "cat page without man page"; + snprintf(cat_path, sizeof cat_path, "%s/%s", cat_section, + page_name); + junk(mandir, cat_path, junk_reason); + } + free(entries); + while (e < nexpected) + free(expected[e++]); + free(expected); +} + + +/* + * Processes a single man section. + */ +static void +process_section(char *mandir, char *section) +{ + char *cat_section; + + if (already_visited(mandir, section, 1)) + return; + if (verbose) + fprintf(stderr, " section %s\n", section); + cat_section = get_cat_section(section); + if (make_writable_dir(mandir, cat_section)) + scan_section(mandir, section, cat_section); +} + +static int +select_sections(struct dirent *entry) +{ + return directory_type(entry->d_name) == MAN_SECTION_DIR; +} + +/* + * Processes a single top-level man directory. If section isn't NULL, + * it will only process that section sub-directory, otherwise it will + * process all of them. + */ +static void +process_mandir(char *dir_name, char *section) +{ + fchdir(starting_dir); + if (already_visited(NULL, dir_name, section == NULL)) + return; + check_writable(dir_name); + if (verbose) + fprintf(stderr, "man directory %s\n", dir_name); + if (pretend) + fprintf(stderr, "cd %s\n", dir_name); + if (chdir(dir_name) < 0) { + warn("%s: chdir", dir_name); + exit_code = 1; + return; + } + if (section != NULL) { + process_section(dir_name, section); + } else { + struct dirent **entries; + int nsections; + int i; + + nsections = scandir(".", &entries, select_sections, alphasort); + if (nsections < 0) { + warn("%s", dir_name); + exit_code = 1; + return; + } + for (i = 0; i < nsections; i++) { + process_section(dir_name, entries[i]->d_name); + free(entries[i]); + } + free(entries); + } +} + +/* + * Processes one argument, which may be a colon-separated list of + * directories. + */ +static void +process_argument(const char *arg) +{ + char *dir; + char *mandir; + char *parg; + + parg = strdup(arg); + if (parg == NULL) + err(1, "out of memory"); + while ((dir = strsep(&parg, ":")) != NULL) { + switch (directory_type(dir)) { + case TOP_LEVEL_DIR: + if (locale != NULL) { + asprintf(&mandir, "%s/%s", dir, locale); + process_mandir(mandir, NULL); + free(mandir); + if (lang_locale != NULL) { + asprintf(&mandir, "%s/%s", dir, + lang_locale); + process_mandir(mandir, NULL); + free(mandir); + } + } else { + process_mandir(dir, NULL); + } + break; + case MAN_SECTION_DIR: { + mandir = get_mandir(dir); + process_mandir(mandir, dir); + break; + } + default: + warnx("%s: directory name not in proper man form", dir); + exit_code = 1; + } + } + free(parg); +} + +static void +determine_locale(void) +{ + char *sep; + + locale = getenv("LC_ALL"); + if (locale == NULL) + locale = getenv("LC_CTYPE"); + if (locale == NULL) + locale = getenv("LANG"); + if (locale == NULL) { + warnx("-L option used, but no locale in environment\n"); + return; + } + sep = strchr(locale, '_'); + if (sep != NULL && isupper(sep[1]) && isupper(sep[2])) { + asprintf(&lang_locale, "%.*s%s", sep - locale, locale, &sep[3]); + } + sep = strchr(locale, '.'); + if (sep != NULL) { + int i; + + sep++; + for (i = 0; locale_device[i] != NULL; i += 2) { + if (strcmp(sep, locale_device[i]) == 0) { + nroff_device = locale_device[i + 1]; + break; + } + } + } + if (verbose) + fprintf(stderr, "nroff device is %s\n", nroff_device); +} + +static void +usage(void) +{ + fprintf(stderr, "usage: %s [-fLnrv] [directories...]\n", getprogname()); + exit(1); +} + +int +main(int argc, char **argv) +{ + int opt; + extern int optind; + + if ((uid = getuid()) == 0) { + fprintf(stderr, "don't run %s as root, use:\n echo", argv[0]); + for (optind = 0; optind < argc; optind++) { + fprintf(stderr, " %s", argv[optind]); + } + fprintf(stderr, " | nice -5 su -m man\n"); + exit(1); + } + while ((opt = getopt(argc, argv, "vnfLrh")) != -1) { + switch (opt) { + case 'f': + force++; + break; + case 'L': + determine_locale(); + break; + case 'n': + pretend++; + break; + case 'r': + rm_junk++; + break; + case 'v': + verbose++; + break; + default: + usage(); + /* NOTREACHED */ + } + } + ngids = getgroups(NGROUPS_MAX, gids); + if ((starting_dir = open(".", 0)) < 0) { + err(1, "."); + } + umask(022); + signal(SIGINT, trap_signal); + signal(SIGHUP, trap_signal); + signal(SIGQUIT, trap_signal); + signal(SIGTERM, trap_signal); + if (optind == argc) { + const char *manpath = getenv("MANPATH"); + if (manpath == NULL) + manpath = DEFAULT_MANPATH; + process_argument(manpath); + } else { + while (optind < argc) + process_argument(argv[optind++]); + } + exit(exit_code); +} diff --git a/usr.bin/makewhatis/Makefile b/usr.bin/makewhatis/Makefile new file mode 100644 index 0000000..d3e632f --- /dev/null +++ b/usr.bin/makewhatis/Makefile @@ -0,0 +1,8 @@ +# $FreeBSD$ + +PROG= makewhatis + +DPADD+= ${LIBZ} +LDADD+= -lz + +.include <bsd.prog.mk> diff --git a/usr.bin/makewhatis/makewhatis.1 b/usr.bin/makewhatis/makewhatis.1 new file mode 100644 index 0000000..58fdb47 --- /dev/null +++ b/usr.bin/makewhatis/makewhatis.1 @@ -0,0 +1,122 @@ +.\" Copyright (c) 2002 John Rochester +.\" All rights reserved. +.\" +.\" 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 AUTHOR 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 AUTHOR 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. +.\" +.\" $FreeBSD$ +.\" +.\" Note: The date here should be updated whenever a non-trivial +.\" change is made to the manual page. +.Dd May 12, 2002 +.Dt MAKEWHATIS 1 +.Os +.Sh NAME +.Nm makewhatis +.Nd "create whatis database" +.Sh SYNOPSIS +.Nm +.Op Fl a +.Op Fl i Ar column +.Op Fl n Ar name +.Op Fl o Ar file +.Op Fl v +.Op Fl L +.Op Ar directories ... +.Sh DESCRIPTION +The +.Nm +utility collects the names and short descriptions from all the unformatted +man pages in the +.Ar directories +and puts them into a file used by the +.Xr whatis 1 +and +.Xr apropos 1 +commands. +Directories may be separated by colons instead of spaces. +If no +.Ar directories +are specified, the contents of the +.Ev MANPATH +environment variable will be used, or if that is not set the default directory +.Pa /usr/share/man +will be processed. +.Pp +The options are as follows: +.Bl -tag -width ".Fl i Ar column" +.It Fl a +Appends to the output file(s) instead of replacing them. The output +will be sorted with duplicate lines removed, but may have obsolete +entries. +.It Fl i Ar column +Indents the description by +.Ar column +characters. The default value is 24. +.It Fl n Ar name +Uses +.Ar name +instead of +.Pa whatis . +.It Fl o Ar file +Outputs all lines to the +.Ar file +instead of +.Pa */whatis . +.It Fl v +Makes +.Nm +more verbose about what it is doing. +.It Fl L +Process only localized subdirectories corresponding to the locale specified +in the standard environment variables. +.El +.Sh ENVIRONMENT +.Bl -tag -width ".Ev MANPATH" -compact +.It Ev LC_ALL , LC_CTYPE , LANG +These variables control what subdirectories will be processed if the +.Fl L +option is used. +.It Ev MANPATH +Determines the set of directories to be processed if none are given on +the command line. +.El +.Sh FILES +.Bl -tag -width ".Pa /usr/share/man" -compact +.It Pa /usr/share/man +Default directory to process if the +.Ev MANPATH +environment variable is not set. +.It Pa */man/whatis +The default output file. +.El +.Sh DIAGNOSTICS +.Ex -std +.Sh SEE ALSO +.Xr whatis 1 , +.Xr apropos 1 +.Sh HISTORY +A previous version of the +.Nm +command appeared in +.Fx 2.1 . +.Sh AUTHORS +.An John Rochester . diff --git a/usr.bin/makewhatis/makewhatis.c b/usr.bin/makewhatis/makewhatis.c new file mode 100644 index 0000000..6f4e9fa --- /dev/null +++ b/usr.bin/makewhatis/makewhatis.c @@ -0,0 +1,1011 @@ +/*- + * Copyright (c) 2002 John Rochester + * All rights reserved. + * + * 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, + * in this position and unchanged. + * 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. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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> +__FBSDID("$FreeBSD$"); + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/param.h> +#include <sys/queue.h> + +#include <ctype.h> +#include <dirent.h> +#include <err.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stringlist.h> +#include <unistd.h> +#include <zlib.h> + +#define DEFAULT_MANPATH "/usr/share/man" +#define LINE_ALLOC 4096 + +static char blank[] = ""; + +/* + * Information collected about each man page in a section. + */ +struct page_info { + char * filename; + char * name; + char * suffix; + int gzipped; + ino_t inode; +}; + +/* + * An entry kept for each visited directory. + */ +struct visited_dir { + dev_t device; + ino_t inode; + SLIST_ENTRY(visited_dir) next; +}; + +/* + * an expanding string + */ +struct sbuf { + char * content; /* the start of the buffer */ + char * end; /* just past the end of the content */ + char * last; /* the last allocated character */ +}; + +/* + * Removes the last amount characters from the sbuf. + */ +#define sbuf_retract(sbuf, amount) \ + ((sbuf)->end -= (amount)) +/* + * Returns the length of the sbuf content. + */ +#define sbuf_length(sbuf) \ + ((sbuf)->end - (sbuf)->content) + +typedef char *edited_copy(char *from, char *to, int length); + +static int append; /* -a flag: append to existing whatis */ +static int verbose; /* -v flag: be verbose with warnings */ +static int indent = 24; /* -i option: description indentation */ +static const char *whatis_name="whatis";/* -n option: the name */ +static char *common_output; /* -o option: the single output file */ +static char *locale; /* user's locale if -L is used */ +static char *lang_locale; /* short form of locale */ + +static int exit_code; /* exit code to use when finished */ +static SLIST_HEAD(, visited_dir) visited_dirs = + SLIST_HEAD_INITIALIZER(visited_dirs); + +/* + * While the whatis line is being formed, it is stored in whatis_proto. + * When finished, it is reformatted into whatis_final and then appended + * to whatis_lines. + */ +static struct sbuf *whatis_proto; +static struct sbuf *whatis_final; +static StringList *whatis_lines; /* collected output lines */ + +static char tmp_file[MAXPATHLEN]; /* path of temporary file, if any */ + +/* A set of possible names for the NAME man page section */ +static const char *name_section_titles[] = { + "NAME", "Name", "NAMN", "BEZEICHNUNG", "\xcc\xbe\xbe\xce", + "\xee\xe1\xfa\xf7\xe1\xee\xe9\xe5", NULL +}; + +/* A subset of the mdoc(7) commands to ignore */ +static char mdoc_commands[] = "ArDvErEvFlLiNmPa"; + +/* + * Frees a struct page_info and its content. + */ +static void +free_page_info(struct page_info *info) +{ + free(info->filename); + free(info->name); + free(info->suffix); + free(info); +} + +/* + * Allocates and fills in a new struct page_info given the + * name of the man section directory and the dirent of the file. + * If the file is not a man page, returns NULL. + */ +static struct page_info * +new_page_info(char *dir, struct dirent *dirent) +{ + struct page_info *info; + int basename_length; + char *suffix; + struct stat st; + + info = (struct page_info *) malloc(sizeof(struct page_info)); + if (info == NULL) + err(1, "malloc"); + basename_length = strlen(dirent->d_name); + suffix = &dirent->d_name[basename_length]; + asprintf(&info->filename, "%s/%s", dir, dirent->d_name); + if ((info->gzipped = basename_length >= 4 && strcmp(&dirent->d_name[basename_length - 3], ".gz") == 0)) { + suffix -= 3; + *suffix = '\0'; + } + for (;;) { + if (--suffix == dirent->d_name || !isalnum(*suffix)) { + if (*suffix == '.') + break; + if (verbose) + warnx("%s: invalid man page name", info->filename); + free(info->filename); + free(info); + return NULL; + } + } + *suffix++ = '\0'; + info->name = strdup(dirent->d_name); + info->suffix = strdup(suffix); + if (stat(info->filename, &st) < 0) { + warn("%s", info->filename); + free_page_info(info); + return NULL; + } + if (!S_ISREG(st.st_mode)) { + if (verbose && !S_ISDIR(st.st_mode)) + warnx("%s: not a regular file", info->filename); + free_page_info(info); + return NULL; + } + info->inode = st.st_ino; + return info; +} + +/* + * Reset an sbuf's length to 0. + */ +static void +sbuf_clear(struct sbuf *sbuf) +{ + sbuf->end = sbuf->content; +} + +/* + * Allocate a new sbuf. + */ +static struct sbuf * +new_sbuf(void) +{ + struct sbuf *sbuf = (struct sbuf *) malloc(sizeof(struct sbuf)); + sbuf->content = (char *) malloc(LINE_ALLOC); + sbuf->last = sbuf->content + LINE_ALLOC - 1; + sbuf_clear(sbuf); + return sbuf; +} + +/* + * Ensure that there is enough room in the sbuf for chars more characters. + */ +static void +sbuf_need(struct sbuf *sbuf, int nchars) +{ + /* let's assume we only need to double it, but check just in case */ + while (sbuf->end + nchars > sbuf->last) { + int alloc; + char *new_content; + + alloc = (sbuf->last - sbuf->content + 1) * 2; + new_content = (char *) malloc(alloc); + memcpy(new_content, sbuf->content, sbuf->end - sbuf->content); + sbuf->end = new_content + (sbuf->end - sbuf->content); + free(sbuf->content); + sbuf->content = new_content; + } +} + +/* + * Appends a string of a given length to the sbuf. + */ +static void +sbuf_append(struct sbuf *sbuf, const char *text, int length) +{ + if (length > 0) { + sbuf_need(sbuf, length); + memcpy(sbuf->end, text, length); + sbuf->end += length; + } +} + +/* + * Appends a null-terminated string to the sbuf. + */ +static void +sbuf_append_str(struct sbuf *sbuf, char *text) +{ + sbuf_append(sbuf, text, strlen(text)); +} + +/* + * Appends an edited null-terminated string to the sbuf. + */ +static void +sbuf_append_edited(struct sbuf *sbuf, char *text, edited_copy copy) +{ + int length = strlen(text); + if (length > 0) { + sbuf_need(sbuf, length); + sbuf->end = copy(text, sbuf->end, length); + } +} + +/* + * Strips any of a set of chars from the end of the sbuf. + */ +static void +sbuf_strip(struct sbuf *sbuf, const char *set) +{ + while (sbuf->end > sbuf->content && strchr(set, sbuf->end[-1]) != NULL) + sbuf->end--; +} + +/* + * Returns the null-terminated string built by the sbuf. + */ +static char * +sbuf_content(struct sbuf *sbuf) +{ + *sbuf->end = '\0'; + return sbuf->content; +} + +/* + * Returns true if no man page exists in the directory with + * any of the names in the StringList. + */ +static int +no_page_exists(char *dir, StringList *names, char *suffix) +{ + char path[MAXPATHLEN]; + int i; + + for (i = 0; i < names->sl_cur; i++) { + snprintf(path, sizeof path, "%s/%s.%s.gz", dir, names->sl_str[i], suffix); + if (access(path, F_OK) < 0) { + path[strlen(path) - 3] = '\0'; + if (access(path, F_OK) < 0) + continue; + } + return 0; + } + return 1; +} + +static void +trap_signal(int sig __unused) +{ + if (tmp_file[0] != '\0') + unlink(tmp_file); + exit(1); +} + +/* + * Attempts to open an output file. Returns NULL if unsuccessful. + */ +static FILE * +open_output(char *name) +{ + FILE *output; + + whatis_lines = sl_init(); + if (append) { + char line[LINE_ALLOC]; + + output = fopen(name, "r"); + if (output == NULL) { + warn("%s", name); + exit_code = 1; + return NULL; + } + while (fgets(line, sizeof line, output) != NULL) { + line[strlen(line) - 1] = '\0'; + sl_add(whatis_lines, strdup(line)); + } + } + if (common_output == NULL) { + snprintf(tmp_file, sizeof tmp_file, "%s.tmp", name); + name = tmp_file; + } + output = fopen(name, "w"); + if (output == NULL) { + warn("%s", name); + exit_code = 1; + return NULL; + } + return output; +} + +static int +linesort(const void *a, const void *b) +{ + return strcmp((const char *)(*(const char **)a), (const char *)(*(const char **)b)); +} + +/* + * Writes the unique sorted lines to the output file. + */ +static void +finish_output(FILE *output, char *name) +{ + int i; + char *prev = NULL; + + qsort(whatis_lines->sl_str, whatis_lines->sl_cur, sizeof(char *), linesort); + for (i = 0; i < whatis_lines->sl_cur; i++) { + char *line = whatis_lines->sl_str[i]; + if (i > 0 && strcmp(line, prev) == 0) + continue; + prev = line; + fputs(line, output); + putc('\n', output); + } + fclose(output); + sl_free(whatis_lines, 1); + if (common_output == NULL) { + rename(tmp_file, name); + unlink(tmp_file); + } +} + +static FILE * +open_whatis(char *mandir) +{ + char filename[MAXPATHLEN]; + + snprintf(filename, sizeof filename, "%s/%s", mandir, whatis_name); + return open_output(filename); +} + +static void +finish_whatis(FILE *output, char *mandir) +{ + char filename[MAXPATHLEN]; + + snprintf(filename, sizeof filename, "%s/%s", mandir, whatis_name); + finish_output(output, filename); +} + +/* + * Tests to see if the given directory has already been visited. + */ +static int +already_visited(char *dir) +{ + struct stat st; + struct visited_dir *visit; + + if (stat(dir, &st) < 0) { + warn("%s", dir); + exit_code = 1; + return 1; + } + SLIST_FOREACH(visit, &visited_dirs, next) { + if (visit->inode == st.st_ino && + visit->device == st.st_dev) { + warnx("already visited %s", dir); + return 1; + } + } + visit = (struct visited_dir *) malloc(sizeof(struct visited_dir)); + visit->device = st.st_dev; + visit->inode = st.st_ino; + SLIST_INSERT_HEAD(&visited_dirs, visit, next); + return 0; +} + +/* + * Removes trailing spaces from a string, returning a pointer to just + * beyond the new last character. + */ +static char * +trim_rhs(char *str) +{ + char *rhs = &str[strlen(str)]; + while (--rhs > str && isspace(*rhs)) + ; + *++rhs = '\0'; + return rhs; +} + +/* + * Returns a pointer to the next non-space character in the string. + */ +static char * +skip_spaces(char *s) +{ + while (*s != '\0' && isspace(*s)) + s++; + return s; +} + +/* + * Returns whether the string contains only digits. + */ +static int +only_digits(char *line) +{ + if (!isdigit(*line++)) + return 0; + while (isdigit(*line)) + line++; + return *line == '\0'; +} + +/* + * Returns whether the line is of one of the forms: + * .Sh NAME + * .Sh "NAME" + * etc. + * assuming that section_start is ".Sh". + */ +static int +name_section_line(char *line, const char *section_start) +{ + char *rhs; + const char **title; + + if (strncmp(line, section_start, 3) != 0) + return 0; + line = skip_spaces(line + 3); + rhs = trim_rhs(line); + if (*line == '"') { + line++; + if (*--rhs == '"') + *rhs = '\0'; + } + for (title = name_section_titles; *title != NULL; title++) + if (strcmp(*title, line) == 0) + return 1; + return 0; +} + +/* + * Copies characters while removing the most common nroff/troff + * markup: + * \(em, \(mi, \fR, \f(XX, \*p, \&, + */ +static char * +de_nroff_copy(char *from, char *to, int fromlen) +{ + char *from_end = &from[fromlen]; + while (from < from_end) { + switch (*from) { + case '\\': + switch (*++from) { + case '(': + if (strncmp(&from[1], "em", 2) == 0 || + strncmp(&from[1], "mi", 2) == 0) { + from += 3; + continue; + } + break; + case 'f': + if (*++from == '(') + from += 3; + else + from++; + continue; + case '*': + if (from[1] == 'p') { + from += 2; + continue; + } + break; + case '&': + from++; + continue; + } + break; + } + *to++ = *from++; + } + return to; +} + +/* + * Appends a string with the nroff formatting removed. + */ +static void +add_nroff(char *text) +{ + sbuf_append_edited(whatis_proto, text, de_nroff_copy); +} + +/* + * Appends "name(suffix), " to whatis_final. + */ +static void +add_whatis_name(char *name, char *suffix) +{ + if (*name != '\0') { + sbuf_append_str(whatis_final, name); + sbuf_append(whatis_final, "(", 1); + sbuf_append_str(whatis_final, suffix); + sbuf_append(whatis_final, "), ", 3); + } +} + +/* + * Processes an old-style man(7) line. This ignores commands with only + * a single number argument. + */ +static void +process_man_line(char *line) +{ + if (*line == '.') { + while (isalpha(*++line)) + ; + line = skip_spaces(line); + if (only_digits(line)) + return; + } else + line = skip_spaces(line); + if (*line != '\0') { + add_nroff(line); + sbuf_append(whatis_proto, " ", 1); + } +} + +/* + * Processes a new-style mdoc(7) line. + */ +static void +process_mdoc_line(char *line) +{ + int xref; + int arg = 0; + char *line_end = &line[strlen(line)]; + int orig_length = sbuf_length(whatis_proto); + char *next; + + if (*line == '\0') + return; + if (line[0] != '.' || !isupper(line[1]) || !islower(line[2])) { + add_nroff(skip_spaces(line)); + return; + } + xref = strncmp(line, ".Xr", 3) == 0; + line += 3; + while ((line = skip_spaces(line)) < line_end) { + if (*line == '"') { + next = ++line; + for (;;) { + next = strchr(next, '"'); + if (next == NULL || *next != '"') + break; + strcpy(next, &next[1]); + line_end--; + next++; + } + } else + next = strpbrk(line, " \t"); + if (next != NULL) + *next++ = '\0'; + else + next = line_end; + if (isupper(*line) && islower(line[1]) && line[2] == '\0') { + if (strcmp(line, "Ns") == 0) { + arg = 0; + line = next; + continue; + } + if (strstr(mdoc_commands, line) != NULL) { + line = next; + continue; + } + } + if (arg > 0 && strchr(",.:;?!)]", *line) == 0) { + if (xref) { + sbuf_append(whatis_proto, "(", 1); + add_nroff(line); + sbuf_append(whatis_proto, ")", 1); + xref = 0; + line = blank; + } else + sbuf_append(whatis_proto, " ", 1); + } + add_nroff(line); + arg++; + line = next; + } + if (sbuf_length(whatis_proto) > orig_length) + sbuf_append(whatis_proto, " ", 1); +} + +/* + * Collects a list of comma-separated names from the text. + */ +static void +collect_names(StringList *names, char *text) +{ + char *arg; + + for (;;) { + arg = text; + text = strchr(text, ','); + if (text != NULL) + *text++ = '\0'; + sl_add(names, arg); + if (text == NULL) + return; + if (*text == ' ') + text++; + } +} + +enum { STATE_UNKNOWN, STATE_MANSTYLE, STATE_MDOCNAME, STATE_MDOCDESC }; + +/* + * Processes a man page source into a single whatis line and adds it + * to whatis_lines. + */ +static void +process_page(struct page_info *page, char *section_dir) +{ + gzFile *in; + char buffer[4096]; + char *line; + StringList *names; + char *descr; + int state = STATE_UNKNOWN; + int i; + + sbuf_clear(whatis_proto); + if ((in = gzopen(page->filename, "r")) == NULL) { + warn("%s", page->filename); + exit_code = 1; + return; + } + while (gzgets(in, buffer, sizeof buffer) != NULL) { + line = buffer; + if (strncmp(line, ".\\\"", 3) == 0) /* ignore comments */ + continue; + switch (state) { + /* + * haven't reached the NAME section yet. + */ + case STATE_UNKNOWN: + if (name_section_line(line, ".SH")) + state = STATE_MANSTYLE; + else if (name_section_line(line, ".Sh")) + state = STATE_MDOCNAME; + continue; + /* + * Inside an old-style .SH NAME section. + */ + case STATE_MANSTYLE: + if (strncmp(line, ".SH", 3) == 0) + break; + trim_rhs(line); + if (strcmp(line, ".") == 0) + continue; + if (strncmp(line, ".IX", 3) == 0) { + line += 3; + line = skip_spaces(line); + } + process_man_line(line); + continue; + /* + * Inside a new-style .Sh NAME section (the .Nm part). + */ + case STATE_MDOCNAME: + trim_rhs(line); + if (strncmp(line, ".Nm", 3) == 0) { + process_mdoc_line(line); + continue; + } else { + if (strcmp(line, ".") == 0) + continue; + sbuf_append(whatis_proto, "- ", 2); + state = STATE_MDOCDESC; + } + /* fall through */ + /* + * Inside a new-style .Sh NAME section (after the .Nm-s). + */ + case STATE_MDOCDESC: + if (strncmp(line, ".Sh", 3) == 0) + break; + trim_rhs(line); + if (strcmp(line, ".") == 0) + continue; + process_mdoc_line(line); + continue; + } + break; + } + gzclose(in); + sbuf_strip(whatis_proto, " \t.-"); + line = sbuf_content(whatis_proto); + /* + * line now contains the appropriate data, but without + * the proper indentation or the section appended to each name. + */ + descr = strstr(line, " - "); + if (descr == NULL) { + descr = strchr(line, ' '); + if (descr == NULL) { + if (verbose) + fprintf(stderr, " ignoring junk description \"%s\"\n", line); + return; + } + *descr++ = '\0'; + } else { + *descr = '\0'; + descr += 3; + } + names = sl_init(); + collect_names(names, line); + sbuf_clear(whatis_final); + if (!sl_find(names, page->name) && no_page_exists(section_dir, names, page->suffix)) { + /* + * Add the page name since that's the only thing that + * man(1) will find. + */ + add_whatis_name(page->name, page->suffix); + } + for (i = 0; i < names->sl_cur; i++) + add_whatis_name(names->sl_str[i], page->suffix); + sl_free(names, 0); + sbuf_retract(whatis_final, 2); /* remove last ", " */ + while (sbuf_length(whatis_final) < indent) + sbuf_append(whatis_final, " ", 1); + sbuf_append(whatis_final, " - ", 3); + sbuf_append_str(whatis_final, skip_spaces(descr)); + sl_add(whatis_lines, strdup(sbuf_content(whatis_final))); +} + +/* + * Sorts pages first by inode number, then by name. + */ +static int +pagesort(const void *a, const void *b) +{ + struct page_info *p1 = *(struct page_info **) a; + struct page_info *p2 = *(struct page_info **) b; + if (p1->inode == p2->inode) + return strcmp(p1->name, p2->name); + return p1->inode - p2->inode; +} + +/* + * Processes a single man section. + */ +static void +process_section(char *section_dir) +{ + struct dirent **entries; + int nentries; + struct page_info **pages; + int npages = 0; + int i; + int prev_inode = 0; + + if (verbose) + fprintf(stderr, " %s\n", section_dir); + + /* + * scan the man section directory for pages + */ + nentries = scandir(section_dir, &entries, NULL, alphasort); + if (nentries < 0) { + warn("%s", section_dir); + exit_code = 1; + return; + } + /* + * collect information about man pages + */ + pages = (struct page_info **) calloc(nentries, sizeof(struct page_info *)); + for (i = 0; i < nentries; i++) { + struct page_info *info = new_page_info(section_dir, entries[i]); + if (info != NULL) + pages[npages++] = info; + free(entries[i]); + } + free(entries); + qsort(pages, npages, sizeof(struct page_info *), pagesort); + /* + * process each unique page + */ + for (i = 0; i < npages; i++) { + struct page_info *page = pages[i]; + if (page->inode != prev_inode) { + prev_inode = page->inode; + if (verbose) + fprintf(stderr, " reading %s\n", page->filename); + process_page(page, section_dir); + } else if (verbose) + fprintf(stderr, " skipping %s, duplicate\n", page->filename); + free_page_info(page); + } + free(pages); +} + +/* + * Returns whether the directory entry is a man page section. + */ +static int +select_sections(struct dirent *entry) +{ + char *p = &entry->d_name[3]; + + if (strncmp(entry->d_name, "man", 3) != 0) + return 0; + while (*p != '\0') { + if (!isalnum(*p++)) + return 0; + } + return 1; +} + +/* + * Processes a single top-level man directory by finding all the + * sub-directories named man* and processing each one in turn. + */ +static void +process_mandir(char *dir_name) +{ + struct dirent **entries; + int nsections; + FILE *fp = NULL; + int i; + + if (already_visited(dir_name)) + return; + if (verbose) + fprintf(stderr, "man directory %s\n", dir_name); + nsections = scandir(dir_name, &entries, select_sections, alphasort); + if (nsections < 0) { + warn("%s", dir_name); + exit_code = 1; + return; + } + if (common_output == NULL && (fp = open_whatis(dir_name)) == NULL) + return; + for (i = 0; i < nsections; i++) { + char section_dir[MAXPATHLEN]; + snprintf(section_dir, sizeof section_dir, "%s/%s", dir_name, entries[i]->d_name); + process_section(section_dir); + free(entries[i]); + } + free(entries); + if (common_output == NULL) + finish_whatis(fp, dir_name); +} + +/* + * Processes one argument, which may be a colon-separated list of + * directories. + */ +static void +process_argument(const char *arg) +{ + char *dir; + char *mandir; + char *parg; + + parg = strdup(arg); + if (parg == NULL) + err(1, "out of memory"); + while ((dir = strsep(&parg, ":")) != NULL) { + if (locale != NULL) { + asprintf(&mandir, "%s/%s", dir, locale); + process_mandir(mandir); + free(mandir); + if (lang_locale != NULL) { + asprintf(&mandir, "%s/%s", dir, lang_locale); + process_mandir(mandir); + free(mandir); + } + } else { + process_mandir(dir); + } + } + free(parg); +} + + +int +main(int argc, char **argv) +{ + int opt; + extern int optind; + extern char *optarg; + FILE *fp = NULL; + + while ((opt = getopt(argc, argv, "ai:n:o:vL")) != -1) { + switch (opt) { + case 'a': + append++; + break; + case 'i': + indent = atoi(optarg); + break; + case 'n': + whatis_name = optarg; + break; + case 'o': + common_output = optarg; + break; + case 'v': + verbose++; + break; + case 'L': + locale = getenv("LC_ALL"); + if (locale == NULL) + locale = getenv("LC_CTYPE"); + if (locale == NULL) + locale = getenv("LANG"); + if (locale != NULL) { + char *sep = strchr(locale, '_'); + if (sep != NULL && isupper(sep[1]) && + isupper(sep[2])) { + asprintf(&lang_locale, "%.*s%s", sep - locale, locale, &sep[3]); + } + } + break; + default: + fprintf(stderr, "usage: %s [-a] [-i indent] [-n name] [-o output_file] [-v] [-L] [directories...]\n", argv[0]); + exit(1); + } + } + + signal(SIGINT, trap_signal); + signal(SIGHUP, trap_signal); + signal(SIGQUIT, trap_signal); + signal(SIGTERM, trap_signal); + SLIST_INIT(&visited_dirs); + whatis_proto = new_sbuf(); + whatis_final = new_sbuf(); + + if (common_output != NULL && (fp = open_output(common_output)) == NULL) + err(1, "%s", common_output); + if (optind == argc) { + const char *manpath = getenv("MANPATH"); + if (manpath == NULL) + manpath = DEFAULT_MANPATH; + process_argument(manpath); + } else { + while (optind < argc) + process_argument(argv[optind++]); + } + if (common_output != NULL) + finish_output(fp, common_output); + exit(exit_code); +} |