| #!/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 |
| } |
| |