########################################################################
#                                                                      #
#              This file is part of the ksh 93u+m package              #
#          Copyright (c) 2021-2024 Contributors to ksh 93u+m           #
#                      and is licensed under the                       #
#                 Eclipse Public License, Version 2.0                  #
#                                                                      #
#                A copy of the License is available at                 #
#      https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html      #
#         (with md5 checksum 84283fa8859daf213bdda5a9f8d1be1d)         #
#                                                                      #
#                  Martijn Dekker <martijn@inlv.org>                   #
#            Johnothan King <johnothanking@protonmail.com>             #
#                                                                      #
########################################################################

# This function integrates AST commands' --man self-documentation into the
# general 'man' command, making it a great deal easier to use. Your pager from
# $PAGER is automatically used if set, otherwise it tries 'less -R'. This works
# for both ksh built-in commands and external commands with --man.
#
# For commands that do not have AST --man self-documentation, it passes control
# to your regular 'man' command so you won't notice a difference. Result: you
# can just use 'man somecommand' for everything.
#
# If you want to pass extra options to your operating system's "man" command,
# put those in the .sh.manopts variable.
#
# This function only handles 'man' commands with a single argument. If more
# or less than one argument is given, it passes control to the system's 'man'.
#
# For path-bound built-ins (the ones starting with /opt/ast/bin in the output
# of the 'builtin' command without arguments), each built-in --man page only
# overrides the regular one if the built-in command would actually be executed
# according to the value of $PATH. For external commands, the regular man page
# is searched before looking for AST --man self-documentation. For path-bound
# built-in commands and external commands with AST --man, you can also give it
# the full pathname, e.g., 'man /opt/ast/bin/cat' or 'man /usr/bin/shcomp'.
#
# Recommended usage:
# 1. Drop this file in a directory in your $FPATH.
# 2. Add 'autoload man' to your ~/.kshrc to override external 'man'.
# Or to try it out, just 'dot'/source this file manually.
#
# The code below illustrates how defining functions in a dedicated ksh
# namespace can be used to compartmentalise code, making it more readable and
# easier to understand. The basic idea is fairly simple: each function and
# variable name N within 'namespace man' is actually treated as '.man.N'. This
# allows using simple and readable names without conflicting with other code
# using the same names.

namespace man
{
	# Check for a built-in with --man, i.e., if:
	# - 'whence -t' says it is a built-in;
	# - it is not :, true, false, or echo;
	# - the name or path we would execute appears in output of 'builtin'.
	# This way, path-bound built-ins' --man is only used if found in $PATH.
	# (Don't let a shell function by the same name of a built-in get in the way:
	# unset it within the command substitution subshell before running 'whence'.)

	builtin_has_selfdoc()
	{
		[[ $(unset -f -- "$1"; whence -t -- "$1") == builtin ]] \
			&& [[ ! $1 =~ ^(:|true|false|echo)$ \
			&& $'\n'$(builtin)$'\n' == *$'\n'"$(unset -f -- "$1"; whence -- "$1")"$'\n'* ]]
	} 2>/dev/null

	# Check if a binary or script has --man self-documentation by grepping
	# for an AST optget(3) usage string.

	extcmd_has_selfdoc()
	{
		typeset p=$(whence -p -- "$1")
		[[ $p == /* ]] \
			&& LC_ALL=C grep -q '\[+NAME?' "$p" \
			&& LC_ALL=C grep -q '\[+DESCRIPTION?' "$p"
	}

	# Show the self-documentation.
	#   Exporting ERROR_OPTIONS tells ksh to emit pretty-printing escape
	# codes even if standard error is not on a terminal. Note that, in
	# fact, all --man output is a glorified error message! Strange but
	# true. So to capture the output, we need to redirect standard error to
	# standard output (2>&1). Also, 'test' and '[' need special invocations.
	#   Also note: '--??man' is safer as some programs may override --man;
	# see any_builtin --??help (e.g., 'whence --??help') for more info.

	show_selfdoc()
	{
		typeset -x COLUMNS=$COLUMNS  # export in local scope
		if ((stdout_on_terminal)); then
			# enable emphasis only for pagers that support ANSI escapes
			typeset pager=${PAGER:-less}
			case ${pager##*/} in
			less | less[[:blank:]]* | most | most[[:blank:]]* )
				typeset -x ERROR_OPTIONS=emphasis ;;
			esac
		else
			COLUMNS=80
		fi
		case $1 in
		test )	command test '--??man' -- ;;
		[ )	command [ '--??man' -- ] ;;
		* )	command "$1" '--??man' ;;
		esac 2>&1
	}

	# Run the user's configured pager. Unset IFS to get default split on
	# space/tab/newline, and disable globbing to avoid processing '*', '?'
	# etc., then expand and run the command from $PAGER, defaulting to
	# 'less -R' if it's unset or empty.

	pager()
	{
		typeset pager=${MANPAGER:-${PAGER-}}
		((!stdout_on_terminal)) && pager=cat
		case $pager in
		'')	pager="less $lessopt" ;;
		less | */less | less[[:blank:]]* | */less[[:blank:]]*)
			pager+=" $lessopt" ;;
		esac
		command -- $pager
	}

	# Shorthand for invoking the OS man(1) command on the default system PATH.

	os_man()
	{
		command -p man ${.sh.manopts-} "$@"
	}

	# Try if a system manual page in a section exists. Unfortunately, we
	# cannot portably rely on the exit status of the 'man' command to check
	# for success, so test if its standard output includes at least three
	# newline control characters. (Thankfully, all OS man commands seem to
	# deactivate the pager if standard output is not on a terminal.)

	try_os_man()
	{
		[[ $2 != */* && $(os_man -s "$1" "$2" 2>/dev/null) == *$'\n'*$'\n'*$'\n'* ]]
	}

	# The main function puts it all together. When given a single argument,
	# it shows manual pages in the following order of preference:
	#   1. --man self-documentation of built-in commands;
	#   2. regular section 1 and section 8 manual pages (for example, we
	#      probably prefer the full ksh.1 manual page over 'ksh --man');
	#   3. --man self-documentation of external commands;
	#   4. regular manual pages in other sections (programming manual).

	main()
	{
		if (($# != 1)) || [[ $1 == -* ]]; then
			os_man "$@"
		elif builtin_has_selfdoc "$1"; then
			show_selfdoc "$1" | pager
		elif try_os_man 1 "$1"; then
			os_man -s 1 "$1"
		elif try_os_man 8 "$1"; then
			os_man -s 8 "$1"
		elif extcmd_has_selfdoc "$1"; then
			show_selfdoc "$1" | pager
		else
			os_man "$1"
		fi
	}
}

# The main function simply invokes 'main' within the namespace. Because 'man'
# cannot not itself be in the namespace block, it has to invoke the full
# canonical name, '.man.main'.
# (If we moved 'man' into the namespace block, it would actually be called
# '.man.man' and have to be invoked as that, making it fairly useless.)
#
# Scoping note: The functions within the namespace all use the POSIX name()
# syntax, which means they do not have their own local scope. However, 'man' is
# defined with the 'function' keyword, which gives it a local scope. Invoking
# the POSIX functions from 'man' makes them share its scope, which means any
# local variables defined and shell options set within those POSIX functions
# are made local to the 'man' function and shared between the POSIX functions
# but not outside them.

function man
{
	typeset IFS	# local default split...
	set -o noglob	# ...and no pathname expansion, for safe field splitting
	# set stdout_on_terminal to 1 if standard output is on a terminal;
	# this needs flagging now as show_selfdoc() will have stdout on a pipe
	[[ -t 1 ]]
	typeset -si stdout_on_terminal=$((! $?))
	# and off we go
	.man.main "$@"
}


# Do a check at autoload time. We depend on a non-buggy 'whence -t' option
# (the ksh2020 version is broken; it claims path-bound builtins are files)

if ((.sh.version < 20211227)); then
	print "WARNING: this man wrapper function requires ksh 93u+m 2021-12-27 or later" >&2
	sleep 1
fi

# Check if 'less' has an -R option. Some versions have only -r.
# In the man namespace functions above, ${.man.lessopt} is simply $lessopt.
if echo test | less -R >/dev/null 2>&1; then
	.man.lessopt=-R
else
	.man.lessopt=-r
fi

# Check for QNX, which uses 'use' instead of 'man'.
# Override .man.main function with QNX version.
# Instead of .sh.manotps, 'use' options may be set in .sh.useopts.

if	command -pv use >/dev/null && [[ $(command -p uname -s) == QNX ]]
then	namespace man
	{
		unset -f os_man try_os_man
		os_use()
		{
			command -p use ${.sh.useopts-} "$@"
		}
		main()
		{
			if (($# != 1)) || [[ $1 == -* ]]; then
				os_use "$@"
			elif builtin_has_selfdoc "$1" || extcmd_has_selfdoc "$1"; then
				show_selfdoc "$1" | pager
			elif os_use "$1" >/dev/null 2>&1; then
				os_use "$1" | pager
			else
				os_use "$1" # show error without pager
			fi
		}
	}
fi
