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: src/usr.bin/makewhatis/makewhatis.c,v 1.9 2002/09/04 23:29:04 dwmalone Exp $");
31
32#include <sys/types.h>
33#include <sys/stat.h>
34#include <sys/param.h>
35#include <sys/queue.h>
36
37/* Workaround for missing #define in sys/queue.h (3806865) */
38#ifndef SLIST_HEAD_INITIALIZER
39#define SLIST_HEAD_INITIALIZER(head) { NULL }
40#endif
41
42#include <ctype.h>
43#include <dirent.h>
44#include <err.h>
45#include <stdio.h>
46#include <stdlib.h>
47#include <string.h>
48#include <stringlist.h>
49#include <unistd.h>
50#include <zlib.h>
51
52#define DEFAULT_MANPATH		"/usr/share/man"
53#define LINE_ALLOC		4096
54
55static char blank[] = 		"";
56
57/*
58 * Information collected about each man page in a section.
59 */
60struct page_info {
61	char *	filename;
62	char *	name;
63	char *	suffix;
64	int	gzipped;
65	ino_t	inode;
66};
67
68/*
69 * An entry kept for each visited directory.
70 */
71struct visited_dir {
72	dev_t		device;
73	ino_t		inode;
74	SLIST_ENTRY(visited_dir)	next;
75};
76
77/*
78 * an expanding string
79 */
80struct sbuf {
81	char *	content;		/* the start of the buffer */
82	char *	end;			/* just past the end of the content */
83	char *	last;			/* the last allocated character */
84};
85
86/*
87 * Removes the last amount characters from the sbuf.
88 */
89#define sbuf_retract(sbuf, amount)	\
90	((sbuf)->end -= (amount))
91/*
92 * Returns the length of the sbuf content.
93 */
94#define sbuf_length(sbuf)		\
95	((sbuf)->end - (sbuf)->content)
96
97typedef char *edited_copy(char *from, char *to, int length);
98
99static int append;			/* -a flag: append to existing whatis */
100static int verbose;			/* -v flag: be verbose with warnings */
101static int indent = 24;			/* -i option: description indentation */
102static const char *whatis_name="whatis";/* -n option: the name */
103static char *common_output;		/* -o option: the single output file */
104static char *locale;			/* user's locale if -L is used */
105static char *lang_locale;		/* short form of locale */
106#ifndef __APPLE__
107static const char *machine;
108#endif /* !__APPLE__ */
109
110static int exit_code;			/* exit code to use when finished */
111static SLIST_HEAD(, visited_dir) visited_dirs =
112    SLIST_HEAD_INITIALIZER(visited_dirs);
113
114/*
115 * While the whatis line is being formed, it is stored in whatis_proto.
116 * When finished, it is reformatted into whatis_final and then appended
117 * to whatis_lines.
118 */
119static struct sbuf *whatis_proto;
120static struct sbuf *whatis_final;
121static StringList *whatis_lines;	/* collected output lines */
122
123static char tmp_file[MAXPATHLEN];	/* path of temporary file, if any */
124
125/* A set of possible names for the NAME man page section */
126static const char *name_section_titles[] = {
127	"NAME", "Name", "NAMN", "BEZEICHNUNG", "\xcc\xbe\xbe\xce",
128	"\xee\xe1\xfa\xf7\xe1\xee\xe9\xe5", NULL
129};
130
131/* A subset of the mdoc(7) commands to ignore */
132static char mdoc_commands[] = "ArDvErEvFlLiNmPa";
133
134/*
135 * Frees a struct page_info and its content.
136 */
137static void
138free_page_info(struct page_info *info)
139{
140	free(info->filename);
141	free(info->name);
142	free(info->suffix);
143	free(info);
144}
145
146/*
147 * Allocates and fills in a new struct page_info given the
148 * name of the man section directory and the dirent of the file.
149 * If the file is not a man page, returns NULL.
150 */
151static struct page_info *
152new_page_info(char *dir, struct dirent *dirent)
153{
154	struct page_info *info;
155	int basename_length;
156	char *suffix;
157	struct stat st;
158
159	info = (struct page_info *) malloc(sizeof(struct page_info));
160	if (info == NULL)
161		err(1, "malloc");
162	basename_length = strlen(dirent->d_name);
163	suffix = &dirent->d_name[basename_length];
164	asprintf(&info->filename, "%s/%s", dir, dirent->d_name);
165	if ((info->gzipped = basename_length >= 4 && strcmp(&dirent->d_name[basename_length - 3], ".gz") == 0)) {
166		suffix -= 3;
167		*suffix = '\0';
168	}
169	for (;;) {
170		if (--suffix == dirent->d_name || !isalnum(*suffix)) {
171			if (*suffix == '.')
172				break;
173			if (verbose)
174				warnx("%s: invalid man page name", info->filename);
175			free(info->filename);
176			free(info);
177			return NULL;
178		}
179	}
180	*suffix++ = '\0';
181	info->name = strdup(dirent->d_name);
182	info->suffix = strdup(suffix);
183	if (stat(info->filename, &st) < 0) {
184		warn("%s", info->filename);
185		free_page_info(info);
186		return NULL;
187	}
188	if (!S_ISREG(st.st_mode)) {
189		if (verbose && !S_ISDIR(st.st_mode))
190			warnx("%s: not a regular file", info->filename);
191		free_page_info(info);
192		return NULL;
193	}
194	info->inode = st.st_ino;
195	return info;
196}
197
198/*
199 * Reset an sbuf's length to 0.
200 */
201static void
202sbuf_clear(struct sbuf *sbuf)
203{
204	sbuf->end = sbuf->content;
205}
206
207/*
208 * Allocate a new sbuf.
209 */
210static struct sbuf *
211new_sbuf(void)
212{
213	struct sbuf *sbuf = (struct sbuf *) malloc(sizeof(struct sbuf));
214	sbuf->content = (char *) malloc(LINE_ALLOC);
215	sbuf->last = sbuf->content + LINE_ALLOC - 1;
216	sbuf_clear(sbuf);
217	return sbuf;
218}
219
220/*
221 * Ensure that there is enough room in the sbuf for nchars more characters.
222 */
223static void
224sbuf_need(struct sbuf *sbuf, int nchars)
225{
226	char *new_content;
227	size_t size, cntsize;
228
229	/* double the size of the allocation until the buffer is big enough */
230	while (sbuf->end + nchars > sbuf->last) {
231		size = sbuf->last + 1 - sbuf->content;
232		size *= 2;
233		cntsize = sbuf->end - sbuf->content;
234
235		new_content = (char *)malloc(size);
236		memcpy(new_content, sbuf->content, cntsize);
237		free(sbuf->content);
238		sbuf->content = new_content;
239		sbuf->end = new_content + cntsize;
240		sbuf->last = new_content + size - 1;
241	}
242}
243
244/*
245 * Appends a string of a given length to the sbuf.
246 */
247static void
248sbuf_append(struct sbuf *sbuf, const char *text, int length)
249{
250	if (length > 0) {
251		sbuf_need(sbuf, length);
252		memcpy(sbuf->end, text, length);
253		sbuf->end += length;
254	}
255}
256
257/*
258 * Appends a null-terminated string to the sbuf.
259 */
260static void
261sbuf_append_str(struct sbuf *sbuf, char *text)
262{
263	sbuf_append(sbuf, text, strlen(text));
264}
265
266/*
267 * Appends an edited null-terminated string to the sbuf.
268 */
269static void
270sbuf_append_edited(struct sbuf *sbuf, char *text, edited_copy copy)
271{
272	int length = strlen(text);
273	if (length > 0) {
274		sbuf_need(sbuf, length);
275		sbuf->end = copy(text, sbuf->end, length);
276	}
277}
278
279/*
280 * Strips any of a set of chars from the end of the sbuf.
281 */
282static void
283sbuf_strip(struct sbuf *sbuf, const char *set)
284{
285	while (sbuf->end > sbuf->content && strchr(set, sbuf->end[-1]) != NULL)
286		sbuf->end--;
287}
288
289/*
290 * Returns the null-terminated string built by the sbuf.
291 */
292static char *
293sbuf_content(struct sbuf *sbuf)
294{
295	*sbuf->end = '\0';
296	return sbuf->content;
297}
298
299/*
300 * Returns true if no man page exists in the directory with
301 * any of the names in the StringList.
302 */
303static int
304no_page_exists(char *dir, StringList *names, char *suffix)
305{
306	char path[MAXPATHLEN];
307	size_t i;
308
309	for (i = 0; i < names->sl_cur; i++) {
310		snprintf(path, sizeof path, "%s/%s.%s.gz", dir, names->sl_str[i], suffix);
311		if (access(path, F_OK) < 0) {
312			path[strlen(path) - 3] = '\0';
313			if (access(path, F_OK) < 0)
314				continue;
315		}
316		return 0;
317	}
318	return 1;
319}
320
321static void
322trap_signal(int sig __unused)
323{
324	if (tmp_file[0] != '\0')
325		unlink(tmp_file);
326	exit(1);
327}
328
329/*
330 * Attempts to open an output file.  Returns NULL if unsuccessful.
331 */
332static FILE *
333open_output(char *name)
334{
335	FILE *output;
336
337	whatis_lines = sl_init();
338	if (append) {
339		char line[LINE_ALLOC];
340
341		output = fopen(name, "r");
342		if (output == NULL) {
343			warn("%s", name);
344			exit_code = 1;
345			return NULL;
346		}
347		while (fgets(line, sizeof line, output) != NULL) {
348			line[strlen(line) - 1] = '\0';
349			sl_add(whatis_lines, strdup(line));
350		}
351	}
352	if (common_output == NULL) {
353		snprintf(tmp_file, sizeof tmp_file, "%s.tmp", name);
354		name = tmp_file;
355	}
356	output = fopen(name, "w");
357	if (output == NULL) {
358		warn("%s", name);
359		exit_code = 1;
360		return NULL;
361	}
362	return output;
363}
364
365static int
366linesort(const void *a, const void *b)
367{
368	return strcmp((*(const char * const *)a), (*(const char * const *)b));
369}
370
371/*
372 * Writes the unique sorted lines to the output file.
373 */
374static void
375finish_output(FILE *output, char *name)
376{
377	size_t i;
378	char *prev = NULL;
379
380	qsort(whatis_lines->sl_str, whatis_lines->sl_cur, sizeof(char *), linesort);
381	for (i = 0; i < whatis_lines->sl_cur; i++) {
382		char *line = whatis_lines->sl_str[i];
383		if (i > 0 && strcmp(line, prev) == 0)
384			continue;
385		prev = line;
386		fputs(line, output);
387		putc('\n', output);
388	}
389	fclose(output);
390	sl_free(whatis_lines, 1);
391	if (common_output == NULL) {
392		rename(tmp_file, name);
393		unlink(tmp_file);
394	}
395}
396
397static FILE *
398open_whatis(char *mandir)
399{
400	char filename[MAXPATHLEN];
401
402	snprintf(filename, sizeof filename, "%s/%s", mandir, whatis_name);
403	return open_output(filename);
404}
405
406static void
407finish_whatis(FILE *output, char *mandir)
408{
409	char filename[MAXPATHLEN];
410
411	snprintf(filename, sizeof filename, "%s/%s", mandir, whatis_name);
412	finish_output(output, filename);
413}
414
415/*
416 * Tests to see if the given directory has already been visited.
417 */
418static int
419already_visited(char *dir)
420{
421	struct stat st;
422	struct visited_dir *visit;
423
424	if (stat(dir, &st) < 0) {
425		warn("%s", dir);
426		exit_code = 1;
427		return 1;
428	}
429	SLIST_FOREACH(visit, &visited_dirs, next) {
430		if (visit->inode == st.st_ino &&
431		    visit->device == st.st_dev) {
432			warnx("already visited %s", dir);
433			return 1;
434		}
435	}
436	visit = (struct visited_dir *) malloc(sizeof(struct visited_dir));
437	visit->device = st.st_dev;
438	visit->inode = st.st_ino;
439	SLIST_INSERT_HEAD(&visited_dirs, visit, next);
440	return 0;
441}
442
443/*
444 * Removes trailing spaces from a string, returning a pointer to just
445 * beyond the new last character.
446 */
447static char *
448trim_rhs(char *str)
449{
450	char *rhs = &str[strlen(str)];
451	while (--rhs > str && isspace(*rhs))
452		;
453	*++rhs = '\0';
454	return rhs;
455}
456
457/*
458 * Returns a pointer to the next non-space character in the string.
459 */
460static char *
461skip_spaces(char *s)
462{
463	while (*s != '\0' && isspace(*s))
464		s++;
465	return s;
466}
467
468/*
469 * Returns whether the string contains only digits.
470 */
471static int
472only_digits(char *line)
473{
474	if (!isdigit(*line++))
475		return 0;
476	while (isdigit(*line))
477		line++;
478	return *line == '\0';
479}
480
481/*
482 * Returns whether the line is of one of the forms:
483 *	.Sh NAME
484 *	.Sh "NAME"
485 *	etc.
486 * assuming that section_start is ".Sh".
487 */
488static int
489name_section_line(char *line, const char *section_start)
490{
491	char *rhs;
492	const char **title;
493
494	if (strncmp(line, section_start, 3) != 0)
495		return 0;
496	line = skip_spaces(line + 3);
497	rhs = trim_rhs(line);
498	if (*line == '"') {
499		line++;
500		if (*--rhs == '"')
501			*rhs = '\0';
502	}
503	for (title = name_section_titles; *title != NULL; title++)
504		if (strcmp(*title, line) == 0)
505			return 1;
506	return 0;
507}
508
509/*
510 * Copies characters while removing the most common nroff/troff
511 * markup:
512 *	\(em, \(mi, \s[+-N], \&
513 *	\fF, \f(fo, \f[font]
514 *	\*s, \*(st, \*[stringvar]
515 */
516static char *
517de_nroff_copy(char *from, char *to, int fromlen)
518{
519	char *from_end = &from[fromlen];
520	while (from < from_end) {
521		switch (*from) {
522		case '\\':
523			switch (*++from) {
524			case '(':
525				if (strncmp(&from[1], "em", 2) == 0 ||
526						strncmp(&from[1], "mi", 2) == 0) {
527					from += 3;
528					continue;
529				}
530				break;
531			case 's':
532				if (*++from == '-')
533					from++;
534				while (isdigit(*from))
535					from++;
536				continue;
537			case 'f':
538			case '*':
539				if (*++from == '(')
540					from += 3;
541				else if (*from == '[') {
542					while (*++from != ']' && from < from_end);
543					from++;
544				} else
545					from++;
546				continue;
547			case '&':
548				from++;
549				continue;
550			}
551			break;
552		}
553		*to++ = *from++;
554	}
555	return to;
556}
557
558/*
559 * Appends a string with the nroff formatting removed.
560 */
561static void
562add_nroff(char *text)
563{
564	sbuf_append_edited(whatis_proto, text, de_nroff_copy);
565}
566
567/*
568 * Appends "name(suffix), " to whatis_final.
569 */
570static void
571add_whatis_name(char *name, char *suffix)
572{
573	if (*name != '\0') {
574		sbuf_append_str(whatis_final, name);
575		sbuf_append(whatis_final, "(", 1);
576		sbuf_append_str(whatis_final, suffix);
577		sbuf_append(whatis_final, "), ", 3);
578	}
579}
580
581/*
582 * Processes an old-style man(7) line.  This ignores commands with only
583 * a single number argument.
584 */
585static void
586process_man_line(char *line)
587{
588	if (*line == '.') {
589		while (isalpha(*++line))
590			;
591		line = skip_spaces(line);
592		if (only_digits(line))
593			return;
594	} else
595		line = skip_spaces(line);
596#ifdef __APPLE__
597	/* 4454557 */
598	if (*line == '"')
599		++line;
600#endif /* __APPLE__ */
601	if (*line != '\0') {
602		add_nroff(line);
603		sbuf_append(whatis_proto, " ", 1);
604	}
605}
606
607/*
608 * Processes a new-style mdoc(7) line.
609 */
610static void
611process_mdoc_line(char *line)
612{
613	int xref;
614	int arg = 0;
615	char *line_end = &line[strlen(line)];
616	int orig_length = sbuf_length(whatis_proto);
617	char *next;
618
619	if (*line == '\0')
620		return;
621	if (line[0] != '.' || !isupper(line[1]) || !islower(line[2])) {
622		add_nroff(skip_spaces(line));
623		sbuf_append(whatis_proto, " ", 1);
624		return;
625	}
626	xref = strncmp(line, ".Xr", 3) == 0;
627	line += 3;
628	while ((line = skip_spaces(line)) < line_end) {
629		if (*line == '"') {
630			next = ++line;
631			for (;;) {
632				next = strchr(next, '"');
633				if (next == NULL)
634					break;
635				memmove(next, next + 1, strlen(next));
636				line_end--;
637				if (*next != '"')
638					break;
639				next++;
640			}
641		} else
642			next = strpbrk(line, " \t");
643		if (next != NULL)
644			*next++ = '\0';
645		else
646			next = line_end;
647		if (isupper(*line) && islower(line[1]) && line[2] == '\0') {
648			if (strcmp(line, "Ns") == 0) {
649				arg = 0;
650				line = next;
651				continue;
652			}
653			if (strstr(mdoc_commands, line) != NULL) {
654				line = next;
655				continue;
656			}
657		}
658		if (arg > 0 && strchr(",.:;?!)]", *line) == 0) {
659			if (xref) {
660				sbuf_append(whatis_proto, "(", 1);
661				add_nroff(line);
662				sbuf_append(whatis_proto, ")", 1);
663				xref = 0;
664				line = blank;
665			} else
666				sbuf_append(whatis_proto, " ", 1);
667		}
668		add_nroff(line);
669		arg++;
670		line = next;
671	}
672	if (sbuf_length(whatis_proto) > orig_length)
673		sbuf_append(whatis_proto, " ", 1);
674}
675
676/*
677 * Collects a list of comma-separated names from the text.
678 */
679static void
680collect_names(StringList *names, char *text)
681{
682	char *arg;
683
684	for (;;) {
685		arg = text;
686		text = strchr(text, ',');
687		if (text != NULL)
688			*text++ = '\0';
689		sl_add(names, arg);
690		if (text == NULL)
691			return;
692		if (*text == ' ')
693			text++;
694	}
695}
696
697enum { STATE_UNKNOWN, STATE_MANSTYLE, STATE_MDOCNAME, STATE_MDOCDESC };
698
699/*
700 * Processes a man page source into a single whatis line and adds it
701 * to whatis_lines.
702 */
703static void
704process_page(struct page_info *page, char *section_dir)
705{
706	gzFile *in;
707	char buffer[4096];
708	char *line;
709	StringList *names;
710	char *descr;
711	int state = STATE_UNKNOWN;
712	size_t i;
713
714	sbuf_clear(whatis_proto);
715	if ((in = gzopen(page->filename, "r")) == NULL) {
716		warn("%s", page->filename);
717		exit_code = 1;
718		return;
719	}
720	while (gzgets(in, buffer, sizeof buffer) != NULL) {
721		line = buffer;
722		if (strncmp(line, ".\\\"", 3) == 0)		/* ignore comments */
723			continue;
724		switch (state) {
725		/*
726		 * haven't reached the NAME section yet.
727		 */
728		case STATE_UNKNOWN:
729			if (name_section_line(line, ".SH"))
730				state = STATE_MANSTYLE;
731			else if (name_section_line(line, ".Sh"))
732				state = STATE_MDOCNAME;
733			continue;
734		/*
735		 * Inside an old-style .SH NAME section.
736		 */
737		case STATE_MANSTYLE:
738			if ((strncmp(line, ".SH", 3) == 0) || (strncmp(line, ".SS", 3) == 0))
739				break;
740			trim_rhs(line);
741			if (strcmp(line, ".") == 0)
742				continue;
743			if (strncmp(line, ".IX", 3) == 0) {
744				line += 3;
745				line = skip_spaces(line);
746			}
747			process_man_line(line);
748			continue;
749		/*
750		 * Inside a new-style .Sh NAME section (the .Nm part).
751		 */
752		case STATE_MDOCNAME:
753			trim_rhs(line);
754			if (strncmp(line, ".Nm", 3) == 0) {
755				process_mdoc_line(line);
756				continue;
757			} else {
758				if (strcmp(line, ".") == 0)
759					continue;
760				sbuf_append(whatis_proto, "- ", 2);
761				state = STATE_MDOCDESC;
762			}
763			/* fall through */
764		/*
765		 * Inside a new-style .Sh NAME section (after the .Nm-s).
766		 */
767		case STATE_MDOCDESC:
768			if (strncmp(line, ".Sh", 3) == 0)
769				break;
770			trim_rhs(line);
771			if (strcmp(line, ".") == 0)
772				continue;
773			process_mdoc_line(line);
774			continue;
775		}
776		break;
777	}
778	gzclose(in);
779	sbuf_strip(whatis_proto, " \t.-");
780	line = sbuf_content(whatis_proto);
781	/*
782	 * line now contains the appropriate data, but without
783	 * the proper indentation or the section appended to each name.
784	 */
785	descr = strstr(line, " - ");
786	if (descr == NULL) {
787		descr = strchr(line, ' ');
788		if (descr == NULL) {
789			if (verbose)
790				fprintf(stderr, "	ignoring junk description \"%s\"\n", line);
791			return;
792		}
793		*descr++ = '\0';
794	} else {
795		*descr = '\0';
796		descr += 3;
797	}
798	names = sl_init();
799	collect_names(names, line);
800	sbuf_clear(whatis_final);
801	if (!sl_find(names, page->name) && no_page_exists(section_dir, names, page->suffix)) {
802		/*
803		 * Add the page name since that's the only thing that
804		 * man(1) will find.
805		 */
806		add_whatis_name(page->name, page->suffix);
807	}
808	for (i = 0; i < names->sl_cur; i++)
809		add_whatis_name(names->sl_str[i], page->suffix);
810	sl_free(names, 0);
811	sbuf_retract(whatis_final, 2);		/* remove last ", " */
812	while (sbuf_length(whatis_final) < indent)
813		sbuf_append(whatis_final, " ", 1);
814	sbuf_append(whatis_final, " - ", 3);
815	sbuf_append_str(whatis_final, skip_spaces(descr));
816	sl_add(whatis_lines, strdup(sbuf_content(whatis_final)));
817}
818
819/*
820 * Sorts pages first by inode number, then by name.
821 */
822static int
823pagesort(const void *a, const void *b)
824{
825	const struct page_info *p1 = *(struct page_info * const *) a;
826	const struct page_info *p2 = *(struct page_info * const *) b;
827	if (p1->inode == p2->inode)
828		return strcmp(p1->name, p2->name);
829	return p1->inode - p2->inode;
830}
831
832/*
833 * Processes a single man section.
834 */
835static void
836process_section(char *section_dir)
837{
838	struct dirent **entries;
839	int nentries;
840	struct page_info **pages;
841	int npages = 0;
842	int i;
843	ino_t prev_inode = 0;
844
845	if (verbose)
846		fprintf(stderr, "  %s\n", section_dir);
847
848	/*
849	 * scan the man section directory for pages
850	 */
851	nentries = scandir(section_dir, &entries, NULL, alphasort);
852	if (nentries < 0) {
853		warn("%s", section_dir);
854		exit_code = 1;
855		return;
856	}
857	/*
858	 * collect information about man pages
859	 */
860	pages = (struct page_info **) calloc(nentries, sizeof(struct page_info *));
861	for (i = 0; i < nentries; i++) {
862		struct page_info *info = new_page_info(section_dir, entries[i]);
863		if (info != NULL)
864			pages[npages++] = info;
865		free(entries[i]);
866	}
867	free(entries);
868	qsort(pages, npages, sizeof(struct page_info *), pagesort);
869	/*
870	 * process each unique page
871	 */
872	for (i = 0; i < npages; i++) {
873		struct page_info *page = pages[i];
874		if (page->inode != prev_inode) {
875			prev_inode = page->inode;
876			if (verbose)
877				fprintf(stderr, "	reading %s\n", page->filename);
878			process_page(page, section_dir);
879		} else if (verbose)
880			fprintf(stderr, "	skipping %s, duplicate\n", page->filename);
881		free_page_info(page);
882	}
883	free(pages);
884}
885
886/*
887 * Returns whether the directory entry is a man page section.
888 */
889static int
890select_sections(struct dirent *entry)
891{
892	char *p = &entry->d_name[3];
893
894	if (strncmp(entry->d_name, "man", 3) != 0)
895		return 0;
896	while (*p != '\0') {
897		if (!isalnum(*p++))
898			return 0;
899	}
900	return 1;
901}
902
903/*
904 * Processes a single top-level man directory by finding all the
905 * sub-directories named man* and processing each one in turn.
906 */
907static void
908process_mandir(char *dir_name)
909{
910	struct dirent **entries;
911	int nsections;
912	FILE *fp = NULL;
913	int i;
914	struct stat st;
915
916	if (already_visited(dir_name))
917		return;
918	if (verbose)
919		fprintf(stderr, "man directory %s\n", dir_name);
920	nsections = scandir(dir_name, &entries, select_sections, alphasort);
921	if (nsections < 0) {
922		warn("%s", dir_name);
923		exit_code = 1;
924		return;
925	}
926	if (common_output == NULL && (fp = open_whatis(dir_name)) == NULL)
927		return;
928	for (i = 0; i < nsections; i++) {
929		char section_dir[MAXPATHLEN];
930		snprintf(section_dir, sizeof section_dir, "%s/%s", dir_name, entries[i]->d_name);
931		process_section(section_dir);
932#ifndef __APPLE__
933		snprintf(section_dir, sizeof section_dir, "%s/%s/%s", dir_name,
934		    entries[i]->d_name, machine);
935		if (stat(section_dir, &st) == 0 && S_ISDIR(st.st_mode))
936			process_section(section_dir);
937#endif /* !__APPLE__ */
938		free(entries[i]);
939	}
940	free(entries);
941	if (common_output == NULL)
942		finish_whatis(fp, dir_name);
943}
944
945/*
946 * Processes one argument, which may be a colon-separated list of
947 * directories.
948 */
949static void
950process_argument(const char *arg)
951{
952	char *dir;
953	char *mandir;
954	char *parg;
955
956	parg = strdup(arg);
957	if (parg == NULL)
958		err(1, "out of memory");
959	while ((dir = strsep(&parg, ":")) != NULL) {
960		if (locale != NULL) {
961			asprintf(&mandir, "%s/%s", dir, locale);
962			process_mandir(mandir);
963			free(mandir);
964			if (lang_locale != NULL) {
965				asprintf(&mandir, "%s/%s", dir, lang_locale);
966				process_mandir(mandir);
967				free(mandir);
968			}
969		} else {
970			process_mandir(dir);
971		}
972	}
973	free(parg);
974}
975
976
977int
978main(int argc, char **argv)
979{
980	int opt;
981	FILE *fp = NULL;
982
983	while ((opt = getopt(argc, argv, "ai:n:o:vL")) != -1) {
984		switch (opt) {
985		case 'a':
986			append++;
987			break;
988		case 'i':
989			indent = atoi(optarg);
990			break;
991		case 'n':
992			whatis_name = optarg;
993			break;
994		case 'o':
995			common_output = optarg;
996			break;
997		case 'v':
998			verbose++;
999			break;
1000		case 'L':
1001			locale = getenv("LC_ALL");
1002			if (locale == NULL)
1003				locale = getenv("LC_CTYPE");
1004			if (locale == NULL)
1005				locale = getenv("LANG");
1006			if (locale != NULL) {
1007				char *sep = strchr(locale, '_');
1008				if (sep != NULL && isupper(sep[1]) &&
1009				    isupper(sep[2])) {
1010					asprintf(&lang_locale, "%.*s%s", sep - locale, locale, &sep[3]);
1011				}
1012			}
1013			break;
1014		default:
1015			fprintf(stderr, "usage: %s [-a] [-i indent] [-n name] [-o output_file] [-v] [-L] [directories...]\n", argv[0]);
1016			exit(1);
1017		}
1018	}
1019
1020	signal(SIGINT, trap_signal);
1021	signal(SIGHUP, trap_signal);
1022	signal(SIGQUIT, trap_signal);
1023	signal(SIGTERM, trap_signal);
1024	SLIST_INIT(&visited_dirs);
1025	whatis_proto = new_sbuf();
1026	whatis_final = new_sbuf();
1027
1028#ifndef __APPLE__
1029	if ((machine = getenv("MACHINE")) == NULL)
1030		machine = MACHINE;
1031#endif /* !__APPLE__ */
1032
1033	if (common_output != NULL && (fp = open_output(common_output)) == NULL)
1034		err(1, "%s", common_output);
1035	if (optind == argc) {
1036		const char *manpath = getenv("MANPATH");
1037		if (manpath == NULL)
1038			manpath = DEFAULT_MANPATH;
1039		process_argument(manpath);
1040	} else {
1041		while (optind < argc)
1042			process_argument(argv[optind++]);
1043	}
1044	if (common_output != NULL)
1045		finish_output(fp, common_output);
1046	exit(exit_code);
1047}
1048