Snit cfg package

David Easton (DPE)19 March 2004 - This Snit based package provides the ability to read and write ini style config files.

Although it should work with any ini style config files, I wrote it to solve a particular problem where I wanted to read and write config files within a metakit virtual filesystem. The inifile parser did not work because it opens the file using access specifiers that did not seem to be supported within the vfs mounted with vfs::mk4::Mount.

The read method is used to read the config file into memory. The write method is used to write the config file from memory to disk. All other operations do not change the file on disk, but only the values in memory.

Limitations: There is no support for comments within a configuration file. i.e. Comments will be lost when a config file is written

As usual, feel free to edit, improve and use the code on this page.

DDG I was adding a dummy option because snit0.97 fails to load `$self configurelist $args' if no option was actually given.

WHD For the record, the correct solution is to delete the call "$self configurelist $args". If you have no options, there's no reason to call it.

DDG I agree. But the problem is that the error stack is a little bit ugly, people don't know what to do in order to fix it.

DPE 11 March 2005 I have deleted "$self configurelist $args" from the constructor and removed the dummy option as suggested by WHD. This is the change I had to make to Jigsaw Puzzler to work with a later version of snit.


################################################################################
#
# Package: cfg
#
# Description:
#     Package of utilities for in-memory configuration file routines
#
#     Provides the following functions:
#
#         cfg create <name>   - Create a cfg handle with name <name>
#         cfg create %AUTO%   - Create a cfg handle
#         cfg list            - List all open cfg handles
#
#         <handle> read <filename>
#         <handle> write <filename>
#         <handle> destroy
#         <handle> show
#
#         <handle> get <section> <key>
#         <handle> set <section> <key> <value>
#         <handle> delete <section> <key>
#         <handle> exists <section> <key>
#
#         <handle> sections
#         <handle> section get <section>
#         <handle> section set <section> <key> <val> ?<key> <value>? ...
#         <handle> section delete <section>
#         <handle> section exists <section>
#
# Author: David Easton
# Date:   Nov 2003
#
################################################################################
 
package provide cfg 1.0
 
namespace eval cfg {
 
     package require snit
     
     snit::type cfg {
         typevariable cfgList [list]
         
         typemethod list {} {
             return $cfgList
         }
         
         # Used for efficient lreplace
         proc K {x y} {
             set x
         }
     
         # Define variable in which to store the cfg information
         variable data
         variable sectionList
         
         constructor {args} {
             lappend cfgList $self
             set sectionList [list]
         }
         
         destructor {
             if {[set i [lsearch $cfgList $self]] != -1} {
                 set cfgList [lreplace $cfgList $i $i]
             }
         }
     
         #
         # Private methods
         #
         
         method AddSection {section} {
             if {[lsearch $sectionList $section] == -1} {
                 lappend sectionList $section
                 set data($section:keyList) [list]
             }
         }
         
         method DelSection {section} {
             if {[set i [lsearch $sectionList $section]] != -1} {
                 set sectionList [lreplace $sectionList $i $i]
                 foreach entry [array names data $section,*] {
                     unset data($entry)
                 }
                 unset data($section:keyList)
             }
         }
         
         method AddKey {section key} {
        
             $self AddSection $section
     
             if {[lsearch $data($section:keyList) $key] == -1} {
                 lappend data($section:keyList) $key
             }
         }
         
         method DelKey {section key} {
             
             if {[set i [lsearch $data($section:keyList) $key]] != -1} {
                 set data($section:keyList) [lreplace $data($section:keyList) $i $i]
                 unset data($section,$key)
             }
         }   
     
         #
         # Public methods
         #
         
         method read {filename} {
             
             set retCode ok
             set section ""
     
             if {[catch {open $filename r} fileId]} {
                 
                 set retCode "error"
                 
             } else {
                 
                 # Read whole file as a list
                 set dataList [split [read -nonewline $fileId] \n]
                 close $fileId
                 
                 # Now process the list
                 foreach line $dataList {
                     # Ignore blank lines
                     if {[string trim $line] == ""} {continue}
                     # Ignore comments
                     if {[string match "#*" $line]} {continue}
                     # Match sections
                     if {[regexp {\[(.*)\]} $line junk section]} {
                         # We have reached a new section
                     }
                     if {[string match "*=*" $line]} {
                         foreach {key val} [split $line =] {break}
                         $self set $section $key $val
                     }
                 }
             }
             
             return -code $retCode
         }
     
         method write {filename} {
             
             set retCode "ok"
                     
             # Make parent directories if needed
             
             if {![info exists [file dirname $filename]]} {            
                 file mkdir [file dirname $filename]
             }
             
             # Open file for writing
             
             if {[catch {open $filename w} fid]} {
                 
                 set retCode "error"
                 
             } else {
                 
                 foreach section $sectionList {
                     puts $fid "\[$section\]"
                     foreach key $data($section:keyList) {
                         puts $fid "$key=$data($section,$key)"
                     }
                     puts $fid ""
                 }
                             
                 flush $fid
                 close $fid
             }      
         }
     
         method sections {} {
             return $sectionList
         }
         
         method get {section key} {
         
             set value ""
             if [info exists data($section,$key)] {
                 set value $data($section,$key)
             }
             return $value
         }
         
         method set {section key value} {
         
             if {![info exists data($section,$key)]} {
                 $self AddKey $section $key
             }
             set data($section,$key) $value
         }
         
         method delete {section key} {
         
             $self DelKey $section $key
         }
         
         method exists {section key} {
             
             return [info exists data($section,$key)]
         }
         
         method show {} {
         
             foreach section $sectionList {
                 puts "\[$section\]"
                 foreach key $data($section:keyList) {
                     puts "$key=$data($section,$key)"
                 }
                 puts ""
             }
         }
     
         method section {args} {
     
             if {[llength $args] < 2} {
                 set message "wrong # args: should be \"$self section <option> <section> ?args?\""
                 return -code error $message
             } else {
                 set cmd [lindex $args 0]
                 set section [lindex $args 1]
                 set args [lrange $args 2 end]
             }
     
             switch -- $cmd {
     
                 get {
                     set res [list]
                     foreach key $data($section:keyList) {
                         lappend res $key $data($section,$key)
                     }
                     return $res
                 }
                 
                 set {
                     set keyValList [concat $args]
                     set message [list]
                     foreach {key value} $keyValList {
                         lappend message [$self set $section $key $value]
                     }
                     return $message
                 }
                 
                 exists {
                     if {[lsearch $sectionList $section] == -1} {
                         return 0
                     } else {
                         return 1
                     }
                 }
                 
                 list {
                     return $data($section:keyList)
                 }
                 
                 delete {
                     return [$self DelSection $section]
                 }
     
                 default {
                     set message "bad option $cmd: must be get, set, exists, list, delete"
                     return -code error $message
                 }
             }
         }
     }
}

Here is how it can be used:

 package require cfg

 # Create a example.cfg config file 
 set cfg [cfg::cfg create %AUTO%]
 $cfg set "General" attribute "This is my value"
 $cfg write example.cfg
 $cfg destroy

 # Read the config file
 set cfg2 [cfg::cfg create %AUTO%]
 $cfg2 read example.cfg
 puts "Value is: [$cfg2 get General attribute]"

 # Show the config file
 $cfg2 show
 
 # Now free it
 $cfg2 destroy

See also