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