catman.c revision 139185
1/*-
2 * Copyright (c) 2002 John Rochester
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer,
10 *    in this position and unchanged.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 * 3. The name of the author may not be used to endorse or promote products
15 *    derived from this software without specific prior written permission
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include <sys/cdefs.h>
30__FBSDID("$FreeBSD: head/usr.bin/catman/catman.c 139185 2004-12-22 16:04:58Z ru $");
31
32#include <sys/types.h>
33#include <sys/stat.h>
34#include <sys/param.h>
35
36#include <ctype.h>
37#include <dirent.h>
38#include <err.h>
39#include <fcntl.h>
40#include <locale.h>
41#include <langinfo.h>
42#include <libgen.h>
43#include <stdio.h>
44#include <stdlib.h>
45#include <string.h>
46#include <unistd.h>
47
48#define DEFAULT_MANPATH		"/usr/share/man"
49
50#define TOP_LEVEL_DIR	0	/* signifies a top-level man directory */
51#define MAN_SECTION_DIR	1	/* signifies a man section directory */
52#define UNKNOWN		2	/* signifies an unclassifiable directory */
53
54#define TEST_EXISTS	0x01
55#define TEST_DIR	0x02
56#define TEST_FILE	0x04
57#define TEST_READABLE	0x08
58#define TEST_WRITABLE	0x10
59#define TEST_EXECUTABLE	0x20
60
61static int verbose;		/* -v flag: be verbose with warnings */
62static int pretend;		/* -n, -p flags: print out what would be done
63				   instead of actually doing it */
64static int force;		/* -f flag: force overwriting all cat pages */
65static int rm_junk;		/* -r flag: remove garbage pages */
66static char *locale;		/* user's locale if -L is used */
67static char *lang_locale;	/* short form of locale */
68static const char *machine;
69static int exit_code;		/* exit code to use when finished */
70
71/*
72 * -T argument for nroff
73 */
74static const char *nroff_device = "ascii";
75
76/*
77 * Mapping from locale to nroff device
78 */
79static const char *locale_device[] = {
80	"KOI8-R",	"koi8-r",
81	"ISO8859-1",	"latin1",
82	"ISO8859-15",	"latin1",
83	NULL
84};
85
86#define	BZ2_CMD		"bzip2"
87#define	BZ2_EXT		".bz2"
88#define	BZ2CAT_CMD	"bz"
89#define	GZ_CMD		"gzip"
90#define	GZ_EXT		".gz"
91#define	GZCAT_CMD	"z"
92enum Ziptype {NONE, BZIP, GZIP};
93
94static uid_t uid;
95static gid_t gids[NGROUPS_MAX];
96static int ngids;
97static int starting_dir;
98static char tmp_file[MAXPATHLEN];
99struct stat test_st;
100
101/*
102 * A hashtable is an array of chains composed of this entry structure.
103 */
104struct hash_entry {
105	ino_t		inode_number;
106	dev_t		device_number;
107	const char	*data;
108	struct hash_entry *next;
109};
110
111#define HASHTABLE_ALLOC	16384	/* allocation for hashtable (power of 2) */
112#define HASH_MASK	(HASHTABLE_ALLOC - 1)
113
114static struct hash_entry *visited[HASHTABLE_ALLOC];
115static struct hash_entry *links[HASHTABLE_ALLOC];
116
117/*
118 * Inserts a string into a hashtable keyed by inode & device number.
119 */
120static void
121insert_hashtable(struct hash_entry **table,
122    ino_t inode_number,
123    dev_t device_number,
124    const char *data)
125{
126	struct hash_entry *new_entry;
127	struct hash_entry **chain;
128
129	new_entry = (struct hash_entry *) malloc(sizeof(struct hash_entry));
130	if (new_entry == NULL)
131		err(1, "can't insert into hashtable");
132	chain = &table[inode_number & HASH_MASK];
133	new_entry->inode_number = inode_number;
134	new_entry->device_number = device_number;
135	new_entry->data = data;
136	new_entry->next = *chain;
137	*chain = new_entry;
138}
139
140/*
141 * Finds a string in a hashtable keyed by inode & device number.
142 */
143static const char *
144find_hashtable(struct hash_entry **table,
145    ino_t inode_number,
146    dev_t device_number)
147{
148	struct hash_entry *chain;
149
150	chain = table[inode_number & HASH_MASK];
151	while (chain != NULL) {
152		if (chain->inode_number == inode_number &&
153		    chain->device_number == device_number)
154			return chain->data;
155		chain = chain->next;
156	}
157	return NULL;
158}
159
160static void
161trap_signal(int sig __unused)
162{
163	if (tmp_file[0] != '\0')
164		unlink(tmp_file);
165	exit(1);
166}
167
168/*
169 * Deals with junk files in the man or cat section directories.
170 */
171static void
172junk(const char *mandir, const char *name, const char *reason)
173{
174	if (verbose)
175		fprintf(stderr, "%s/%s: %s\n", mandir, name, reason);
176	if (rm_junk) {
177		fprintf(stderr, "rm %s/%s\n", mandir, name);
178		if (!pretend && unlink(name) < 0)
179			warn("%s/%s", mandir, name);
180	}
181}
182
183/*
184 * Returns TOP_LEVEL_DIR for .../man, MAN_SECTION_DIR for .../manXXX,
185 * and UNKNOWN for everything else.
186 */
187static int
188directory_type(char *dir)
189{
190	char *p;
191
192	for (;;) {
193		p = strrchr(dir, '/');
194		if (p == NULL || p[1] != '\0')
195			break;
196		*p = '\0';
197	}
198	if (p == NULL)
199		p = dir;
200	else
201		p++;
202	if (strncmp(p, "man", 3) == 0) {
203		p += 3;
204		if (*p == '\0')
205			return TOP_LEVEL_DIR;
206		while (isalnum((unsigned char)*p) || *p == '_') {
207			if (*++p == '\0')
208				return MAN_SECTION_DIR;
209		}
210	}
211	return UNKNOWN;
212}
213
214/*
215 * Tests whether the given file name (without a preceding path)
216 * is a proper man page name (like "mk-amd-map.8.gz").
217 * Only alphanumerics and '_' are allowed after the last '.' and
218 * the last '.' can't be the first or last characters.
219 */
220static int
221is_manpage_name(char *name)
222{
223	char *lastdot = NULL;
224	char *n = name;
225
226	while (*n != '\0') {
227		if (!isalnum((unsigned char)*n)) {
228			switch (*n) {
229			case '_':
230				break;
231			case '-':
232			case '+':
233			case '[':
234			case ':':
235				lastdot = NULL;
236				break;
237			case '.':
238				lastdot = n;
239				break;
240			default:
241				return 0;
242			}
243		}
244		n++;
245	}
246	return lastdot > name && lastdot + 1 < n;
247}
248
249static int
250is_bzipped(char *name)
251{
252	int len = strlen(name);
253	return len >= 5 && strcmp(&name[len - 4], BZ2_EXT) == 0;
254}
255
256static int
257is_gzipped(char *name)
258{
259	int len = strlen(name);
260	return len >= 4 && strcmp(&name[len - 3], GZ_EXT) == 0;
261}
262
263/*
264 * Converts manXXX to catXXX.
265 */
266static char *
267get_cat_section(char *section)
268{
269	char *cat_section;
270
271	cat_section = strdup(section);
272	strncpy(cat_section, "cat", 3);
273	return cat_section;
274}
275
276/*
277 * Tests to see if the given directory has already been visited.
278 */
279static int
280already_visited(char *mandir, char *dir, int count_visit)
281{
282	struct stat st;
283
284	if (stat(dir, &st) < 0) {
285		if (mandir != NULL)
286			warn("%s/%s", mandir, dir);
287		else
288			warn("%s", dir);
289		exit_code = 1;
290		return 1;
291	}
292	if (find_hashtable(visited, st.st_ino, st.st_dev) != NULL) {
293		if (mandir != NULL)
294			warnx("already visited %s/%s", mandir, dir);
295		else
296			warnx("already visited %s", dir);
297		return 1;
298	}
299	if (count_visit)
300		insert_hashtable(visited, st.st_ino, st.st_dev, "");
301	return 0;
302}
303
304/*
305 * Returns a set of TEST_* bits describing a file's type and permissions.
306 * If mod_time isn't NULL, it will contain the file's modification time.
307 */
308static int
309test_path(char *name, time_t *mod_time)
310{
311	int result;
312
313	if (stat(name, &test_st) < 0)
314		return 0;
315	result = TEST_EXISTS;
316	if (mod_time != NULL)
317		*mod_time = test_st.st_mtime;
318	if (S_ISDIR(test_st.st_mode))
319		result |= TEST_DIR;
320	else if (S_ISREG(test_st.st_mode))
321		result |= TEST_FILE;
322	if (test_st.st_uid == uid) {
323		test_st.st_mode >>= 6;
324	} else {
325		int i;
326		for (i = 0; i < ngids; i++) {
327			if (test_st.st_gid == gids[i]) {
328				test_st.st_mode >>= 3;
329				break;
330			}
331		}
332	}
333	if (test_st.st_mode & S_IROTH)
334		result |= TEST_READABLE;
335	if (test_st.st_mode & S_IWOTH)
336		result |= TEST_WRITABLE;
337	if (test_st.st_mode & S_IXOTH)
338		result |= TEST_EXECUTABLE;
339	return result;
340}
341
342/*
343 * Checks whether a file is a symbolic link.
344 */
345static int
346is_symlink(char *path)
347{
348	struct stat st;
349
350	return lstat(path, &st) >= 0 && S_ISLNK(st.st_mode);
351}
352
353/*
354 * Tests to see if the given directory can be written to.
355 */
356static void
357check_writable(char *mandir)
358{
359	if (verbose && !(test_path(mandir, NULL) & TEST_WRITABLE))
360		fprintf(stderr, "%s: not writable - will only be able to write to existing cat directories\n", mandir);
361}
362
363/*
364 * If the directory exists, attempt to make it writable, otherwise
365 * attempt to create it.
366 */
367static int
368make_writable_dir(char *mandir, char *dir)
369{
370	int test;
371
372	if ((test = test_path(dir, NULL)) != 0) {
373		if (!(test & TEST_WRITABLE) && chmod(dir, 0755) < 0) {
374			warn("%s/%s: chmod", mandir, dir);
375			exit_code = 1;
376			return 0;
377		}
378	} else {
379		if (verbose || pretend)
380			fprintf(stderr, "mkdir %s\n", dir);
381		if (!pretend) {
382			unlink(dir);
383			if (mkdir(dir, 0755) < 0) {
384				warn("%s/%s: mkdir", mandir, dir);
385				exit_code = 1;
386				return 0;
387			}
388		}
389	}
390	return 1;
391}
392
393/*
394 * Processes a single man page source by using nroff to create
395 * the preformatted cat page.
396 */
397static void
398process_page(char *mandir, char *src, char *cat, enum Ziptype zipped)
399{
400	int src_test, cat_test;
401	time_t src_mtime, cat_mtime;
402	char cmd[MAXPATHLEN];
403	dev_t src_dev;
404	ino_t src_ino;
405	const char *link_name;
406
407	src_test = test_path(src, &src_mtime);
408	if (!(src_test & (TEST_FILE|TEST_READABLE))) {
409		if (!(src_test & TEST_DIR)) {
410			warnx("%s/%s: unreadable", mandir, src);
411			exit_code = 1;
412			if (rm_junk && is_symlink(src))
413				junk(mandir, src, "bogus symlink");
414		}
415		return;
416	}
417	src_dev = test_st.st_dev;
418	src_ino = test_st.st_ino;
419	cat_test = test_path(cat, &cat_mtime);
420	if (cat_test & (TEST_FILE|TEST_READABLE)) {
421		if (!force && cat_mtime >= src_mtime) {
422			if (verbose) {
423				fprintf(stderr, "\t%s/%s: up to date\n",
424				    mandir, src);
425			}
426			return;
427		}
428	}
429	/*
430	 * Is the man page a link to one we've already processed?
431	 */
432	if ((link_name = find_hashtable(links, src_ino, src_dev)) != NULL) {
433		if (verbose || pretend) {
434			fprintf(stderr, "%slink %s -> %s\n",
435			    verbose ? "\t" : "", cat, link_name);
436		}
437		if (!pretend)
438			link(link_name, cat);
439		return;
440	}
441	insert_hashtable(links, src_ino, src_dev, strdup(cat));
442	if (verbose || pretend) {
443		fprintf(stderr, "%sformat %s -> %s\n",
444		    verbose ? "\t" : "", src, cat);
445		if (pretend)
446			return;
447	}
448	snprintf(tmp_file, sizeof tmp_file, "%s.tmp", cat);
449	snprintf(cmd, sizeof cmd,
450	    "%scat %s | tbl | nroff -T%s -man | col | %s > %s.tmp",
451	    zipped == BZIP ? BZ2CAT_CMD : zipped == GZIP ? GZCAT_CMD : "",
452	    src, nroff_device,
453	    zipped == BZIP ? BZ2_CMD : zipped == GZIP ? GZ_CMD : "cat",
454	    cat);
455	if (system(cmd) != 0)
456		err(1, "formatting pipeline");
457	if (rename(tmp_file, cat) < 0)
458		warn("%s", cat);
459	tmp_file[0] = '\0';
460}
461
462/*
463 * Scan the man section directory for pages and process each one,
464 * then check for junk in the corresponding cat section.
465 */
466static void
467scan_section(char *mandir, char *section, char *cat_section)
468{
469	struct dirent **entries;
470	char **expected = NULL;
471	int npages;
472	int nexpected = 0;
473	int i, e;
474	enum Ziptype zipped;
475	char *page_name;
476	char page_path[MAXPATHLEN];
477	char cat_path[MAXPATHLEN];
478	char zip_path[MAXPATHLEN];
479
480	/*
481	 * scan the man section directory for pages
482	 */
483	npages = scandir(section, &entries, NULL, alphasort);
484	if (npages < 0) {
485		warn("%s/%s", mandir, section);
486		exit_code = 1;
487		return;
488	}
489	if (verbose || rm_junk) {
490		/*
491		 * Maintain a list of all cat pages that should exist,
492		 * corresponding to existing man pages.
493		 */
494		expected = (char **) calloc(npages, sizeof(char *));
495	}
496	for (i = 0; i < npages; free(entries[i++])) {
497		page_name = entries[i]->d_name;
498		snprintf(page_path, sizeof page_path, "%s/%s", section,
499		    page_name);
500		if (!is_manpage_name(page_name)) {
501			if (!(test_path(page_path, NULL) & TEST_DIR)) {
502				junk(mandir, page_path,
503				    "invalid man page name");
504			}
505			continue;
506		}
507		zipped = is_bzipped(page_name) ? BZIP :
508		    is_gzipped(page_name) ? GZIP : NONE;
509		if (zipped != NONE) {
510			snprintf(cat_path, sizeof cat_path, "%s/%s",
511			    cat_section, page_name);
512			if (expected != NULL)
513				expected[nexpected++] = strdup(page_name);
514			process_page(mandir, page_path, cat_path, zipped);
515		} else {
516			/*
517			 * We've got an uncompressed man page,
518			 * check to see if there's a (preferred)
519			 * compressed one.
520			 */
521			snprintf(zip_path, sizeof zip_path, "%s%s",
522			    page_path, GZ_EXT);
523			if (test_path(zip_path, NULL) != 0) {
524				junk(mandir, page_path,
525				    "man page unused due to existing " GZ_EXT);
526			} else {
527				if (verbose) {
528					fprintf(stderr,
529						"warning, %s is uncompressed\n",
530						page_path);
531				}
532				snprintf(cat_path, sizeof cat_path, "%s/%s",
533				    cat_section, page_name);
534				if (expected != NULL) {
535					asprintf(&expected[nexpected++],
536					    "%s", page_name);
537				}
538				process_page(mandir, page_path, cat_path, NONE);
539			}
540		}
541	}
542	free(entries);
543	if (expected == NULL)
544	    return;
545	/*
546	 * scan cat sections for junk
547	 */
548	npages = scandir(cat_section, &entries, NULL, alphasort);
549	e = 0;
550	for (i = 0; i < npages; free(entries[i++])) {
551		const char *junk_reason;
552		int cmp = 1;
553
554		page_name = entries[i]->d_name;
555		if (strcmp(page_name, ".") == 0 || strcmp(page_name, "..") == 0)
556			continue;
557		/*
558		 * Keep the index into the expected cat page list
559		 * ahead of the name we've found.
560		 */
561		while (e < nexpected &&
562		    (cmp = strcmp(page_name, expected[e])) > 0)
563			free(expected[e++]);
564		if (cmp == 0)
565			continue;
566		/* we have an unexpected page */
567		snprintf(cat_path, sizeof cat_path, "%s/%s", cat_section,
568		    page_name);
569		if (!is_manpage_name(page_name)) {
570			if (test_path(cat_path, NULL) & TEST_DIR)
571				continue;
572			junk_reason = "invalid cat page name";
573		} else if (!is_gzipped(page_name) && e + 1 < nexpected &&
574		    strncmp(page_name, expected[e + 1], strlen(page_name)) == 0 &&
575		    strlen(expected[e + 1]) == strlen(page_name) + 3) {
576			junk_reason = "cat page unused due to existing " GZ_EXT;
577		} else
578			junk_reason = "cat page without man page";
579		junk(mandir, cat_path, junk_reason);
580	}
581	free(entries);
582	while (e < nexpected)
583		free(expected[e++]);
584	free(expected);
585}
586
587
588/*
589 * Processes a single man section.
590 */
591static void
592process_section(char *mandir, char *section)
593{
594	char *cat_section;
595
596	if (already_visited(mandir, section, 1))
597		return;
598	if (verbose)
599		fprintf(stderr, "  section %s\n", section);
600	cat_section = get_cat_section(section);
601	if (make_writable_dir(mandir, cat_section))
602		scan_section(mandir, section, cat_section);
603	free(cat_section);
604}
605
606static int
607select_sections(struct dirent *entry)
608{
609	return directory_type(entry->d_name) == MAN_SECTION_DIR;
610}
611
612/*
613 * Processes a single top-level man directory.  If section isn't NULL,
614 * it will only process that section sub-directory, otherwise it will
615 * process all of them.
616 */
617static void
618process_mandir(char *dir_name, char *section)
619{
620	fchdir(starting_dir);
621	if (already_visited(NULL, dir_name, section == NULL))
622		return;
623	check_writable(dir_name);
624	if (verbose)
625		fprintf(stderr, "man directory %s\n", dir_name);
626	if (pretend)
627		fprintf(stderr, "cd %s\n", dir_name);
628	if (chdir(dir_name) < 0) {
629		warn("%s: chdir", dir_name);
630		exit_code = 1;
631		return;
632	}
633	if (section != NULL) {
634		process_section(dir_name, section);
635	} else {
636		struct dirent **entries;
637		char *machine_dir;
638		int nsections;
639		int i;
640
641		nsections = scandir(".", &entries, select_sections, alphasort);
642		if (nsections < 0) {
643			warn("%s", dir_name);
644			exit_code = 1;
645			return;
646		}
647		for (i = 0; i < nsections; i++) {
648			process_section(dir_name, entries[i]->d_name);
649			asprintf(&machine_dir, "%s/%s", entries[i]->d_name,
650			    machine);
651			if (test_path(machine_dir, NULL) & TEST_DIR)
652				process_section(dir_name, machine_dir);
653			free(machine_dir);
654			free(entries[i]);
655		}
656		free(entries);
657	}
658}
659
660/*
661 * Processes one argument, which may be a colon-separated list of
662 * directories.
663 */
664static void
665process_argument(const char *arg)
666{
667	char *dir;
668	char *mandir;
669	char *section;
670	char *parg;
671
672	parg = strdup(arg);
673	if (parg == NULL)
674		err(1, "out of memory");
675	while ((dir = strsep(&parg, ":")) != NULL) {
676		switch (directory_type(dir)) {
677		case TOP_LEVEL_DIR:
678			if (locale != NULL) {
679				asprintf(&mandir, "%s/%s", dir, locale);
680				process_mandir(mandir, NULL);
681				free(mandir);
682				if (lang_locale != NULL) {
683					asprintf(&mandir, "%s/%s", dir,
684					    lang_locale);
685					process_mandir(mandir, NULL);
686					free(mandir);
687				}
688			} else {
689				process_mandir(dir, NULL);
690			}
691			break;
692		case MAN_SECTION_DIR: {
693			mandir = strdup(dirname(dir));
694			section = strdup(basename(dir));
695			process_mandir(mandir, section);
696			free(mandir);
697			free(section);
698			break;
699			}
700		default:
701			warnx("%s: directory name not in proper man form", dir);
702			exit_code = 1;
703		}
704	}
705	free(parg);
706}
707
708static void
709determine_locale(void)
710{
711	char *sep;
712
713	if ((locale = setlocale(LC_CTYPE, "")) == NULL) {
714		warnx("-L option used, but no locale found\n");
715		return;
716	}
717	sep = strchr(locale, '_');
718	if (sep != NULL && isupper((unsigned char)sep[1])
719			&& isupper((unsigned char)sep[2])) {
720		asprintf(&lang_locale, "%.*s%s", (int)(sep - locale),
721		    locale, &sep[3]);
722	}
723	sep = nl_langinfo(CODESET);
724	if (sep != NULL && *sep != '\0' && strcmp(sep, "US-ASCII") != 0) {
725		int i;
726
727		for (i = 0; locale_device[i] != NULL; i += 2) {
728			if (strcmp(sep, locale_device[i]) == 0) {
729				nroff_device = locale_device[i + 1];
730				break;
731			}
732		}
733	}
734	if (verbose) {
735		if (lang_locale != NULL)
736			fprintf(stderr, "short locale is %s\n", lang_locale);
737		fprintf(stderr, "nroff device is %s\n", nroff_device);
738	}
739}
740
741static void
742usage(void)
743{
744	fprintf(stderr, "usage: %s [-fLnrv] [directories...]\n", getprogname());
745	exit(1);
746}
747
748int
749main(int argc, char **argv)
750{
751	int opt;
752
753	if ((uid = getuid()) == 0) {
754		fprintf(stderr, "don't run %s as root, use:\n   echo", argv[0]);
755		for (optind = 0; optind < argc; optind++) {
756			fprintf(stderr, " %s", argv[optind]);
757		}
758		fprintf(stderr, " | nice -5 su -m man\n");
759		exit(1);
760	}
761	while ((opt = getopt(argc, argv, "vnfLrh")) != -1) {
762		switch (opt) {
763		case 'f':
764			force++;
765			break;
766		case 'L':
767			determine_locale();
768			break;
769		case 'n':
770			pretend++;
771			break;
772		case 'r':
773			rm_junk++;
774			break;
775		case 'v':
776			verbose++;
777			break;
778		default:
779			usage();
780			/* NOTREACHED */
781		}
782	}
783	ngids = getgroups(NGROUPS_MAX, gids);
784	if ((starting_dir = open(".", 0)) < 0) {
785		err(1, ".");
786	}
787	umask(022);
788	signal(SIGINT, trap_signal);
789	signal(SIGHUP, trap_signal);
790	signal(SIGQUIT, trap_signal);
791	signal(SIGTERM, trap_signal);
792
793	if ((machine = getenv("MACHINE")) == NULL)
794		machine = MACHINE;
795
796	if (optind == argc) {
797		const char *manpath = getenv("MANPATH");
798		if (manpath == NULL)
799			manpath = DEFAULT_MANPATH;
800		process_argument(manpath);
801	} else {
802		while (optind < argc)
803			process_argument(argv[optind++]);
804	}
805	exit(exit_code);
806}
807