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