xref: /freebsd/usr.bin/man/man.sh (revision 46a9fb7287f41eedf321d81a68a826f231d11bfe)
1 #! /bin/sh
2 #
3 # SPDX-License-Identifier: BSD-2-Clause
4 #
5 #  Copyright (c) 2010 Gordon Tetlow
6 #  All rights reserved.
7 #
8 #  Redistribution and use in source and binary forms, with or without
9 #  modification, are permitted provided that the following conditions
10 #  are met:
11 #  1. Redistributions of source code must retain the above copyright
12 #     notice, this list of conditions and the following disclaimer.
13 #  2. Redistributions in binary form must reproduce the above copyright
14 #     notice, this list of conditions and the following disclaimer in the
15 #     documentation and/or other materials provided with the distribution.
16 #
17 #  THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 #  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 #  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 #  ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 #  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 #  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 #  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 #  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 #  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 #  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 #  SUCH DAMAGE.
28 #
29 
30 # Rendering a manual page is fast. Even a manual page several 100k in size
31 # takes less than a CPU second. If it takes much longer, it is very likely
32 # that a tool like mandoc(1) is running in an infinite loop. In this case
33 # it is better to terminate it.
34 ulimit -t 20
35 
36 # do not ignore the exit status of roff tools
37 set -o pipefail
38 
39 # Usage: add_to_manpath path
40 # Adds a variable to manpath while ensuring we don't have duplicates.
41 # Returns true if we were able to add something. False otherwise.
42 add_to_manpath() {
43 	case "$manpath" in
44 	*:$1)	decho "  Skipping duplicate manpath entry $1" 2 ;;
45 	$1:*)	decho "  Skipping duplicate manpath entry $1" 2 ;;
46 	*:$1:*)	decho "  Skipping duplicate manpath entry $1" 2 ;;
47 	*)	if [ -d "$1" ]; then
48 			decho "  Adding $1 to manpath"
49 			manpath="$manpath:$1"
50 			return 0
51 		fi
52 		;;
53 	esac
54 
55 	return 1
56 }
57 
58 # Usage: build_manlocales
59 # Builds a correct MANLOCALES variable.
60 build_manlocales() {
61 	# If the user has set manlocales, who are we to argue.
62 	if [ -n "$MANLOCALES" ]; then
63 		return
64 	fi
65 
66 	parse_configs
67 
68 	# Trim leading colon
69 	MANLOCALES=${manlocales#:}
70 
71 	decho "Available manual locales: $MANLOCALES"
72 }
73 
74 # Usage: build_mansect
75 # Builds a correct MANSECT variable.
76 build_mansect() {
77 	# If the user has set mansect, who are we to argue.
78 	if [ -n "$MANSECT" ]; then
79 		return
80 	fi
81 
82 	parse_configs
83 
84 	# Trim leading colon
85 	MANSECT=${mansect#:}
86 
87 	if [ -z "$MANSECT" ]; then
88 		MANSECT=$man_default_sections
89 	fi
90 	decho "Using manual sections: $MANSECT"
91 }
92 
93 # Usage: build_manpath
94 # Builds a correct MANPATH variable.
95 build_manpath() {
96 	local IFS
97 
98 	# If the user has set a manpath, who are we to argue.
99 	if [ -n "$MANPATH" ]; then
100 		case "$MANPATH" in
101 		*:) PREPEND_MANPATH=${MANPATH} ;;
102 		:*) APPEND_MANPATH=${MANPATH} ;;
103 		*::*)
104 			PREPEND_MANPATH=${MANPATH%%::*}
105 			APPEND_MANPATH=${MANPATH#*::}
106 			;;
107 		*) return ;;
108 		esac
109 	fi
110 
111 	if [ -n "$PREPEND_MANPATH" ]; then
112 		IFS=:
113 		for path in $PREPEND_MANPATH; do
114 			add_to_manpath "$path"
115 		done
116 		unset IFS
117 	fi
118 
119 	search_path
120 
121 	decho "Adding default manpath entries"
122 	IFS=:
123 	for path in $man_default_path; do
124 		add_to_manpath "$path"
125 	done
126 	unset IFS
127 
128 	parse_configs
129 
130 	if [ -n "$APPEND_MANPATH" ]; then
131 		IFS=:
132 		for path in $APPEND_MANPATH; do
133 			add_to_manpath "$path"
134 		done
135 		unset IFS
136 	fi
137 	# Trim leading colon
138 	MANPATH=${manpath#:}
139 
140 	decho "Using manual path: $MANPATH"
141 }
142 
143 # Usage: check_cat catglob
144 # Checks to see if a cat glob is available.
145 check_cat() {
146 	if exists "$1"; then
147 		use_cat=yes
148 		catpage=$found
149 		setup_cattool "$catpage"
150 		decho "    Found catpage \"$catpage\""
151 		return 0
152 	else
153 		return 1
154 	fi
155 }
156 
157 # Usage: check_man manglob catglob
158 # Given 2 globs, figures out if the manglob is available, if so, check to
159 # see if the catglob is also available and up to date.
160 check_man() {
161 	if exists "$1"; then
162 		# We have a match, check for a cat page
163 		manpage=$found
164 		setup_cattool "$manpage"
165 		decho "    Found manpage \"$manpage\""
166 
167 		if [ -n "${use_width}" ]; then
168 			# non-standard width
169 			unset use_cat
170 			decho "    Skipping catpage: non-standard page width"
171 		elif exists "$2" && is_newer $found "$manpage"; then
172 			# cat page found and is newer, use that
173 			use_cat=yes
174 			catpage=$found
175 			setup_cattool "$catpage"
176 			decho "    Using catpage \"$catpage\""
177 		else
178 			# no cat page or is older
179 			unset use_cat
180 			decho "    Skipping catpage: not found or old"
181 		fi
182 		return 0
183 	fi
184 
185 	return 1
186 }
187 
188 # Usage: decho "string" [debuglevel]
189 # Echoes to stderr string prefaced with -- if high enough debuglevel.
190 decho() {
191 	if [ $debug -ge ${2:-1} ]; then
192 		echo "-- $1" >&2
193 	fi
194 }
195 
196 # Usage: exists glob
197 #
198 # Returns true if glob resolves to a real file and store the first
199 # found filename in the variable $found
200 exists() {
201 	if [ -z "$1" ]; then
202 		return 1
203 	fi
204 
205 	local IFS
206 
207 	# Don't accidentally inherit callers IFS (breaks perl manpages)
208 	unset IFS
209 
210 	# Use some globbing tricks in the shell to determine if a file
211 	# exists or not.
212 	set +f
213 	for file in "$1"*
214 	do
215 		if [ -r "$file" ]; then
216 			found="$file"
217 			set -f
218 			return 0
219 		fi
220 	done
221 	set -f
222 
223 	return 1
224 }
225 
226 # Usage: find_file path section subdir pagename
227 # Returns: true if something is matched and found.
228 # Search the given path/section combo for a given page.
229 find_file() {
230 	local manroot catroot mann man0 catn cat0
231 
232 	manroot="$1/man$2"
233 	catroot="$1/cat$2"
234 	if [ -n "$3" ]; then
235 		manroot="$manroot/$3"
236 		catroot="$catroot/$3"
237 	fi
238 
239 	if [ ! -d "$manroot" -a ! -d "$catroot" ]; then
240 		return 1
241 	fi
242 	decho "  Searching directory $manroot" 2
243 
244 	mann="$manroot/$4.$2"
245 	man0="$manroot/$4.0"
246 	catn="$catroot/$4.$2"
247 	cat0="$catroot/$4.0"
248 
249 	# This is the behavior as seen by the original man utility.
250 	# Let's not change that which doesn't seem broken.
251 	if check_man "$mann" "$catn"; then
252 		return 0
253 	elif check_man "$man0" "$cat0"; then
254 		return 0
255 	elif check_cat "$catn"; then
256 		return 0
257 	elif check_cat "$cat0"; then
258 		return 0
259 	fi
260 
261 	return 1
262 }
263 
264 # Usage: is_newer file1 file2
265 # Returns true if file1 is newer than file2 as calculated by mtime.
266 is_newer() {
267 	if ! [ "$1" -ot "$2" ]; then
268 		decho "    mtime: $1 not older than $2" 3
269 		return 0
270 	else
271 		decho "    mtime: $1 older than $2" 3
272 		return 1
273 	fi
274 }
275 
276 # Usage: manpath_parse_args "$@"
277 # Parses commandline options for manpath.
278 manpath_parse_args() {
279 	local cmd_arg
280 
281 	OPTIND=1
282 	while getopts 'Ldq' cmd_arg; do
283 		case "${cmd_arg}" in
284 		L)	Lflag=Lflag ;;
285 		d)	debug=$(( $debug + 1 )) ;;
286 		q)	qflag=qflag ;;
287 		*)	manpath_usage ;;
288 		esac
289 	done >&2
290 }
291 
292 # Usage: manpath_usage
293 # Display usage for the manpath(1) utility.
294 manpath_usage() {
295 	echo 'usage: manpath [-Ldq]' >&2
296 	exit 1
297 }
298 
299 # Usage: manpath_warnings
300 # Display some warnings to stderr.
301 manpath_warnings() {
302 	if [ -n "$Lflag" -a -n "$MANLOCALES" ]; then
303 		echo "(Warning: MANLOCALES environment variable set)" >&2
304 	fi
305 }
306 
307 # Usage: man_check_for_so path
308 # Returns: True if able to resolve the file, false if it ended in tears.
309 # Detects the presence of the .so directive and causes the file to be
310 # redirected to another source file.
311 man_check_for_so() {
312 	local IFS line tstr
313 
314 	unset IFS
315 	if [ -n "$catpage" ]; then
316 		return 0
317 	fi
318 
319 	# We need to loop to accommodate multiple .so directives.
320 	while true
321 	do
322 		line=$($cattool "$manpage" 2>/dev/null | grep -E -m1 -v '^\.\\"[ ]*|^[ ]*$')
323 		case "$line" in
324                '.so /'*) break ;; # ignore absolute path
325                '.so '*) trim "${line#.so}"
326 			decho "$manpage includes $tstr"
327 			# Glob and check for the file.
328 			if ! check_man "$1/$tstr" ""; then
329 				decho "  Unable to find $tstr"
330 				return 1
331 			fi
332 			;;
333 		*)	break ;;
334 		esac
335 	done
336 
337 	return 0
338 }
339 
340 # Usage: man_display_page
341 # Display either the manpage or catpage depending on the use_cat variable
342 man_display_page() {
343 	local IFS pipeline testline
344 
345 	# We are called with IFS set to colon. This causes really weird
346 	# things to happen for the variables that have spaces in them.
347 	unset IFS
348 
349 	# If we are supposed to use a catpage and we aren't using troff(1)
350 	# just zcat the catpage and we are done.
351 	if [ -z "$tflag" -a -n "$use_cat" ]; then
352 		if [ -n "$wflag" ]; then
353 			echo "$catpage (source: \"$manpage\")"
354 			ret=0
355 		else
356 			if [ $debug -gt 0 ]; then
357 				decho "Command: $cattool \"$catpage\" | $MANPAGER"
358 				ret=0
359 			else
360 				$cattool "$catpage" | $MANPAGER
361 				ret=$?
362 			fi
363 		fi
364 		return
365 	fi
366 
367 	# Okay, we are using the manpage, do we just need to output the
368 	# name of the manpage?
369 	if [ -n "$wflag" ]; then
370 		echo "$manpage"
371 		ret=0
372 		return
373 	fi
374 
375 	if [ -n "$use_width" ]; then
376 		mandoc_args="-O width=${use_width}"
377 	fi
378 	testline="mandoc -Tlint -Wunsupp >/dev/null 2>&1"
379 	if [ -n "$tflag" ]; then
380 		pipeline="mandoc -Tps $mandoc_args"
381 	else
382 		pipeline="mandoc $mandoc_args | $MANPAGER"
383 	fi
384 
385 	if ! $cattool "$manpage" | eval "$testline"; then
386 		if which -s groff; then
387 			man_display_page_groff
388 		else
389 			echo "This manpage needs groff(1) to be rendered" >&2
390 			echo "First install groff(1): " >&2
391 			echo "pkg install groff " >&2
392 			ret=1
393 		fi
394 		return
395 	fi
396 
397 	if [ $debug -gt 0 ]; then
398 		decho "Command: $cattool \"$manpage\" | eval \"$pipeline\""
399 		ret=0
400 	else
401 		$cattool "$manpage" | eval "$pipeline"
402 		ret=$?
403 	fi
404 }
405 
406 # Usage: man_display_page_groff
407 # Display the manpage using groff
408 man_display_page_groff() {
409 	local EQN NROFF PIC TBL TROFF REFER VGRIND
410 	local IFS l nroff_dev pipeline preproc_arg tool
411 
412 	# So, we really do need to parse the manpage. First, figure out the
413 	# device flag (-T) we have to pass to eqn(1) and groff(1). Then,
414 	# setup the pipeline of commands based on the user's request.
415 
416 	# If the manpage is from a particular charset, we need to setup nroff
417 	# to properly output for the correct device.
418 	case "${manpage}" in
419 	*.${man_charset}/*)
420 		# I don't pretend to know this; I'm just copying from the
421 		# previous version of man(1).
422 		case "$man_charset" in
423 		KOI8-R)		nroff_dev="koi8-r" ;;
424 		ISO8859-1)	nroff_dev="latin1" ;;
425 		ISO8859-15)	nroff_dev="latin1" ;;
426 		UTF-8)		nroff_dev="utf8" ;;
427 		*)		nroff_dev="ascii" ;;
428 		esac
429 
430 		NROFF="$NROFF -T$nroff_dev"
431 		EQN="$EQN -T$nroff_dev"
432 
433 		# Iff the manpage is from the locale and not just the charset,
434 		# then we need to define the locale string.
435 		case "${manpage}" in
436 		*/${man_lang}_${man_country}.${man_charset}/*)
437 			NROFF="$NROFF -dlocale=$man_lang.$man_charset"
438 			;;
439 		*/${man_lang}.${man_charset}/*)
440 			NROFF="$NROFF -dlocale=$man_lang.$man_charset"
441 			;;
442 		esac
443 
444 		# Allow language specific calls to override the default
445 		# set of utilities.
446 		l=$(echo $man_lang | tr [:lower:] [:upper:])
447 		for tool in EQN NROFF PIC TBL TROFF REFER VGRIND; do
448 			eval "$tool=\${${tool}_$l:-\$$tool}"
449 		done
450 		;;
451 	*)	NROFF="$NROFF -Tascii"
452 		EQN="$EQN -Tascii"
453 		;;
454 	esac
455 
456 	if [ -z "$MANCOLOR" ]; then
457 		NROFF="$NROFF -P-c"
458 	fi
459 
460 	if [ -n "${use_width}" ]; then
461 		NROFF="$NROFF -rLL=${use_width}n -rLT=${use_width}n"
462 	fi
463 
464 	if [ -n "$MANROFFSEQ" ]; then
465 		set -- -$MANROFFSEQ
466 		OPTIND=1
467 		while getopts 'egprtv' preproc_arg; do
468 			case "${preproc_arg}" in
469 			e)	pipeline="$pipeline | $EQN" ;;
470 			g)	;; # Ignore for compatibility.
471 			p)	pipeline="$pipeline | $PIC" ;;
472 			r)	pipeline="$pipeline | $REFER" ;;
473 			t)	pipeline="$pipeline | $TBL" ;;
474 			v)	pipeline="$pipeline | $VGRIND" ;;
475 			*)	usage ;;
476 			esac
477 		done
478 		# Strip the leading " | " from the resulting pipeline.
479 		pipeline="${pipeline#" | "}"
480 	else
481 		pipeline="$TBL"
482 	fi
483 
484 	if [ -n "$tflag" ]; then
485 		pipeline="$pipeline | $TROFF"
486 	else
487 		pipeline="$pipeline | $NROFF | $MANPAGER"
488 	fi
489 
490 	if [ $debug -gt 0 ]; then
491 		decho "Command: $cattool \"$manpage\" | eval \"$pipeline\""
492 		ret=0
493 	else
494 		$cattool "$manpage" | eval "$pipeline"
495 		ret=$?
496 	fi
497 }
498 
499 # Usage: man_find_and_display page
500 # Search through the manpaths looking for the given page.
501 man_find_and_display() {
502 	local found_page locpath p path sect
503 
504 	# Check to see if it's a file. But only if it has a '/' in
505 	# the filename.
506 	case "$1" in
507 	*/*)	if [ -f "$1" -a -r "$1" ]; then
508 			decho "Found a usable page, displaying that"
509 			unset use_cat
510 			manpage="$1"
511 			setup_cattool "$manpage"
512 			p=$(cd "$(dirname "$manpage")" && pwd)
513 			case "$(basename "$p")" in
514 				man*|cat*) p=$p/.. ;;
515 				*) p=$p/../.. ;;
516 			esac
517 			if man_check_for_so "$p"; then
518 				found_page=yes
519 				man_display_page
520 			fi
521 			return
522 		fi
523 		;;
524 	esac
525 
526 	IFS=:
527 	for sect in $MANSECT; do
528 		decho "Searching section $sect" 2
529 		for path in $MANPATH; do
530 			for locpath in $locpaths; do
531 				p=$path/$locpath
532 				p=${p%/.} # Rid ourselves of the trailing /.
533 
534 				# Check if there is a MACHINE specific manpath.
535 				if find_file $p $sect $MACHINE "$1"; then
536 					if man_check_for_so $p; then
537 						found_page=yes
538 						man_display_page
539 						if [ -n "$aflag" ]; then
540 							continue 2
541 						else
542 							return
543 						fi
544 					fi
545 				fi
546 
547 				# Check if there is a MACHINE_ARCH
548 				# specific manpath.
549 				if find_file $p $sect $MACHINE_ARCH "$1"; then
550 					if man_check_for_so $p; then
551 						found_page=yes
552 						man_display_page
553 						if [ -n "$aflag" ]; then
554 							continue 2
555 						else
556 							return
557 						fi
558 					fi
559 				fi
560 
561 				# Check plain old manpath.
562 				if find_file $p $sect '' "$1"; then
563 					if man_check_for_so $p; then
564 						found_page=yes
565 						man_display_page
566 						if [ -n "$aflag" ]; then
567 							continue 2
568 						else
569 							return
570 						fi
571 					fi
572 				fi
573 			done
574 		done
575 	done
576 	unset IFS
577 
578 	# Nothing? Well, we are done then.
579 	if [ -z "$found_page" ]; then
580 		echo "No manual entry for \"$1\"" >&2
581 		ret=1
582 		return
583 	fi
584 }
585 
586 # Usage: man_parse_opts "$@"
587 # Parses commandline options for man.
588 man_parse_opts() {
589 	local cmd_arg
590 
591 	OPTIND=1
592 	while getopts 'K:M:P:S:adfhkm:op:tw' cmd_arg; do
593 		case "${cmd_arg}" in
594 		K)	Kflag=Kflag
595 			REGEXP=$OPTARG ;;
596 		M)	MANPATH=$OPTARG ;;
597 		P)	MANPAGER=$OPTARG ;;
598 		S)	MANSECT=$OPTARG ;;
599 		a)	aflag=aflag ;;
600 		d)	debug=$(( $debug + 1 )) ;;
601 		f)	fflag=fflag ;;
602 		h)	man_usage 0 ;;
603 		k)	kflag=kflag ;;
604 		m)	mflag=$OPTARG ;;
605 		o)	oflag=oflag ;;
606 		p)	MANROFFSEQ=$OPTARG ;;
607 		t)	tflag=tflag ;;
608 		w)	wflag=wflag ;;
609 		*)	man_usage ;;
610 		esac
611 	done >&2
612 
613 	shift $(( $OPTIND - 1 ))
614 
615 	# Check the args for incompatible options.
616 
617 	case "${Kflag}${fflag}${kflag}${tflag}${wflag}" in
618 	Kflagfflag*)	echo "Incompatible options: -K and -f"; man_usage ;;
619 	Kflag*kflag*)	echo "Incompatible options: -K and -k"; man_usage ;;
620 	Kflag*tflag)	echo "Incompatible options: -K and -t"; man_usage ;;
621 	fflagkflag*)	echo "Incompatible options: -f and -k"; man_usage ;;
622 	fflag*tflag*)	echo "Incompatible options: -f and -t"; man_usage ;;
623 	fflag*wflag)	echo "Incompatible options: -f and -w"; man_usage ;;
624 	*kflagtflag*)	echo "Incompatible options: -k and -t"; man_usage ;;
625 	*kflag*wflag)	echo "Incompatible options: -k and -w"; man_usage ;;
626 	*tflagwflag)	echo "Incompatible options: -t and -w"; man_usage ;;
627 	esac
628 
629 	# Short circuit for whatis(1) and apropos(1)
630 	if [ -n "$fflag" ]; then
631 		do_whatis "$@"
632 		exit
633 	fi
634 
635 	if [ -n "$kflag" ]; then
636 		do_apropos "$@"
637 		exit
638 	fi
639 }
640 
641 # Usage: man_setup
642 # Setup various trivial but essential variables.
643 man_setup() {
644 	# Setup machine and architecture variables.
645 	if [ -n "$mflag" ]; then
646 		MACHINE_ARCH=${mflag%%:*}
647 		MACHINE=${mflag##*:}
648 	fi
649 	if [ -z "$MACHINE_ARCH" ]; then
650 		MACHINE_ARCH=$($SYSCTL -n hw.machine_arch)
651 	fi
652 	if [ -z "$MACHINE" ]; then
653 		MACHINE=$($SYSCTL -n hw.machine)
654 	fi
655 	decho "Using architecture: $MACHINE_ARCH:$MACHINE"
656 
657 	setup_pager
658 	build_manpath
659 	build_mansect
660 	man_setup_locale
661 	man_setup_width
662 }
663 
664 # Usage: man_setup_width
665 # Set up page width.
666 man_setup_width() {
667 	local sizes
668 
669 	unset use_width
670 	case "$MANWIDTH" in
671 	[0-9]*)
672 		if [ "$MANWIDTH" -gt 0 2>/dev/null ]; then
673 			use_width=$MANWIDTH
674 		fi
675 		;;
676 	[Tt][Tt][Yy])
677 		if { sizes=$($STTY size 0>&3 2>/dev/null); } 3>&1; then
678 			set -- $sizes
679 			if [ $2 -gt 80 ]; then
680 				use_width=$(($2-2))
681 			fi
682 		fi
683 		;;
684 	esac
685 	if [ -n "$use_width" ]; then
686 		decho "Using non-standard page width: ${use_width}"
687 	else
688 		decho 'Using standard page width'
689 	fi
690 }
691 
692 # Usage: man_setup_locale
693 # Setup necessary locale variables.
694 man_setup_locale() {
695 	local lang_cc
696 	local locstr
697 
698 	locpaths='.'
699 	man_charset='US-ASCII'
700 
701 	# Setup locale information.
702 	if [ -n "$oflag" ]; then
703 		decho 'Using non-localized manpages'
704 	else
705 		# Use the locale tool to give us proper locale information
706 		eval $( $LOCALE )
707 
708 		if [ -n "$LANG" ]; then
709 			locstr=$LANG
710 		else
711 			locstr=$LC_CTYPE
712 		fi
713 
714 		case "$locstr" in
715 		C)		;;
716 		C.UTF-8)	;;
717 		POSIX)		;;
718 		[a-z][a-z]_[A-Z][A-Z]\.*)
719 				lang_cc="${locstr%.*}"
720 				man_lang="${locstr%_*}"
721 				man_country="${lang_cc#*_}"
722 				man_charset="${locstr#*.}"
723 				locpaths="$locstr"
724 				locpaths="$locpaths:$man_lang.$man_charset"
725 				if [ "$man_lang" != "en" ]; then
726 					locpaths="$locpaths:en.$man_charset"
727 				fi
728 				locpaths="$locpaths:."
729 				;;
730 		*)		echo 'Unknown locale, assuming C' >&2
731 				;;
732 		esac
733 	fi
734 
735 	decho "Using locale paths: $locpaths"
736 }
737 
738 # Usage: man_usage [exitcode]
739 # Display usage for the man utility.
740 man_usage() {
741 	echo 'Usage:'
742 	echo ' man [-adho] [-t | -w] [-M manpath] [-P pager] [-S mansect]'
743 	echo '     [-m arch[:machine]] [-p [eprtv]] [mansect] page [...]'
744 	echo ' man -K | -f | -k expression [...] -- Search manual pages'
745 
746 	# When exit'ing with -h, it's not an error.
747 	exit ${1:-1}
748 }
749 
750 # Usage: parse_configs
751 # Reads the end-user adjustable config files.
752 parse_configs() {
753 	local IFS file files
754 
755 	if [ -n "$parsed_configs" ]; then
756 		return
757 	fi
758 
759 	unset IFS
760 
761 	# Read the global config first in case the user wants
762 	# to override config_local.
763 	if [ -r "$config_global" ]; then
764 		parse_file "$config_global"
765 	fi
766 
767 	# Glob the list of files to parse.
768 	set +f
769 	files=$(echo $config_local)
770 	set -f
771 
772 	for file in $files; do
773 		if [ -r "$file" ]; then
774 			parse_file "$file"
775 		fi
776 	done
777 
778 	parsed_configs='yes'
779 }
780 
781 # Usage: parse_file file
782 # Reads the specified config files.
783 parse_file() {
784 	local file line tstr var
785 
786 	file="$1"
787 	decho "Parsing config file: $file"
788 	while read line; do
789 		decho "  $line" 2
790 		case "$line" in
791 		\#*)		decho "    Comment" 3
792 				;;
793 		MANPATH*)	decho "    MANPATH" 3
794 				trim "${line#MANPATH}"
795 				add_to_manpath "$tstr"
796 				;;
797 		MANLOCALE*)	decho "    MANLOCALE" 3
798 				trim "${line#MANLOCALE}"
799 				manlocales="$manlocales:$tstr"
800 				;;
801 		MANCONFIG*)	decho "    MANCONFIG" 3
802 				trim "${line#MANCONFIG}"
803 				config_local="$tstr"
804 				;;
805 		MANSECT*)	decho "    MANSECT" 3
806 				trim "${line#MANSECT}"
807 				mansect="$mansect:$tstr"
808 				;;
809 		# Set variables in the form of FOO_BAR
810 		*_*[\ \	]*)	var="${line%%[\ \	]*}"
811 				trim "${line#$var}"
812 				eval "$var=\"$tstr\""
813 				decho "    Parsed $var" 3
814 				;;
815 		esac
816 	done < "$file"
817 }
818 
819 # Usage: search_path
820 # Traverse $PATH looking for manpaths.
821 search_path() {
822 	local IFS p path
823 
824 	decho "Searching PATH for man directories"
825 
826 	IFS=:
827 	for path in $PATH; do
828 		if add_to_manpath "$path/man"; then
829 			:
830 		elif add_to_manpath "$path/MAN"; then
831 			:
832 		else
833 			case "$path" in
834 			*/bin)	p="${path%/bin}/share/man"
835 				add_to_manpath "$p"
836 				p="${path%/bin}/man"
837 				add_to_manpath "$p"
838 				;;
839 			esac
840 		fi
841 	done
842 	unset IFS
843 
844 	if [ -z "$manpath" ]; then
845 		decho '  Unable to find any manpaths, using default'
846 		manpath=$man_default_path
847 	fi
848 }
849 
850 # Usage: search_whatis cmd [arglist]
851 # Do the heavy lifting for apropos/whatis
852 search_whatis() {
853 	local IFS bad cmd f good key keywords loc opt out path rval wlist
854 
855 	cmd="$1"
856 	shift
857 
858 	whatis_parse_args "$@"
859 
860 	build_manpath
861 	build_manlocales
862 	setup_pager
863 
864 	if [ "$cmd" = "whatis" ]; then
865 		opt="-w"
866 	fi
867 
868 	f='whatis'
869 
870 	IFS=:
871 	for path in $MANPATH; do
872 		if [ \! -d "$path" ]; then
873 			decho "Skipping non-existent path: $path" 2
874 			continue
875 		fi
876 
877 		if [ -f "$path/$f" -a -r "$path/$f" ]; then
878 			decho "Found whatis: $path/$f"
879 			wlist="$wlist $path/$f"
880 		fi
881 
882 		for loc in $MANLOCALES; do
883 			if [ -f "$path/$loc/$f" -a -r "$path/$loc/$f" ]; then
884 				decho "Found whatis: $path/$loc/$f"
885 				wlist="$wlist $path/$loc/$f"
886 			fi
887 		done
888 	done
889 	unset IFS
890 
891 	if [ -z "$wlist" ]; then
892 		echo "$cmd: no whatis databases in $MANPATH" >&2
893 		exit 1
894 	fi
895 
896 	rval=0
897 	for key in $keywords; do
898 		out=$(grep -Ehi $opt -- "$key" $wlist)
899 		if [ -n "$out" ]; then
900 			good="$good\\n$out"
901 		else
902 			bad="$bad\\n$key: nothing appropriate"
903 			rval=1
904 		fi
905 	done
906 
907 	# Strip leading carriage return.
908 	good=${good#\\n}
909 	bad=${bad#\\n}
910 
911 	if [ -n "$good" ]; then
912 		printf '%b\n' "$good" | $MANPAGER
913 	fi
914 
915 	if [ -n "$bad" ]; then
916 		printf '%b\n' "$bad" >&2
917 	fi
918 
919 	exit $rval
920 }
921 
922 # Usage: setup_cattool page
923 # Finds an appropriate decompressor based on extension
924 setup_cattool() {
925 	case "$1" in
926 	*.bz)	cattool='/usr/bin/bzcat' ;;
927 	*.bz2)	cattool='/usr/bin/bzcat' ;;
928 	*.gz)	cattool='/usr/bin/gzcat' ;;
929 	*.lzma)	cattool='/usr/bin/lzcat' ;;
930 	*.xz)	cattool='/usr/bin/xzcat' ;;
931 	*.zst)	cattool='/usr/bin/zstdcat' ;;
932 	*)	cattool='/usr/bin/zcat -f' ;;
933 	esac
934 }
935 
936 # Usage: setup_pager
937 # Correctly sets $MANPAGER
938 setup_pager() {
939 	# Setup pager.
940 	if [ -z "$MANPAGER" ]; then
941 		if [ -n "$MANCOLOR" ]; then
942 			MANPAGER="less -sR"
943 		else
944 			if [ -n "$PAGER" ]; then
945 				MANPAGER="$PAGER"
946 			else
947 				MANPAGER="less -s"
948 			fi
949 		fi
950 	fi
951 	decho "Using pager: $MANPAGER"
952 }
953 
954 # Usage: trim string
955 # Trims whitespace from beginning and end of a variable
956 trim() {
957 	tstr=$1
958 	while true; do
959 		case "$tstr" in
960 		[\ \	]*)	tstr="${tstr##[\ \	]}" ;;
961 		*[\ \	])	tstr="${tstr%%[\ \	]}" ;;
962 		*)		break ;;
963 		esac
964 	done
965 }
966 
967 # Usage: whatis_parse_args "$@"
968 # Parse commandline args for whatis and apropos.
969 whatis_parse_args() {
970 	local cmd_arg
971 	OPTIND=1
972 	while getopts 'd' cmd_arg; do
973 		case "${cmd_arg}" in
974 		d)	debug=$(( $debug + 1 )) ;;
975 		*)	whatis_usage ;;
976 		esac
977 	done >&2
978 
979 	shift $(( $OPTIND - 1 ))
980 
981 	keywords="$*"
982 }
983 
984 # Usage: whatis_usage
985 # Display usage for the whatis/apropos utility.
986 whatis_usage() {
987 	echo "usage: $cmd [-d] keyword [...]"
988 	exit 1
989 }
990 
991 
992 
993 # Supported commands
994 do_apropos() {
995 	[ $(stat -f %i /usr/bin/man) -ne $(stat -f %i /usr/bin/apropos) ] && \
996 		exec apropos "$@"
997 	search_whatis apropos "$@"
998 }
999 
1000 # Usage: do_full_search reg_exp
1001 # Do a full search of the regular expression passed
1002 # as parameter in all man pages
1003 do_full_search() {
1004 	local gflags re
1005 	re=${1}
1006 
1007 	# Build grep(1) flags
1008 	gflags="-H"
1009 
1010 	# wflag implies -l for grep(1)
1011 	if [ -n "$wflag" ]; then
1012 		gflags="${gflags} -l"
1013 	fi
1014 
1015 	gflags="${gflags} --label"
1016 
1017 	set +f
1018 	for mpath in $(echo "${MANPATH}" | tr : '[:blank:]'); do
1019 		for section in $(echo "${MANSECT}" | tr : '[:blank:]'); do
1020 			for manfile in ${mpath}/man${section}/*.${section}*; do
1021 				mandoc "${manfile}" 2>/dev/null |
1022 					grep -E ${gflags} "${manfile}" -e "${re}"
1023 			done
1024 		done
1025 	done
1026 	set -f
1027 }
1028 
1029 do_man() {
1030 	local IFS
1031 
1032 	man_parse_opts "$@"
1033 	man_setup
1034 
1035 	shift $(( $OPTIND - 1 ))
1036 	IFS=:
1037 	for sect in $MANSECT; do
1038 		if [ "$sect" = "$1" ]; then
1039 			decho "Detected manual section as first arg: $1"
1040 			MANSECT="$1"
1041 			shift
1042 			break
1043 		fi
1044 	done
1045 	unset IFS
1046 	pages="$*"
1047 
1048 	if [ -z "$pages" -a -z "${Kflag}" ]; then
1049 		echo 'What manual page do you want?' >&2
1050 		exit 1
1051 	fi
1052 
1053 	if [ ! -z "${Kflag}" ]; then
1054 		# Short circuit because -K flag does a sufficiently
1055 		# different thing like not showing the man page at all
1056 		do_full_search "${REGEXP}"
1057 	fi
1058 
1059 	for page in "$@"; do
1060 		decho "Searching for \"$page\""
1061 		man_find_and_display "$page"
1062 	done
1063 
1064 	exit ${ret:-0}
1065 }
1066 
1067 do_manpath() {
1068 	manpath_parse_args "$@"
1069 	if [ -z "$qflag" ]; then
1070 		manpath_warnings
1071 	fi
1072 	if [ -n "$Lflag" ]; then
1073 		build_manlocales
1074 		echo $MANLOCALES
1075 	else
1076 		build_manpath
1077 		echo $MANPATH
1078 	fi
1079 	exit 0
1080 }
1081 
1082 do_whatis() {
1083 	[ $(stat -f %i /usr/bin/man) -ne $(stat -f %i /usr/bin/whatis) ] && \
1084 		exec whatis "$@"
1085 	search_whatis whatis "$@"
1086 }
1087 
1088 # User's PATH setting decides on the groff-suite to pick up.
1089 EQN=eqn
1090 NROFF='groff -S -P-h -Wall -mtty-char -mandoc'
1091 PIC=pic
1092 REFER=refer
1093 TBL=tbl
1094 TROFF='groff -S -mandoc'
1095 VGRIND=vgrind
1096 
1097 LOCALE=/usr/bin/locale
1098 STTY=/bin/stty
1099 SYSCTL=/sbin/sysctl
1100 
1101 debug=0
1102 man_default_sections='1:8:2:3:3lua:n:4:5:6:7:9:l'
1103 man_default_path='/usr/share/man:/usr/share/openssl/man:/usr/local/share/man:/usr/local/man'
1104 cattool='/usr/bin/zcat -f'
1105 
1106 config_global='/etc/man.conf'
1107 
1108 # This can be overridden via a setting in /etc/man.conf.
1109 config_local='/usr/local/etc/man.d/*.conf'
1110 
1111 # Set noglobbing for now. I don't want spurious globbing.
1112 set -f
1113 
1114 case "$0" in
1115 *apropos)	do_apropos "$@" ;;
1116 *manpath)	do_manpath "$@" ;;
1117 *whatis)	do_whatis "$@" ;;
1118 *)		do_man "$@" ;;
1119 esac
1120