blob: 1fd0b7a8b5fcc252e3b3cb3313b0b01f06763d2b [file] [log] [blame] [edit]
#!/bin/sh
###############################################################################
# BRLTTY - A background process providing access to the console screen (when in
# text mode) for a blind person using a refreshable braille display.
#
# Copyright (C) 1995-2023 by The BRLTTY Developers.
#
# BRLTTY comes with ABSOLUTELY NO WARRANTY.
#
# This is free software, placed under the terms of the
# GNU Lesser General Public License, as published by the Free Software
# Foundation; either version 2.1 of the License, or (at your option) any
# later version. Please see the file LICENSE-LGPL for details.
#
# Web Page: http://brltty.app/
#
# This software is maintained by Dave Mielke <dave@mielke.cc>.
###############################################################################
testMode=false
readonly initialDirectory="$(pwd)"
readonly programName="$(basename "${0}")"
programMessage() {
local message="${1}"
[ -z "${message}" ] || echo >&2 "${programName}: ${message}"
}
setVariable() {
eval "${1}"'="${2}"'
}
getVariable() {
if [ -n "${2}" ]
then
eval "${2}"'="${'"${1}"'}"'
else
eval 'echo "${'"${1}"'}"'
fi
}
defineEnumeration() {
local prefix="${1}"
shift 1
local name
local value=1
for name
do
local variable="${prefix}${name}"
readonly "${variable}"="${value}"
value=$((value + 1))
done
}
defineEnumeration programLogLevel_ error warning notice task note detail
programLogLevel=$((${programLogLevel_task}))
logMessage() {
local level="${1}"
local message="${2}"
local variable="programLogLevel_${level}"
local value=$((${variable}))
[ "${value}" -gt 0 ] || programMessage "unknown log level: ${level}"
[ "${value}" -gt "${programLogLevel}" ] || programMessage "${message}"
}
logError() {
logMessage error "${@}"
}
logWarning() {
logMessage warning "${@}"
}
logNotice() {
logMessage notice "${@}"
}
logTask() {
logMessage task "${@}"
}
logNote() {
logMessage note "${@}"
}
logDetail() {
logMessage detail "${@}"
}
programTerminationCommandCount=0
runProgramTerminationCommands() {
set +e
while [ "${programTerminationCommandCount}" -gt 0 ]
do
set -- $(getVariable "programTerminationCommand${programTerminationCommandCount}")
programTerminationCommandCount=$((programTerminationCommandCount - 1))
local process="${1}"
local directory="${2}"
shift 2
[ "${process}" = "${$}" ] && {
cd "${directory}"
"${@}"
}
done
}
pushProgramTerminationCommand() {
[ "${programTerminationCommandCount}" -gt 0 ] || trap runProgramTerminationCommands exit
setVariable "programTerminationCommand$((programTerminationCommandCount += 1))" "${$} $(pwd) ${*}"
}
needTemporaryDirectory() {
local variable="${1:-temporaryDirectory}"
local _directory
getVariable "${variable}" _directory
[ -n "${_directory}" ] || {
[ -n "${TMPDIR}" -a -d "${TMPDIR}" -a -r "${TMPDIR}" -a -w "${TMPDIR}" -a -x "${TMPDIR}" ] || export TMPDIR="/tmp"
_directory="$(mktemp -d "${TMPDIR}/${programName}.$(date +"%Y%m%d-%H%M%S").XXXXXX")"
pushProgramTerminationCommand rm -f -r -- "${_directory}"
cd "${_directory}"
setVariable "${variable}" "${_directory}"
}
}
resolveDirectory() {
local path="${1}"
local variable="${2}"
local absolute="$(cd "${path}" && pwd)"
if [ -n "${variable}" ]
then
setVariable "${variable}" "${absolute}"
else
echo "${absolute}"
fi
}
programDirectory="$(dirname "${0}")"
readonly programDirectory="$(resolveDirectory "${programDirectory}")"
parseParameterString() {
local valuesArray="${1}"
local parameters="${2}"
local code="${3}"
set -- ${parameters//,/ }
local parameter
for parameter
do
local name="${parameter%%=*}"
[ "${name}" = "${parameter}" ] && continue
[ -n "${name}" ] || continue
local value="${parameter#*=}"
local qualifier="${name%%:*}"
[ "${qualifier}" = "${name}" ] || {
[ -n "${qualifier}" ] || continue
[ "${qualifier}" = "${code}" ] || continue
name="${name#*:}"
}
setVariable "${valuesArray}[${name^^*}]" "${value}"
done
}
stringHead() {
local string="${1}"
local length="${2}"
[ "${length}" -eq 0 ] || expr substr "${string}" 1 "${length}"
}
stringTail() {
local string="${1}"
local start="${2}"
local length=$((${#string} - start))
[ "${length}" -eq 0 ] || expr substr "${string}" $((start + 1)) "${length}"
}
stringReplace() {
local string="${1}"
local pattern="${2}"
local replacement="${3}"
local flags="${4}"
echo "${string}" | sed -e "s/${pattern}/${replacement}/${flags}"
}
stringReplaceAll() {
local string="${1}"
local pattern="${2}"
local replacement="${3}"
stringReplace "${string}" "${pattern}" "${replacement}" "g"
}
stringQuoted() {
local string="${1}"
local pattern="'"
local replacement="'"'"'"'"'"'"'"
string="$(stringReplaceAll "${string}" "${pattern}" "${replacement}")"
echo "'${string}'"
}
stringWrapped() {
local string="${1}"
local width="${2}"
local result=""
local paragraph=""
while true
do
local length="$(expr "${string}" : $'[^\n]*\n')"
local line
if [ "${length}" -eq 0 ]
then
line="${string}"
string=""
else
line="$(stringHead "${string}" $((length - 1)))"
string="$(stringTail "${string}" "${length}")"
fi
[ -z "${line}" ] || [ "${line}" != "${line# }" ] || {
[ -z "${paragraph}" ] || paragraph="${paragraph} "
paragraph="${paragraph}${line}"
continue
}
while [ "${#paragraph}" -gt "${width}" ]
do
local head="$(stringHead "${paragraph}" $((width + 1)))"
head="${head% *}"
[ "${#head}" -le "${width}" ] || {
head="${paragraph%% *}"
[ "${head}" != "${paragraph}" ] || break
}
result="${result} $(stringQuoted "${head}")"
paragraph="$(stringTail "${paragraph}" $((${#head} + 1)))"
done
[ -z "${paragraph}" ] || {
result="${result} $(stringQuoted "${paragraph}")"
paragraph=""
}
[ -n "${string}" ] || {
[ -z "${line}" ] || result="${result} $(stringQuoted "${line}")"
break
}
result="${result} $(stringQuoted "${line}")"
done
echo "${result}"
}
syntaxError() {
local message="${1}"
logError "${message}"
exit 2
}
semanticError() {
local message="${1}"
logError "${message}"
exit 3
}
internalError() {
local message="${1}"
logError "${message}"
exit 4
}
findSiblingCommand() {
local resultVariable="${1}"
shift 1
local command
for command in "${@}"
do
local path="${programDirectory}/${command}"
[ -f "${path}" ] || continue
[ -x "${path}" ] || continue
setVariable "${resultVariable}" "${path}"
return 0
done
return 1
}
findHostCommand() {
local pathVariable="${1}"
local command="${2}"
local path="$(which "${command}")"
[ -n "${path}" ] || return 1
setVariable "${pathVariable}" "${path}"
return 0
}
verifyHostCommand() {
local pathVariable="${1}"
local command="${2}"
findHostCommand "${pathVariable}" "${command}" || {
semanticError "host command not found: ${command}"
}
}
executeHostCommand() {
logDetail "executing host command: ${*}"
"${testMode}" || "${@}" || {
local status="${?}"
logWarning "host command failed with exit status ${status}: ${*}"
return "${status}"
}
}
verifyActionFlags() {
local allFlag="${1}"
shift 1
local allRequested
getVariable "${allFlag}" allRequested
local actionFlag
for actionFlag in "${@}"
do
local actionRequested
getVariable "${actionFlag}" actionRequested
"${actionRequested}" && {
"${allRequested}" && syntaxError "conflicting actions"
return
}
done
"${allRequested}" || syntaxError "no actions"
for actionFlag in "${@}"
do
setVariable "${actionFlag}" true
done
}
testInteger() {
local value="${1}"
[ "${value}" = "0" ] || {
value="${value#-}"
[ -n "${value}" ] || return 1
[ "$(expr "${value}" : '^[1-9][0-9]*$')" -eq "${#value}" ] || return 1
}
return 0
}
verifyInteger() {
local label="${1}"
local value="${2}"
local minimum="${3}"
local maximum="${4}"
testInteger "${value}" || {
semanticError "${label} not an integer: ${value}"
}
[ -n "${minimum}" ] && {
[ "${value}" -lt "${minimum}" ] && {
semanticError "${label} out of range: ${value} < ${minimum}"
}
}
[ -n "${maximum}" ] && {
[ "${value}" -gt "${maximum}" ] && {
semanticError "${label} out of range: ${value} > ${maximum}"
}
}
}
testContainingDirectory() {
local directory="${1}"
shift 1
local path
for path
do
[ -e "${directory}/${path}" ] || return 1
done
return 0
}
findContainingDirectory() {
local variable="${1}"
local directory="${2}"
shift 2
local value
getVariable "${variable}" value
[ -n "${value}" ] && return 0
while :
do
testContainingDirectory "${directory}" "${@}" && break
local parent="$(dirname "${directory}")"
[ "${parent}" = "${directory}" ] && return 1
directory="${parent}"
done
export "${variable}"="${directory}"
}
testDirectory() {
local path="${1}"
[ -e "${path}" ] || return 1
[ -d "${path}" ] || semanticError "not a directory: ${path}"
return 0
}
verifyWritableDirectory() {
local path="${1}"
testDirectory "${path}" || semanticError "directory not found: ${path}"
[ -w "${path}" ] || semanticError "directory not writable: ${path}"
}
testFile() {
local path="${1}"
[ -e "${path}" ] || return 1
[ -f "${path}" ] || semanticError "not a file: ${path}"
return 0
}
verifyInputFile() {
local path="${1}"
testFile "${path}" || semanticError "file not found: ${path}"
[ -r "${path}" ] || semanticError "file not readable: ${path}"
}
verifyOutputFile() {
local path="${1}"
if testFile "${path}"
then
[ -w "${path}" ] || semanticError "file not writable: ${path}"
else
verifyWritableDirectory "$(dirname "${path}")"
fi
}
verifyExecutableFile() {
local path="${1}"
testFile "${path}" || semanticError "file not found: ${path}"
[ -x "${path}" ] || semanticError "file not executable: ${path}"
}
verifyInputDirectory() {
local path="${1}"
testDirectory "${path}" || semanticError "directory not found: ${path}"
}
verifyOutputDirectory() {
local path="${1}"
if testDirectory "${path}"
then
[ -w "${path}" ] || semanticError "directory not writable: ${path}"
rm -f -r -- "${path}/"*
else
mkdir -p "${path}"
fi
}
programParameterCount=0
programParameterCountMinimum=-1
programParameterLabelWidth=0
addProgramParameter() {
local label="${1}"
local variable="${2}"
local usage="${3}"
local default="${4}"
setVariable "programParameterLabel_${programParameterCount}" "${label}"
setVariable "programParameterVariable_${programParameterCount}" "${variable}"
setVariable "programParameterUsage_${programParameterCount}" "${usage}"
setVariable "programParameterDefault_${programParameterCount}" "${default}"
local length="${#label}"
[ "${length}" -le "${programParameterLabelWidth}" ] || programParameterLabelWidth="${length}"
setVariable "${variable}" ""
programParameterCount=$((programParameterCount + 1))
}
optionalProgramParameters() {
if [ "${programParameterCountMinimum}" -lt 0 ]
then
programParameterCountMinimum="${programParameterCount}"
optionalProgramParameterLabel="${1}"
optionalProgramParameterUsage="${2}"
else
logWarning "program parameters are already optional"
fi
}
tooManyProgramParameters() {
syntaxError "too many parameters"
}
programOptionLetters=""
programOptionString=""
programOptionOperandWidth=0
programOptionValue_counter=0
programOptionValue_flag=false
programOptionValue_list=""
programOptionValue_string=""
addProgramOption() {
local letter="${1}"
local type="${2}"
local variable="${3}"
local usage="${4}"
local default="${5}"
[ "$(expr "${letter}" : '[[:alnum:]]*$')" -eq 1 ] || internalError "invalid program option: -${letter}"
[ -z "$(getVariable "programOptionType_${letter}")" ] || internalError "duplicate program option definition: -${letter}"
local operand
case "${type}"
in
flag | counter)
operand=""
;;
string.* | list.*)
operand="${type#*.}"
type="${type%%.*}"
[ -n "${operand}" ] || internalError "missing program option operand type: -${letter}"
;;
*) internalError "invalid program option type: ${type} (-${letter})";;
esac
setVariable "programOptionType_${letter}" "${type}"
setVariable "programOptionVariable_${letter}" "${variable}"
setVariable "programOptionOperand_${letter}" "${operand}"
setVariable "programOptionUsage_${letter}" "${usage}"
setVariable "programOptionDefault_${letter}" "${default}"
local value="$(getVariable "programOptionValue_${type}")"
setVariable "${variable}" "${value}"
local length="${#operand}"
[ "${length}" -le "${programOptionOperandWidth}" ] || programOptionOperandWidth="${length}"
programOptionLetters="${programOptionLetters} ${letter}"
programOptionString="${programOptionString}${letter}"
[ "${length}" -eq 0 ] || programOptionString="${programOptionString}:"
}
addTestModeOption() {
addProgramOption t flag testMode "test mode - log (but don't execute) the host commands"
}
programUsageLineCount=0
programUsageLineWidth="${COLUMNS:-72}"
addProgramUsageLine() {
local line="${1}"
setVariable "programUsageLine_${programUsageLineCount}" "${line}"
programUsageLineCount=$((programUsageLineCount + 1))
}
addProgramUsageText() {
local text="${1}"
local prefix="${2}"
local width=$((programUsageLineWidth - ${#prefix}))
while [ "${width}" -lt 1 ]
do
[ "${prefix% }" != "${prefix}" ] || break
prefix="${prefix%?}"
width=$((width + 1))
done
local indent="$(stringReplaceAll "${prefix}" '.' ' ')"
[ "${width}" -gt 0 ] || {
addProgramUsageLine "${prefix}"
indent="$(stringTail "${indent}" $((-width + 1)))"
prefix="${indent}"
width=1
}
eval set -- "$(stringWrapped "${text}" "${width}")"
for line
do
addProgramUsageLine "${prefix}${line}"
prefix="${indent}"
done
}
writeProgramUsageLines() {
local index=0
while [ "${index}" -lt "${programUsageLineCount}" ]
do
getVariable "programUsageLine_${index}"
index=$((index + 1))
done
}
showProgramUsageSummary() {
set -- ${programOptionLetters}
local purpose="$(showProgramUsagePurpose)"
[ -z "${purpose}" ] || {
addProgramUsageText "${purpose}"
addProgramUsageLine
}
local line="Syntax: ${programName}"
[ "${#}" -eq 0 ] || line="${line} [-option ...]"
local index=0
local suffix=""
while [ "${index}" -lt "${programParameterCount}" ]
do
line="${line} "
[ "${index}" -lt "${programParameterCountMinimum}" ] || {
line="${line}["
suffix="${suffix}]"
}
line="${line}$(getVariable "programParameterLabel_${index}")"
index=$((index + 1))
done
[ -z "${optionalProgramParameterLabel}" ] || {
line="${line} [${optionalProgramParameterLabel} ...]"
[ -z "${optionalProgramParameterUsage}" ] || {
addProgramParameter "${optionalProgramParameterLabel} ..." optionalProgramParameterVariable "${optionalProgramParameterUsage}"
}
}
line="${line}${suffix}"
addProgramUsageLine "${line}"
[ "${programParameterCount}" -eq 0 ] || {
addProgramUsageLine
addProgramUsageLine "Parameters:"
local indent=$((programParameterLabelWidth + 2))
local index=0
while [ "${index}" -lt "${programParameterCount}" ]
do
local line="$(getVariable "programParameterLabel_${index}")"
while [ "${#line}" -lt "${indent}" ]
do
line="${line} "
done
local usage="$(getVariable "programParameterUsage_${index}")"
local default="$(getVariable "programParameterDefault_${index}")"
[ -z "${default}" ] || usage="${usage} - the default is ${default}"
addProgramUsageText "${usage}" " ${line}"
index=$((index + 1))
done
}
[ "${#}" -eq 0 ] || {
addProgramUsageLine
addProgramUsageLine "Options:"
local indent=$((3 + programOptionOperandWidth + 2))
local letter
for letter
do
local line="-${letter} $(getVariable "programOptionOperand_${letter}")"
while [ "${#line}" -lt "${indent}" ]
do
line="${line} "
done
usage="$(getVariable "programOptionUsage_${letter}")"
local default="$(getVariable "programOptionDefault_${letter}")"
[ -z "${default}" ] || usage="${usage} - the default is ${default}"
addProgramUsageText "${usage}" " ${line}"
done
}
local notes="$(showProgramUsageNotes)"
[ -z "${notes}" ] || {
addProgramUsageLine
addProgramUsageText "${notes}"
}
writeProgramUsageLines
}
addProgramOption h flag programOption_showUsageSummary "show this usage summary, and then exit"
addProgramOption q counter programOption_quietCount "decrease output verbosity"
addProgramOption v counter programOption_verboseCount "increase output verbosity"
parseProgramOptions() {
local letter
while getopts ":${programOptionString}" letter
do
case "${letter}"
in
\?) syntaxError "unrecognized option: -${OPTARG}";;
:) syntaxError "missing operand: -${OPTARG}";;
*) local variable type
setVariable variable "$(getVariable "programOptionVariable_${letter}")"
setVariable type "$(getVariable "programOptionType_${letter}")"
case "${type}"
in
counter) setVariable "${variable}" $((${variable} + 1));;
flag) setVariable "${variable}" true;;
list) setVariable "${variable}" "$(getVariable "${variable}") $(stringQuoted "${OPTARG}")";;
string) setVariable "${variable}" "${OPTARG}";;
*) internalError "unimplemented program option type: ${type} (-${letter})";;
esac
;;
esac
done
}
parseProgramArguments() {
[ "${programParameterCountMinimum}" -ge 0 ] || programParameterCountMinimum="${programParameterCount}"
parseProgramOptions "${@}"
shift $((OPTIND - 1))
if "${programOption_showUsageSummary}"
then
showProgramUsageSummary
exit 0
fi
local programParameterIndex=0
while [ "${#}" -gt 0 ]
do
[ "${programParameterIndex}" -lt "${programParameterCount}" ] || break
setVariable "$(getVariable "programParameterVariable_${programParameterIndex}")" "${1}"
shift 1
programParameterIndex=$((programParameterIndex + 1))
done
[ "${programParameterIndex}" -ge "${programParameterCountMinimum}" ] || {
syntaxError "$(getVariable "programParameterLabel_${programParameterIndex}") not specified"
}
readonly programLogLevel=$((programLogLevel + programOption_verboseCount - programOption_quietCount))
processExtraProgramParameters "${@}"
}
handleInitialHelpOption() {
if [ "${#}" -gt 0 ]
then
if [ "${1}" = "-h" ]
then
addProgramUsageText "$(cat)"
writeProgramUsageLines
exit 0
fi
fi
}
####################################################################
# The following functions are stubs that may be copied into the #
# main script and augmented. They need to be defined after this #
# prologue is embeded and before the program arguments are parsed. #
####################################################################
showProgramUsagePurpose() {
cat <<END_OF_PROGRAM_USAGE_PURPOSE
END_OF_PROGRAM_USAGE_PURPOSE
}
showProgramUsageNotes() {
cat <<END_OF_PROGRAM_USAGE_NOTES
END_OF_PROGRAM_USAGE_NOTES
}
processExtraProgramParameters() {
[ "${#}" -eq 0 ] || tooManyProgramParameters
}