#!/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-2025 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 . ############################################################################### 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 <