One more inifile parser

Difference between version 18 and 19 - Previous - Next
See also [Tcllib]'s [inifile] package.
----

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...)

The next step to take is to program the write access. Should not be much more difficult. 
But as usual, many different ideas of how to pass parameters to and results back from my procedures spook through my brain, I still don't have a general solution for it... 

I'm still not sure if it is required to support programmatic access to the ''comments'' of an ini file. I think, the usual programmer is mostly interested in getting the values. Ok, if a program ''writes'' an ini file it possibly makes sense to write comments, though, and to respect position and order of existing, perhaps manually edited comments.
----[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 :-) 2019-11-10: fixed: http://chiselapp.com/user/MHo/repository/tcl-modules/home 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
<<categories>> File | Parsing