Inspired of some old PowerBASIC/DOS-code I've written 1000 years ago to handle INI-files, and in conjunction with what I said on the inifile page, I've started to write another INI file parser in TCL by myself as a evening fun project. This is the first result; don't expect anything from it.... (Only reading is possible for now! Not much error checking is done.)
proc iniRead1 {ini args} {
set h [open $ini r]
catch {array set i [join $args]}; # optional defaults
set currentSection {}
while {[gets $h line] > -1} {
# maybe sometimes: interpret trailing \, respect {blocks} like Tcl
set line [string trim $line]; # remove junk
# ;comments and blank lines are not of interest yet
if {[string length $line] == 0 || [string range $line 0 0] == ";"} {
continue; # I think we don't need no regexps for such primitive stuff
}
# recognize Sections
if {[string length $line] > 2 &&
[string range $line 0 0] == {[} &&
[string range $line end end] == {]}} {
set currentSection [string range $line 1 end-1]
}
# recognize Keywords, but only if a section already exists
set eq [string first = $line]; # keyword must be at least 1 char long...
if {$eq > 0 && [string length $currentSection]} {
incr eq -1; set Key [string range $line 0 $eq]
incr eq 2; set Val [string range $line $eq end]
# to be done: (optional) \backslash and %envvar%-substitution
if {[string range $Val 0 0] == {"} && [string range $Val end end] == {"}} {
set Val [string range $Val 1 end-1]
}
set i($currentSection,$Key) $Val
}
}
close $h
return [array get i]
}I don't use any sophisticated or mysterious techniques in the code above; it should be portable to any language you can think of.... However, in my old PowerBASIC-Units I had implemented a few extra functions to overcome some restrictions of standard INI's, e.g.:- reading of "keywordless" sections - the whole text after a [sectionname] is returned (to support large sections of text in inis)
- optional replacement of %EnvironmentVariables% in the values
- support of an index appended to the ini file which speeds up access to inis to a level comparable to random files... (remember: in DOS-days it wasn't wise to waste memory; so my program was designed to give back the values of only one section with one call; so - each call was a new file access, no buffering in memory...)
MHo 2011-04-06: The above code contains several bugs. A new version can be downloaded here, including documentation: http://home.arcor.de/hoffenbar/prog/iniread.zip
. MHo 2017-03-23: Link unreachable. I'm in the process of fixing this :-) The new source code is as follows:# iniread.tcl: Neue Routinen zum LESEN von INI-Dateien
# Matthias Hoffmann
# 28.12.2010-12.02.2011
# 11.01.2012: v0.2: Fehlermeldungen verbessert.
# 19.03.2013: v0.3: Bugfix.
package provide iniread 0.3
namespace eval ini {
variable iniBuf
array set iniBuf {}
namespace export *
}
# Wird intern automatisch aufgerufen.
#
proc ini::parse {iniFile} {
variable iniBuf
set h [open $iniFile r]
set currentSection {}
lappend iniBuf("",files) $iniFile
set iniBuf(${iniFile}_sections) [list]
while {[gets $h line] > -1} {
# maybe: interpret trailing \, respect {blocks} like Tcl
set line [string trim $line]; # remove junk
# ;comments and blanklines are not of interest yet
if {[string length $line] == 0 || [string range $line 0 0] == ";"} {
continue; # we don't need no regexps
}
# recognize Sections
if {[string length $line] > 2 &&
[string range $line 0 0] == {[} &&
[string range $line end end] == {]}} {
# INIs in Windows are case insensitive!
set currentSection [string toupper [string range $line 1 end-1]]
lappend iniBuf(${iniFile}_sections) $currentSection
set iniBuf($iniFile,${currentSection}_keys) [list]
}
# recognize Keywords, but only if a section already exists
set eq [string first = $line]; # keyword must be at least 1 char long...
if {$eq > 0 && [string length $currentSection]} {
incr eq -1; set Key [string toupper [string trim [string range $line 0 $eq]]]
incr eq 2; set Val [string trim [string range $line $eq end]]
# maybe: (optional) \backslash-Interpretation
set firstChr [string range $Val 0 0]
set lastChr [string range $Val end end]
# vorher: trim. Prüfen Logik von Win32-API, wenn nur "wert, oder wert"
if {$firstChr == "\x22" && $lastChr == "\x22" ||
$firstChr == "\x27" && $lastChr == "\x27"} {
set Val [string range $Val 1 end-1]
}
set iniBuf($iniFile,$currentSection,$Key) [ini::repenv $Val]
lappend iniBuf($iniFile,${currentSection}_keys) $Key; # NEU
}
}
close $h
set iniBuf(${iniFile}_read) 1
}
# ÜBERSICHT DER FUNKTION:
#
# get file section key [default]
# - liefert Wert eines Schlüssels der Sektion der Datei oder Vorgabe
#
# get - liefert Namen aller bisher gelesener (gepufferter) INI-Dateien
# get file - liefert Namen aller Sektionen der Datei
# get file section - liefert Namen aller Schlüssel der Sektion der Datei
#
# get "" - liefert Liste aller Schlüssel-/Werte-Paare aller Sektionen aller Dateien
# get file "" - liefert Liste aller Schlüssel-/Werte-Paare aller Sektionen der Datei
# get file section "" - liefert Liste aller Schlüssel-/Werte-Paare der Sektion der Datei
#
proc ini::get {args} {
variable iniBuf
if {[llength $args] == 0} {
if {[info exists iniBuf("",files)]} {
return $iniBuf("",files)
} else {
return [list]
}
}
set iniFile [lindex $args 0]
if {[string equal $iniFile ""]} {
if {[info exists iniBuf("",files)]} {
set ret [list]
foreach fil $iniBuf("",files) {
set rets [list]
foreach sec $iniBuf(${fil}_sections) {
set retk [list]
foreach key $iniBuf($fil,${sec}_keys) {
lappend retk [list $key $iniBuf($fil,$sec,$key)]
}
lappend rets [list $sec $retk]
}
lappend ret [list $fil $rets]
}
return $ret
} else {
return [list]
}
}
if {![info exists iniBuf(${iniFile}_read)]} {
ini::parse $iniFile
}
if {[llength $args] == 1} {
return $iniBuf(${iniFile}_sections)
}
set s [string toupper [string trim [lindex $args 1]]]
if {[llength $args] == 2} {
if {[string equal $s ""]} {
set ret [list]
foreach sec $iniBuf(${iniFile}_sections) {
set retk [list]
foreach key $iniBuf($iniFile,${sec}_keys) {
# jedes Key/Value-Paar ist eine Liste
lappend retk [list $key $iniBuf($iniFile,$sec,$key)]
}
lappend ret [list $sec $retk]; # jede Sektion ist eine Liste
}
return $ret
} elseif {[info exists iniBuf($iniFile,${s}_keys)]} {
return $iniBuf($iniFile,${s}_keys)
} else {
return -code error "file '$iniFile': section '[lindex $args 1]' missing"
}
}
set k [string toupper [string trim [lindex $args 2]]]
if {[string equal $k ""]} {
if {[info exists iniBuf($iniFile,${s}_keys)]} {
set ret [list]
foreach key $iniBuf($iniFile,${s}_keys) {
lappend ret $key $iniBuf($iniFile,$s,$key)
}
return $ret
} else {
return -code error "file '$iniFile': section '[lindex $args 1]' missing"
}
}
if {[info exists iniBuf($iniFile,$s,$k)]} {
return $iniBuf($iniFile,$s,$k)
} elseif {[llength $args] > 3} {
return [lindex $args 3];# Default zurückgeben
} else {
return -code error "file '$iniFile': section '[lindex $args 1]': key '[lindex $args 2]' missing"
}
}
# Die folgenden beiden Routinen wurden aus READPROF übernommen (um unabhängig
# zu sein) und teilweise angepaßt.
#
#-------------------------------------------------------------------------------
# Holt eine EINZELNE VARIABLE aus der Umgebung (wird INTERN benutzt). Gibt es die
# Variable nicht, wird gemaess DOS/Windows-Verhalten ein LEERSTRING zurückgegeben.
# envvar - Umgebungsvariablen-Name.
# Rück - Wert.
#
proc ini::envvar {var} {
set var [string trim $var %]; # eigentlich nur EIN % vorn und hinten!
return [expr { [info exists ::env($var)] ? $::env($var) : "" }]
}
#-------------------------------------------------------------------------------
# Ersetzt in einer Zeichenkette %Vars% durch Werte (kann unabhängig von jedem
# externen Programm genutzt werden)
# args - Zeichenkette, die %Variablen%-Referenzen enthalten kann
# Rück - Zeichenkette mit aufgelösten Variablen-Referenzen; existiert eine %Var%
# nicht, wird sie durch Leerstring ersetzt (entspricht OS-.BATch-Logik)
# ACHTUNG: Wegen subst-Erfordernis (regsub ersetzt nur eine Ebene) prinzipiell
# unsicher, daher über safe-Slave!
#
proc ini::repenv {str} {
regsub -nocase -all {%[^ %]{1}[^%]*%} $str {[__env &]} tmp
if {[string compare $str $tmp]} {
set id [interp create -safe]; # Safe-Interpreter anlegen und absichern!
interp eval $id {
foreach cmd [info commands] {
if {$cmd != {rename} && $cmd != {if} && $cmd != {subst}} {
rename $cmd {}
}
}
rename if {}; rename rename {}
}
# Trick '$id eval {namespace delete ::}' geht nicht, da 'subst' bleiben muss!
interp hide $id subst; # subst selbst von aussen allerdings verstecken!
interp alias $id __env {} ini::envvar; # Umweg zum Lesen von env, denn
# subSpec {$::env([string trim "&" %])} geht nicht, da im Slave kein env()!
# Achtung: exp berücksichtigt nicht den denkbaren Sonderfall env(%name%)!
catch {$id invokehidden subst -nobackslashes -novariables $tmp} tmp
interp delete $id
}
return $tmp
}- MHo, 2013-03-19: v0.3 - bugfix and one additional testcase in testsuite

