1#!/bin/sh 2# -*-tcl-*- 3# the next line restarts using tclsh\ 4exec tclsh "$0" "$@" 5 6#------------------------------------------------------------------------- 7# TITLE: 8# expand.tcl 9# 10# VERSION: 11# 2.0 12# 13# AUTHOR: 14# Will Duquette 15# 16# DESCRIPTION: 17# Usage: tclsh expand.tcl [options] files.... 18# 19# Reads files, writing input to output. Most text 20# is output unchanged. Certain text is evaluated as Tcl code; 21# the result of the Tcl code, if any, is output. If the Tcl 22# code results in an error, the error result is output. 23# 24# Before reading any input, expand.tcl reads any exprules.tcl 25# file in the current directory, or alternatively a tcl file 26# specified by the "-rules" command line option. This allows the 27# caller to define special formatting macros for general use 28# and override them as needed. The rules file can also read 29# arguments from the command line, after options are removed but 30# before the files are processed. 31# 32# On an error in a macro, expand can "ignore" the macro, 33# "output" the macro unchanged, "fail" (the default), halting 34# processing, depending on the value of the "-error" option. 35# 36# Output is written to stdout, by default; the "-out" option 37# sends it to a file, instead. If the specified file is "nul", 38# then no output is written at all. The rules can also control 39# the output via the setoutput command. 40# 41# Any text in brackets, e.g., "[" and "]" is treated as a Tcl 42# command, and evaluated. The bracket characters can be changed 43# using ::expand::setbrackets. 44# 45# Normally Expand reads the output files only once; a rules file 46# can choose multiple passes using the ::expand::setpasses command. The 47# ::expand::exppass command returns the number of the current pass, 48# starting at 1. 49# 50# LICENSE: 51# Copyright (C) 2000 by William H. Duquette. See license.txt, 52# distributed with this file, for license information. 53# 54# CHANGE LOG: 55# 56# 06/27/98: Released V1.0 on web. 57# 06/27/98: Changed exp_extract to handle multi-character bracket 58# tokens. Added exp_stripBrackets to remove multi-character 59# bracket tokens. 60# 06/27/98: Added function setbrackets to allow the user to choose the 61# bracket tokens. 62# 06/27/98: Added brand new command line option parser. The new parser 63# can be used by the rules file's begin_hook. 64# 65# 06/28/98: Version 1.1 released. 66# 67# 06/29/98: Added init_hook. 68# 06/29/98: Added setoutput command. 69# 06/29/98: Added setpasses/exppass and multi-pass processing. 70# 06/29/98: Fixed potential bug in exp_getCmd: using "info complete" 71# with changed left and right brackets. 72# 06/30/98: Added -testmode flag: causes error output to go to 73# stdout instead of stderr to aid testing. 74# 07/01/98: Added a tclsh80 starter at the top of the file. 75# 07/01/98: exp_error calls "exit 1" instead of "exit 0" again. 76# 07/02/98: Added expandText and include commands. 77# 07/03/98: Renamed exp_write to expwrite, and made it public, 78# for use with setoutput. 79# 07/07/98: Released Expand V1.2 80# 81# 10/10/99: Added raw_text_hook. 82# 01/15/00: Rewrote popArg, in an attempt to prevent an odd bug 83# that manifests only on certain platforms. 84# 01/15/00: Released Expand V1.3 85# 86# 02/03/00: Found a bug in expandText; it isn't safe to extract 87# the command name from an arbitrary Tcl script using 88# lindex, as many valid scripts aren't valid lists. I 89# now use scan instead of lindex. 90# 91# 04/17/00: Version 2 rewrite begins. The code is cleaned up and 92# placed in the ::expand:: namespace. 93# 94# 05/07/00: Version 2 rewrite ends (for now). 95 96#------------------------------------------------------------------------- 97# Namespace: all of the expand code exists in the ::expand:: namespace, 98# leaving the global namespace for the user's rules. 99 100namespace eval ::expand:: { 101 # Exported Commands 102 namespace export {[a-z]*} 103 104 # Expand Variables 105 106 # Macro bracketing sequences. 107 variable leftBracket "\[" 108 variable rightBracket "\]" 109 110 # What to output when an error is detected: 111 # "nothing", "macro", "error", "fail" 112 variable errorOutputMode fail 113 114 # Number of passes to make over the input 115 variable numberOfPasses 1 116 117 # The current output channel 118 variable outputChannel "" 119 120 # A command can push its context onto a stack, causing any text 121 # that follows it to be saved separately. Later on, a paired command 122 # can pop the stack, acquiring the saved text and including it in its own 123 # output. 124 variable level 0 125 variable context 126 variable contextName 127 variable contextData 128 set context($level) "" 129 set contextName($level) ":0" 130 131 # Status variables 132 variable currentFileName "" 133 variable currentPass 0 134} 135 136#------------------------------------------------------------------------- 137# User settings: These commands allow the users to set, and in some 138# cases retrieve, various expansion parameters. 139 140# lb 141# 142# Return the left bracket sequence. 143 144proc ::expand::lb {} { 145 variable leftBracket 146 147 return $leftBracket 148} 149 150# rb 151# 152# Return the right bracket sequence. 153 154proc ::expand::rb {} { 155 variable rightBracket 156 157 return $rightBracket 158} 159 160# setbrackets lb rb 161# 162# Set the bracket sequences 163proc ::expand::setbrackets {lb rb} { 164 variable leftBracket 165 variable rightBracket 166 167 if {$lb == "" || $rb == ""} { 168 error "Empty string specified as left or right bracket." 169 } 170 171 set leftBracket $lb 172 set rightBracket $rb 173 174 return 175} 176 177# setErrorOutputMode mode 178# 179# Set the error output mode 180proc ::expand::setErrorOutputMode {mode} { 181 variable errorOutputMode 182 183 if {![oneOf {fail nothing macro error} $mode]} { 184 error "Invalid error output mode '$mode'" 185 } 186 187 set errorOutputMode $mode 188} 189 190# Return the current file name 191proc ::expand::expfile {} { 192 variable currentFileName 193 194 return $currentFileName 195} 196 197# Return the number of the current pass. 198proc ::expand::exppass {} { 199 variable currentPass 200 201 return $currentPass 202} 203 204# Set the number of passes 205proc ::expand::setpasses {passes} { 206 variable numberOfPasses 207 208 set numberOfPasses $passes 209 210 if {$numberOfPasses < 1} { 211 error "setpasses: must be >= 1" 212 } 213} 214 215#------------------------------------------------------------------------- 216# User hooks: a rule set can redefine these hooks to do anything desired. 217# The init_hook doesn't contribute to the output, but the other hooks do. 218# Since the hooks do nothing by default, and are to be redefined by the 219# user, they are defined in the global name space. 220 221# Initialization Hook: called when the rule set is loaded. 222proc init_hook {} {} 223 224# Begin Hook: Called at the beginning of each pass. 225proc begin_hook {} {} 226 227# End Hook: Called at the end of each pass. 228proc end_hook {} {} 229 230# Begin File Hook: Called before each file is processed. 231proc begin_file_hook {fileName} {} 232 233# End File Hook: Called after each file is processed. 234proc end_file_hook {fileName} {} 235 236# Raw Text Hook: All plain (non-macro) text is passed through this 237# function. 238proc raw_text_hook {text} {return $text} 239 240#------------------------------------------------------------------------- 241# Context: Every expansion takes place in its own context; however, 242# a macro can push a new context, causing the text it returns and all 243# subsequent text to be saved separately. Later, a matching macro can 244# pop the context, acquiring all text saved since the first command, 245# and use that in its own output. 246 247# cpush name 248# 249# pushes an empty context onto the stack. All output text will be added 250# to this context until it is popped. 251 252proc ::expand::cpush {name} { 253 variable level 254 variable context 255 variable contextName 256 257 incr level 258 set context($level) {} 259 set contextName($level) $name 260} 261 262# cis name 263# 264# Returns true if the current context has the given name. 265 266proc ::expand::cis {name} { 267 variable level 268 variable contextName 269 270 return [expr [string compare $name $contextName($level)] == 0] 271} 272 273# cname 274# 275# Returns the current context name. 276 277proc ::expand::cname {} { 278 variable level 279 variable contextName 280 281 return $contextName($level) 282} 283 284# csave name value 285# 286# Save or retrieve value in the current context 287 288proc ::expand::csave {name value} { 289 variable contextData 290 variable level 291 292 set contextData($level-$name) $value 293} 294 295# cget name 296# 297# Get the value of a context variable 298proc ::expand::cget {name} { 299 variable contextData 300 variable level 301 302 if {![info exists contextData($level-$name)]} { 303 error "*** Error, context var $name doesn't exist in this context" 304 } 305 306 return $contextData($level-$name) 307} 308 309# cvar name 310# 311# Get a context variable's real name, e.g., for appending or lappending 312proc ::expand::cvar {name} { 313 variable contextData 314 variable level 315 316 if {![info exists contextData($level-$name)]} { 317 error "*** Error, context var $name doesn't exist in this context" 318 } 319 320 return ::expand::contextData($level-$name) 321} 322 323# cpop 324# 325# Pops a context level off of the stack, returning the accumulated text. 326 327proc ::expand::cpop {name} { 328 variable level 329 variable context 330 variable contextName 331 variable contextData 332 333 if {$level == 0} { 334 error "*** Error, context mismatch: got unexpected '$name'" 335 } 336 337 if {"$contextName($level)" != "$name"} { 338 error \ 339 "*** Error, context mismatch: expected $contextName($level), got $name" 340 } 341 342 set result $context($level) 343 set context($level) "" 344 set contextName($level) "" 345 346 foreach name [array names contextData $level-*] { 347 unset contextData($name) 348 } 349 350 incr level -1 351 352 return $result 353} 354 355# ContextAppend text 356# 357# This private command appends text to the current context. It is for 358# use only by the Expand code; macros should return their text. 359 360proc ::expand::ContextAppend {text} { 361 variable context 362 variable level 363 364 append context($level) $text 365} 366 367#------------------------------------------------------------------------- 368# Macro-expansion: The following code is the heart of the program. 369# Given a text string, and the current variable settings, this code 370# returns an expanded string, with all macros replaced. 371# 372# If a fatal error is detected during expansion, expandText throws 373# an error for its caller to handle. An error detected while 374# expanding a particular macro is only fatal if the errorOutputMode 375# is "fail"; otherwise, the result of the expansion attempt is 376# output according to the mode. 377# 378# All non-macro text is passed through the raw_text_hook. 379 380# Expands a string using the current macro definitions and Expand 381# variable settings. 382proc ::expand::expandText {inputString} { 383 variable errorOutputMode 384 global errorInfo 385 386 cpush expandText 387 388 while {[string length $inputString] > 0} { 389 set plainText [ExtractToToken inputString [lb] exclude] 390 391 # FIRST, If there was plain text, append it to the output, and 392 # continue. 393 if {$plainText != ""} { 394 ContextAppend [raw_text_hook $plainText] 395 if {[string length $inputString] == 0} { 396 break 397 } 398 } 399 400 # NEXT, A macro is the next thing; process it. 401 if {[catch "GetMacro inputString" macro]} { 402 error "*** Error reading macro from input: $macro" 403 } 404 405 # Expand the macro, and output the result, or 406 # handle an error. 407 if {![catch "uplevel #0 [list $macro]" result]} { 408 ContextAppend $result 409 continue 410 } 411 412 switch $errorOutputMode { 413 nothing { } 414 macro { 415 ContextAppend "[lb]$macro[rb]" 416 } 417 error { 418 ContextAppend "[lb]$macro[rb]\n" 419 ContextAppend "*** Error in preceding macro: $result\n$errorInfo" 420 } 421 fail { 422 error "*** Error in macro:\n[lb]$macro[rb]\n$result" 423 } 424 } 425 } 426 427 return [cpop expandText] 428} 429 430# ExtractToToken string token mode 431# 432# Extract text from a string, up to or including a particular 433# token. Remove the extracted text from the string. 434# mode determines whether the found token is removed; 435# it should be "include" or "exclude". The string is 436# modified in place, and the extracted text is returned. 437proc ::expand::ExtractToToken {string token mode} { 438 upvar $string theString 439 440 # First, determine the offset 441 switch $mode { 442 include { set offset [expr [string length $token] - 1] } 443 exclude { set offset -1 } 444 default { error "::expand::ExtractToToken: unknown mode $mode" } 445 } 446 447 # Next, find the first occurrence of the token. 448 set tokenPos [string first $token $theString] 449 450 # Next, return the entire string if it wasn't found, or just 451 # the part upto or including the character. 452 if {$tokenPos == -1} { 453 set theText $theString 454 set theString "" 455 } else { 456 set newEnd [expr $tokenPos + $offset] 457 set newBegin [expr $newEnd + 1] 458 set theText [string range $theString 0 $newEnd] 459 set theString [string range $theString $newBegin end] 460 } 461 462 return $theText 463} 464 465# Get the next complete command, removing it from the string. 466proc ::expand::GetMacro {string} { 467 upvar $string theString 468 469 # FIRST, it's an error if the string doesn't begin with a 470 # character. 471 if {[string first [lb] $theString] != 0} { 472 error "::expand::GetMacro: assertion failure, next text isn't a command! '$theString'" 473 } 474 475 # NEXT, extract a full macro 476 set macro [ExtractToToken theString [lb] include] 477 while {[string length $theString] > 0} { 478 append macro [ExtractToToken theString [rb] include] 479 480 # Verify that the command really ends with the [rb] characters, 481 # whatever they are. If not, break because of unexpected 482 # end of file. 483 if {![IsBracketed $macro]} { 484 break; 485 } 486 487 set strippedMacro [StripBrackets $macro] 488 489 if {[info complete "puts \[$strippedMacro\]"]} { 490 return $strippedMacro 491 } 492 } 493 494 if {[string length $macro] > 40} { 495 set macro "[string range $macro 0 39]...\n" 496 } 497 error "*** Error, unexpected EOF in macro:\n$macro" 498} 499 500# Strip left and right bracket tokens from the ends of a macro, 501# provided that it's properly bracketed. 502proc ::expand::StripBrackets {macro} { 503 set llen [string length [lb]] 504 set rlen [string length [rb]] 505 set tlen [string length $macro] 506 507 return [string range $macro $llen [expr $tlen - $rlen - 1]] 508} 509 510# Return 1 if the macro is properly bracketed, and 0 otherwise. 511proc ::expand::IsBracketed {macro} { 512 set llen [string length [lb]] 513 set rlen [string length [rb]] 514 set tlen [string length $macro] 515 516 set leftEnd [string range $macro 0 [expr $llen - 1]] 517 set rightEnd [string range $macro [expr $tlen - $rlen] end] 518 519 if {$leftEnd != [lb]} { 520 return 0 521 } elseif {$rightEnd != [rb]} { 522 return 0 523 } else { 524 return 1 525 } 526} 527 528#------------------------------------------------------------------------- 529# File handling: these routines, some public and some private, handle 530# processing of files. 531 532# expand fileList outputFile 533# 534# This is the basic algorithm of the Expand tool. Given a list of files 535# to expand, it executes the following sequence. Return values of all 536# handlers, except for the initHandlers, is written to the current output 537# file. 538# 539# - For each pass, 540# - Set ::expand::currentPass. 541# - Call the begin_hook. 542# - For each file in the file list, 543# - Set ::expand::currentFileName 544# - Call the begin_file_hook. 545# - read file and expand its contents 546# - Call the end_file_hook. 547# - Call the end_hook. 548# - Close the current output file. 549 550proc ::expand::expand {fileList outputFile} { 551 variable currentPass 552 variable numberOfPasses 553 variable currentFileName 554 555 for {set currentPass 1} {$currentPass <= $numberOfPasses} \ 556 {incr currentPass} { 557 558 # First, if this is any pass but the last, set output to nul; 559 # otherwise, set output to the requested output file. 560 if {$currentPass < $numberOfPasses} { 561 setoutput nul 562 } else { 563 setoutput $outputFile 564 } 565 566 # Next, execute the beginning hook 567 set currentFileName "" 568 expwrite [begin_hook] 569 570 # Next, expand each of the files on the command line. 571 foreach file $fileList { 572 if {[catch "ExpandFile [list $file]" result]} { 573 puts stderr $result 574 exit 1 575 } 576 expwrite $result 577 } 578 579 # Next, execute the end hook 580 expwrite [end_hook] 581 } 582 583 # Next, close the output file. 584 setoutput nul 585} 586 587# ExpandFile 588# 589# Helper routine for ::expand::expand. It expands a single file, 590# calling the begin and end file handlers and returning the expanded 591# result. 592 593proc ::expand::ExpandFile {fileName} { 594 variable currentFileName 595 596 # Set the current file 597 set currentFileName $fileName 598 599 # Call the begin_file_hook 600 set output [begin_file_hook $fileName] 601 602 # Expand the file 603 set contents [readFile $fileName] 604 605 if {[catch [list expandText $contents] result]} { 606 error "*** Error expanding $fileName:\n$result" 607 } 608 609 append output $result 610 611 # Call the endFileHandlers 612 append output [end_file_hook $fileName] 613 614 return $output 615} 616 617# include file 618# 619# Reads a file into memory, and expands its contents. 620 621proc ::expand::include {fileName} { 622 # Get the file's contents, and prepare to output it. 623 set contents [readFile $fileName] 624 625 if {[catch [list expandText $contents] result]} { 626 error "*** Error including $fileName:\n$result" 627 } 628 629 return $result 630} 631 632# readFile file 633# 634# Reads a file into memory, returning its contents. 635proc ::expand::readFile {fileName} { 636 # Open the file. 637 if {[catch "open $fileName" fin]} { 638 error "Could not read file '$fileName': $fin" 639 } 640 641 # Read the contents and close the file. 642 set contents [read $fin] 643 close $fin 644 645 return $contents 646} 647 648#------------------------------------------------------------------------- 649# Output Management 650 651# Set the output file 652proc ::expand::setoutput {fileName} { 653 variable outputChannel 654 655 # Close any existing file 656 if {$outputChannel != "" && $outputChannel != "stdout"} { 657 close $outputChannel 658 } 659 660 # Pick stdout, no output at all, or a real file 661 if {$fileName == ""} { 662 set outputChannel stdout 663 } elseif {$fileName == "nul"} { 664 set outputChannel "" 665 } else { 666 if {[catch "open $fileName w" outputChannel]} { 667 error "Could not open output file $fileName" 668 } 669 } 670 671 return 672} 673 674# Output a bunch of text to the output file. 675proc ::expand::expwrite {text} { 676 variable outputChannel 677 678 if {$outputChannel != ""} { 679 puts -nonewline $outputChannel $text 680 } 681} 682 683#------------------------------------------------------------------------- 684# getoptions: command line option parsing 685# 686# The getoptions function parses a list as a command line, removing 687# options and their values. Any remaining tokens and options remain 688# in the list and can be parsed by another call to getoptions or in 689# any other way the caller prefers. 690# 691# getoptions is called as follows: 692# 693# getoptions arglist [-strict] [{optionDef... }] 694# 695# "arglist" is the name of a list variable, typically argv. It is 696# passed by name, and modified in place. If the "-strict" option 697# is specified, unrecognized options are flagged as errors. 698# The call may include any number of option definitions, including 699# none. The call "getoptions argv -strict", for example, will ensure 700# that no options remain in the list contained in "argv". 701# 702# Option definitions may take the following forms. In each, NAME is 703# the option name, which must begin with a "-" character, and VAR is 704# the name of a variable in the caller's scope to receive the option's value. 705# 706# {NAME VAR flag} 707# If the option appears on the command line, the variable 708# is set to 1, otherwise to 0. 709# 710# {NAME VAR enum VAL1 VAL2....} 711# If the option appears on the command line, the next argument 712# must be one of the enumerated values, VAL1, VAL2, etc. The 713# variable is set to the value, or VAL1 if the option does not 714# appear on the command line. If the option's value is not one of 715# the valid choices, an error message will be displayed and the 716# program will halt. None of the enumerated values may begin with 717# a "-" character. 718# 719# {NAME VAR string DEFVALUE} 720# The named variable is set to the value following the option on 721# the command line. If the option doesn't appear, the variable is 722# set to the DEFVALUE. The option's value may not begin with 723# "-" character, as if it does, the most likely explanation is 724# that the option's real value is missing and the next argument is 725# another option name. 726 727# Utility routine: pops an arg off of the front of an arglist. 728proc ::expand::popArg {arglist} { 729 upvar $arglist args 730 731 if {[llength $args] == 0} { 732 set arg "" 733 } elseif {[llength $args] == 1} { 734 set arg $args 735 set args "" 736 } else { 737 set arg [lindex $args 0] 738 set args [lrange $args 1 end] 739 } 740 741 return $arg 742} 743 744proc ::expand::getoptions {arglist strictOrDefs {defsOrNil ""}} { 745 # First, the arglist is called by name. 746 upvar $arglist args 747 748 # Next, strictOrDefs is either the "-strict" option or the 749 # definition list. 750 if {$strictOrDefs == "-strict"} { 751 set strictFlag 1 752 set defList $defsOrNil 753 } else { 754 set strictFlag 0 755 set defList $strictOrDefs 756 } 757 758 # Next, get names of the options 759 set optNames {} 760 set optTypes {flag enum string} 761 set optLens {3 5 4} 762 foreach def $defList { 763 if {[llength $def] < 3} { 764 error "Error in option definition: $def" 765 } 766 lappend optNames [lindex $def 0] 767 set varName [lindex $def 1] 768 set optType [lindex $def 2] 769 set i [lsearch -exact $optTypes $optType] 770 771 if {$i == -1} { 772 error "Unknown option type: $optType" 773 } 774 775 if {[llength $def] < [lindex $optLens $i]} { 776 error "Error in option definition: $def" 777 } 778 779 upvar $varName theVar 780 switch $optType { 781 flag {set theVar 0} 782 enum - 783 string {set theVar [lindex $def 3]} 784 } 785 } 786 787 # Next, process the options on the command line. 788 set errorCount 0 789 set newList {} 790 for {set arg [popArg args]} {$arg != ""} {set arg [popArg args]} { 791 # First, does it look like an option? If not, add it to the 792 # output list. 793 if {[string index $arg 0] != "-"} { 794 lappend newList $arg 795 continue 796 } 797 798 # Next, Is the argument unknown? Flag an error or just skip it. 799 set i [lsearch -exact $optNames $arg] 800 if {$i == -1} { 801 if {$strictFlag} { 802 puts stderr "*** Unknown option: $arg" 803 incr errorCount 804 } else { 805 lappend newList $arg 806 } 807 808 continue 809 } 810 811 # Next, process the argument 812 set def [lindex $defList $i] 813 set varName [lindex $def 1] 814 set optType [lindex $def 2] 815 816 upvar $varName theVar 817 switch $optType { 818 flag { 819 set theVar 1 820 } 821 822 enum { 823 set vals [lreplace $def 0 2] 824 set theVar [popArg args] 825 if {$theVar == "" || [string index $theVar 0] == "-"} { 826 puts stderr "*** Missing option value: $arg" 827 incr errorCount 828 continue 829 } 830 if {[lsearch -exact $vals $theVar] == -1} { 831 puts stderr "*** Invalid option value: $arg $theVar" 832 incr errorCount 833 } 834 } 835 836 string { 837 set theVar [popArg args] 838 if {$theVar == "" || [string index $theVar 0] == "-"} { 839 puts stderr "*** Missing option value: $arg" 840 incr errorCount 841 } 842 } 843 } 844 } 845 846 # Next, if there are any errors, halt. 847 if {$errorCount > 0} { 848 exit 1 849 } 850 851 # Next, return the new argument list. 852 set args $newList 853 return 854} 855 856#------------------------------------------------------------------------- 857# Importing macros into the global namespace 858 859# GlobalizeMacros args 860# 861# args is a list of glob patterns matching the macros to be imported. 862# The prefix ::expand:: is added automatically. 863 864proc ::expand::GlobalizeMacros {args} { 865 set globList {} 866 867 foreach arg $args { 868 lappend globList ::expand::$arg 869 } 870 871 namespace eval :: "namespace import -force $globList" 872} 873 874#------------------------------------------------------------------------- 875# Standard Rule Set: 876# 877# These are the rules that are always available. 878 879proc ::expand::standardRuleSet {} { 880 GlobalizeMacros cget cis cname cpop cpush csave cvar expandText expfile 881 GlobalizeMacros exppass expwrite getoptions include lb popArg rb 882 GlobalizeMacros readFile setErrorOutputMode setbrackets setoutput 883 GlobalizeMacros setpasses textToID 884} 885 886#------------------------------------------------------------------------- 887# Rule Set: Web Rules 888# 889# These macros are for creating HTML pages. They are only defined when 890# webRuleSet is called. 891 892proc ::expand::webRuleSet {} { 893 GlobalizeMacros dot tag link mailto today 894} 895 896# Output a big black dot. 897proc ::expand::dot {} { 898 return "•" 899} 900 901# Format an html tag. name is the tag name, args is a list of 902# of attribute names and values 903proc ::expand::tag {name args} { 904 set result "<$name" 905 foreach {attr val} $args { 906 append result " $attr=\"$val\"" 907 } 908 append result ">" 909} 910 911# Format a link. If text is given, use it as the displayed text; 912# otherwise use the url. 913proc ::expand::link {url {text ""}} { 914 if {$text == ""} { 915 set text $url 916 } 917 918 return "[tag a href $url]$text[tag /a]" 919} 920 921# Format an email URL 922proc ::expand::mailto {address {name ""}} { 923 if {$name == ""} { 924 set name $address 925 } 926 927 return "[tag a href mailto:$address]$name[tag /a]" 928} 929 930# Return today's date. Use dd MONTH yyyy unless some other format is 931# proposed. 932proc ::expand::today {{format ""}} { 933 set secs [clock seconds] 934 935 if {$format == ""} { 936 set format "%d %B %Y" 937 } 938 return [string trimleft [clock format $secs -format $format] "0"] 939} 940 941 942#------------------------------------------------------------------------- 943# Miscellaneous utility commands 944 945# oneOf list value 946# 947# Checks to see if a value is in a list. 948 949proc ::expand::oneOf {list value} { 950 return [expr {[lsearch -exact $list $value] != -1}] 951} 952 953# Converts a generic string to an ID string. Leading and trailing 954# whitespace and internal punctuation is removed, internal whitespace 955# is converted to "_", and the text is converted to lower case. 956proc ::expand::textToID {text} { 957 # First, trim any white space and convert to lower case 958 set text [string trim [string tolower $text]] 959 960 # Next, substitute "_" for internal whitespace, and delete any 961 # non-alphanumeric characters (other than "_", of course) 962 regsub -all {[ ]+} $text "_" text 963 regsub -all {[^a-z0-9_]} $text "" text 964 965 return $text 966} 967 968#------------------------------------------------------------------------- 969# Main-line code: This is the implementation of the Expand tool 970# itself. It is executed only if this is the top-level script. 971 972proc ::expand::ShowHelp { } { 973 puts {tclsh expand.tcl [options] files... 974 975 -help Displays this text. 976 -rules file Specify the name of the rules file 977 (exprules.tcl is the default) 978 -out file Specify the name of the output file, or "nul" for 979 no output. Output is to stdout, by default. 980 -errout mode nothing, macro, error, or fail (fail is the default) 981 -web Enable the optional web rule set. 982 files... Names of files to process.} 983} 984 985if {"[info script]" == "$argv0"} { 986 987 # First, parse the command line 988 ::expand::getoptions argv { 989 {-help ::expand::helpFlag flag} 990 {-errout ::expand::errorOutputMode enum fail nothing macro error} 991 {-rules ::expand::rulesFile string "exprules.tcl"} 992 {-web ::expand::webFlag flag} 993 {-out ::expand::outputFile string ""} 994 } 995 996 # Next, if they asked for help or if there are no arguments left, 997 # show help and stop. 998 if {$::expand::helpFlag || [llength $argv] == 0} { 999 ::expand::ShowHelp 1000 exit 0 1001 } 1002 1003 # Next, load the standard macros 1004 ::expand::standardRuleSet 1005 1006 # Next, load optional rule sets. 1007 if {$::expand::webFlag} { 1008 ::expand::webRuleSet 1009 } 1010 1011 # Next, load the rules file. (Should only do it if file exists; 1012 # should die if there are any errors) 1013 if {[file exists $::expand::rulesFile]} { 1014 if {[catch "source $::expand::rulesFile" result]} { 1015 puts "*** Error in rules file $::expand::rulesFile: $result" 1016 exit 1 1017 } 1018 } elseif {$::expand::rulesFile != "exprules.tcl"} { 1019 puts "*** Rules file $rulesFile not found." 1020 exit 1 1021 } 1022 1023 # Next, call the init_hook. 1024 if {[catch init_hook result]} { 1025 puts "*** Error executing init_hook: $result" 1026 exit 1 1027 } 1028 1029 # Next, make sure the command line contains no additional options 1030 ::expand::getoptions argv -strict 1031 1032 # Next, process the files 1033 ::expand::expand $argv $::expand::outputFile 1034} 1035 1036 1037