xref: /openbsd-src/usr.bin/mandoc/main.c (revision 99fd087599a8791921855f21bd7e36130f39aadc)
1 /*	$OpenBSD: main.c,v 1.247 2020/02/24 21:15:05 schwarze Exp $ */
2 /*
3  * Copyright (c) 2008-2012 Kristaps Dzonsons <kristaps@bsd.lv>
4  * Copyright (c) 2010-2012, 2014-2020 Ingo Schwarze <schwarze@openbsd.org>
5  * Copyright (c) 2010 Joerg Sonnenberger <joerg@netbsd.org>
6  *
7  * Permission to use, copy, modify, and distribute this software for any
8  * purpose with or without fee is hereby granted, provided that the above
9  * copyright notice and this permission notice appear in all copies.
10  *
11  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
12  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
14  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18  */
19 
20 #include <sys/types.h>
21 #include <sys/ioctl.h>
22 #include <sys/param.h>	/* MACHINE */
23 #include <sys/stat.h>
24 #include <sys/wait.h>
25 
26 #include <assert.h>
27 #include <ctype.h>
28 #include <err.h>
29 #include <errno.h>
30 #include <fcntl.h>
31 #include <glob.h>
32 #include <limits.h>
33 #include <signal.h>
34 #include <stdio.h>
35 #include <stdint.h>
36 #include <stdlib.h>
37 #include <string.h>
38 #include <termios.h>
39 #include <time.h>
40 #include <unistd.h>
41 
42 #include "mandoc_aux.h"
43 #include "mandoc.h"
44 #include "mandoc_xr.h"
45 #include "roff.h"
46 #include "mdoc.h"
47 #include "man.h"
48 #include "mandoc_parse.h"
49 #include "tag.h"
50 #include "main.h"
51 #include "manconf.h"
52 #include "mansearch.h"
53 
54 #define BINM_APROPOS	"apropos"
55 #define BINM_MAN	"man"
56 #define BINM_MAKEWHATIS	"makewhatis"
57 #define BINM_WHATIS	"whatis"
58 #define OSENUM		MANDOC_OS_OPENBSD
59 
60 enum	outmode {
61 	OUTMODE_DEF = 0,
62 	OUTMODE_FLN,
63 	OUTMODE_LST,
64 	OUTMODE_ALL,
65 	OUTMODE_ONE
66 };
67 
68 enum	outt {
69 	OUTT_ASCII = 0,	/* -Tascii */
70 	OUTT_LOCALE,	/* -Tlocale */
71 	OUTT_UTF8,	/* -Tutf8 */
72 	OUTT_TREE,	/* -Ttree */
73 	OUTT_MAN,	/* -Tman */
74 	OUTT_HTML,	/* -Thtml */
75 	OUTT_MARKDOWN,	/* -Tmarkdown */
76 	OUTT_LINT,	/* -Tlint */
77 	OUTT_PS,	/* -Tps */
78 	OUTT_PDF	/* -Tpdf */
79 };
80 
81 struct	outstate {
82 	struct tag_files *tag_files;	/* Tagging state variables. */
83 	void		 *outdata;	/* data for output */
84 	int		  use_pager;
85 	int		  wstop;	/* stop after a file with a warning */
86 	int		  had_output;	/* Some output was generated. */
87 	enum outt	  outtype;	/* which output to use */
88 };
89 
90 
91 int			  mandocdb(int, char *[]);
92 
93 static	void		  check_xr(void);
94 static	int		  fs_lookup(const struct manpaths *,
95 				size_t ipath, const char *,
96 				const char *, const char *,
97 				struct manpage **, size_t *);
98 static	int		  fs_search(const struct mansearch *,
99 				const struct manpaths *, const char *,
100 				struct manpage **, size_t *);
101 static	void		  glob_esc(char **, const char *, const char *);
102 static	void		  outdata_alloc(struct outstate *, struct manoutput *);
103 static	void		  parse(struct mparse *, int, const char *,
104 				struct outstate *, struct manoutput *);
105 static	void		  passthrough(int, int);
106 static	void		  process_onefile(struct mparse *, struct manpage *,
107 				int, struct outstate *, struct manconf *);
108 static	void		  run_pager(struct tag_files *);
109 static	pid_t		  spawn_pager(struct tag_files *);
110 static	void		  usage(enum argmode) __attribute__((__noreturn__));
111 static	int		  woptions(char *, enum mandoc_os *, int *);
112 
113 static	const int sec_prios[] = {1, 4, 5, 8, 6, 3, 7, 2, 9};
114 static	char		  help_arg[] = "help";
115 static	char		 *help_argv[] = {help_arg, NULL};
116 
117 
118 int
119 main(int argc, char *argv[])
120 {
121 	struct manconf	 conf;		/* Manpaths and output options. */
122 	struct outstate	 outst;		/* Output state. */
123 	struct winsize	 ws;		/* Result of ioctl(TIOCGWINSZ). */
124 	struct mansearch search;	/* Search options. */
125 	struct manpage	*res;		/* Complete list of search results. */
126 	struct manpage	*resn;		/* Search results for one name. */
127 	struct mparse	*mp;		/* Opaque parser object. */
128 	const char	*conf_file;	/* -C: alternate config file. */
129 	const char	*os_s;		/* -I: Operating system for display. */
130 	const char	*progname, *sec;
131 	char		*defpaths;	/* -M: override manpaths. */
132 	char		*auxpaths;	/* -m: additional manpaths. */
133 	char		*oarg;		/* -O: output option string. */
134 	char		*tagarg;	/* -O tag: default value. */
135 	unsigned char	*uc;
136 	size_t		 ressz;		/* Number of elements in res[]. */
137 	size_t		 resnsz;	/* Number of elements in resn[]. */
138 	size_t		 i, ib, ssz;
139 	int		 options;	/* Parser options. */
140 	int		 show_usage;	/* Invalid argument: give up. */
141 	int		 prio, best_prio;
142 	int		 startdir;
143 	int		 c;
144 	enum mandoc_os	 os_e;		/* Check base system conventions. */
145 	enum outmode	 outmode;	/* According to command line. */
146 
147 	progname = getprogname();
148 	mandoc_msg_setoutfile(stderr);
149 	if (strncmp(progname, "mandocdb", 8) == 0 ||
150 	    strcmp(progname, BINM_MAKEWHATIS) == 0)
151 		return mandocdb(argc, argv);
152 
153 	if (pledge("stdio rpath tmppath tty proc exec", NULL) == -1) {
154 		mandoc_msg(MANDOCERR_PLEDGE, 0, 0, "%s", strerror(errno));
155 		return mandoc_msg_getrc();
156 	}
157 
158 	/* Search options. */
159 
160 	memset(&conf, 0, sizeof(conf));
161 	conf_file = NULL;
162 	defpaths = auxpaths = NULL;
163 
164 	memset(&search, 0, sizeof(struct mansearch));
165 	search.outkey = "Nd";
166 	oarg = NULL;
167 
168 	if (strcmp(progname, BINM_MAN) == 0)
169 		search.argmode = ARG_NAME;
170 	else if (strcmp(progname, BINM_APROPOS) == 0)
171 		search.argmode = ARG_EXPR;
172 	else if (strcmp(progname, BINM_WHATIS) == 0)
173 		search.argmode = ARG_WORD;
174 	else if (strncmp(progname, "help", 4) == 0)
175 		search.argmode = ARG_NAME;
176 	else
177 		search.argmode = ARG_FILE;
178 
179 	/* Parser options. */
180 
181 	options = MPARSE_SO | MPARSE_UTF8 | MPARSE_LATIN1;
182 	os_e = MANDOC_OS_OTHER;
183 	os_s = NULL;
184 
185 	/* Formatter options. */
186 
187 	memset(&outst, 0, sizeof(outst));
188 	outst.tag_files = NULL;
189 	outst.outtype = OUTT_LOCALE;
190 	outst.use_pager = 1;
191 
192 	show_usage = 0;
193 	outmode = OUTMODE_DEF;
194 
195 	while ((c = getopt(argc, argv,
196 	    "aC:cfhI:iK:klM:m:O:S:s:T:VW:w")) != -1) {
197 		if (c == 'i' && search.argmode == ARG_EXPR) {
198 			optind--;
199 			break;
200 		}
201 		switch (c) {
202 		case 'a':
203 			outmode = OUTMODE_ALL;
204 			break;
205 		case 'C':
206 			conf_file = optarg;
207 			break;
208 		case 'c':
209 			outst.use_pager = 0;
210 			break;
211 		case 'f':
212 			search.argmode = ARG_WORD;
213 			break;
214 		case 'h':
215 			conf.output.synopsisonly = 1;
216 			outst.use_pager = 0;
217 			outmode = OUTMODE_ALL;
218 			break;
219 		case 'I':
220 			if (strncmp(optarg, "os=", 3) != 0) {
221 				mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0,
222 				    "-I %s", optarg);
223 				return mandoc_msg_getrc();
224 			}
225 			if (os_s != NULL) {
226 				mandoc_msg(MANDOCERR_BADARG_DUPE, 0, 0,
227 				    "-I %s", optarg);
228 				return mandoc_msg_getrc();
229 			}
230 			os_s = optarg + 3;
231 			break;
232 		case 'K':
233 			options &= ~(MPARSE_UTF8 | MPARSE_LATIN1);
234 			if (strcmp(optarg, "utf-8") == 0)
235 				options |=  MPARSE_UTF8;
236 			else if (strcmp(optarg, "iso-8859-1") == 0)
237 				options |=  MPARSE_LATIN1;
238 			else if (strcmp(optarg, "us-ascii") != 0) {
239 				mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0,
240 				    "-K %s", optarg);
241 				return mandoc_msg_getrc();
242 			}
243 			break;
244 		case 'k':
245 			search.argmode = ARG_EXPR;
246 			break;
247 		case 'l':
248 			search.argmode = ARG_FILE;
249 			outmode = OUTMODE_ALL;
250 			break;
251 		case 'M':
252 			defpaths = optarg;
253 			break;
254 		case 'm':
255 			auxpaths = optarg;
256 			break;
257 		case 'O':
258 			oarg = optarg;
259 			break;
260 		case 'S':
261 			search.arch = optarg;
262 			break;
263 		case 's':
264 			search.sec = optarg;
265 			break;
266 		case 'T':
267 			if (strcmp(optarg, "ascii") == 0)
268 				outst.outtype = OUTT_ASCII;
269 			else if (strcmp(optarg, "lint") == 0) {
270 				outst.outtype = OUTT_LINT;
271 				mandoc_msg_setoutfile(stdout);
272 				mandoc_msg_setmin(MANDOCERR_BASE);
273 			} else if (strcmp(optarg, "tree") == 0)
274 				outst.outtype = OUTT_TREE;
275 			else if (strcmp(optarg, "man") == 0)
276 				outst.outtype = OUTT_MAN;
277 			else if (strcmp(optarg, "html") == 0)
278 				outst.outtype = OUTT_HTML;
279 			else if (strcmp(optarg, "markdown") == 0)
280 				outst.outtype = OUTT_MARKDOWN;
281 			else if (strcmp(optarg, "utf8") == 0)
282 				outst.outtype = OUTT_UTF8;
283 			else if (strcmp(optarg, "locale") == 0)
284 				outst.outtype = OUTT_LOCALE;
285 			else if (strcmp(optarg, "ps") == 0)
286 				outst.outtype = OUTT_PS;
287 			else if (strcmp(optarg, "pdf") == 0)
288 				outst.outtype = OUTT_PDF;
289 			else {
290 				mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0,
291 				    "-T %s", optarg);
292 				return mandoc_msg_getrc();
293 			}
294 			break;
295 		case 'W':
296 			if (woptions(optarg, &os_e, &outst.wstop) == -1)
297 				return mandoc_msg_getrc();
298 			break;
299 		case 'w':
300 			outmode = OUTMODE_FLN;
301 			break;
302 		default:
303 			show_usage = 1;
304 			break;
305 		}
306 	}
307 
308 	if (show_usage)
309 		usage(search.argmode);
310 
311 	/* Postprocess options. */
312 
313 	switch (outmode) {
314 	case OUTMODE_DEF:
315 		switch (search.argmode) {
316 		case ARG_FILE:
317 			outmode = OUTMODE_ALL;
318 			outst.use_pager = 0;
319 			break;
320 		case ARG_NAME:
321 			outmode = OUTMODE_ONE;
322 			break;
323 		default:
324 			outmode = OUTMODE_LST;
325 			break;
326 		}
327 		break;
328 	case OUTMODE_FLN:
329 		if (search.argmode == ARG_FILE)
330 			outmode = OUTMODE_ALL;
331 		break;
332 	case OUTMODE_ALL:
333 		break;
334 	case OUTMODE_LST:
335 	case OUTMODE_ONE:
336 		abort();
337 	}
338 
339 	if (oarg != NULL) {
340 		if (outmode == OUTMODE_LST)
341 			search.outkey = oarg;
342 		else {
343 			while (oarg != NULL) {
344 				if (manconf_output(&conf.output,
345 				    strsep(&oarg, ","), 0) == -1)
346 					return mandoc_msg_getrc();
347 			}
348 		}
349 	}
350 
351 	if (outst.outtype != OUTT_TREE || conf.output.noval == 0)
352 		options |= MPARSE_VALIDATE;
353 
354 	if (outmode == OUTMODE_FLN ||
355 	    outmode == OUTMODE_LST ||
356 	    !isatty(STDOUT_FILENO))
357 		outst.use_pager = 0;
358 
359 	if (outst.use_pager &&
360 	    (conf.output.width == 0 || conf.output.indent == 0) &&
361 	    ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 &&
362 	    ws.ws_col > 1) {
363 		if (conf.output.width == 0 && ws.ws_col < 79)
364 			conf.output.width = ws.ws_col - 1;
365 		if (conf.output.indent == 0 && ws.ws_col < 66)
366 			conf.output.indent = 3;
367 	}
368 
369 	if (outst.use_pager == 0) {
370 		if (pledge("stdio rpath", NULL) == -1) {
371 			mandoc_msg(MANDOCERR_PLEDGE, 0, 0,
372 			    "%s", strerror(errno));
373 			return mandoc_msg_getrc();
374 		}
375 	}
376 
377 	/* Parse arguments. */
378 
379 	if (argc > 0) {
380 		argc -= optind;
381 		argv += optind;
382 	}
383 
384 	/*
385 	 * Quirks for help(1) and man(1),
386 	 * in particular for a section argument without -s.
387 	 */
388 
389 	if (search.argmode == ARG_NAME) {
390 		if (*progname == 'h') {
391 			if (argc == 0) {
392 				argv = help_argv;
393 				argc = 1;
394 			}
395 		} else if (argc > 1 &&
396 		    ((uc = (unsigned char *)argv[0]) != NULL) &&
397 		    ((isdigit(uc[0]) && (uc[1] == '\0' ||
398 		      isalpha(uc[1]))) ||
399 		     (uc[0] == 'n' && uc[1] == '\0'))) {
400 			search.sec = (char *)uc;
401 			argv++;
402 			argc--;
403 		}
404 		if (search.arch == NULL)
405 			search.arch = getenv("MACHINE");
406 		if (search.arch == NULL)
407 			search.arch = MACHINE;
408 		if (outmode == OUTMODE_ONE)
409 			search.firstmatch = 1;
410 	}
411 
412 	/*
413 	 * Use the first argument for -O tag in addition to
414 	 * using it as a search term for man(1) or apropos(1).
415 	 */
416 
417 	if (conf.output.tag != NULL && *conf.output.tag == '\0') {
418 		tagarg = argc > 0 && search.argmode == ARG_EXPR ?
419 		    strchr(*argv, '=') : NULL;
420 		conf.output.tag = tagarg == NULL ? *argv : tagarg + 1;
421 	}
422 
423 	/* Read the configuration file. */
424 
425 	if (search.argmode != ARG_FILE)
426 		manconf_parse(&conf, conf_file, defpaths, auxpaths);
427 
428 	/* man(1): Resolve each name individually. */
429 
430 	if (search.argmode == ARG_NAME) {
431 		if (argc < 1) {
432 			if (outmode != OUTMODE_FLN)
433 				usage(ARG_NAME);
434 			if (conf.manpath.sz == 0) {
435 				warnx("The manpath is empty.");
436 				mandoc_msg_setrc(MANDOCLEVEL_BADARG);
437 			} else {
438 				for (i = 0; i + 1 < conf.manpath.sz; i++)
439 					printf("%s:", conf.manpath.paths[i]);
440 				printf("%s\n", conf.manpath.paths[i]);
441 			}
442 			manconf_free(&conf);
443 			return (int)mandoc_msg_getrc();
444 		}
445 		for (res = NULL, ressz = 0; argc > 0; argc--, argv++) {
446 			(void)mansearch(&search, &conf.manpath,
447 			    1, argv, &resn, &resnsz);
448 			if (resnsz == 0)
449 				(void)fs_search(&search, &conf.manpath,
450 				    *argv, &resn, &resnsz);
451 			if (resnsz == 0 && strchr(*argv, '/') == NULL) {
452 				if (search.arch != NULL &&
453 				    arch_valid(search.arch, OSENUM) == 0)
454 					warnx("Unknown architecture \"%s\".",
455 					    search.arch);
456 				else if (search.sec != NULL)
457 					warnx("No entry for %s in "
458 					    "section %s of the manual.",
459 					    *argv, search.sec);
460 				else
461 					warnx("No entry for %s in "
462 					    "the manual.", *argv);
463 				mandoc_msg_setrc(MANDOCLEVEL_BADARG);
464 				continue;
465 			}
466 			if (resnsz == 0) {
467 				if (access(*argv, R_OK) == -1) {
468 					mandoc_msg_setinfilename(*argv);
469 					mandoc_msg(MANDOCERR_BADARG_BAD,
470 					    0, 0, "%s", strerror(errno));
471 					mandoc_msg_setinfilename(NULL);
472 					continue;
473 				}
474 				resnsz = 1;
475 				resn = mandoc_calloc(resnsz, sizeof(*res));
476 				resn->file = mandoc_strdup(*argv);
477 				resn->ipath = SIZE_MAX;
478 				resn->form = FORM_SRC;
479 			}
480 			if (outmode != OUTMODE_ONE || resnsz == 1) {
481 				res = mandoc_reallocarray(res,
482 				    ressz + resnsz, sizeof(*res));
483 				memcpy(res + ressz, resn,
484 				    sizeof(*resn) * resnsz);
485 				ressz += resnsz;
486 				continue;
487 			}
488 
489 			/* Search for the best section. */
490 
491 			best_prio = 40;
492 			for (ib = i = 0; i < resnsz; i++) {
493 				sec = resn[i].file;
494 				sec += strcspn(sec, "123456789");
495 				if (sec[0] == '\0')
496 					continue; /* No section at all. */
497 				prio = sec_prios[sec[0] - '1'];
498 				if (search.sec != NULL) {
499 					ssz = strlen(search.sec);
500 					if (strncmp(sec, search.sec, ssz) == 0)
501 						sec += ssz;
502 				} else
503 					sec++; /* Prefer without suffix. */
504 				if (*sec != '/')
505 					prio += 10; /* Wrong dir name. */
506 				if (search.sec != NULL &&
507 				    (strlen(sec) <= ssz  + 3 ||
508 				     strcmp(sec + strlen(sec) - ssz,
509 				      search.sec) != 0))
510 					prio += 20; /* Wrong file ext. */
511 				if (prio >= best_prio)
512 					continue;
513 				best_prio = prio;
514 				ib = i;
515 			}
516 			res = mandoc_reallocarray(res, ressz + 1,
517 			    sizeof(*res));
518 			memcpy(res + ressz++, resn + ib, sizeof(*resn));
519 		}
520 
521 	/* apropos(1), whatis(1): Process the full search expression. */
522 
523 	} else if (search.argmode != ARG_FILE) {
524 		if (mansearch(&search, &conf.manpath,
525 		    argc, argv, &res, &ressz) == 0)
526 			usage(search.argmode);
527 
528 		if (ressz == 0) {
529 			warnx("nothing appropriate");
530 			mandoc_msg_setrc(MANDOCLEVEL_BADARG);
531 			goto out;
532 		}
533 
534 	/* mandoc(1): Take command line arguments as file names. */
535 
536 	} else {
537 		ressz = argc > 0 ? argc : 1;
538 		res = mandoc_calloc(ressz, sizeof(*res));
539 		for (i = 0; i < ressz; i++) {
540 			if (argc > 0)
541 				res[i].file = mandoc_strdup(argv[i]);
542 			res[i].ipath = SIZE_MAX;
543 			res[i].form = FORM_SRC;
544 		}
545 	}
546 
547 	switch (outmode) {
548 	case OUTMODE_FLN:
549 		for (i = 0; i < ressz; i++)
550 			puts(res[i].file);
551 		goto out;
552 	case OUTMODE_LST:
553 		for (i = 0; i < ressz; i++)
554 			printf("%s - %s\n", res[i].names,
555 			    res[i].output == NULL ? "" :
556 			    res[i].output);
557 		goto out;
558 	default:
559 		break;
560 	}
561 
562 	if (search.argmode == ARG_FILE && auxpaths != NULL) {
563 		if (strcmp(auxpaths, "doc") == 0)
564 			options |= MPARSE_MDOC;
565 		else if (strcmp(auxpaths, "an") == 0)
566 			options |= MPARSE_MAN;
567 	}
568 
569 	mchars_alloc();
570 	mp = mparse_alloc(options, os_e, os_s);
571 
572 	/*
573 	 * Remember the original working directory, if possible.
574 	 * This will be needed if some names on the command line
575 	 * are page names and some are relative file names.
576 	 * Do not error out if the current directory is not
577 	 * readable: Maybe it won't be needed after all.
578 	 */
579 	startdir = open(".", O_RDONLY | O_DIRECTORY);
580 
581 	for (i = 0; i < ressz; i++) {
582 		process_onefile(mp, res + i, startdir, &outst, &conf);
583 		if (outst.wstop && mandoc_msg_getrc() != MANDOCLEVEL_OK)
584 			break;
585 	}
586 	if (startdir != -1) {
587 		(void)fchdir(startdir);
588 		close(startdir);
589 	}
590 
591 	if (outst.outdata != NULL) {
592 		switch (outst.outtype) {
593 		case OUTT_HTML:
594 			html_free(outst.outdata);
595 			break;
596 		case OUTT_UTF8:
597 		case OUTT_LOCALE:
598 		case OUTT_ASCII:
599 			ascii_free(outst.outdata);
600 			break;
601 		case OUTT_PDF:
602 		case OUTT_PS:
603 			pspdf_free(outst.outdata);
604 			break;
605 		default:
606 			break;
607 		}
608 	}
609 	mandoc_xr_free();
610 	mparse_free(mp);
611 	mchars_free();
612 
613 out:
614 	mansearch_free(res, ressz);
615 	if (search.argmode != ARG_FILE)
616 		manconf_free(&conf);
617 
618 	if (outst.tag_files != NULL) {
619 		fclose(stdout);
620 		tag_write();
621 		run_pager(outst.tag_files);
622 		tag_unlink();
623 	} else if (outst.had_output && outst.outtype != OUTT_LINT)
624 		mandoc_msg_summary();
625 
626 	return (int)mandoc_msg_getrc();
627 }
628 
629 static void
630 usage(enum argmode argmode)
631 {
632 	switch (argmode) {
633 	case ARG_FILE:
634 		fputs("usage: mandoc [-ac] [-I os=name] "
635 		    "[-K encoding] [-mdoc | -man] [-O options]\n"
636 		    "\t      [-T output] [-W level] [file ...]\n", stderr);
637 		break;
638 	case ARG_NAME:
639 		fputs("usage: man [-acfhklw] [-C file] [-M path] "
640 		    "[-m path] [-S subsection]\n"
641 		    "\t   [[-s] section] name ...\n", stderr);
642 		break;
643 	case ARG_WORD:
644 		fputs("usage: whatis [-afk] [-C file] "
645 		    "[-M path] [-m path] [-O outkey] [-S arch]\n"
646 		    "\t      [-s section] name ...\n", stderr);
647 		break;
648 	case ARG_EXPR:
649 		fputs("usage: apropos [-afk] [-C file] "
650 		    "[-M path] [-m path] [-O outkey] [-S arch]\n"
651 		    "\t       [-s section] expression ...\n", stderr);
652 		break;
653 	}
654 	exit((int)MANDOCLEVEL_BADARG);
655 }
656 
657 static void
658 glob_esc(char **dst, const char *src, const char *suffix)
659 {
660 	while (*src != '\0') {
661 		if (strchr("*?[", *src) != NULL)
662 			*(*dst)++ = '\\';
663 		*(*dst)++ = *src++;
664 	}
665 	while (*suffix != '\0')
666 		*(*dst)++ = *suffix++;
667 }
668 
669 static int
670 fs_lookup(const struct manpaths *paths, size_t ipath,
671 	const char *sec, const char *arch, const char *name,
672 	struct manpage **res, size_t *ressz)
673 {
674 	struct stat	 sb;
675 	glob_t		 globinfo;
676 	struct manpage	*page;
677 	char		*file, *cp;
678 	int		 globres;
679 	enum form	 form;
680 
681 	const char *const slman = "/man";
682 	const char *const slash = "/";
683 	const char *const sglob = ".[01-9]*";
684 
685 	form = FORM_SRC;
686 	mandoc_asprintf(&file, "%s/man%s/%s.%s",
687 	    paths->paths[ipath], sec, name, sec);
688 	if (stat(file, &sb) != -1)
689 		goto found;
690 	free(file);
691 
692 	mandoc_asprintf(&file, "%s/cat%s/%s.0",
693 	    paths->paths[ipath], sec, name);
694 	if (stat(file, &sb) != -1) {
695 		form = FORM_CAT;
696 		goto found;
697 	}
698 	free(file);
699 
700 	if (arch != NULL) {
701 		mandoc_asprintf(&file, "%s/man%s/%s/%s.%s",
702 		    paths->paths[ipath], sec, arch, name, sec);
703 		if (stat(file, &sb) != -1)
704 			goto found;
705 		free(file);
706 	}
707 
708 	cp = file = mandoc_malloc(strlen(paths->paths[ipath]) * 2 +
709 	    strlen(slman) + strlen(sec) * 2 + strlen(slash) +
710 	    strlen(name) * 2 + strlen(sglob) + 1);
711 	glob_esc(&cp, paths->paths[ipath], slman);
712 	glob_esc(&cp, sec, slash);
713 	glob_esc(&cp, name, sglob);
714 	*cp = '\0';
715 	globres = glob(file, 0, NULL, &globinfo);
716 	if (globres != 0 && globres != GLOB_NOMATCH)
717 		mandoc_msg(MANDOCERR_GLOB, 0, 0,
718 		    "%s: %s", file, strerror(errno));
719 	free(file);
720 	if (globres == 0)
721 		file = mandoc_strdup(*globinfo.gl_pathv);
722 	globfree(&globinfo);
723 	if (globres == 0) {
724 		if (stat(file, &sb) != -1)
725 			goto found;
726 		free(file);
727 	}
728 	if (res != NULL || ipath + 1 != paths->sz)
729 		return -1;
730 
731 	mandoc_asprintf(&file, "%s.%s", name, sec);
732 	globres = stat(file, &sb);
733 	free(file);
734 	return globres;
735 
736 found:
737 	warnx("outdated mandoc.db lacks %s(%s) entry, run %s %s",
738 	    name, sec, BINM_MAKEWHATIS, paths->paths[ipath]);
739 	if (res == NULL) {
740 		free(file);
741 		return 0;
742 	}
743 	*res = mandoc_reallocarray(*res, ++*ressz, sizeof(**res));
744 	page = *res + (*ressz - 1);
745 	page->file = file;
746 	page->names = NULL;
747 	page->output = NULL;
748 	page->bits = NAME_FILE & NAME_MASK;
749 	page->ipath = ipath;
750 	page->sec = (*sec >= '1' && *sec <= '9') ? *sec - '1' + 1 : 10;
751 	page->form = form;
752 	return 0;
753 }
754 
755 static int
756 fs_search(const struct mansearch *cfg, const struct manpaths *paths,
757 	const char *name, struct manpage **res, size_t *ressz)
758 {
759 	const char *const sections[] =
760 	    {"1", "8", "6", "2", "3", "5", "7", "4", "9", "3p"};
761 	const size_t nsec = sizeof(sections)/sizeof(sections[0]);
762 
763 	size_t		 ipath, isec;
764 
765 	assert(cfg->argmode == ARG_NAME);
766 	if (res != NULL)
767 		*res = NULL;
768 	*ressz = 0;
769 	for (ipath = 0; ipath < paths->sz; ipath++) {
770 		if (cfg->sec != NULL) {
771 			if (fs_lookup(paths, ipath, cfg->sec, cfg->arch,
772 			    name, res, ressz) != -1 && cfg->firstmatch)
773 				return 0;
774 		} else {
775 			for (isec = 0; isec < nsec; isec++)
776 				if (fs_lookup(paths, ipath, sections[isec],
777 				    cfg->arch, name, res, ressz) != -1 &&
778 				    cfg->firstmatch)
779 					return 0;
780 		}
781 	}
782 	return -1;
783 }
784 
785 static void
786 process_onefile(struct mparse *mp, struct manpage *resp, int startdir,
787     struct outstate *outst, struct manconf *conf)
788 {
789 	int	 fd;
790 
791 	/*
792 	 * Changing directories is not needed in ARG_FILE mode.
793 	 * Do it on a best-effort basis.  Even in case of
794 	 * failure, some functionality may still work.
795 	 */
796 	if (resp->ipath != SIZE_MAX)
797 		(void)chdir(conf->manpath.paths[resp->ipath]);
798 	else if (startdir != -1)
799 		(void)fchdir(startdir);
800 
801 	mandoc_msg_setinfilename(resp->file);
802 	if (resp->file != NULL) {
803 		if ((fd = mparse_open(mp, resp->file)) == -1) {
804 			mandoc_msg(resp->ipath == SIZE_MAX ?
805 			    MANDOCERR_BADARG_BAD : MANDOCERR_OPEN,
806 			    0, 0, "%s", strerror(errno));
807 			mandoc_msg_setinfilename(NULL);
808 			return;
809 		}
810 	} else
811 		fd = STDIN_FILENO;
812 
813 	if (outst->use_pager) {
814 		outst->use_pager = 0;
815 		outst->tag_files = tag_init(conf->output.tag);
816 	}
817 
818 	if (outst->had_output && outst->outtype <= OUTT_UTF8) {
819 		if (outst->outdata == NULL)
820 			outdata_alloc(outst, &conf->output);
821 		terminal_sepline(outst->outdata);
822 	}
823 
824 	if (resp->form == FORM_SRC)
825 		parse(mp, fd, resp->file, outst, &conf->output);
826 	else {
827 		passthrough(fd, conf->output.synopsisonly);
828 		outst->had_output = 1;
829 	}
830 
831 	if (ferror(stdout)) {
832 		if (outst->tag_files != NULL) {
833 			mandoc_msg(MANDOCERR_WRITE, 0, 0, "%s: %s",
834 			    outst->tag_files->ofn, strerror(errno));
835 			tag_unlink();
836 			outst->tag_files = NULL;
837 		} else
838 			mandoc_msg(MANDOCERR_WRITE, 0, 0, "%s",
839 			    strerror(errno));
840 	}
841 	mandoc_msg_setinfilename(NULL);
842 }
843 
844 static void
845 parse(struct mparse *mp, int fd, const char *file,
846     struct outstate *outst, struct manoutput *outconf)
847 {
848 	static int		 previous;
849 	struct roff_meta	*meta;
850 
851 	assert(fd >= 0);
852 	if (file == NULL)
853 		file = "<stdin>";
854 
855 	if (previous)
856 		mparse_reset(mp);
857 	else
858 		previous = 1;
859 
860 	mparse_readfd(mp, fd, file);
861 	if (fd != STDIN_FILENO)
862 		close(fd);
863 
864 	/*
865 	 * With -Wstop and warnings or errors of at least the requested
866 	 * level, do not produce output.
867 	 */
868 
869 	if (outst->wstop && mandoc_msg_getrc() != MANDOCLEVEL_OK)
870 		return;
871 
872 	if (outst->outdata == NULL)
873 		outdata_alloc(outst, outconf);
874 	else if (outst->outtype == OUTT_HTML)
875 		html_reset(outst);
876 
877 	mandoc_xr_reset();
878 	meta = mparse_result(mp);
879 
880 	/* Execute the out device, if it exists. */
881 
882 	outst->had_output = 1;
883 	if (meta->macroset == MACROSET_MDOC) {
884 		switch (outst->outtype) {
885 		case OUTT_HTML:
886 			html_mdoc(outst->outdata, meta);
887 			break;
888 		case OUTT_TREE:
889 			tree_mdoc(outst->outdata, meta);
890 			break;
891 		case OUTT_MAN:
892 			man_mdoc(outst->outdata, meta);
893 			break;
894 		case OUTT_PDF:
895 		case OUTT_ASCII:
896 		case OUTT_UTF8:
897 		case OUTT_LOCALE:
898 		case OUTT_PS:
899 			terminal_mdoc(outst->outdata, meta);
900 			break;
901 		case OUTT_MARKDOWN:
902 			markdown_mdoc(outst->outdata, meta);
903 			break;
904 		default:
905 			break;
906 		}
907 	}
908 	if (meta->macroset == MACROSET_MAN) {
909 		switch (outst->outtype) {
910 		case OUTT_HTML:
911 			html_man(outst->outdata, meta);
912 			break;
913 		case OUTT_TREE:
914 			tree_man(outst->outdata, meta);
915 			break;
916 		case OUTT_MAN:
917 			mparse_copy(mp);
918 			break;
919 		case OUTT_PDF:
920 		case OUTT_ASCII:
921 		case OUTT_UTF8:
922 		case OUTT_LOCALE:
923 		case OUTT_PS:
924 			terminal_man(outst->outdata, meta);
925 			break;
926 		default:
927 			break;
928 		}
929 	}
930 	if (mandoc_msg_getmin() < MANDOCERR_STYLE)
931 		check_xr();
932 }
933 
934 static void
935 check_xr(void)
936 {
937 	static struct manpaths	 paths;
938 	struct mansearch	 search;
939 	struct mandoc_xr	*xr;
940 	size_t			 sz;
941 
942 	if (paths.sz == 0)
943 		manpath_base(&paths);
944 
945 	for (xr = mandoc_xr_get(); xr != NULL; xr = xr->next) {
946 		if (xr->line == -1)
947 			continue;
948 		search.arch = NULL;
949 		search.sec = xr->sec;
950 		search.outkey = NULL;
951 		search.argmode = ARG_NAME;
952 		search.firstmatch = 1;
953 		if (mansearch(&search, &paths, 1, &xr->name, NULL, &sz))
954 			continue;
955 		if (fs_search(&search, &paths, xr->name, NULL, &sz) != -1)
956 			continue;
957 		if (xr->count == 1)
958 			mandoc_msg(MANDOCERR_XR_BAD, xr->line,
959 			    xr->pos + 1, "Xr %s %s", xr->name, xr->sec);
960 		else
961 			mandoc_msg(MANDOCERR_XR_BAD, xr->line,
962 			    xr->pos + 1, "Xr %s %s (%d times)",
963 			    xr->name, xr->sec, xr->count);
964 	}
965 }
966 
967 static void
968 outdata_alloc(struct outstate *outst, struct manoutput *outconf)
969 {
970 	switch (outst->outtype) {
971 	case OUTT_HTML:
972 		outst->outdata = html_alloc(outconf);
973 		break;
974 	case OUTT_UTF8:
975 		outst->outdata = utf8_alloc(outconf);
976 		break;
977 	case OUTT_LOCALE:
978 		outst->outdata = locale_alloc(outconf);
979 		break;
980 	case OUTT_ASCII:
981 		outst->outdata = ascii_alloc(outconf);
982 		break;
983 	case OUTT_PDF:
984 		outst->outdata = pdf_alloc(outconf);
985 		break;
986 	case OUTT_PS:
987 		outst->outdata = ps_alloc(outconf);
988 		break;
989 	default:
990 		break;
991 	}
992 }
993 
994 static void
995 passthrough(int fd, int synopsis_only)
996 {
997 	const char	 synb[] = "S\bSY\bYN\bNO\bOP\bPS\bSI\bIS\bS";
998 	const char	 synr[] = "SYNOPSIS";
999 
1000 	FILE		*stream;
1001 	char		*line, *cp;
1002 	size_t		 linesz;
1003 	ssize_t		 len, written;
1004 	int		 lno, print;
1005 
1006 	stream = NULL;
1007 	line = NULL;
1008 	linesz = 0;
1009 
1010 	if (fflush(stdout) == EOF) {
1011 		mandoc_msg(MANDOCERR_FFLUSH, 0, 0, "%s", strerror(errno));
1012 		goto done;
1013 	}
1014 	if ((stream = fdopen(fd, "r")) == NULL) {
1015 		close(fd);
1016 		mandoc_msg(MANDOCERR_FDOPEN, 0, 0, "%s", strerror(errno));
1017 		goto done;
1018 	}
1019 
1020 	lno = print = 0;
1021 	while ((len = getline(&line, &linesz, stream)) != -1) {
1022 		lno++;
1023 		cp = line;
1024 		if (synopsis_only) {
1025 			if (print) {
1026 				if ( ! isspace((unsigned char)*cp))
1027 					goto done;
1028 				while (isspace((unsigned char)*cp)) {
1029 					cp++;
1030 					len--;
1031 				}
1032 			} else {
1033 				if (strcmp(cp, synb) == 0 ||
1034 				    strcmp(cp, synr) == 0)
1035 					print = 1;
1036 				continue;
1037 			}
1038 		}
1039 		for (; len > 0; len -= written) {
1040 			if ((written = write(STDOUT_FILENO, cp, len)) == -1) {
1041 				mandoc_msg(MANDOCERR_WRITE, 0, 0,
1042 				    "%s", strerror(errno));
1043 				goto done;
1044 			}
1045 		}
1046 	}
1047 	if (ferror(stream))
1048 		mandoc_msg(MANDOCERR_GETLINE, lno, 0, "%s", strerror(errno));
1049 
1050 done:
1051 	free(line);
1052 	if (stream != NULL)
1053 		fclose(stream);
1054 }
1055 
1056 static int
1057 woptions(char *arg, enum mandoc_os *os_e, int *wstop)
1058 {
1059 	char		*v, *o;
1060 	const char	*toks[11];
1061 
1062 	toks[0] = "stop";
1063 	toks[1] = "all";
1064 	toks[2] = "base";
1065 	toks[3] = "style";
1066 	toks[4] = "warning";
1067 	toks[5] = "error";
1068 	toks[6] = "unsupp";
1069 	toks[7] = "fatal";
1070 	toks[8] = "openbsd";
1071 	toks[9] = "netbsd";
1072 	toks[10] = NULL;
1073 
1074 	while (*arg) {
1075 		o = arg;
1076 		switch (getsubopt(&arg, (char * const *)toks, &v)) {
1077 		case 0:
1078 			*wstop = 1;
1079 			break;
1080 		case 1:
1081 		case 2:
1082 			mandoc_msg_setmin(MANDOCERR_BASE);
1083 			break;
1084 		case 3:
1085 			mandoc_msg_setmin(MANDOCERR_STYLE);
1086 			break;
1087 		case 4:
1088 			mandoc_msg_setmin(MANDOCERR_WARNING);
1089 			break;
1090 		case 5:
1091 			mandoc_msg_setmin(MANDOCERR_ERROR);
1092 			break;
1093 		case 6:
1094 			mandoc_msg_setmin(MANDOCERR_UNSUPP);
1095 			break;
1096 		case 7:
1097 			mandoc_msg_setmin(MANDOCERR_BADARG);
1098 			break;
1099 		case 8:
1100 			mandoc_msg_setmin(MANDOCERR_BASE);
1101 			*os_e = MANDOC_OS_OPENBSD;
1102 			break;
1103 		case 9:
1104 			mandoc_msg_setmin(MANDOCERR_BASE);
1105 			*os_e = MANDOC_OS_NETBSD;
1106 			break;
1107 		default:
1108 			mandoc_msg(MANDOCERR_BADARG_BAD, 0, 0, "-W %s", o);
1109 			return -1;
1110 		}
1111 	}
1112 	return 0;
1113 }
1114 
1115 /*
1116  * Wait until moved to the foreground,
1117  * then fork the pager and wait for the user to close it.
1118  */
1119 static void
1120 run_pager(struct tag_files *tag_files)
1121 {
1122 	int	 signum, status;
1123 	pid_t	 man_pgid, tc_pgid;
1124 	pid_t	 pager_pid, wait_pid;
1125 
1126 	man_pgid = getpgid(0);
1127 	tag_files->tcpgid = man_pgid == getpid() ? getpgid(getppid()) :
1128 	    man_pgid;
1129 	pager_pid = 0;
1130 	signum = SIGSTOP;
1131 
1132 	for (;;) {
1133 		/* Stop here until moved to the foreground. */
1134 
1135 		tc_pgid = tcgetpgrp(tag_files->ofd);
1136 		if (tc_pgid != man_pgid) {
1137 			if (tc_pgid == pager_pid) {
1138 				(void)tcsetpgrp(tag_files->ofd, man_pgid);
1139 				if (signum == SIGTTIN)
1140 					continue;
1141 			} else
1142 				tag_files->tcpgid = tc_pgid;
1143 			kill(0, signum);
1144 			continue;
1145 		}
1146 
1147 		/* Once in the foreground, activate the pager. */
1148 
1149 		if (pager_pid) {
1150 			(void)tcsetpgrp(tag_files->ofd, pager_pid);
1151 			kill(pager_pid, SIGCONT);
1152 		} else
1153 			pager_pid = spawn_pager(tag_files);
1154 
1155 		/* Wait for the pager to stop or exit. */
1156 
1157 		while ((wait_pid = waitpid(pager_pid, &status,
1158 		    WUNTRACED)) == -1 && errno == EINTR)
1159 			continue;
1160 
1161 		if (wait_pid == -1) {
1162 			mandoc_msg(MANDOCERR_WAIT, 0, 0,
1163 			    "%s", strerror(errno));
1164 			break;
1165 		}
1166 		if (!WIFSTOPPED(status))
1167 			break;
1168 
1169 		signum = WSTOPSIG(status);
1170 	}
1171 }
1172 
1173 static pid_t
1174 spawn_pager(struct tag_files *tag_files)
1175 {
1176 	const struct timespec timeout = { 0, 100000000 };  /* 0.1s */
1177 #define MAX_PAGER_ARGS 16
1178 	char		*argv[MAX_PAGER_ARGS];
1179 	const char	*pager;
1180 	char		*cp;
1181 	size_t		 cmdlen;
1182 	int		 argc, use_ofn;
1183 	pid_t		 pager_pid;
1184 
1185 	pager = getenv("MANPAGER");
1186 	if (pager == NULL || *pager == '\0')
1187 		pager = getenv("PAGER");
1188 	if (pager == NULL || *pager == '\0')
1189 		pager = "more -s";
1190 	cp = mandoc_strdup(pager);
1191 
1192 	/*
1193 	 * Parse the pager command into words.
1194 	 * Intentionally do not do anything fancy here.
1195 	 */
1196 
1197 	argc = 0;
1198 	while (argc + 5 < MAX_PAGER_ARGS) {
1199 		argv[argc++] = cp;
1200 		cp = strchr(cp, ' ');
1201 		if (cp == NULL)
1202 			break;
1203 		*cp++ = '\0';
1204 		while (*cp == ' ')
1205 			cp++;
1206 		if (*cp == '\0')
1207 			break;
1208 	}
1209 
1210 	/* For more(1) and less(1), use the tag file. */
1211 
1212 	use_ofn = 1;
1213 	if (*tag_files->tfn != '\0' && (cmdlen = strlen(argv[0])) >= 4) {
1214 		cp = argv[0] + cmdlen - 4;
1215 		if (strcmp(cp, "less") == 0 || strcmp(cp, "more") == 0) {
1216 			argv[argc++] = mandoc_strdup("-T");
1217 			argv[argc++] = tag_files->tfn;
1218 			if (tag_files->tagname != NULL) {
1219 				argv[argc++] = mandoc_strdup("-t");
1220 				argv[argc++] = tag_files->tagname;
1221 				use_ofn = 0;
1222 			}
1223 		}
1224 	}
1225 	if (use_ofn)
1226 		argv[argc++] = tag_files->ofn;
1227 	argv[argc] = NULL;
1228 
1229 	switch (pager_pid = fork()) {
1230 	case -1:
1231 		mandoc_msg(MANDOCERR_FORK, 0, 0, "%s", strerror(errno));
1232 		exit(mandoc_msg_getrc());
1233 	case 0:
1234 		break;
1235 	default:
1236 		(void)setpgid(pager_pid, 0);
1237 		(void)tcsetpgrp(tag_files->ofd, pager_pid);
1238 		if (pledge("stdio rpath tmppath tty proc", NULL) == -1) {
1239 			mandoc_msg(MANDOCERR_PLEDGE, 0, 0,
1240 			    "%s", strerror(errno));
1241 			exit(mandoc_msg_getrc());
1242 		}
1243 		tag_files->pager_pid = pager_pid;
1244 		return pager_pid;
1245 	}
1246 
1247 	/* The child process becomes the pager. */
1248 
1249 	if (dup2(tag_files->ofd, STDOUT_FILENO) == -1) {
1250 		mandoc_msg(MANDOCERR_DUP, 0, 0, "%s", strerror(errno));
1251 		_exit(mandoc_msg_getrc());
1252 	}
1253 	close(tag_files->ofd);
1254 	assert(tag_files->tfd == -1);
1255 
1256 	/* Do not start the pager before controlling the terminal. */
1257 
1258 	while (tcgetpgrp(STDOUT_FILENO) != getpid())
1259 		nanosleep(&timeout, NULL);
1260 
1261 	execvp(argv[0], argv);
1262 	mandoc_msg(MANDOCERR_EXEC, 0, 0, "%s: %s", argv[0], strerror(errno));
1263 	_exit(mandoc_msg_getrc());
1264 }
1265