ussg-page (22653B)
1 #!/usr/bin/env tclsh8.6 2 3 chan configure stderr -buffering line 4 5 # proc until {test body} {uplevel 1 [list while [concat ! $test] $body]} 6 7 proc opts {{consume {h v H: M: help version headers: markdown:}} argl} { 8 # will always output a list n=2. 9 # [lindex [opts ...] 0] = a list of lists: option, argument(s) 10 # [lindex [opts ...] 1] = a list of arguments not consumed 11 # loptpile contains the long options followed by the number of arguments they consume 12 # a : not immediately preceded by a backslash means that consumed++ 13 # two backslashes collapse to one. 14 # soptpile contains the options which are one letter (one may write -HM header markdown) 15 set loptpile [list] 16 set soptpile [list] 17 foreach {option} $consume { 18 set optname "" 19 set colons 0 20 set prevl "" 21 foreach {l} [split $option {}] { 22 if {$l == ":" && $prevl != "\\"} {incr colons} 23 if {($l != "\\" || $prevl == "\\") && $colons < 1} { 24 append optname $l 25 } 26 if {$prevl != "\\"} {set prevl $l} {set prevl ""} 27 } 28 if {[string length $optname] > 1} {lappend loptpile $optname $colons} {lappend soptpile $optname $colons} 29 } 30 set consumingargs [list] 31 set consumedargs [list] 32 set unconsumedargs [list] 33 set n 0 34 set dashdash 0 35 while {$n < [llength $argl]} { 36 set arg [lindex $argl $n] 37 set colons 0 38 if {$dashdash} { 39 lappend unconsumedargs $arg 40 } { 41 if {[string index $arg 0] == "-"} { 42 if {[string index $arg 1] == "-"} { 43 if {[string length $arg] == 2} {set dashdash 1} { 44 # long opt; consume argument immediately after, even if it starts with a dash 45 # if not a real opt, don't care 46 if {[set colons [dict get $loptpile [string range $arg 2 end]]] != ""} { 47 set consumedarg [list [string range $arg 2 end]] 48 if {$colons != 0} { 49 if {[llength [lrange $argl [expr {$n+1}] [expr {$n+$colons}]]] == $colons} { 50 lappend consumedarg [lrange $argl [expr {$n+1}] [expr {$n+$colons}]] 51 incr n $colons 52 } { 53 return -code error "Reached end of arguments list while consuming arguments for option $arg ([lrange $argl [expr {$n+1}] [expr {$n+$colons}]])" 54 } 55 } 56 lappend consumedargs $consumedarg 57 } { 58 lappend unconsumedargs $arg 59 } 60 } 61 } { 62 if {[string length $arg] == 1} {lappend unconsumedargs $arg} { 63 # short option 64 foreach {letter} [split [string range $arg 1 end] {}] { 65 if {[catch {dict get $soptpile $letter} colons] == 0} { 66 #set colons [dict get $soptpile $letter] 67 set consumedarg [list $letter] 68 if {$colons != 0} { 69 if {[llength [lrange $argl [expr {$n+1}] [expr {$n+$colons}]]] == $colons} { 70 foreach {ar} [lrange $argl [expr {$n+1}] [expr {$n+$colons}]] {lappend consumedarg $ar} 71 incr n $colons 72 } { 73 return -code error "Reached end of arguments list while consuming arguments for option $arg ([lrange $argl [expr {$n+1}] [expr {$n+$colons}]])" 74 } 75 } 76 lappend consumedargs $consumedarg 77 } { 78 # unrecognized dash option - warn user and treat as a separate non-option arg 79 puts stderr [format "warning: option -%s not recognized by this program. Treating as a SEPARATE non-option argument - if this wasn't intended, put the argument containing this option after a \'--\'!" $letter] 80 lappend unconsumedargs [join [list - $letter] {}] 81 } 82 # if there's even zero colons 83 } 84 # foreach letter 85 } 86 # if only dash, else short option 87 } 88 # if two dashes 89 } else { 90 lappend unconsumedargs $arg 91 } 92 # if one dash 93 } 94 # if dashdash 95 incr n 96 } 97 # while 98 return [list $consumedargs $unconsumedargs] 99 } 100 101 set version v0.0-alpha 102 set programname "ussg-page page generator" 103 104 proc printversion {} { 105 puts stderr [format "%s %s (Tcl)" $::programname $::version] 106 puts stderr {All Rights Reserved © 2022 by Ellenor Agnes Bjornsdottir 107 Most activity in the nature of redistributing this application, 108 unmodified or modified, is permitted. See LICENCE for more info. 109 110 This program is, to the extent permitted by applicable law, supplied 111 with no warranty, not even the implied warranties for 112 merchantability or fitness for purpose. Where legally possible, my 113 liability at any remaining warranty claims will be limited to the 114 amount I received in return for you receiving this software. 115 116 Exceptionally: 117 For stable releases, I warrant that the software has been 118 demonstrated to produce usable output on a common UNIX operating 119 system with Tcl 8.6, and using Orc's Discount markdown engine, 120 with the included input data. Your recovery is limited to damage 121 caused by improper operation of the software, which you did not 122 cause, which doesn't appear to be related to defective input 123 or hardware. Bug reports are welcome and are preferred over 124 legal process if possible. 125 } 126 } 127 128 129 set help {usage: %0 [--..] [-hv] [-H headers] [-M /usr/bin/markdown] [--] input [output] 130 131 Generate a complete HTML page from input and headers to output. 132 headers and input are as described in interface.md and 133 ussg-page.1. 134 135 If both input and output are blank, convert stdin to stdout. 136 137 If either input or output are single dashes, convert the respective 138 standard file. 139 140 A double dash terminates option processing, and all arguments 141 are taken as non-options thereafter, including other double 142 dashes. 143 144 Options: 145 -h, --help 146 Display this help. 147 -v, --version 148 Displays the version of this program. 149 -H/--headers [headers] 150 headers file, read before input. input is treated as if 151 it's catenated after headers. 152 May be specified more than once. 153 -M/--markdown [markdown] 154 'Markdown' command line. Must accept Markdown on standard 155 input and produce fragmentary HTML on standard output. 156 157 Note: we do not check that you are actually using Markdown. 158 If you are instead using, e.g. mdoc, this should work, and 159 exactly this use is in place on Umbrellix.net and is 160 supported. 161 Default if not specified: env markdown 162 -R/--restricted 163 Restricted mode. Does not scribble on an already existing 164 output file. Instead, balks with EX_CANTCREAT (73). 165 This is not default, but it is recommended to use this if 166 possible. 167 -N/--noheaders 168 The input file does not contain headers. It starts 169 immediately with content. 170 -Q/--quick 171 Not applicable if either file is the standard descriptor. 172 The mtimes of the input and output files are compared. 173 Should the output file not exist, or should it be older 174 than the input file, operate normally. Otherwise, 175 terminate early. 176 177 Specifying multiple short options together will result in them 178 being interpreted separately, including dash options that 179 consume arguments. Unknown dash options will be treated as 180 non-option arguments; you will receive a warning on stderr 181 if this occurs. 182 183 Example (without unknown options): 184 185 %0 -HM .headers "$HOME/.local/src/discount/markdown [options]" 186 Run this program, stdin to stdout, with a headers 187 sidestream of .headers and a Markdown command line 188 of "$HOME/.local/src/discount/markdown [options]" 189 (with dquotes, this'll be interpreted by your shell, then 190 by Tcl in a usage of [open |...]) 191 192 %0 -HM .headers "$HOME/.local/src/discount/markdown [options]" -- -malchik -zhenschina 193 Run this program, ./-malchik to ./-zhenschina, with a 194 headers sidestream of .headers and a Markdown command 195 line of "$HOME/.local/src/discount/markdown [options]" 196 (with dquotes, this'll be interpreted by your shell, then 197 by Tcl in a usage of [open |...]) 198 The etymology of the nonsense words used is Russian for 199 boy and woman. 200 201 Example (with unknown options): 202 203 %0 -HMYZ .headers "$HOME/.local/src/discount/markdown [options]" 204 Run this program, ./-Y to ./-Z, with a headers 205 sidestream of .headers and a Markdown command line 206 of "$HOME/.local/src/discount/markdown [options]" 207 Note: That's probably not what you want, and the behavior will 208 change if those options ever become recognized. 209 Do not invoke this program this way except for a joke. 210 } 211 212 proc printhelp {} { 213 puts stderr [string map [list %0 $::argv0] $::help] 214 } 215 216 set inputfd stdin 217 set outputfd stdout 218 set inputfile "" 219 set outputfile "" 220 set headersfiles [list] 221 set headersfds [list] 222 set markdown "env markdown" 223 set scribble 1 224 set moving 0 ;# 1 means that we'll have to move data after. 225 226 ## Begin command routine 227 228 set headers [list] 229 230 # 0 if it can only be set once, 1 if it can be set inf times 231 # assume 1 232 # Headers are case insensitive and stored in lowercase, so ... 233 set headermulti { 234 title 0 235 title-separator 0 236 template 0 237 x-site-title 0 238 x-site-logo 0 239 x-site-description 0 240 x-synoptic-title 0 241 x-synoptic-text 0 242 x-synoptic-image 0 243 favicon 0 244 style 1 245 verbatim 1 246 execverbatim 1 247 plugin 1 248 url-prefix 0 249 navbar-prefix 0 250 } 251 252 set parsedargs [opts [list h v H: M: N Q help version headers: markdown: noheaders quick] $argv] 253 # opts [list h v H: M: help version headers: markdown:] [list input --markdown /usr/bin/markdown output -HMEQI .headers /usr/bin/markdown -- -EQI -- -- --] 254 # warning: option -E not recognized by this program. Treating as a SEPARATE non-option argument - if this wasn't intended, put the argument containing this option after a '--'! 255 # warning: option -Q not recognized by this program. Treating as a SEPARATE non-option argument - if this wasn't intended, put the argument containing this option after a '--'! 256 # warning: option -I not recognized by this program. Treating as a SEPARATE non-option argument - if this wasn't intended, put the argument containing this option after a '--'! 257 # {{markdown /usr/bin/markdown} {H .headers} {M /usr/bin/markdown}} {input output -E -Q -I -EQI -- -- --} 258 259 set firstblankline 0 260 set quick 0 261 262 lassign $parsedargs opts arg 263 264 foreach {opt} $opts { 265 switch -exact -- [lindex $opt 0] { 266 h - 267 help { 268 printhelp 269 exit 64 270 } 271 272 v - 273 version { 274 printversion 275 exit 64 276 } 277 278 H - 279 headers { 280 lappend ::headersfiles [file normalize [lindex $opt 1]] 281 } 282 283 M - 284 markdown { 285 set ::markdown [lindex $opt 1] 286 } 287 288 Q - 289 quick { 290 set ::quick 1 291 } 292 293 N - 294 noheaders { 295 set ::firstblankline 1 296 } 297 298 R - 299 restricted { 300 set ::scribble 0 301 } 302 } 303 } 304 305 proc picktmpfilename {filename} { 306 # blocks until filename does not exist 307 set ext [clock seconds] 308 while {[file exists [format "%s.%s" $filename $ext]]} { 309 incr ext 310 } 311 return [format "%s.%s" $filename $ext] 312 } 313 314 if {[llength $arg] == 2} { 315 set inputfile [lindex $arg 0] 316 set outputfile [lindex $arg 1] 317 if {$outputfile == "-"} { 318 set outputfile "" 319 } else { 320 # In the positive, output file desc is already set correctly. 321 # Alternatively... 322 if {!$::scribble && [file exists $outputfile]} { 323 puts stderr [format "Error: output file \'%s\' already exists. You asked me not to scribble on it, so I am not scribbling on it." $outputfile] 324 exit 73 325 } 326 set outputdir [file dirname $outputfile] 327 set outputmkdir [list file mkdir] 328 if {![file exists $outputdir]} { 329 while {![file exists $outputdir]} { 330 lappend outputmkdir $outputdir 331 set outputdir [file dirname $outputdir] 332 } 333 } 334 if {[catch $outputmkdir mkdirerr]} { 335 puts stderr [format "Error: directories to contain output file \'%s\' could not be created. \[file mkdir\] reports:" $::outputfile] 336 puts stderr $mkdirerr 337 exit 73 338 } 339 if {$::quick && $::inputfile != "-"} { 340 # spend time to save time 341 if {[file exists $::outputfile] && 342 [file exists $::inputfile]} { 343 set ::canexit 1 344 if {[set outputmtime [file mtime $::outputfile]] >= [set inputmtime [file mtime $::inputfile]]} { 345 puts stderr [format "Info: output file \'%s\' is newer (%s) than input file \'%s\' (%s)" $::outputfile $outputmtime $::inputfile $inputmtime] 346 foreach header $::headersfiles { 347 if {[set outputmtime [file mtime $::outputfile]] >= [set inputmtime [file mtime $header]]} { 348 puts stderr [format "Info: output file \'%s\' is newer (%s) than header file \'%s\' (%s)" $::outputfile $outputmtime $header $inputmtime] 349 } else { 350 puts stderr [format "Info: output file \'%s\' was modified %s, header file \'%s\' was modified (%s)" $::outputfile $outputmtime $header $inputmtime] 351 set ::canexit 0 352 } 353 } 354 } { 355 puts stderr [format "Info: output file \'%s\' was modified %s, input file \'%s\' was modified (%s)" $::outputfile $outputmtime $::inputfile $inputmtime] 356 set ::canexit 0 357 } 358 } { 359 puts stderr [format "Info: only one of the two files exists"] 360 set ::canexit 0 361 } 362 if {$::canexit} { 363 puts stderr [format "Info: output file \'%s\' is considered up to date; exiting." $::outputfile]; exit 364 } else { 365 puts stderr [format "Info: output file \'%s\' is considered out of date; continuing." $::outputfile] 366 } 367 } { 368 puts stderr [format "Info: quick mode disabled, or input file == -"] 369 } 370 if {[catch {open [set ::tmpoutputfile [picktmpfilename [set outputfile [file normalize $outputfile]]]] w} provoutputfd]} { 371 puts stderr [format "Error: temporary output file \'%s\' could not be opened for writing. \[open\] reports:" $::tmpoutputfile] 372 puts stderr $provoutputfd 373 exit 73 374 } { 375 set ::outputfd $provoutputfd 376 set ::moving 1 377 } 378 } 379 } 380 #continue 381 if {[llength $arg] > 0} { 382 set inputfile [lindex $arg 0] 383 if {$inputfile == "-"} { 384 set outputfile "" 385 } else { 386 # In the positive, the input file desc is already set correctly. 387 if {[catch {open [file normalize $inputfile] r} provinputfd]} { 388 puts stderr [format "Error: input file \'%s\' could not be opened for reading. \[open\] reports:" $::inputfile] 389 puts stderr $provinputfd 390 exit 66 391 } { 392 set ::inputfd $provinputfd 393 } 394 } 395 } 396 foreach headersfile $::headersfiles { 397 if {[catch {open [file normalize $headersfile] r} provinputfd]} { 398 puts stderr [format "Error: headers file \'%s\' could not be opened for reading. Balking now; this is fatal. \[open\] reports:" $::headersfile] 399 puts stderr $provinputfd 400 exit 66 401 } { 402 lappend ::headersfds $provinputfd 403 } 404 } 405 406 proc parseheader {lin} { 407 set content [string range [join [lassign [split $lin ":"] word] ":"] 1 end] 408 set word [string tolower $word] 409 puts stderr [format "info: found header %s: %s" $word $content] 410 if {[catch {dict get $::headermulti $word} multi] == 0} { 411 if {$multi} { 412 dict lappend ::headers $word $content 413 } { 414 dict set ::headers $word $content 415 # We will just allow overriding. 416 } 417 } { 418 # Assume 1 419 dict lappend ::headers $word $content 420 } 421 } 422 423 foreach headersfd $::headersfds { 424 while {![eof $headersfd]} { 425 gets $headersfd lin 426 if {$lin != ""} {parseheader $lin} 427 } 428 # we are done, we can close the header now. 429 close $headersfd 430 } 431 432 #firstblankline was set earlier 433 # if it's already 1 (user specified -N) we skip this. 434 435 while {![eof $inputfd] && !$firstblankline} { 436 gets $inputfd lin 437 if {$lin == ""} { 438 set firstblankline 1 439 puts stderr "info: blank line found. Begin processing document as document." 440 } { 441 parseheader $lin 442 } 443 } 444 445 if {[eof $inputfd]} { 446 puts stderr "Error: document file ended without a document. As this would produce an empty document, this is not allowed." 447 exit 66 448 } 449 # Hold up. Stop accepting lines from inputfd. 450 451 # By this stage, we must have a Template: header. 452 # In the template, %%article%% must be on a line by itself as it's expected to be quite large, so unsuitable for [string map]. 453 if {[catch {dict get $::headers template} templatehdr]} { 454 puts stderr $::headers 455 puts stderr [format "Error: template file not specified in headers or document file. Without a template, we cannot create a document."] 456 exit 78 457 } { 458 if {[catch {open [file normalize $templatehdr] r} provinputfd]} { 459 puts stderr [format "Error: template file \'%s\' could not be opened for reading. Balking now; this is fatal. \[open\] reports:" $templatehdr] 460 puts stderr $provinputfd 461 exit 66 462 } { 463 set templfd $provinputfd 464 } 465 } 466 467 proc templcmdsrc {script} { 468 namespacesrc ::templcmds $script 469 } 470 471 proc namespacesrc {namespace script} { 472 if {[catch {open [file normalize $script] r} provinputfd]} { 473 puts stderr [format "Error: namespace \'%s\' file \'%s\' could not be opened for reading. Balking now; this is fatal. \[open\] reports:" $namespace $script] 474 puts stderr $provinputfd 475 exit 66 476 } { 477 set fp $provinputfd 478 } 479 #set fp [open $script r] 480 set ev [list namespace eval $namespace [read $fp]] 481 close $fp 482 uplevel "#0" $ev 483 } 484 485 # proc markdown: fd inputfd 486 # attributes: might block 487 # globals: markdown outputfd 488 # processfd markdown inputfd outputfd 489 # side effects: closes inputfd 490 proc markdown {inputfd} { 491 processfd $::markdown $inputfd $::outputfd 492 chan close $inputfd 493 } 494 495 # proc processfd: string markdown, fd inputfd, fd outputfd 496 # attributes: might block 497 # process the remainder of inputfd, blocking if no data is available, 498 # with the program markdown (which must be a suitable Unix filter), 499 # outputting to outputfd 500 # leaves inputfd at EOF 501 proc processfd {markdown inputfd outputfd} { 502 # finally, our raison d'etre ! 503 # we expect to get eof on input. 504 if {[catch {open [format "|%s" $markdown] r+} err]} { 505 puts stderr [format "Error: processor \'%s\' could not be executed for reading and writing. \[open\] reports:" $markdown] 506 puts stderr $err 507 exit 70 508 } { 509 set mkdownfd $err 510 } 511 chan copy $inputfd $mkdownfd 512 chan flush $mkdownfd 513 set result [catch {chan close $mkdownfd write} return] 514 # begone, input document 515 chan copy $mkdownfd $outputfd 516 chan flush $outputfd 517 set rresult [catch {chan close $mkdownfd read} rreturn] 518 # begone, markdown 519 if {$result != 0} {puts stderr [format "Warning: On close write, processor reported %s" $return]} 520 if {$rresult != 0} {puts stderr [format "Warning: On close read, processor reported %s" $rreturn]} 521 } 522 523 namespace eval ::templcmds { 524 proc title {} { 525 # output page title 526 if {[catch {dict get $::headers title} title]} { 527 if {![catch {dict get $::headers navbar-prefix} navprefix]} {set prefix $navprefix} {set prefix ""} 528 if {![catch {dict get $::headers x-site-title} navprefix]} {set title $navprefix} {set title "Site Title Not Set"} 529 if {![catch {dict get $::headers title-separator} sep]} {set sep $sep} {set sep "::"} 530 set filename [string map [list [file normalize $prefix] ""] $::outputfile] 531 append title [format " %s " $sep] 532 append title [string map [list "<" "<" ">" ">" [file separator] [format " %s " $sep]] [string trimleft $filename [file separator]]] 533 } 534 puts $::outputfd [format "<title>%s</title>" $title] 535 } 536 537 proc xsitelogo {} { 538 # output Site Logo code 539 if {![catch {dict get $::headers x-site-logo} title]} { puts -nonewline $::outputfd $title } 540 } 541 542 proc xsitetitle {} { 543 # output Site Title 544 if {![catch {dict get $::headers x-site-title} title]} { puts -nonewline $::outputfd $title } 545 } 546 547 proc xsitedescription {} { 548 if {![catch {dict get $::headers x-site-description} title]} { puts -nonewline $::outputfd $title } 549 } 550 551 proc heads {} { 552 # here we check for synoptic headers, and output them as appropriate 553 # e.g. <link rel="stylesheet" href="/pub/style/style.css" type="text/css" media="screen, handheld" title="default"> 554 if {![catch {dict get $::headers x-synoptic-title} title]} { 555 puts $::outputfd [format "<meta property=\"og:title\" content=\"%s\" />" $title] 556 } 557 if {![catch {dict get $::headers x-synoptic-text} title]} { 558 puts $::outputfd [format "<meta property=\"og:description\" content=\"%s\" />" $title] 559 } 560 if {![catch {dict get $::headers x-synoptic-sitename} title]} { 561 puts $::outputfd [format "<meta property=\"og:site_name\" content=\"%s\" />" $title] 562 } 563 if {![catch {dict get $::headers x-synoptic-image} title]} { 564 puts $::outputfd [format "<meta property=\"og:image\" content=\"%s\" />" $title] 565 } 566 if {![catch {dict get $::headers x-synoptic-url} title]} { 567 puts $::outputfd [format "<meta property=\"og:url\" content=\"%s\" />" $title] 568 } 569 if {![catch {dict get $::headers favicon} title]} { 570 puts $::outputfd [format "<link rel=\"shortcut icon\" href=\"/%s\" type=\"image/vnd.microsoft.icon\" />" $title] 571 } 572 if {![catch {dict get $::headers style} title]} { 573 puts $::outputfd [format "<link rel=\"stylesheet\" href=\"/%s\" type=\"text/css\" />" $title] 574 } 575 if {![catch {dict get $::headers raw-head} rawheadhdr]} { 576 foreach {rawhead} $rawheadhdr { 577 puts $::outputfd $rawhead 578 } 579 } 580 } 581 582 proc article {} { 583 markdown $::inputfd 584 } 585 586 proc verbatim {num} { 587 set verbatimhdr [lindex [dict get $::headers verbatim] $num] 588 if {![file exists $verbatimhdr]} { 589 puts stderr [format "Warning: verbatim file no. %s \'%s\' does not exist." $num $verbatimhdr] 590 return 591 } 592 if {[catch {open [file normalize $verbatimhdr] r} provinputfd]} { 593 puts stderr [format "Warning: verbatim file no. %s \'%s\' could not be opened for reading. \[open\] reports:" $num $verbatimhdr] 594 puts stderr $provinputfd 595 return 596 } { 597 chan copy $provinputfd $::outputfd 598 chan flush $::outputfd 599 close $provinputfd 600 } 601 } 602 603 namespace export * 604 namespace ensemble create 605 } 606 607 if {![catch {dict get $::headers plugin} pluginhdr]} { 608 foreach {script} $pluginhdr { 609 templcmdsrc $script 610 } 611 } 612 613 proc templcmd {command} { 614 if {[string is entier [string trimleft $command "%"]]} { 615 templcmds verbatim [string trimleft $command "%"] 616 } { 617 templcmds [string trimleft $command "%"] 618 } 619 chan flush $::outputfd 620 } 621 622 # We expect the template to be small enough that reading it, in full, into memory, will not be a problem even on the smallest system we expect to work on. 623 set template [list] 624 while {![eof $templfd]} { 625 lappend template [gets $templfd] 626 } 627 # We're done, we can close the template now 628 close $templfd 629 foreach {tplline} $template { 630 if {[string first "%" [string trimleft $tplline " \t"]] == 0} { 631 # special 632 # a single percentage sign, with only tabs or spaces before it, is a template command. 633 # if it's "article", then print the article. if it's a number, print that number of verbatim (or nothing if it doesn't exist) 634 set tplcmd [string trimleft $tplline " \t"] 635 templcmd $tplcmd 636 } { 637 # just print to $::outputfd 638 puts $::outputfd $tplline 639 } 640 } 641 chan flush $::outputfd 642 chan close $::outputfd 643 644 # Out of the loop: we're getting there... 645 # Moving? If not, we're probably done. 646 if {${moving}} { 647 puts stderr [format "Info: Moving temporary file \'%s\' to its permanent location, \'%s\'" $::tmpoutputfile $::outputfile] 648 if {[catch {file rename -force $::tmpoutputfile $::outputfile} err]} { 649 puts stderr [format "Error: While moving temporary file \'%s\' to its permanent location, \'%s\', \[file rename\] reported:" $::tmpoutputfile $::outputfile] 650 puts stderr $err 651 exit 74 652 } 653 }