#! /bin/bash
#
# lesspipe - an advanced preprocessor for less
# Copyright (C) 2017, 2022  Paulina Laura Emilia
#
# This file is part of lesspipe.
#
# Lesspipe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Lesspipe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with lesspipe. If not, see <https://www.gnu.org/licenses/>.
#
# This program contains code derived on 2022-03-13 from the ‘less’ program,
# originally by:
#	Copyright (C) 1984-2022  Mark Nudelman
# with its source code available on the following website:
#	https://greenwoodsoftware.com/less/download.html#source
#
# This program contains code derived on 2022-04-01 from ‘shell-term-background’
# scripts, originally by:
#	Copyright (C) 2019-2020, Rocky Bernstein <rocky@gnu.org>
# with their source code available on the following website:
#	https://github.com/rocky/shell-term-background
#

# Enable useful additional ShellCheck checks.
# shellcheck enable=quote-safe-variables
# shellcheck enable=require-double-brackets

# Set some useful options.
shopt -s extglob lastpipe

# Declare global variables and initialize them to avoid inheriting them from
# the environment.
declare -i colors=0
declare -- current_encoding=
declare -i debug=0
declare -a ignore=()
declare -i italic=0
declare -a less_commands=()
declare -i less_is_more=0
declare -A less_options=()
declare -- less_pending_option=
declare -a parent_process=()
declare -a prefer=()
declare -i processing_level=1
declare -- program_name=lesspipe
declare -- terminal_background=
declare -A theme=()
declare -- working_directory=

# Declare exported global variables that may be inherited from the environment.
declare -x -- LESSOPEN
declare -x -i LINES COLUMNS

#
# Checks if a given command exists.
#

function command_exists {
	command -v -- "$1" > /dev/null 2>&1
}

#
# Checks all given commands exists.
#

function command_exists_all {
	local c
	for c; do
		command_exists "$c" ||
			return 1
	done
}

#
# Checks if any of given commands exists.
#

function command_exists_any {
	local c
	for c; do
		command_exists "$c" &&
			return 0
	done
	return 1
}

function color_archive {
	if (( colors >= 8 )) && command_exists colortar; then
		"$@" | colortar
		return "${PIPESTATUS[0]}"
	else
		"$@"
	fi
}

#
# Shows information about Debian package and its content. Function intended
# as a fallback in case ‘dpkg’ command is not available.
#

function debinfo {
	local -- command file outdir tar
	local -a control data
	file=$1
	tar=tar

	outdir=$(temporary_directory) ||
		return 2

	# Try to decompress the containing archive.
	command=$(order_commands bsdtar ar)
	case $command in
	ar)
		(oldpwd=$PWD; cd "$outdir" && ar x "$oldpwd/$file") ;;
	bsdtar)
		bsdtar -x -C "$outdir" -f "$file" &&
			tar=bsdtar ;;
	*)
		return 1
	esac || return 2

	# Find names of ‘control’ and ‘data’ archives.
	control=("$outdir/control.tar"?(.*))
	data=("$outdir/data.tar"?(.*))

	# Try to decompress ‘control’ and ‘data’ archives.
	# We can do it in place since they’re only temporary files anyway.
	local f
	for f in "${control[0]}" "${data[0]}"; do
		case $f in
		*.gz)   command=$(order_commands pigz gzip) ;;
		*.bz2)  command=$(order_commands bzip2) ;;
		*.lzma) command=$(order_commands lzma xz) ;;
		*.xz)   command=$(order_commands xz)
		esac

		case $command in
		bzip2)  bzip2 -d -- "$f" ;;
		gzip)   ignore_decompression_warnings gzip -d -- "$f" ;;
		lzma)   ignore_decompression_warnings lzma -d -- "$f" ;;
		pigz)   pigz -d -- "$f" ;;
		xz)     ignore_decompression_warnings xz -d -- "$f" ;;
		*)      return 1
		esac || return 2
	done

	# Print package version.
	print "Debian package, version $(< "$outdir/debian-binary")." ||
		return 3

	# Show contents of the control archive.
	color_archive "$tar" -tv -f "$outdir/control.tar" ||
		return 2

	# Show package metadata.
	"$tar" -xO -f "$outdir/control.tar" ./control ||
		return 2

	# Show contents of the data archive.
	color_archive "$tar" -tv -f "$outdir/data.tar" ||
		return 2
}

#
# Detect type and encoding of a file and pass it to the filtering function.
#

function detect_file_type {
	local -- encoding encoding_override file name type type_description
	local -- type_full type_override
	file=$1
	name=$2
	type_override=$3
	encoding_override=$4

	# Get media type of a file. If we are overriding only
	# type, we still need to do this to detect encoding.
	type_full=$(file -Lbs --mime -- "$file") ||
		die "cannot detect file type: 'file' command failed"
	type=${type_full%%;*}

	# Try to correct file type detection unless we override it.
	#
	# Some file types do not have oficially registered media types. We
	# use unregistered tree for them, per RFC 6838. While such types are
	# discouraged, we use them because our usage is exclusively internal.
	if [[ -n $type_override ]]; then
		type=$type_override
	else
		# Some file types, while correctly detected by the ’file’
		# utility, have their media type reported generically. In
		# such cases, let’s rerun ‘file’ to report human-readable
		# type description.
		if [[ $type = @(application/octet-stream|text/plain) ]]; then
			type_description=$(file -Lbs -- "$file") ||
				die "cannot detect file type: 'file' command failed"

			case $type_description in
			'Apple binary property list')
				type=application/x.plist ;;
			'DER Encoded Certificate request,'*)
				type=application/x.certificate-request-der ;;
			'DER Encoded Certificate,'*|'Certificate,'*)
				type=application/x.certificate-der ;;
			'DER Encoded Key Pair,'*)
				type=application/x.private-key-der ;;
			@(GPG|PGP)' '*[Ee]'ncrypted '*)
				type=application/pgp-encrypted ;;
			'lzop compressed data '*)
				type=application/x.lzop ;;
			'NetCDF Data Format '*)
				type=application/x.netcdf ;;
			'PEM certificate request')
				type=application/x.certificate-request-pem ;;
			'PEM certificate')
				type=application/x.certificate-pem ;;
			'PEM '*' private key')
				type=application/x.private-key-pem ;;
			'Perl POD document, '*)
				type=text/x.pod ;;
			'perl Storable '*)
				type=application/x.perl-storable ;;
			'SQLite 2.x database')
				type=application/x.sqlite2 ;;
			'Squashfs filesystem,'*)
				type=application/x.squashfs ;;
			esac
		fi

		# Some file types may be only detected on the basis of their
		# file name. Others are detected incorrectly which also needs
		# to be fixed this way.
		shopt -s nocasematch
		case $type\;$name in
		application/json\;*.ipynb)
			type=application/x.ipynb+json ;;
		application/octet-stream\;*.@(br|tbr))
			type=application/x.brotli ;;
		application/octet-stream\;*.crl)
			type=application/x.certificate-revocation-list-der ;;
		text/*\;*.fish)
			type=application/x.fish ;;
		text/*\;*.@(md|mkd|markdown))
			type=text/markdown ;;
		text/*\;*.pod)
			type=text/x.pod ;;
		text/*\;*.@(texinfo|texi|txi))
			type=text/x.texinfo ;;
		text/html\;*.xht?(ml)|\
		text/xml\;*.@(htm?(l)|xht?(ml)))
			type=application/xhtml+xml ;;
		text/plain\;*.crl?(.pem))
			type=application/x.certificate-revocation-list-pem ;;
		text/plain\;*.key?(.pem))
			type=application/x.private-key-pem ;;
		esac
		shopt -u nocasematch
	fi

	# Try to detect encoding unless we override it.
	if [[ -n $encoding_override ]]; then
		encoding=$encoding_override
	else
		# Base our initial guess on the output of the ‘file’ command.
		# We need to do this conditionally because some implementations
		# of the ‘file’ utility do not actually provide encoding.
		[[ $type_full = *\ charset=* ]] &&
			encoding=${type_full##* charset=}

		# Canonicalize encoding names to be compatible with POSIX
		# locale character map names.
		case $encoding in
		ebcdic)
			encoding=EBCDIC-INT ;;
		iso-8859-1)
			encoding=ISO-8859-1 ;;
		unknown-8bit)
			encoding=unknown ;;
		us-ascii)
			encoding=ASCII ;;
		utf-*)
			encoding=UTF-${encoding#utf-}
			case $encoding in
			*be)  encoding=${encoding%be}BE ;;
			*le)  encoding=${encoding%le}LE
			esac
		esac

		# Try to determine file encoding if ‘file’ command does not
		# detect encoding or the detected encoding is ambiguous, and
		# one of supported encoding detector utilities is installed.
		if [[ -z $encoding || $encoding = @(ISO-8859-1|unknown) ]]; then
			local -- command encoding_new
			local -i status

			command=$(order_commands uchardet enca)
			case $command in
			enca)
				encoding_new=$(enca -Pi -- "$file") ;;
			uchardet)
				encoding_new=$(uchardet -- "$file") &&
					encoding_new=${encoding_new##*:\ } ;;
			*)
				status=1
			esac || status=2

			# Set to a new value only when the command succeeds
			# and doesn’t report that the encoding is unknown.
			(( !status )) && [[ -n $encoding_new && $encoding_new != @(unknown|\?\?\?) ]] &&
				encoding=$encoding_new
		fi

		# If encoding is still unknown, have the value explicitly
		# say it.
		[[ -z $encoding ]] &&
			encoding=unknown
	fi

	if (( debug )); then
		message "detected file type '%s'" "$type"
		message "detected file encoding '%s'" "$encoding"
	fi

	run_filter "$file" "$name" "$type" "$encoding"
}

#
# Determine number of colors supported by the terminal type.
#
# We don’t use terminfo database for this purpose, since we’re not in the 1980s
# anymore, and anyway it won’t tell us too much. Intead, we base the color
# detection code on documentation for nosh’s TerminalCapabilities class:
#
#	http://jdebp.uk/Softwares/nosh/guide/TerminalCapabilities.html
#
# There are few differences: we also test for support of 88 colors; rxvt without
# ‘256color’ component in TERM environment variable is assumed to support only
# 88 colors (per documentation) and Linux terminal only 16 colors; unrecognized
# types of terminals are not assumed to support any colors at all.
#

function determine_colors {
	# If parent ‘less’ or ‘more’ command cannot display raw control
	# characters, assume no support for colors. We check it only in cases
	# when the full parent process command line is available, because
	# otherwise the check is unreliable.
	if [[ ${parent_process[0]} = @(less|more) && -n ${parent_process[1]} ]] && (( !less_options[r] )); then
		colors=0
	# Sometimes we can assume support for 24-bit colors immediately
	# by a presence of certain environment variables or by them having
	# a certain value.
	elif [[ $COLORTERM = @(truecolor|24bit) ||
		-n $VTE_VERSION ||
		-n $KONSOLE_VERSION ||
		-n $ALACRITTY_SOCKET ||
		-n $ROXTERM_ID ]]
	then
		colors=16777216
	else
		local -i has_24bit=0
		local -i has_256=0
		local -i has_88=0
		local -i has_16=0
		local -i has_8=0

		# Split TERM and COLORTERM environment variables into their
		# components and then try to detect color level support
		# by presence of certain values.
		set -f
		local -- IFS=-+.
		local c
		for c in $TERM $COLORTERM; do
			case $c in
			24bit|gnome|kitty|st|truecolor|vte)
				has_24bit=1 ;;
			256color|teken|tmux|xterm)
				has_256=1 ;;
			88color|rxvt)
				has_88=1 ;;
			linux)
				has_16=1 ;;
			cons[0-9]*|pcvt[0-9]*)
				has_8=1
			esac
		done
		unset -v IFS
		set +f

		# If COLORTERM variable is itself present, regardless of its
		# value, assume support for 16 colors.
		[[ -n $COLORTERM ]] &&
			has_16=1

		# Set the ‘colors’ variable to the highest number of colors
		# determined by the above comparisons.
		if (( has_24bit )); then
			colors=16777216
		elif (( has_256 )); then
			colors=256
		elif (( has_88 )); then
			colors=88
		elif (( has_16 )); then
			colors=16
		elif (( has_8 )); then
			colors=8
		else
			colors=0
		fi
	fi

	(( debug )) &&
		message 'detected support for %d colors' "$colors"
}

function determine_current_encoding {
	# Determine current character encoding first by running the ‘locale’
	# command. If it fails or does not exist, fall back to checking
	# environment variables. If everything fails, assume an ASCII encoding.
	if ! { command_exists locale && current_encoding=$(locale charmap 2> /dev/null); }; then
		local -- language
		language=${LC_ALL:-${LC_CTYPE:-$LANG}}
		language=${language%@*}

		# Consider only those locales that include character set
		# suffix in their identifier, since those without it may have
		# character set varying between different systems.
		if [[ $language = *.* ]]; then
			current_encoding=${language#*.}

			# On some operating systems, character set name
			# ‘EUC’ may refer to different encodings depending
			# on the locale identifier. We must disambiguate it
			# on the basis of its territory suffix.
			if [[ $current_encoding = EUC ]]; then
				language=${language%.EUC}

				case $language in
				zh_CN) current_encoding=GB2312 ;;
				??_??) current_encoding+=-${language#??_}
				esac
			fi
		else
			current_encoding=${LESSCHARSET:-ASCII}
		fi
	fi

	# Normalize encoding identifier to value used by GNU C library,
	# with the exception of the ASCII encoding.
	current_encoding=${current_encoding^^}
	case $current_encoding in
	646|ANSI_X3.4-1968|US-ASCII|'')
	                  current_encoding=ASCII ;;
	ANSI1251|WINDOWS) current_encoding=CP1251 ;;
	ARMSCII8)         current_encoding=ARMSCII-8 ;;
	BIG5HK?(SCS))     current_encoding=BIG5-HKSCS ;;
	CP866|CP1047)     current_encoding=IBM${current_encoding#CP} ;;
	DOS)              current_encoding=IBM437 ;;
	EBCDIC)           current_encoding=EBCDIC-INT ;;
	EUCCN)            current_encoding=GB2312 ;;
	EUC[!-]*)         current_encoding=EUC-${current_encoding#EUC} ;;
	GEORGIANPS)       current_encoding=GEORGIAN-PS ;;
	IBM-*)            current_encoding=IBM${current_encoding#IBM-} ;;
	ISO8859)          current_encoding=ISO-8859-1 ;;
	ISO8859-*)        current_encoding=ISO-${current_encoding#ISO} ;;
	ISO8859*)         current_encoding=ISO-8859-${current_encoding#ISO8859} ;;
	KOI8[!-]*)        current_encoding=KOI8-${current_encoding#KOI8} ;;
	LATIN[0-9]*)      current_encoding=ISO-8859-${current_encoding#LATIN} ;;
	NEXT)             current_encoding=NEXTSTEP ;;
	SHIFTJISX0213)    current_encoding=SHIFT_JISX0213 ;;
	SJIS|PCK)         current_encoding=SHIFT_JIS ;;
	TIS620)           current_encoding=TIS-620 ;;
	UJIS)             current_encoding=EUC-JP ;;
	UTF8)             current_encoding=UTF-8 ;;
	esac

	(( debug )) &&
		message "detected '%s' character encoding." "$current_encoding"
}

#
# Determine command line arguments the parent ‘less’ command was called with.
#
# Since we need to be feature-to-feature, bug-to-bug compatible with GNU less
# to correctly parse its options, the following functions have been taken
# from its source code and translated into Bash. Some parts of it have been
# rewritten to take advantage of Bash being a higher level language than C.
# Code layout, variable names and comments have been largely unchanged
# from the original, so the code may be undercommented and stylistically
# divergent compared to the rest of this program.
#
# While the original code has been dual-licensed under GNU GPLv3+ and
# a custom Less License, the translated Bash version is available only
# under the terms of the former.
#

function determine_less_arguments {
	local -- s

	# Do not try to determine arguments if we only know the command name
	# of the parent process.
	[[ -z ${parent_process[1]} ]] &&
		return

	# If the name of the executable program is ‘more’, act like
	# LESS_IS_MORE is set.
	#
	# If the parent process’s name isn’t ‘less’ or ‘more’, do not check
	# its arguments.
	case ${parent_process[0]} in
	less) ;;
	more) less_is_more=1 ;;
	*)    return
	esac

	[[ -n $LESS_IS_MORE ]] &&
		less_is_more=1

	s=LESS
	(( less_is_more )) &&
		s=MORE
	less_scan_option "${!s}"

	for s in "${parent_process[@]:1}"; do
		if [[ $s = [-+]?* || -n $less_pending_option ]]; then
			[[ $s = -- ]] &&
				break
			less_scan_option "$s"
		else
			break
		fi
	done

	if (( debug )); then
		message 'detected less commands: %s' "${less_commands[*]}"
		message 'detected less options: %s' "${less_options[*]@K}"
	fi
}

# Table of the ‘less’ options used by the following functions.

declare -a less_option_table=(
	'?:help:BOOL:0'
	'a:search-skip-screen:TRIPLE:2'
	'b:buffers:NUMBER:64'
	'B:auto-buffers:BOOL:1'
	'c:clear-screen:TRIPLE:0'
	'd:dumb:BOOL:0'
	'D:color:STRING:'
	'e:quit-at-eof:TRIPLE:0'
	'f:force:BOOL:0'
	'F:quit-if-one-screen:BOOL:0'
	'g:hilite-search:TRIPLE:2'
	'h:max-back-scroll:NUMBER:-1'
	'i:ignore-case:TRIPLE:0'
	'j:jump-target:STRING:0123456789.-'
	'J:status-column:BOOL:0'
	'k:lesskey-file:STRING:'
	'K:quit-on-intr:BOOL:0'
	'L:no-lessopen:BOOL:1'
	'm:long-prompt:TRIPLE:0'
	'n:line-numbers:TRIPLE:1'
	'o:log-file:STRING:'
	'O:LOG-FILE:STRING:'
	'p:pattern:STRING:'
	'P:prompt:STRING:'
	'q:quiet:TRIPLE:0'
	'q:silent:TRIPLE:0'
	'r:raw-control-chars:TRIPLE:0'
	's:squeeze-blank-lines:BOOL:0'
	'S:chop-long-lines:BOOL:0'
	't:tag:STRING:'
	'T:tag-file:STRING:'
	'u:underline-special:TRIPLE:0'
	'V:version:BOOL:0'
	'w:hilite-unread:TRIPLE:0'
	'x:tabs:STRING:0123456789,'
	'X:no-init:BOOL:0'
	'y:max-forw-scroll:NUMBER:-1'
	'z:window:NUMBER:-1'
	'":quotes:STRING:'
	'~:tilde:BOOL:1'
	'#:shift:STRING:0123456789.'
	'determine_file_size:file-size:BOOL:0'
	'follow_name:follow-name:BOOL:0'
	'incremental_search:incsearch:BOOL:0'
	'lesskey_source:lesskey-src:STRING:'
	'line_number_width:line-num-width:NUMBER:7'
	'mouse:mouse:TRIPLE:0'
	'no_keypad:no-keypad:BOOL:0'
	'no_history_duplicates:no-histdups:BOOL:0'
	'rscroll:rscroll:STRING:'
	'save_marks:save-marks:BOOL:0'
	'status_column_width:status-col-width:NUMBER:2'
	'use_backslash:use-backslash:BOOL:0'
	'use_color:use-color:BOOL:0'
	'wheel_lines:wheel-lines:NUMBER:0')

# Scan to end of number.
#
# Function loosely based on less’s ‘getnum’ function.

function less_optnum {
	local -- neg

	s=${s#"${s%%[!$' \t']*}"}
	neg=
	if [[ $s = -* ]]; then
		neg=-
		s=${s#-}
	fi
	[[ $s != [0-9]* ]] &&
		return
	str=${s%%[!0-9]*}
	s=${s#"$str"}
	str=$neg$str
}

# Scan to end of string or to an $ character.

function less_optstring {
	local -- validchars
	validchars=$1

	while [[ -n $s ]]; do
		if (( less_options[use_backslash] && ${#s} > 1 )) && [[ $s = \\* ]]; then
			s=${s:1}
		else
			[[ $s = \$* ||
			   ( -n $validchars && $validchars != *"${s:0:1}"* ) ]] &&
				break
		fi
		str+=${s:0:1}
		s=${s:1}
	done
}

# Scan an argument (either from the command line or from the
# LESS environment variable) and process it.

function less_scan_option {
	local -- o oletter oname otype optc optname odefault maxo poname printopt
	local -i ambig exact lc len maxlen set_default uppercase
	local -x s str
	s=$1

	[[ -z $s ]] &&
		return

	# If we have a pending option which requires an argument,
	# handle it now.
	#
	# This happens if the previous option was, for example, "-P"
	# without a following string.  In that case, the current
	# option is simply the argument for the previous option.
	if [[ -n $less_pending_option ]]; then
		IFS=: read -r oletter _ otype _ <<< "$less_pending_option"
		case $otype in
		STRING)
			[[ -n $s ]] &&
				less_options[$oletter]=$s ;;
		NUMBER)
			less_optnum
			[[ -n $str ]] &&
				less_options[$oletter]=$str
		esac
		less_pending_option=
		return
	fi

	set_default=0
	optname=

	while [[ -n $s ]]; do
		optc=${s:0:1}
		s=${s:1}

		# Check some special cases first.
		case $optc in
		' ' | $'\t' | \$)
			continue ;;
		-)
			case $s in
			# "--" indicates an option name instead of a letter.
			-*)
				s=${s#-}
				optname=$s ;;
			# "-+" means set these options back to their defaults.
			# (They may have been set otherwise by previous
			# options.)
			+*)
				set_default=1
				s=${s#+} ;;
			*)
				set_default=0
			esac
			continue ;;
		+)
			if [[ -z $s ]]; then
				(( debug )) &&
					message 'Value is required after +'
				return
			fi
			str=
			less_optstring
			if [[ -n $str ]]; then
				(( debug )) &&
					message "Found command '%s'" "$str"
				less_commands+=("${str#+}")
			fi
			continue ;;
		[0-9])
			# Special "more" compatibility form "-<number>"
			# instead of -z<number> to set the scrolling
			# window size.
			s=$optc$s
			optc=z ;;
		n)
			(( less_is_more )) &&
				optc=z
		esac

		# Not a special case.
		# Look up the option letter in the option table.
		#
		# Some functions that existed in the original code have been
		# inlined for better performance and interoperation with the
		# rest of the code.
		lc=0
		if [[ -z $optname ]]; then
			printopt=-$optc
			[[ $optc = [a-z] ]] &&
				lc=1
			for o in "${less_option_table[@]}" ''; do
				IFS=: read -r oletter oname otype odefault <<< "$o"
				[[ $oletter = "$optc" ]] &&
					break
				[[ $otype = TRIPLE && ${oletter^} = "$optc" ]] &&
					break
			done
		else
			printopt=$optname
			[[ $optname = [a-z]* ]] &&
				lc=1
			maxo=
			maxlen=0
			ambig=0
			exact=0

			# Check all options.
			for o in "${less_option_table[@]}"; do
				IFS=: read -r _ oname otype _ <<< "$o"

				# Try normal match first (uppercase == 0),
				# then, then if it's a TRIPLE option,
				# try uppercase match (uppercase == 1).
				for uppercase in 0 1; do
					if (( uppercase && lc )); then
						len=-1
					else
						len=${#oname}
						poname=${optname:0:len}
						(( uppercase )) &&
							poname=${poname,,}
						while [[ ${poname:0:len} != "${oname:0:len}" ]]; do
							(( len-- ))
						done
					fi
					if (( len < 0 )) || [[ ${optname:len} = [A-Za-z-]* ]]; then
						# We didn't use all of the option name.
						continue
					fi
					if (( !exact && len == maxlen )); then
						# Already had a partial match,
						# and now there's another one that
						# matches the same length.
						ambig=1
					elif (( len > maxlen )); then
						# Found a better match than
						# the one we had.
						maxo=$o
						maxlen=$len
						ambig=0
						(( exact = len == ${#oname} ))
					fi
					[[ $otype != TRIPLE ]] &&
						break
				done
			done
			IFS=: read -r oletter oname otype odefault <<< "$maxo"
			o=$maxo
			(( ambig )) &&
				o=
			s=${optname:maxlen}
			optname=
			if [[ -z $s || $s = ' '* ]]; then
				# The option name matches exactly.
				:
			elif [[ $s = =* ]]; then
				# The option name is followed by "=value".
				if [[ -n $o && $otype = !(STRING|NUMBER) ]]
				then
					(( debug )) &&
						message 'The %s option should not be followed by =' "$printopt"
					return
				fi
				s=${s#=}
			else
				# The specified name is longer than the
				# real option name.
				o=
			fi
		fi

		if [[ -z $o ]]; then
			if (( ambig )); then
				(( debug )) &&
					message '%s is an ambiguous abbreviation' "$printopt"
			else
				(( debug )) &&
					message 'There is no %s option' "$printopt"
			fi
			return
		fi
		(( debug )) &&
			message "found option '%s' ('%s', type %s, default '%s')" "$oletter" "$oname" "$otype" "$odefault"

		str=
		case $otype in
		BOOL)
			if (( set_default )); then
				less_options[$oletter]=$odefault
			else
				(( less_options[\$oletter] = !odefault ))
			fi ;;
		TRIPLE)
			if (( set_default )); then
				less_options[$oletter]=$odefault
			else
				(( less_options[\$oletter] = lc
					? (odefault == 1 ? 0 : 1)
					: (odefault == 2 ? 0 : 2) ))
			fi ;;
		STRING)
			if [[ -z $s ]]; then
				# Set less_pending_option and return.
				# We will get the string next time
				# less_scan_option is called.
				less_pending_option=$o
				return
			fi
			s=${s#"${s%%[! ]*}"}
			less_optstring "$odefault"
			[[ -n $str ]] &&
				less_options[$oletter]=$str ;;
		NUMBER)
			if [[ -z $s ]]; then
				less_pending_option=$o
				return
			fi
			less_optnum
			[[ -n $str ]] &&
				less_options[$oletter]=$str
		esac
	done
}

#
# Determine name of the parent process and its arguments.
#

function determine_parent_process {
	# Read full process command line only on systems that support doing
	# it in an unambiguous way using /proc filesystem. Otherwise, read
	# only command name.
	if [[ -r /proc/$PPID/cmdline ]]; then
		mapfile -t -d '' parent_process < "/proc/$PPID/cmdline" ||
			return
	else
		local -- args
		args=$(ps -o args= -p "$PPID") ||
			return

		parent_process=("${args%% *}")
	fi

	# Remove path to the executable from the name of the process
	parent_process[0]=${parent_process[0]##*/}

	(( debug )) &&
		message_plain "%s: detected parent process '%s'" "${0##/*/}" "${parent_process[*]}"
}

#
# Determine name the ‘lesspipe’ was called with.
#
# Since shell cannot distinguish whether a script has been invoked in the
# command line with a complete path or just with a program name, we need
# to do a few heuristics.
#

function determine_program_name {
	# If out parent process is ‘less’ or ‘more’, then ‘lesspipe’ has been
	# called by a way of LESSOPEN environment variable, hence program name
	# must be contained in it.
	if [[ ${parent_process[0]} = @(less|more) ]]; then
		program_name=${LESSOPEN##*(\|)?(-)?([[:space:]])}
		program_name=${program_name%%[[:space:]]*}
	else
		# Otherwise, the program name is contained in the BASH_ARGV0
		# variable.
		program_name=$BASH_ARGV0

		# If BASH_ARGV0 is an absolute path, then ‘lesspipe’ was
		# probably called with a sole program name, hence we have
		# to strip from the front any component of the PATH
		# environment variable.
		if [[ $program_name = /* ]]; then
			local -a paths
			IFS=: read -r -a paths <<< "$PATH:"

			local p
			for p in "${paths[@]}"; do
				[[ $program_name = "$p"/* ]] &&
					program_name=${program_name#"$p"/}
			done
		fi
	fi

	(( debug )) &&
		message "detected program name '%s'" "$program_name"
}

#
# Determine whether the terminal uses a light or a dark background.
#
# The code that queries the terminal for the background and foreground color
# and then compares their brightness is based on ‘shell-term-background’
# scripts by Rocky Bernstein (with contributions by other people, see
# README file in the linked repository for details):
#
#	https://github.com/rocky/shell-term-background
#
# The original code has been licensed under the GNU GPLv2+, while license
# of the adaptation for our purposes is GPLv3+, like the rest of this program.
#
# The code that checks COLORFGBG environment variable has been based
# on the documentation of the iproute2 suite.
#

function determine_terminal_background {
	local -- {b,f}g_{r,g,b} space saveterm
	local -a components
	local -i error osc osc_reply

	# Do not try to determine the background if we do not support colors.
	(( colors < 8 )) &&
		return

	# Try to first determine background and foreground colors by querying
	# the controlling terminal with OSC 10 and 11 codes, since this method
	# seems to be the most reliable.
	#
	# Before sending a query, we try to set the terminal to the raw mode
	# and disable echoing characters.
	if saveterm=$(stty -g) && stty raw -echo min 0; then
		local g
		for g in f0 b1; do
			# Split loop iterator into list of color component
			# variables and OSC command number.
			components=("${g:0:1}g_"{r,g,b})
			osc=1${g:1}

			# Send query to the terminal, read the response up
			# until the last character of the ST ANSI escape
			# sequence and split it into OSC command number, name
			# of the color space and its color components. We have
			# to also set a timeout in case the terminal does not
			# answer, for example if it does not support querying
			# colors.
			#
			# The timeout is probably the lowest that is possible,
			# as determined experimentally, since lower values
			# under some terminals result in the response ending
			# up in the shell prompt.
			#
			# shellcheck disable=SC1003
			printf '\e]%d;?\e\\' "$osc" >&0
			IFS=$'\e];:/' read -rs -d \\ -t 0.05 _ _ osc_reply space "${components[@]}" _

			# Check if the response is in the correct format.
			if (( osc_reply != osc )) || [[ $space != rgb ]]; then
				error=1
				break
			fi

			local v
			for v in "${components[@]}"; do
				if [[ -z ${!v} || ${!v} = *[!A-Fa-f0-9]* ]]; then
					error=1
					break 2
				fi
			done
		done

		# If terminal replied correctly, compare brightness between
		# a background and foreground color. If the foreground is
		# brighter than the background, we likely use a dark theme.
		#
		# Brightness is calculated using the approximate formula
		# y = 0.375r + 0.5g + 0.125b, scaled up by the factor of 8
		# to avoid losing precision. This gives us a value between
		# 0 and 524280.
		if (( !error )); then
			# shellcheck disable=SC2154
			if (( 3 * 16#"$fg_r" + 4 * 16#"$fg_g" + 16#"$fg_b" > 3 * 16#"$bg_r" + 4 * 16#"$bg_g" + 16#"$bg_b" )); then
				terminal_background=dark
			else
				terminal_background=light
			fi
		fi

		# Restore saved terminal options.
		stty "$saveterm"
	fi <> /dev/tty > /dev/null 2>&1

	# If querying the terminal did not determine anything, try to check
	# COLORFGBG environment variable set by some terminals.
	#
	# The COLORFGBG environment variable usually consists of two or three
	# color codes separated by semicolons, with the last value containing
	# the code for background color.
	if [[ -z $terminal_background && $COLORFGBG = *\;+([0-9]) ]]; then
		bg=${COLORFGBG##*\;}

		# Only 0-6 and 8 codes encode dark backgrounds, with the rest
		# being light backgrounds.
		if (( bg >= 0 && bg <= 6 || bg == 8 )); then
			terminal_background=dark
		else
			terminal_background=light
		fi
	fi

	# If it is still not known, assume dark background, since this is
	# probably the most common setting.
	[[ -z $terminal_background ]] &&
		terminal_background=dark

	(( debug )) &&
		message 'detected %s terminal background' "$terminal_background"
}

#
# Determine size of the current terminal.
#

function determine_terminal_size {
	shopt -s checkwinsize
	(: Variables will be set only after a child process, like this subshell, finishes)
	shopt -u checkwinsize

	# If Bash fails to detect terminal size, assume default dimensions of 80×24.
	(( LINES * COLUMNS > 0 )) ||
		LINES=24 COLUMNS=80

	(( debug )) &&
		message "terminal size is %d×%d" "$COLUMNS" "$LINES"
}

#
# Print message to the standard error and exit.
#

function die {
	# Restore standard error if redirected.
	[[ -t 5 ]] &&
		exec 2>&5-

	message "$@"
	exit 1
}

#
# If colors are supported, run command while pretending that its standard
# output is a terminal. This is needed for applications that change their
# behavior based on whether they output to a terminal and not provide any
# option to override their detection.
#

function fake_tty {
	if (( colors >= 8 )) && [[ ! -t 1 ]]; then
		command=$(order_commands unbuffer ptyrun script)
		case $command in
		ptyrun)
			ptyrun "$@" ;;
		script)
			fake_tty_script "$@" ;;
		unbuffer)
			unbuffer "$@" ;;
		*)
			"$@"
		esac
	else
		"$@"
	fi
}

#
# Execute ‘script’ command with correct arguments.
#

function fake_tty_script {
	local -- variant

	# Since options to ‘script’ may vary between the various systems,
	# we have to determine the variant installed.
	if script -eq -c '' /dev/null; then
		variant=linux
	elif script -q -c '' /dev/null; then
		variant=netbsd
	elif script -Fq /dev/null true; then
		variant=freebsd
	else
		# If there is no suitable variant installed, just return from
		# the function with an error.
		return 1
	fi > /dev/null 2>&1

	# If no command is given in the function arguments, act as a function
	# that just checks whether whether a suitable variant of ‘script’ is
	# present on the system. Since if no suitable variant was found we
	# returned from the function earlier, we need to just return
	# with a success status.
	[[ -z $1 ]] &&
		return

	# Run command with arguments and environment depending on detected
	# variant. We redirect null device to the standard input to avoid
	# the spawned pseudo-terminal hijacking the ‘less’ keyboard input,
	# and also avoid it messing with terminal output.
	if [[ $variant = freebsd ]]; then
		# Reportedly, some variants of FreeBSD’s ‘script’ do not
		# inherit exit status of the child process. In such case,
		# we need to wrap the command in a shell script that will
		# echo the exit status on file descriptor 5, which we will
		# then capture in a wrapper function.
		if script -Fq /dev/null false > /dev/null 2>&1; then
			fake_tty_capture_status script -Fq -- /dev/null "$BASH" -c '"$@" 5>&-; echo "$?" >&5' _ "$@"
		else
			script -Fq -- /dev/null "$@"
		fi
	else
		local -- cmd

		# Serialize command so it can be passed to the shell.
		printf -v cmd '%q ' "$@"
		cmd=${cmd% }

		# Since the ‘script’ command executes interpreter given
		# in the SHELL environment variable, we need to explicitly
		# set it to path to GNU Bash so we won’t pass a command
		# to a shell with an incompatible syntax, e.g. csh.
		local -x SHELL
		SHELL=$BASH

		# Older versions of NetBSD’s ‘script’ don’t support the ‘-e’
		# option, so we have again to capture exit status indirectly.
		if [[ $variant = netbsd ]]; then
			fake_tty_capture_status script -fq -c "$cmd"' 5>&-; echo "$?" >&5' /dev/null
		else
			script -efq -c "$cmd" /dev/null
		fi
	fi < /dev/null
}

#
# Capture exit status echoed on file descriptor 5.
#

function fake_tty_capture_status {
	local -i fd status

	exec {fd}>&1
	status=$("$@" 5>&1 >&"$fd") ||
		status=$?
	exec {fd}>&-

	return "$status"
}

#
# Some decompression programs exit with status 2 in case a warning occurs.
# This function wraps them to treat it as a successful decompression.
#

function ignore_decompression_warnings {
	"$@" || return "$(( $? != 2 ))"
}

#
# Print message to the standard error, with program name.
#

function message {
	local -- msg
	msg=$1
	shift

	message_plain "%s: $msg" "$program_name" "$@"
}

#
# Print message to the standard error, without program name.
#

function message_plain {
	local -- msg
	msg=$1
	shift

	# shellcheck disable=SC2059
	printf "$msg\n" "$@" >&2
}

#
# Get name of the command to run based on the order of preference.
#

function order_commands {
	local -- command

	# Try to first find a command based on the explicit user preference.
	local c
	for c in "${prefer[@]}"; do
		# Check whether a command is one of the possible commands.
		local d
		for d in "$@"; do
			if [[ $c = "$d" ]]; then
				if order_commands_check "$d"; then
					command=$d
					break 2
				fi
				break
			fi
		done
	done

	# If a command is not yet found, try a default order.
	if [[ -z $command ]]; then
		local d
		for d in "$@"; do
			if order_commands_check "$d"; then
				command=$d
				break
			fi
		done
	fi

	(( debug )) && [[ -n $command ]] &&
		message "found command '%s'" "$command"
	print "$command"
}

function order_commands_check {
	local -- command
	command=$1

	# Check whether a command is ignored.
	local c
	for c in "${ignore[@]}"; do
		[[ $command = "$c" ]] &&
			return 1
	done

	# Some commands have additional requirements which we have to also
	# check.
	case $command in
	bat)
		# ‘bat’ may be either a colorizer, or a Qt4 version of the Bacula
		# Administration Tool console. We have to check which one is
		# installed on the system, and succeed only if it’s the colorizer.
		command_exists bat && bat --version > /dev/null 2>&1 && (( colors >= 8 )) ;;
	colorls)
		# We only want to detect only OpenBSD’s version of ‘colorls’,
		# so we check whether it supports a ‘-G’ option which is not
		# supported by other implementations.
		command_exists colorls && (( colors >= 8 )) && colorls -G -d . > /dev/null 2>&1 ;;
	debinfo)
		command_exists_all tar ar || command_exists bsdtar ;;
	perl_storable)
		command_exists perl && perl -MData::Dumper -MStorable -e 1 ;;
	rpm2cpio)
		command_exists_all rpm2cpio cpio ;;
	script)
		command_exists script && fake_tty_script ;;
	# Generic rules go below:
	batcat|chroma|colordiff|enscript|gls|highlight|pygmentize|source-highlight)
		# Requires terminal color support.
		command_exists "$command" && (( colors >= 8 )) ;;
	text-vimcolor|vimcat)
		# Requires terminal 16 color support.
		command_exists "$command" && (( colors >= 16 )) ;;
	fish_indent)
		# Requires terminal true color support.
		command_exists "$command" && (( colors >= 16777216 )) ;;
	libreoffice|pdftohtml)
		# Requires HTML parsing command.
		command_exists "$command" && command_exists_any elinks html2text links lynx pandoc w3m ;;
	*)
		# No additional requirements.
		command_exists "$command"
	esac
}

#
# Parse arguments passed to ‘lesspipe’.
#

function parse_arguments {
	local -- arg file getopt_out init option prog
	local -i getopt_enhanced getopt_status

	# Because sometimes we want to print debug messages even before
	# arguments are completely parsed, let’s determine whether ’--debug’
	# option is present in a quick-and-dirty way.
	if [[ $* = *--debug* ]]; then
		local a
		for a; do
			case $a in
			--debug)     debug=1 ;;
			--init?(=*)) init=y ;;
			--)          break
			esac
		done
	fi

	# Do not print debug messages if we’re just evaluating initialization
	# commands.
	(( debug )) && [[ -n $init && ! -t 1 ]] &&
		debug=0

	# We have to determine few things before we can even parse arguments.
	determine_parent_process
	determine_program_name

	# Reset previously set variables to their default values so we can set
	# them later again correctly.
	debug=0
	init=

	# Test whether enhanced getopt is available.
	getopt_out=$(getopt -T) ||
		getopt_status=$?
	(( getopt_status == 4 )) && [[ -z $getopt_out ]] &&
		getopt_enhanced=1

	# If enhanced getopt is available, try to first filter arguments
	# through it.
	if (( getopt_enhanced )); then
		getopt_out=$(getopt -n "$program_name" -o '' \
			-l debug \
			-l help \
			-l ignore: \
			-l init:: \
			-l italic \
			-l prefer: \
			-l theme: \
			-l version \
			-- "$@"
		) ||
			usage
		eval "set -- $getopt_out"
	fi

	while :; do
		option=$1
		shift

		# Option parsing fallback if enhanced getopt is not available.
		if (( !getopt_enhanced )); then
			case $option in
			--@(debug|help|ignore|italic|prefer|theme|version)|--)
				;;
			--init)
				set -- '' "$@" ;;
			--@(ignore|init|prefer|theme)=*)
				arg=${option#*=}
				option=${option%%=*}
				set -- "$arg" "$@" ;;
			-*)
				message "unrecognized option '%s'" "$option"
				usage ;;
			*)
				set -- "$option" "$@"
				break
			esac
		fi

		case $option in
		--debug)
			debug=1 ;;
		--help)
			show_help ;;
		--ignore)
			ignore+=("$1")
			shift ;;
		--init)
			if [[ -z $1 ]]; then
				init=auto
			else
				init=$(validate_argument --init "$1" auto sh/bash/ksh/zsh csh/tcsh fish) ||
					usage
			fi
			shift ;;
		--italic)
			italic=1 ;;
		--prefer)
			prefer+=("$1")
			shift ;;
		--theme)
			if [[ $1 = ?*=* ]]; then
				prog=$(validate_argument --theme "${1%%=*}" bat/batcat chroma enscript highlight pygmentize) ||
					usage
				theme[$prog]=${1#*=}
			else
				message "argument '%s' for '--theme' must contain a program name" "$1"
				usage
			fi
			shift ;;
		--version)
			show_version ;;
		--)
			break ;;
		*)
			exit 1
		esac
	done

	if [[ -z $init ]]; then
		# Show initialization code if we are called without any
		# file names, for compatibility with some other ‘lesspipe’
		# implementations.
		if (( !$# )); then
			init=auto
		else
			file=$1
			shift
		fi
	fi

	# Show error if there are too many arguments.
	if (( $# )); then
		message "extra operand '%s'" "$1"
		usage
	fi

	if [[ -n $init ]]; then
		show_initialization "$init"
	else
		process "$file"
	fi
}

function usage {
	message_plain "Try '%s --help' for more information." "$program_name"
	exit 1
}

#
# Print line of text.
#

function print {
	printf %s\\n "$*"
}

#
# Processing is split into multiple, chained functions. This allows us not
# only to keep functions single-purpose and of reasonable size, but also to
# go back in execution of the program where it’s needed, since Bash lacks
# goto statements.
#

function process {
	local -- file name
	local -i status
	file=$1

	determine_less_arguments

	# Do not process file at all if ‘less’ is following appended data.
	local c
	for c in "${less_commands[@]}"; do
		[[ $c = F ]] &&
			return 1
	done

	# Create a working directory for temporary files created during
	# processing.
	working_directory_create ||
		die 'cannot create working directory'

	# If we’re reading from stdin or a pipe, save its contents
	# to a temporary file.
	if [[ $file = - || -p $file ]]; then
		# shellcheck disable=SC2015
		outfile=$(temporary_file) && cat -- "$file" > "$outfile" ||
			die 'cannot save input to a temporary file'
		file=$outfile
	fi

	# Fail if file cannot be read or does not exist.
	[[ -r $file ]] ||
		die "cannot access '%s'" "$file"

	determine_current_encoding
	determine_colors
	determine_terminal_size

	# Set name to the file path with all non-trailing components stripped
	# from it.
	name=${file%%*(/)}
	name=${name##*/}

	# Silence errors unless we are in the debug mode.
	(( !debug )) &&
		exec 5>&2 2> /dev/null

	detect_file_type "$file" "$name"
	status=$?

	# Restore standard error.
	(( !debug )) &&
		exec 2>&5-

	return "$status"
}

#
# Sends output of a command to be processed again.
#
# Every invocation of this function should be followed by ‘|| status=$?’
# to accurately capture exit status.
#

function reprocess {
	local -- name outfile type
	name=$1
	type=$2
	shift 2

	# Fail if we exceeded maximum number of invocations of this function
	# for one file. This will prevent resource exhaustion if user tries
	# to open maliciously designed recursively compressed files or similar.
	(( processing_level++ < 256 )) ||
		die 'exceeded maximum processing level'

	# Unless we are passing through output of a command already run
	# during the filtering phase, run command and save its output
	# to a temporary file.
	if [[ $1 = passthrough ]]; then
		outfile=$2
	else
		outfile=$(temporary_file) ||
			return 1

		"$@" > "$outfile" ||
			return 2
	fi

	# Check whether command has output anything.
	[[ -s $outfile ]] ||
		return 1

	# Send output of a command to further processing.
	detect_file_type "$outfile" "$name" "$type"
}

#
# Reprocess compressed files.
#

function reprocess_compressed {
	local -- command name
	name=$1
	shift

	# Before we pass decompressed file back to type detection function,
	# we first remove its name suffix. Abbreviated tar file suffixes
	# (like .tgz) also need a special treatment.
	shopt -s nocasematch
	case $name in
	*.br|*.bz2|*.gz|*.lrz|*.lz|*.lz4|*.lzma|*.xz|*.Z|*.zst)
		name=${name%.*} ;;
	*.taZ|*.taz|*.tb2|*.tbr|*.tbz|*.tbz2|*.tgz|*.tl4|*.tlr|*.tlrz|*.tlz|*.tlz4|*.txz|*.tZ|*.tz2|*.tz4|*.tzs|*.tzst|*.tzstd)
		name=${name%.*}.tar
	esac

	# If the file is a compressed tar archive, we don’t really have
	# to decompress it first, because all modern tar implementations can
	# do it by themselves. Nor we should do it, because these archives
	# can be sometimes very large and in decompressed form they may take
	# too much space in the temporary directory.
	case $name in
	# Actively prefer libarchive cpio if installed, since GNU cpio
	# doesn’t support decompression.
	*.cpio) command=$(order_commands bsdcpio cpio bsdtar) ;;
	*.tar)  command=$(order_commands tar bsdtar)
	esac
	shopt -u nocasematch

	# Not all implementations of cpio and tar can decompress all listed
	# types of files. There are also unlikely cases when a file with such
	# name isn’t actually a cpio or tar archive. Hence, if listing the
	# contents of the archive with a cpio or tar command fails, we need
	# to decompress the archive separately.
	case $command in
	cpio|bsdcpio)
		color_archive "$command" -itv -F "$file" &&
			return 0 ;;
	tar|bsdtar)
		color_archive "$command" -tv -f "$file" &&
			return 0
	esac

	# Some decompression programs exit with status 2 in case a warning
	# occurs. We need to wrap them in a function that resets status to 0
	# in such cases.
	[[ $1 = @(gzip|lzma|lzop|xz) ]]
		set -- ignore_decompression_warnings "$@"

	reprocess "$name" '' "$@"
}

#
# Select and run a filter program.
#

function run_filter {
	local -- command encoding encoding_original file file_original name
	local -- outfile type
	local -a args
	local -i status
	file=$1
	name=$2
	type=$3
	encoding=$4

	# Because some filter programs determine file encoding based
	# on the metadata contained in its content and convert encoding on
	# their own, we need to save the original file path and encoding so
	# we can pass them to the filter program to avoid double conversions.
	file_original=$file
	encoding_original=$encoding

	# Convert file to the current character encoding if its own encoding
	# is different, unless it is ASCII, unknown, or the file is binary.
	if [[ $encoding = !(unknown|ASCII|binary|"$current_encoding") ]] && command_exists iconv; then
		# Detect whether system iconv supports //TRANSLIT option.
		iconv -t //TRANSLIT <<< '' > /dev/null 2>&1 &&
			args=(-t //TRANSLIT)

		if outfile=$(temporary_file) && iconv -s -f "$encoding" "${args[@]}" -- "$file" > "$outfile"; then
			file=$outfile
			encoding=$current_encoding
		fi
	fi

	# Decide which filter to use based on the file type.
	args=()
	status=0

	case $type in
	application/epub+zip)
		command=$(order_commands pandoc)
		case $command in
		pandoc)
			pandoc -f epub -t plain --columns "$COLUMNS" -- "$file" ;;
		*)
			run_filter "$file" "$name" application/zip binary || status=$?
		esac || status=2 ;;
	application/gzip)
		command=$(order_commands pigz gzip)
		case $command in
		gzip)
			reprocess_compressed "$name" gzip -cd -- "$file" ;;
		pigz)
			reprocess_compressed "$name" pigz -cd -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	# application/json - see at the bottom.
	application/octet-stream)
		command=$(order_commands strings)
		case $command in
		strings)
			strings -a -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/pdf)
		command=$(order_commands pdftotext pdftohtml pdfinfo)
		case $command in
		pdfinfo)
			pdfinfo -- "$file" ;;
		pdftohtml)
			reprocess "$name" text/html pdftohtml -i -s -q -noframes -nodrm -stdout -- "$file" || status=$? ;;
		pdftotext)
			pdftotext -layout -nopgbrk -q -- "$file" - ;;
		*)
			status=1
		esac || status=2 ;;
	application/pgp-encrypted)
		command=$(order_commands gpg2 gpg)
		case $command in
		gpg|gpg2)
			shopt -s nocasematch
			name=${name%.@(asc|gpg|pgp)}
			shopt -u nocasematch

			reprocess "$name" '' "$command" -dq -- "$file" || status=$? ;;
		*)
			status=1
		esac || status=2 ;;
	application/postscript)
		command=$(order_commands ps2ascii)
		case $command in
		ps2ascii)
			[[ $file = -* ]] &&
				file=./$file

			ps2ascii "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/vnd.debian.binary-package)
		command=$(order_commands dpkg debinfo)
		case $command in
		debinfo)
			debinfo "$file" || status=$? ;;
		dpkg)
			dpkg -I -- "$file" && color_archive dpkg -c -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/vnd.ms-cab-compressed)
		command=$(order_commands cabextract bsdtar)
		case $command in
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		cabextract)
			cabextract -l -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/vnd.oasis.opendocument.@(spreadsheet|text)?(-template)|\
	application/vnd.openxmlformats-officedocument.@(spreadsheetml.sheet|wordprocessingml.document)|\
	application/vnd.sun.xml.@(calc|writer)?(.template)|\
	application/@(msword|vnd.ms-excel)|\
	text/rtf)
		case $type in
		application/vnd.oasis.opendocument.text?(-template)|\
		application/vnd.openxmlformats-officedocument.wordprocessingml.document|\
		text/rtf)
			# We prefer pandoc for text documents because it is
			# faster.
			command=$(order_commands pandoc libreoffice) ;;
		*)
			command=$(order_commands libreoffice) ;;
		esac

		case $command in
		libreoffice)
			# Note: no display of presentations, since LibreOffice
			# doesn’t allow to convert them from the command line.
			[[ $file = -* ]] &&
				file=./$file

			outfile=$(temporary_directory) && libreoffice --convert-to html --outdir "$outfile" "$file" >&2 &&
				{ reprocess "$name" text/html passthrough "$outfile"/*.html || status=$?; } ;;
		pandoc)
			local -- format
			case $type in
			application/vnd.oasis.opendocument.text?(-template))
				format=odt ;;
			application/vnd.openxmlformats-officedocument.wordprocessingml.document)
				format=docx ;;
			text/rtf)
				format=rtf
			esac

			pandoc -f "$format" -t plain --columns "$COLUMNS" -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/vnd.sqlite3)
		command=$(order_commands sqlite3)
		case $command in
		sqlite3)
			[[ $file = -* ]] &&
				file=./$file

			sqlite3 "$file" .dump ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-7z-compressed)
		command=$(order_commands 7z 7za 7zr bsdtar)
		case $command in
		7z|7za|7zr)
			"$command" l -- "$file" ;;
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-archive)
		command=$(order_commands ar bsdtar)
		case $command in
		ar)
			color_archive ar -tv -- "$file" ;;
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-arc)
		command=$(order_commands arc)
		case $command in
		arc)
			[[ $file = -* ]] &&
				file=./$file

			arc -v "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-arj)
		command=$(order_commands arj)
		case $command in
		arj)
			arj l -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-bittorrent)
		command=$(order_commands torrenttools aria2c transmission-show torrentinfo torrentinfo-console)
		case $command in
		aria2c)
			aria2c -S -- "$file" ;;
		torrentinfo)
			(( colors < 8 )) &&
				args=(-n)

			torrentinfo -e "${args[@]}" -- "$file" ;;
		torrentinfo-console)
			torrentinfo-console "$file" ;;
		torrenttools)
			fake_tty torrenttools info -- "$file" ;;
		transmission-show)
			transmission-show -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x.brotli)
		command=$(order_commands brotli)
		case $command in
		brotli)
			reprocess_compressed "$name" brotli -cd -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x-bzip2)
		command=$(order_commands bzip2)
		case $command in
		bzip2)
			reprocess_compressed "$name" bzip2 -cd -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x.@(certificate?(-request|-revocation-list)|private-key)-@(der|pem))
		command=$(order_commands openssl)
		case $command in
		openssl)
			local -- subcommand format

			# Determine subcommand used for processing given file type.
			case $type in
			application/x.certificate-request-*)
				subcommand=req ;;
			application/x.certificate-revocation-list-*)
				subcommand=crl ;;
			application/x.certificate-*)
				subcommand=x509 ;;
			application/x.private-key-*)
				subcommand=pkey
			esac

			# Determine input format.
			case $type in
			*-der) format=der ;;
			*-pem) format=pem
			esac

			openssl "$subcommand" -in "$file" -inform "$format" -noout -text ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-compress)
		command=$(order_commands uncompress gzip)
		case $command in
		gzip)
			reprocess_compressed "$name" gzip -cd -- "$file" ;;
		uncompress)
			reprocess_compressed "$name" uncompress -c -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x-cpio)
		command=$(order_commands cpio bsdtar)
		case $command in
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		cpio)
			color_archive cpio -itv -F "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-dvi)
		command=$(order_commands dvi2tty)
		case $command in
		dvi2tty)
			[[ $file = -* ]] &&
				file=./$file

			dvi2tty -q -w "$COLUMNS" "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x.fish)
		command=$(order_commands fish_indent)
		case $command in
		fish_indent)
			fish_indent --ansi -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-hdf5)
		command=$(order_commands h5dump)
		case $command in
		h5dump)
			h5dump -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-gettext-translation)
		command=$(order_commands msgunfmt)
		case $command in
		msgunfmt)
			if (( colors >= 8 )); then
				args=(--color)
			else
				args=(--color=never)
			fi

			msgunfmt "${args[@]}" -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x.ipynb+json)
		command=$(order_commands pandoc)
		case $command in
		pandoc)
			pandoc -f ipynb -t plain --columns "$COLUMNS" -- "$file" ;;
		*)
			run_filter "$file" "$name" application/json "$encoding" || status=$? ;;
		esac || status=2 ;;
	application/x-iso9660-image)
		command=$(order_commands isoinfo bsdtar)
		case $command in
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		isoinfo)
			outfile=$(temporary_file) && isoinfo -d -i "$file" > "$outfile" && {
				local -- line
				local -i has_joliet has_rock_ridge

				# Detect Joliet and Rock Ridge extensions.
				while IFS= read -r line; do
					case $line in
					Joliet\ *)      has_joliet=1 ;;
					Rock\ Ridge\ *) has_rock_ridge=1
					esac
				done < "$outfile"

				if (( has_rock_ridge )); then
					args=(-R)
				elif (( has_joliet )); then
					args=(-J)
				else
					args=()
				fi

				cat -- "$outfile" && isoinfo -l "${args[@]}" -i "$file"
			} ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-lzh-compressed)
		command=$(order_commands lha bsdtar)
		case $command in
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		lha)
			[[ $file = -* ]] &&
				file=./$file

			lha -v "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-matlab-data)
		command=$(order_commands matdump)
		case $command in
		matdump)
			matdump -d -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x.netcdf)
		command=$(order_commands ncdump)
		case $command in
		ncdump)
			ncdump -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x.plist)
		command=$(order_commands plistutil)
		case $command in
		plistutil)
			reprocess "$name" text/xml plistutil -f xml -i "$file" || status=$? ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-lrzip)
		command=$(order_commands lrzip)
		case $command in
		lrzip)
			reprocess_compressed "$name" lrzip -cd -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x-lz4)
		command=$(order_commands lz4)
		case $command in
		lz4)
			reprocess_compressed "$name" lz4 -cdq -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x-lzip)
		command=$(order_commands lzip)
		case $command in
		lzip)
			reprocess_compressed "$name" lzip -cd -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x-lzma)
		command=$(order_commands lzma xz)
		case $command in
		lzma)
			reprocess_compressed "$name" lzma -cd -- "$file" ;;
		xz)
			reprocess_compressed "$name" xz -cd -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x.lzop)
		command=$(order_commands lzop)
		case $command in
		lzop)
			reprocess_compressed "$name" lzop -cd -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x.perl-storable)
		command=$(order_commands perl_storable)
		case $command in
		perl_storable)
			perl -MData::Dumper -MStorable=retrieve -e '$Data::Dumper::Indent = 1; print Dumper retrieve shift' -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-rar)
		command=$(order_commands lsar bsdtar)
		case $command in
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		lsar)
			lsar -l -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-rpm)
		command=$(order_commands rpm rpm2cpio bsdtar)
		case $command in
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		rpm)
			color_archive rpm -qilpv --changelog --nomanifest -- "$file" ;;
		rpm2cpio)
			rpm2cpio "$file" | color_archive cpio -itv
			(( !PIPESTATUS[0] && !PIPESTATUS[1] )) ;;
		*)
			status=1
		esac || status=2 ;;
	application/x.sqlite2)
		command=$(order_commands sqlite)
		case $command in
		sqlite)
			[[ $file = -* ]] &&
				file=./$file

			sqlite "$file" .dump ;;
		*)
			status=1
		esac || status=2 ;;
	application/x.squashfs)
		command=$(order_commands unsquashfs)
		case $command in
		unsquashfs)
			[[ $file = -* ]] &&
				file=./$file

			unsquashfs -s "$file" && color_archive unsquashfs -ll "$file" ;;
		*)
			status=1
		esac || status=2 ;;
# 	application/x-sharedlib)
# 		{ command_exists nm && { nm -- "$file" || status=2; }; } || status=1 ;;
	application/x-tar)
		command=$(order_commands tar bsdtar)
		case $command in
		bsdtar|tar)
			color_archive "$command" -tv -f "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-xar)
		command=$(order_commands xar bsdtar)
		case $command in
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		xar)
			color_archive xar -tv -f "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/x-xz)
		command=$(order_commands xz)
		case $command in
		xz)
			reprocess_compressed "$name" xz -cd -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	application/x-zoo)
		command=$(order_commands zoo)
		case $command in
		zoo)
			[[ $file = -* ]] &&
				file=./$file

			zoo -l "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	# application/zip - see at the bottom.
	application/zstd)
		command=$(order_commands zstd)
		case $command in
		zstd)
			reprocess_compressed "$name" zstd -cdq -M1073741824 -- "$file" ;;
		*)
			status=1
		esac || status=$? ;;
	audio/flac)
		command=$(order_commands metaflac)
		case $command in
		metaflac)
			metaflac --list -- "$file" ;;
		*)
			run_filter "$file" "$name" audio/x.generic binary || status=$?
		esac || status=2 ;;
	audio/midi)
		command=$(order_commands timidity)
		case $command in
		timidity)
			timidity -idv -Ol -- "$file" ;;
		*)
			run_filter "$file" "$name" audio/x.generic binary || status=$?
		esac || status=2 ;;
	audio/mpeg)
		command=$(order_commands id3v2 mp3info2 mp3info)
		case $command in
		id3v2)
			[[ $file = -* ]] &&
				file=./$file

			id3v2 -l "$file" ;;
		mp3info)
			mp3info -- "$file" ;;
		mp3info2)
			mp3info2 -- "$file" ;;
		*)
			run_filter "$file" "$name" audio/x.generic binary || status=$?
		esac || status=2 ;;
	@(audio|video)/ogg)
		command=$(order_commands ogginfo)
		case $command in
		ogginfo)
			ogginfo -v -- "$file" ;;
		*)
			run_filter "$file" "$name" audio/x.generic binary || status=$?
		esac || status=2 ;;
	image/vnd.djvu)
		command=$(order_commands djvutxt)
		case $command in
		djvutxt)
			[[ $file = -* ]] &&
				file=./$file

			djvutxt "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	inode/directory)
		command=$(order_commands tree gls colorls ls)
		case $command in
		ls|colorls|gls)
			if (( colors >= 8 )); then
				# If we found a command whose color option is
				# already known, set it. Otherwise, explicitly
				# check what is supported.
				case $command in
				colorls)
					args=(-G) ;;
				gls)
					args=(--color) ;;
				*)
					if "$command" --color -d .; then
						args=(--color)
					elif "$command" -G -d .; then
						args=(-G)
					fi > /dev/null 2>&1
				esac
			fi

			if [[ ${args[0]} = -G ]]; then
				CLICOLOR_FORCE=1 "$command" -AGhlRq -- "$file"
			else
				"$command" -AhlRq "${args[@]}" -- "$file"
			fi ;;
		tree)
			if (( colors >= 8 )); then
				args=(-C)
			else
				args=(-n)
			fi

			# File descriptor 3 has to be explicitly closed,
			# otherwise ‘tree’ outputs JSON for some reason.
			tree -Daghlpsu "${args[@]}" -- "$file" 3>&- ;;
		*)
			status=1
		esac || status=2 ;;
	text/csv)
		command=$(order_commands csvlook pandoc)
		case $command in
		csvlook)
			csvlook -- "$file" ;;
		pandoc)
			pandoc -f csv -t plain --columns "$COLUMNS" -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	text/html|\
	application/xhtml+xml)
		# Force interpretation as a local file
		[[ $file_original != /* ]] &&
			file_original=./$file_original

		# If colors are available, try elinks first.
		if (( colors >= 16 )); then
			command=$(order_commands elinks lynx links w3m pandoc html2text)
		else
			command=$(order_commands lynx links elinks w3m pandoc html2text)
		fi

		case $command in
		elinks)
			local -- colormode
			if (( colors >= 16777216 )); then
				colormode=4
			elif (( colors >= 256 )); then
				colormode=3
			elif (( colors >= 88 )); then
				colormode=2
			elif (( colors >= 16 )); then
				colormode=1
			else
				colormode=-1
			fi

			elinks -dump -dump-width "$COLUMNS" -dump-color-mode "$colormode" -force-html -no-numbering -no-references "$file_original" ;;
		html2text)
			# There are different, incompatible versions of html2text.
			# Try to detect which one is installed and call it with
			# the correct options.
			if html2text --version > /dev/null 2>&1; then
				reprocess "$name" text/markdown html2text -b "$COLUMNS" -- "$file_original" "$encoding_original" || status=$?
			elif html2text -from_encoding ASCII <<< '' > /dev/null 2>&1; then
				html2text -from_encoding "$encoding_original" -width "$COLUMNS" "$file_original"
			else
				html2text -width "$COLUMNS" "$file_original"
			fi ;;
		links)
			links -dump -force-html -html-numbered-links 0 -width "$COLUMNS" "$file_original" ;;
		lynx)
			lynx -dump -force_html -nolist -width "$COLUMNS" -- "$file_original" ;;
		pandoc)
			pandoc -f html -t plain --columns "$COLUMNS" -- "$file" ;;
		w3m)
			w3m -dump -cols "$COLUMNS" -T text/html "$file_original" ;;
		*)
			status=1
		esac || status=2 ;;
	text/markdown)
		command=$(order_commands mdcat pandoc)
		case $command in
		mdcat)
			(( colors < 8 )) &&
				args=(-c)

			mdcat -l --columns "$COLUMNS" "${args[@]}" -- "$file" ;;
		pandoc)
			pandoc -t plain --columns "$COLUMNS" -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	text/troff)
		local -- guess macro
		local -a preproc
		preproc=(k)

		# Try to guess preprocessors and macro package using ‘grog’
		# if available.
		if command_exists grog && guess=$(grog -- "$file"); then
			local -a words
			IFS=' ' read -r -a words <<< "$guess"

			local w
			for w in "${words[@]}"; do
				case $w in
				-[eGgjpRst])
					preproc+=("${w#-}") ;;
				-m*)
					macro=${w#-m} ;;
				--)
					break
				esac
			done

			# Manual pages generated by ‘pod2man’ are misinterpreted
			# as using ‘ms’ macros. Correct it based on the file name.
			[[ $macro = s && $name = *.@([0-9]*([a-z])|[lno]) ]] &&
				macro=andoc
		else
			# Try to guess macro package from the file name.
			case $name in
			*.[0-9]*([a-z])|*.[lno])
			          macro=andoc ;;
			*.me|*.e) macro=e ;;
			*.mm|*.m) macro=m ;;
			*.ms|*.s) macro=s ;;
			*.mom)    macro=om ;;
			*.mmse)   macro=mse ;;
			*.www)    macro=www
			esac

			# Use default set of preprocessors.
			preproc+=(s R t e G g p)
		fi

		# Only man/mdoc macros can by parsed by ‘mandoc’.
		case $macro in
		an?(doc)|doc?(_old))
			command=$(order_commands groff mandoc) ;;
		*)
			command=$(order_commands groff)
		esac

		case $command in
		groff)
			args=()

			# Determine preprocessors available on this system.
			local p
			for p in "${preproc[@]}"; do
				case $p in
				e)  command_exists eqn     && args+=(-e) ;;
				G)  command_exists grap    && args+=(-G) ;;
				g)  command_exists grn     && args+=(-g) ;;
				j)  command_exists chem    && args+=(-j) ;;
				k)  command_exists preconv && args+=(-k) ;;
				p)  command_exists pic     && args+=(-p) ;;
				R)  command_exists refer   && args+=(-R) ;;
				s)  command_exists soelim  && args+=(-s) ;;
				t)  command_exists tbl     && args+=(-t)
				esac
			done

			# Determine the TTY output device.
			args+=(-T)
			case $current_encoding in
			IBM1047|EBCDIC-*)
				args+=(cp1047) ;;
			ISO-8859-1|ISO-8859-15)
				args+=(latin1) ;;
			UTF-8)
				args+=(utf8) ;;
			*)
				args+=(ascii)
			esac

			# Add macro package if required.
			[[ -n $macro ]] &&
				args+=(-m "$macro")

			# Add argument for italics if turned on.
			(( italic )) &&
				args+=(-P -i)

			# Disable SGR escape sequences if colored output
			# is not allowed.
			(( colors < 8 )) &&
				export GROFF_NO_SGR=1

			squeeze_blanks groff "${args[@]}" -r LL="$COLUMNS"n -r LT="$COLUMNS"n -- "$file" ;;
		mandoc)
			mandoc -O width="$COLUMNS" -T locale -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	text/x-diff)
		command=$(order_commands colordiff)
		case $command in
		colordiff)
			colordiff < "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	text/x-po)
		command=$(order_commands msgcat)
		case $command in
		msgcat)
			if (( colors >= 8 )); then
				args=(--color)
			else
				args=(--color=never)
			fi

			msgcat "${args[@]}" -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	text/x.pod)
		command=$(order_commands pod2text perldoc)
		case $command in
		perldoc)
			perldoc -- "$file" ;;
		pod2text)
			(( colors >= 8 )) && pod2text -c <<< =pod > /dev/null 2>&1 &&
				args=(-c)

			pod2text -w "$COLUMNS" "${args[@]}" -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	text/x.texinfo)
		command=$(order_commands texi2any)
		case $command in
		texi2any)
			texi2any -f "$COLUMNS" --plaintext -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	# Generic types. We put them at the bottom so they will not match over
	# any of the specific types above.
	application/json|application/*+json)
		command=$(order_commands jq)
		case $command in
		jq)
			if (( colors >= 8 )); then
				args=(-C)
			else
				args=(-M)
			fi

			jq "${args[@]}" . -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	application/zip|application/*+zip)
		command=$(order_commands zipinfo unzip bsdtar)
		case $command in
		bsdtar)
			color_archive bsdtar -tv -f "$file" ;;
		unzip)
			unzip -lv -- "$file" ;;
		zipinfo)
			color_archive zipinfo -l -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	audio/*|image/*|video/*)
		if [[ $type = image/* ]]; then
			command=$(order_commands identify mediainfo exiftool)
		else
			command=$(order_commands mediainfo exiftool)
		fi

		case $command in
		exiftool)
			exiftool -- "$file" ;;
		identify)
			identify -verbose -- "$file" ;;
		mediainfo)
			mediainfo -f -- "$file" ;;
		*)
			status=1
		esac || status=2 ;;
	text/*)
		local -- style

		command=$(order_commands highlight source-highlight pygmentize text-vimcolor vimcat batcat bat chroma enscript)
		case $command in
		bat|batcat)
			local -- bat_italic

			if (( italic )); then
				bat_italic=always
			else
				bat_italic=never
			fi

			# Set default theme for ‘bat’ if not specified.
			if [[ -z ${theme[bat]} ]]; then
				if (( colors >= 16777216 )); then
					determine_terminal_background

					if [[ $terminal_background = light ]]; then
						theme[bat]=OneHalfLight
					else
						theme[bat]=default
					fi
				elif (( colors >= 16 )); then
					theme[bat]=base16
				else
					theme[bat]=ansi
				fi
			fi

			"$command" --color always --file-name "$name" --italic-text "$bat_italic" --pager never --style plain --theme "${theme[bat]}" -- "$file" ;;
		chroma)
			if (( colors >= 16777216 )); then
				style=16m
			elif (( colors >= 256 )); then
				style=256
			elif (( colors >= 16 )); then
				style=16
			else
				style=
			fi

			# Set default theme for ‘chroma’ if not specified.
			if [[ -z ${theme[chroma]} ]]; then
				determine_terminal_background

				if [[ $terminal_background = light ]]; then
					theme[chroma]=pygments
				else
					theme[chroma]=swapoff
				fi
			fi

			chroma -f "terminal$style" -s "${theme[chroma]}" -- "$file" ;;
		enscript)
			enscript -E -o - -w ansi --color "${args[@]}" --style "${theme[enscript]:-emacs}" -- "$file" ;;
		highlight)
			if (( colors >= 16777216 )); then
				style=truecolor
			elif (( colors >= 256 )); then
				style=xterm256
			else
				style=ansi
			fi

			# Set default theme for ‘highlight’ if not specified.
			if [[ -z ${theme[highlight]} ]]; then
				determine_terminal_background

				if [[ $terminal_background = light ]]; then
					theme[highlight]=edit-kwrite
				else
					theme[highlight]=edit-vim-dark
				fi
			fi

			highlight -O "$style" -s "${theme[highlight]}" --force "${args[@]}" -- "$file" ;;
		pygmentize)
			if (( colors >= 16777216 )); then
				style=16m
			elif (( colors >= 256 )); then
				style=256
			else
				style=
			fi

			# Set default theme for ‘pygmentize’ if not specified.
			if [[ -z ${theme[pygmentize]} ]]; then
				determine_terminal_background

				if [[ $terminal_background = light ]]; then
					theme[pygmentize]=default
				else
					theme[pygmentize]=rrt
				fi
			fi

			pygmentize -g -f "terminal$style" -O "style=${theme[pygmentize]}" -- "$file" ;;
		source-highlight)
			if (( colors >= 256 )); then
				style=256
			else
				style=
			fi

			source-highlight -f "esc$style" --failsafe --infer-lang --style-file "esc$style.style" -i "$file" ;;
		text-vimcolor)
			text-vimcolor --format ansi -- "$file" ;;
		vimcat)
			vimcat -o - -- "$file" ;;
		*)
			# If still no filter program is found and we are
			# processing an intermediate file in a temporary
			# directory, just print its content.
			if [[ $file = "$working_directory"/* ]]; then
				cat -- "$file"
			else
				status=1
			fi
		esac || status=2 ;;
	# If not any of the supported file types.
	*)
		status=1
	esac

	# Process as a generic file type if no command for specific file type
	# has been found.
	if (( status == 1 )) && [[ $type = !(application/octet-stream|text/plain) ]]; then
		status=0

		if [[ $encoding = binary ]]; then
			run_filter "$file" "$name" application/octet-stream binary
		else
			run_filter "$file" "$name" text/plain "$encoding"
		fi || status=$?
	fi

	return "$status"
}

#
# Quote a string to be passed as an argument to the shell.
#

function shell_quote {
	local -- shell str
	shell=$1
	str=$2

	# Fish shell uses completely different rules of escaping nested
	# single quotes.
	if [[ $shell = fish ]]; then
		str=${str//\\/\\\\}
		str=${str//\'/\\\'}
	else
		str=${str//\'/\'\\\'\'}

		# C shell requires escaping history expansion character
		# everywhere.
		[[ $shell = csh ]] &&
			str=${str//!/\\!}
	fi

	print "'$str'"
}

#
# Print help text.
#

function show_help {
	echo help
	exit 0
}

#
# Print initialization code for shells that sets ‘lesspipe’ as a preprocessor
# to ‘less’.
#

function show_initialization {
	local -- lessopen shell val
	local -a words
	shell=$1

	# If we want to shell to be detected automatically, look into the name
	# of the parent process.
	if [[ $shell = auto ]]; then
		# shellcheck disable=SC2209
		case ${parent_process[0]#-} in
		csh|tcsh) shell=csh ;;
		fish)     shell=fish ;;
		*)        shell=sh
		esac
	fi

	# Set name of the ‘lesspipe’ program as the first word. If program name
	# is a relative path, it needs to be first turned into an absolute one.
	case $program_name in
	[!/]*/*) words=("$PWD/${program_name#./}") ;;
	*)       words=("$program_name")
	esac

	# Append options passed into ‘lesspipe’, with the exception of those
	# that suppress file processing.
	(( debug )) &&
		words+=(--debug)

	for val in "${ignore[@]}"; do
		words+=(--ignore "$val")
	done

	(( italic )) &&
		words+=(--italic)

	for val in "${prefer[@]}"; do
		words+=(--prefer "$val")
	done

	for val in "${!theme[@]}"; do
		words+=(--theme "$val=${theme[$val]}")
	done

	# Since ‘less’ executes value of LESSOPEN as a shell command, quote
	# words that contain a shell special character.
	local -i i
	for i in "${!words[@]}"; do
		[[ ${words[i]} = *[![:word:],.:/@=+-]* ]] &&
			words[i]=$(shell_quote sh "${words[i]}")
	done

	# Determine value of the desired LESSOPEN environment variable
	# and quote it correctly depending on the shell.
	lessopen=$(shell_quote "$shell" "||${words[*]} -- %s")

	# Print code of the function/alias that invokes ‘less’ with
	# the correct LESSOPEN variable, depending on the shell.
	case $shell in
	csh)  # Since C shell has no functions but only aliases, we
	      # unfortunately have to quote everything twice.
	      print "alias less $(shell_quote csh "(setenv LESSOPEN $lessopen; \\less !*)")" ;;
	fish) print "function less; set -lx -- LESSOPEN $lessopen; command less \$argv; end" ;;
	sh)   print "less() { LESSOPEN=$lessopen command less \"\$@\"; }"
	esac

	exit 0
}

#
# Print version text.
#

function show_version {
	print 'lesspipe 1.0'; print
	print 'Copyright (C) 2022 Paulina Laura Emilia'
	print 'License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.'
	exit 0
}

#
# Squeeze blank lines in the command output if less option ‘-s’ is not set.
#

function squeeze_blanks {
	if (( !less_options[s] )); then
		local -- line
		local -i blank

		"$@" | while IFS= read -r line; do
			if [[ -n $line ]]; then
				blank=0
			else
				(( blank )) &&
					continue
				blank=1
			fi

			print "$line"
		done
		return "${PIPESTATUS[0]}"
	else
		"$@"
	fi
}

#
# Create a temporary file or a directory in script’s general temporary
# directory.
#

function temporary_directory {
	mktemp -d -- "$working_directory/XXXXXXXXXX"
}

function temporary_file {
	mktemp -- "$working_directory/XXXXXXXXXX"
}

#
# Validates argument with a set of possible values.
#

function validate_argument {
	local -- argument option
	local -a matches synonyms
	local -i count
	option=$1
	argument=$2
	shift 2

	# Check if argument matches any of the valid values.
	local v
	for v; do
		IFS=/ read -r -a synonyms <<< "$v/"

		# Handle synonyms.
		local s
		for s in "${synonyms[@]}"; do
			# Prefer exact matches.
			if [[ $s = "$argument" ]]; then
				matches=("$v")
				break 2
			elif [[ $s = "$argument"* ]]; then
				matches+=("$v")
				break
			fi
		done
	done

	# Further action depends on a number of matches.
	count=${#matches[@]}
	if (( count == 1 )); then
		# Succeed if there is exactly one match.
		print "${matches[0]%%/*}"
	else
		# Fails if there are no matches (invalid argument) or
		# multiple matches (ambiguous argument).
		if (( !count )); then
			message "invalid argument '%s' for '%s'" "$argument" "$option"
		else
			message "ambiguous argument '%s' for '%s'" "$argument" "$option"
		fi

		# Print list of valid arguments.
		message_plain 'Valid arguments are:'
		for v; do
			# Print synonyms on one line.
			IFS=/ read -r -a synonyms <<< "$v/"

			local -i i
			for i in "${!synonyms[@]}"; do
				if (( i == 0 )); then
					printf '  - ' >&2
				else
					printf ', ' >&2
				fi
				printf "'%s'" "${synonyms[i]}" >&2
			done
			printf \\n >&2
		done

		return 1
	fi
}

#
# Securely create or destroy a temporary working directory.
#

function working_directory_create {
	working_directory=$(mktemp -d -- "${TMPDIR:-/tmp}"/lesspipe.XXXXXXXXXX) ||
		return 1

	[[ -z $working_directory || ! -d $working_directory ]] &&
		return 1

	# Set read-only attribute to prevent us from accidentally changing
	# the working directory variable to an invalid path or path containing
	# preexisting files.
	readonly working_directory

	# Remove the working directory when the script exits.
	trap working_directory_destroy EXIT
}

function working_directory_destroy {
	# While this should not be neccessary, first check whether the working
	# directory variable contains a valid path to the temporary directory
	# created previously by us to prevent accidental deletion of files
	# that should not have been deleted.
	[[ $working_directory = "${TMPDIR:-/tmp}"/lesspipe.?????????? && -d $working_directory ]] &&
		rm -rf -- "$working_directory"
}

parse_arguments "$@"
