blob: f0d3ad3efc489ad93d80e22aef78a7869bea1fe5 [file] [log] [blame] [edit]
###############################################################################
# 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>.
###############################################################################
set initialDirectory [pwd]
set scriptDirectory [file normalize [file dirname $::argv0]]
set prologueDirectory [file normalize [file dirname [info script]]]
try {
package require Tclx
} trap {TCL PACKAGE UNFOUND} {} {
proc intersect3 {list1 list2} {
foreach number {1 2} {
set length$number [llength [set list$number [lsort [set list$number]]]]
set index$number 0
set only$number [list]
}
set both [list]
while {1} {
if {$index1 == $length1} {
if {$index2 < $length2} {
lvarcat only2 [lrange $list2 $index2 end]
}
break
}
if {$index2 == $length2} {
lvarcat only1 [lrange $list1 $index1 end]
break
}
switch -exact -- [string compare [lindex $list1 $index1] [lindex $list2 $index2]] {
-1 {
lappend only1 [lindex $list1 $index1]
incr index1
}
1 {
lappend only2 [lindex $list2 $index2]
incr index2
}
0 {
lappend both [lindex $list1 $index1]
incr index1
incr index2
}
}
}
return [list $only1 $both $only2]
}
proc lcontain {list element} {
return [expr {[lsearch -exact $list $element] >= 0}]
}
proc lempty {list} {
return [expr {[llength $list] == 0}]
}
proc lrmdups {list} {
return [lsort -unique $list]
}
proc lvarcat {listVariable first args} {
upvar 1 $listVariable list
eval lappend list $first
foreach arg $args {
eval lappend list $arg
}
return $list
}
proc lvarpop {listVariable {index 0}} {
upvar 1 $listVariable list
set result [lindex $list $index]
set list [lreplace $list $index $index]
return $result
}
proc lvarpush {listVariable string {index 0}} {
upvar 1 $listVariable list
set list [linsert $list $index $string]
return ""
}
if {[package vcompare $tcl_version 8.4] < 0} {
proc readdir {directory} {
set workingDirectory [pwd]
cd $directory
set names [glob -nocomplain *]
cd $workingDirectory
return $names
}
} else {
proc readdir {directory} {
return [glob -directory $directory -tails -nocomplain *]
}
}
}
proc makeEnumeration {arrayVariable elements} {
upvar 1 $arrayVariable array
set value 0
foreach element $elements {
set array($element) $value
incr value
}
}
proc getProgramName {} {
return [file tail [info script]]
}
proc writeProgramMessage {message} {
if {[string length $message] > 0} {
set stream stderr
puts $stream "[getProgramName]: $message"
flush $stream
}
}
makeEnumeration logLevels {error warning notice task note detail}
set logLevel $logLevels(task)
proc logMessage {level message} {
global logLevels logLevel
if {$logLevels($level) <= $logLevel} {
writeProgramMessage $message
}
}
proc logError {{message ""}} {
logMessage error $message
}
proc logWarning {{message ""}} {
logMessage warning $message
}
proc logNote {{message ""}} {
logMessage note $message
}
proc logDetail {{message ""}} {
logMessage detail $message
}
proc syntaxError {{message ""}} {
logError $message
exit 2
}
proc semanticError {{message ""}} {
logError $message
exit 3
}
proc toRelativePath {to {from .}} {
set variables {from to}
foreach variable $variables {
set $variable [file split [file normalize [set $variable]]]
}
set count [expr {min([llength $from], [llength $to])}]
while {$count > 0} {
if {![string equal [lindex $from 0] [lindex $to 0]]} {
break
}
foreach variable $variables {
lvarpop $variable
}
incr count -1
}
if {[llength [set to [concat [lrepeat [llength $from] ..] $to]]] == 0} {
set to {.}
}
return [eval file join $to]
}
proc testContainingDirectory {directory names} {
foreach name $names {
if {![file exists [file join $directory $name]]} {
return 0
}
}
return 1
}
proc findContainingDirectory {variable directory names} {
if {[info exists $variable]} {
if {[string length [set $variable]] > 0} {
return 1
}
}
set directory [file normalize $directory]
while {1} {
if {[testContainingDirectory $directory $names]} {
break
}
if {[string equal [set parent [file dirname $directory]] $directory]} {
return 0
}
set directory $parent
}
set $variable $directory
return 1
}
proc withChannel {variable channel body} {
uplevel 1 [list set $variable $channel]
try {
try {
uplevel 1 $body
} on return {result} {
return -level 2 $result
}
} finally {
close $channel
uplevel 1 [list unset $variable]
}
}
proc readFile {file} {
withChannel channel [open $file {RDONLY}] {
return [read $channel]
}
}
proc writeFile {file data} {
withChannel channel [open $file {WRONLY CREAT TRUNC}] {
puts -nonewline $channel $data
}
}
proc replaceFile {file data} {
set components [file split $file]
if {[llength $components] == 0} {
return -code error "file argument is empty"
}
set name ".[lindex $components end]"
set path "[eval file join [lreplace $components end end] $name]"
set oldFile "$path.old"
set newFile "$path.new"
file delete $newFile
writeFile $newFile $data
if {[file exists $file]} {
file delete $oldFile
file rename $file $oldFile
}
file rename $newFile $file
file delete $oldFile
}
proc updateFile {file data {inTestMode 0}} {
if {![file exists $file]} {
if {$inTestMode} {
logMessage notice "test mode - file not created: $file"
} else {
logMessage notice "creating file: $file"
writeFile $file $data
}
} elseif {![string equal $data [readFile $file]]} {
if {$inTestMode} {
logMessage notice "test mode - file not updated: $file"
} else {
logMessage notice "updating file: $file"
replaceFile $file $data
}
} else {
return 0
}
return 1
}
proc forEachLine {lineVariable file body} {
upvar 1 $lineVariable line
try {
withChannel channel [open $file {RDONLY}] {
while {[gets $channel line] >= 0} {
uplevel 1 $body
}
}
} on return {result} {
return -level 2 $result
}
}
proc readLines {file} {
set lines [list]
forEachLine line $file {
lappend lines $line
}
return $lines
}
proc isWhitespace {string} {
return [string is space $string]
}
proc wrapLine {line width} {
set lines [list]
while {[set length [string length [set line [string trimleft $line]]]] > 0} {
if {$width < $length} {
set index $width
while {$index >= 0} {
if {[isWhitespace [string index $line $index]]} {
break
}
incr index -1
}
if {$index < 0} {
set index $width
while {[incr index] < $length} {
if {[isWhitespace [string index $line $index]]} {
break
}
}
}
} else {
set index $length
}
lappend lines [string trimright [string range $line 0 $index-1]]
set line [string range $line $index end]
}
return $lines
}
proc formatLines {lines {width 79}} {
set result [list]
set paragraph ""
set finishParagraph {
if {[string length $paragraph] > 0} {
lvarcat result [wrapLine $paragraph $width]
set paragraph ""
}
}
foreach line $lines {
if {[set length [string length [string trimright $line]]] > 0} {
if {![string equal [string index $line 0] " "]} {
if {[string length $paragraph] > 0} {
append paragraph " "
}
append paragraph $line
continue
}
}
eval $finishParagraph
lappend result $line
}
eval $finishParagraph
return [join $result \n]
}
proc formatColumns {rows} {
set spacer [string repeat " " 2]
set lines [list]
foreach row $rows {
set index 0
foreach cell $row {
if {![info exists widths($index)]} {
set widths($index) 0
}
set widths($index) [max $widths($index) [string length $cell]]
incr index
}
}
foreach row $rows {
set line ""
set index 0
foreach cell $row {
if {[set width $widths($index)] > 0} {
append line $cell
append line [string repeat " " [expr {$width - [string length $cell]}]]
append line $spacer
}
incr index
}
if {[string length [set line [string trimright $line]]] > 0} {
if {[string length $lines] > 0} {
append lines "\n"
}
append lines $line
}
}
return $lines
}
proc formatChoicesPhrase {choices} {
set separator " "
if {[set count [llength $choices]] > 2} {
set separator ",$separator"
}
if {$count > 1} {
lappend choices "or [lvarpop choices end]"
}
return [join $choices $separator]
}
proc nextElement {listVariable {elementVariable ""}} {
upvar 1 $listVariable list
if {[llength $list] == 0} {
return 0
}
if {[string length $elementVariable] > 0} {
uplevel 1 [list set $elementVariable [lindex $list 0]]
set list [lreplace $list 0 0]
}
return 1
}
proc processCommandOptions {valuesArray argumentsVariable definitions {optionsVariable ""}} {
upvar 1 $valuesArray values
upvar 1 $argumentsVariable arguments
set prefix -
set options [dict create]
set optionIndex 0
foreach definition $definitions {
set description "option\[$optionIndex\]"
if {![nextElement definition name]} {
return -code error "name not specified for $description"
}
if {[dict exists $options $name]} {
return -code error "duplicate name for $description: $name"
}
if {![nextElement definition type]} {
return -code error "type not specified for $description"
}
if {[lcontain {counter flag toggle} $type]} {
set values($name) 0
} else {
if {[set index [string first . $type]] < 0} {
set operand $type
} else {
set operand [string range $type $index+1 end]
set type [string range $type 0 $index-1]
}
if {![string equal $type untyped]} {
if {[catch [list string is $type ""]] != 0} {
return -code error "invalid type for $description: $type"
}
}
dict set options $name operand $operand
}
dict set options $name type $type
if {[nextElement definition usage]} {
dict set options $name usage $usage
}
incr optionIndex
}
if {[string length $optionsVariable] > 0} {
uplevel 1 [list set $optionsVariable $options]
}
while {[llength $arguments] > 0} {
if {[string length [set argument [lindex $arguments 0]]] == 0} {
break
}
if {![string equal [string index $argument 0] $prefix]} {
break
}
set arguments [lreplace $arguments 0 0]
if {[string equal $argument $prefix]} {
break
}
if {[string equal [set name [string range $argument 1 end]] $prefix]} {
break
}
if {[set count [dict size [set subset [dict filter $options key $name*]]]] == 0} {
logError "unknown option: $prefix$name"
return 0
}
if {$count > 1} {
logError "ambiguous option: $prefix$name ([join [lsort [dict keys $subset]] ", "])"
return 0
}
set name [lindex [dict keys $subset] 0]
set option [dict get $subset $name]
switch -exact [set type [dict get $option type]] {
counter {
set value [expr {$values($name) + 1}]
}
flag {
set value 1
}
toggle {
set value [expr {!$values($name)}]
}
default {
if {[llength $arguments] == 0} {
logError "missing operand: $prefix$name"
return 0
}
set value [lindex $arguments 0]
set arguments [lreplace $arguments 0 0]
if {![string equal $type untyped]} {
if {[catch [list string is $type -strict $value] result] != 0} {
return -code error "unimplemented option type: $type"
}
if {!$result} {
logError "operand not $type: $prefix$name $value"
return 0
}
}
}
}
set values($name) $value
}
return 1
}
proc formatCommandOptionsUsage {options} {
set rows [list]
foreach name [lsort [dict keys $options]] {
set option [dict get $options $name]
set row [list]
lappend row "-$name"
foreach property {operand usage} {
if {[dict exists $option $property]} {
lappend row [dict get $option $property]
} else {
lappend row ""
}
}
lappend rows $row
}
return [formatColumns $rows]
}
proc showCommandUsage {name optionsDescriptor argumentsUsage getArgumentsUsageSummary} {
set optionsUsage [formatCommandOptionsUsage $optionsDescriptor]
set usage "Syntax: $name"
if {[string length $optionsUsage] > 0} {
append usage " \[-option ...\]"
}
if {[string length $argumentsUsage] > 0} {
append usage " $argumentsUsage"
}
if {[string length $getArgumentsUsageSummary] > 0} {
if {[string length [set lines [formatLines [$getArgumentsUsageSummary]]]] > 0} {
append usage \n
append usage $lines
}
}
if {[string length $optionsUsage] > 0} {
append usage \n
append usage "The following options may be specified:\n$optionsUsage"
}
puts stdout $usage
}
proc noMorePositionalArguments {arguments} {
if {[nextElement arguments]} {
syntaxError "excess positional arguments: [join $arguments " "]"
}
}
proc processProgramArguments {
optionValuesArray optionDefinitions
{argumentValuesList ""}
{argumentsUsage ""}
{getArgumentsUsageSummary ""}
} {
upvar 1 $optionValuesArray optionValues
set argumentValues $::argv
lappend optionDefinitions {help flag "show this usage summary and then exit"}
lappend optionDefinitions {quiet counter "decrease verbosity"}
lappend optionDefinitions {verbose counter "increase verbosity"}
if {![processCommandOptions optionValues argumentValues $optionDefinitions optionsDescriptor]} {
syntaxError
}
global logLevel
incr logLevel $optionValues(verbose)
incr logLevel -$optionValues(quiet)
if {$optionValues(help)} {
showCommandUsage [getProgramName] $optionsDescriptor $argumentsUsage $getArgumentsUsageSummary
exit 0
}
if {[string length $argumentValuesList] > 0} {
uplevel 1 [list set $argumentValuesList $argumentValues]
} else {
noMorePositionalArguments $argumentValues
}
}