CMcC 4Jun2010
Config.tcl uses parsetcl to parse, then regenerate (I hesitate to say 'compile', but that is in fact what it is) a Tcl-like little language for configuration.
The little language looks like section-name {section} where the section is a Tcl script whose command-part is the name of a section variable and whose single argument is its value.
Values may contain arbitrary commands and comments, and may refer to section-local variables, fully-namespace qualified variables and section-qualified variables.
In short, it looks a lot like a Windows .ini file, but has Tcl syntax. Yay! Enjoy!
Thank you, Lars H, for parsetcl, which made this possible.
A version of the parsetcl script may be found here: http://abel.math.umu.se/~lars/tcl/parsetcl.tcl
# Config.tcl - support tcl-like config files
if {[info exists argv0] && ($argv0 eq [info script])} {
lappend auto_path ~/Desktop/Work/Wub/ [file dirname [info script]]
}
if {[catch {package require Debug}]} {
#proc Debug.config {args} {}
proc Debug.config {args} {puts stderr HTTP@[uplevel subst $args]}
} else {
Debug define config 10
}
package require parsetcl
package provide Config 1.0
namespace eval parsetcl {
proc unparse {tree} {
eval $tree
}
# Lr - literal raw
proc Lr {interval text args} {
return $text
}
# Lb - literal braced
proc Lb {interval text args} {
return \{$text\}
}
# Lb - literal quoted
proc Lq {interval text args} {
return \"$text\"
}
# Sb - backslash substitution
proc Sb {interval text args} {
return "\\$text"
}
# Sv - scalar variable substitution
proc Sv {interval text args} {
return "\$[eval [lindex $args 0]]"
}
# Sa - array variable substitution
proc Sa {interval text args} {
foreach a [lrange $args 1 end] {
append result [eval $a]
}
return "\$[eval [lindex $args 0]]($result)"
}
# Sc - command substitution
proc Sc {interval text args} {
set cmd {}
foreach a $args {
lappend cmd [eval $a]
}
return "\[[join $cmd]\]"
}
# Mr - raw merge
proc Mr {interval text args} {
foreach a $args {
append result [eval $a]
}
return $result
}
# Mq - quoted merge
proc Mq {interval text args} {
foreach a $args {
append result [eval $a]
}
return \"$result\"
}
# Mb - braced merge
proc Mb {interval text args} {
foreach a $args {
append result [eval $a]
}
return \{$result\}
}
# Cd - complete command sans {*}
proc Cd {interval text args} {
set cmd {}
foreach a $args {
lappend cmd [eval $a]
}
return [join $cmd]
}
# Cx - {*}-construct
proc Cx {interval text args} {
set c {}
foreach a $args {
lappend c [eval $a]
}
return \{*\}[join $c]
}
# Ce - complete commands with {*}-constructs
proc Ce {interval text args} {
set c {}
foreach a $args {
lappend c [eval $a]
}
return [join $c]
}
# Cp - command prefix in Ce node
proc Cp {interval text args} {
set c {}
foreach a $args {
lappend c [eval $a]
}
return [join $c]
}
# Cr - non-prefix range of command words in a Ce node
proc Cr {interval text args} {
set c {}
foreach a $args {
lappend c [eval $a]
}
return [join $c]
}
# Rs - script - each arg is a command
proc Rs {interval text args} {
set cmd {}
foreach a $args {
lappend cmd [eval $a]
}
return "\{\n[join $cmd \n]\n\}"
}
# Rx - parsed expr
proc Rx {interval text args} {
set cmd {}
foreach a $args {
lappend cmd [eval $a]
}
return "\{\n[join $cmd]\n\}"
}
namespace export -clear *
namespace ensemble create -subcommands {}
}
oo::class create Config {
method parse {script} {
set parse [parsetcl simple_parse_script $script]
#puts stderr "Parse: $parse"
#puts stderr "Format: [parsetcl format_tree $parse { } { }]"
#puts stderr "UnParse: [parsetcl unparse $parse]"
set rendered {}
parsetcl walk_tree parse index Cd {
# body
#puts stderr "walk: [lindex $parse {*}$index]"
set cmd [lindex $parse {*}$index]
lassign $cmd . . . left right
set ll [parsetcl unparse $left]
if {![string match L* [lindex $left 0]]} {
error "section name '$ll' must be a literal ($left)"
}
if {[llength $cmd] != 5} {
error "section $ll must have one argument only"
}
# get body
set body [parsetcl simple_parse_script [lindex [parsetcl unparse $right] 0]]
set NS [info object namespace [self]]
# perform variable rewrite
parsetcl walk_tree body bi Sv {
set s [lindex $body {*}$bi 3 2]
if {![string match ::* $s] && [string match *::* $s]} {
# this is section-relative var - we need to make a fully qualified NS
set s "${NS}::_C::$s"
lset body {*}$bi 3 2 $s
}
#puts stderr "VAR: $s"
}
# perform script transformation
set rb ""
parsetcl walk_tree body bi Rs {} C.* {
set bcmd [lindex $body {*}$bi]
lassign $bcmd . . . bl br
if {[llength $bi] == 1} {
set bll [parsetcl unparse $bl]
if {![string match L* [lindex $bl 0]]} {
error "section name '$bll' must be a literal ($bl)"
}
if {[llength $bcmd] != 5} {
error "section $bll must have one argument only"
}
set brl [parsetcl unparse $br]
#puts stderr "BCMD $bi: ($bl) ($br) '$brl'"
lappend rb "variable $bll $brl"
} elseif {[parsetcl unparse $bl] eq "expr"} {
#puts stderr "EXPR: ($br) -> ([parsetcl parse_semiwords [parsetcl unparse $br]])"
}
}
lappend rendered "namespace eval $ll [list [join $rb \;]]"
} C.* {
error "Don't know how to handle [lindex $parse {*}$index]"
}
set rendered [join $rendered \;]
#puts stderr "RENDERED: $rendered"
namespace eval _C $rendered
return $parse
}
method extract {} {
set result {}
foreach ns [namespace children _C] {
foreach var [info vars ${ns}::*] {
dict set result [namespace tail $ns] [namespace tail $var] [set $var]
}
}
return $result
}
constructor {args} {
Debug.config {Creating Config [self] $args}
if {[llength $args]%2} {
set cf [lindex $args end]
set args [lrange $args 0 end-1]
dict set args config $cf
}
variable config ""
variable {*}$args
catch {set args [dict merge [Site var? Config] $args]} ;# allow .ini file to modify defaults
my eval $config
}
}
if {[info exists argv0] && ($argv0 eq [info script])} {
Debug on config 10
Config create config
config parse {
section {
var val ;# this is a variable
var1 val1
v1 2
v2 [expr {$v1 ^ 2}]
}
sect1 {
v1 [expr {$section::v1 ^ 10}]
ap [list moop {*}$::auto_path]
}
}
puts stderr "DICT: [config extract]"
}