ini.tcl 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. # ini.tcl --
  2. #
  3. # Querying and modifying old-style windows configuration files (.ini)
  4. #
  5. # Copyright (c) 2003-2007 Aaron Faupell <afaupell@users.sourceforge.net>
  6. # Copyright (c) 2008-2012 Andreas Kupries <andreas_kupries@users.sourceforge.net>
  7. #
  8. # See the file "license.terms" for information on usage and redistribution
  9. # of this file, and for a DISCLAIMER OF ALL WARRANTIES.
  10. #
  11. # RCS: @(#) $Id: ini.tcl,v 1.17 2012/01/05 21:04:55 andreas_kupries Exp $
  12. package provide inifile 0.3
  13. namespace eval ini {
  14. variable nexthandle 0
  15. variable commentchar \;
  16. }
  17. proc ::ini::open {ini args} {
  18. variable nexthandle
  19. while {[string match -* [::set opt [lindex $args 0]]]} {
  20. switch -exact -- $opt {
  21. -- {
  22. ::set args [lrange $args 1 end]
  23. break
  24. }
  25. -encoding {
  26. ::set enc [lindex $args 1]
  27. ::set args [lrange $args 2 end]
  28. }
  29. default {
  30. return -code error \
  31. -errorcode {INIFILE OPTION INVALID} \
  32. "Invalid option $opt, expected -encoding"
  33. }
  34. }
  35. }
  36. ::set remainder [llength $args]
  37. if {$remainder > 1} {
  38. return -code error \
  39. -errorcode {WRONG-ARGS INIFILE} \
  40. "wrong\#args: should be \"ini::open ?-encoding E? ?mode?\""
  41. } elseif {$remainder == 1} {
  42. ::set mode [lindex $args 0]
  43. } else {
  44. ::set mode r+
  45. }
  46. if { ![regexp {^(w|r)\+?$} $mode] } {
  47. return -code error \
  48. -errorcode {INIFILE MODE INVALID} \
  49. "$mode is not a valid access mode"
  50. }
  51. ::set fh ini$nexthandle
  52. ::set tmp [::open $ini $mode]
  53. fconfigure $tmp -translation crlf
  54. if {[info exists enc]} {
  55. if {[catch {
  56. fconfigure $tmp -encoding $enc
  57. } msg]} {
  58. ::close $tmp
  59. return -code error $msg
  60. }
  61. }
  62. namespace eval ::ini::$fh {
  63. variable data; array set data {}
  64. variable comments; array set comments {}
  65. variable sections; array set sections {}
  66. }
  67. ::set ::ini::${fh}::channel $tmp
  68. ::set ::ini::${fh}::file [_normalize $ini]
  69. ::set ::ini::${fh}::mode $mode
  70. incr nexthandle
  71. if { [string match "r*" $mode] } {
  72. _loadfile $fh
  73. }
  74. return $fh
  75. }
  76. # close the file and delete all stored info about it
  77. # this does not save any changes. see ::ini::commit
  78. proc ::ini::close {fh} {
  79. _valid_ns $fh
  80. variable ::ini::${fh}::channel
  81. ::close $channel
  82. namespace delete ::ini::$fh
  83. return
  84. }
  85. # write all changes to disk
  86. proc ::ini::commit {fh} {
  87. _valid_ns $fh
  88. variable ::ini::${fh}::data
  89. variable ::ini::${fh}::comments
  90. variable ::ini::${fh}::sections
  91. variable ::ini::${fh}::channel
  92. variable ::ini::${fh}::file
  93. variable ::ini::${fh}::mode
  94. variable commentchar
  95. if { $mode == "r" } {
  96. return -code error \
  97. -errorcode {INIFILE READ-ONLY} \
  98. "cannot write to read-only file"
  99. }
  100. ::close $channel
  101. ::set channel [::open $file w]
  102. ::set char $commentchar
  103. #seek $channel 0 start
  104. foreach sec [array names sections] {
  105. if { [info exists comments($sec)] } {
  106. puts $channel "$char [join $comments($sec) "\n$char "]\n"
  107. }
  108. puts $channel "\[$sec\]"
  109. foreach key [lsort -dictionary [array names data [_globescape $sec]\000*]] {
  110. ::set key [lindex [split $key \000] 1]
  111. if {[info exists comments($sec\000$key)]} {
  112. puts $channel "$char [join $comments($sec\000$key) "\n$char "]"
  113. }
  114. puts $channel "$key=$data($sec\000$key)"
  115. }
  116. puts $channel ""
  117. }
  118. ::close $channel
  119. ::set channel [::open $file r+]
  120. return
  121. }
  122. # internal command to read in a file
  123. # see open and revert for public commands
  124. proc ::ini::_loadfile {fh} {
  125. variable ::ini::${fh}::data
  126. variable ::ini::${fh}::comments
  127. variable ::ini::${fh}::sections
  128. variable ::ini::${fh}::channel
  129. variable ::ini::${fh}::file
  130. variable ::ini::${fh}::mode
  131. variable commentchar
  132. ::set cur {}
  133. ::set com {}
  134. ::set char $commentchar
  135. seek $channel 0 start
  136. foreach line [split [read $channel] "\n"] {
  137. # bug 3612465 - allow and ignore leading and trailing whitespace.
  138. ::set line [string trim $line]
  139. if { [string match "$char*" $line] } {
  140. lappend com [string trim [string range $line [string length $char] end]]
  141. } elseif { [string match {\[*\]} $line] } {
  142. ::set cur [string range $line 1 end-1]
  143. if { $cur == "" } { continue }
  144. ::set sections($cur) 1
  145. if { $com != "" } {
  146. ::set comments($cur) $com
  147. ::set com {}
  148. }
  149. } elseif { [string match {*=*} $line] } {
  150. ::set line [split $line =]
  151. ::set key [string trim [lindex $line 0]]
  152. if { $key == "" || $cur == "" } { continue }
  153. ::set value [string trim [join [lrange $line 1 end] =]]
  154. if { [regexp "^(\".*\")\s+${char}(.*)$" $value -> 1 2] } {
  155. ::set value $1
  156. lappend com $2
  157. }
  158. ::set data($cur\000$key) $value
  159. if { $com != "" } {
  160. ::set comments($cur\000$key) $com
  161. ::set com {}
  162. }
  163. }
  164. }
  165. return
  166. }
  167. # internal command to escape glob special characters
  168. proc ::ini::_globescape {string} {
  169. return [string map {* \\* ? \\? \\ \\\\ \[ \\\[ \] \\\]} $string]
  170. }
  171. # internal command to check if a section or key is nonexistant
  172. proc ::ini::_exists {fh sec args} {
  173. variable ::ini::${fh}::sections
  174. variable ::ini::${fh}::data
  175. if { ![info exists sections($sec)] } {
  176. return -code error \
  177. -errorcode {INIFILE SECTION INVALID} \
  178. "no such section \"$sec\""
  179. }
  180. if { [llength $args] > 0 } {
  181. ::set key [lindex $args 0]
  182. if { ![info exists data($sec\000$key)] } {
  183. return -code error \
  184. -errorcode {INIFILE KEY INVALID} \
  185. "can't read key \"$key\""
  186. }
  187. }
  188. return
  189. }
  190. # internal command to check validity of a handle
  191. if { [package vcompare [package provide Tcl] 8.4] < 0 } {
  192. proc ::ini::_normalize {path} {
  193. return $path
  194. }
  195. proc ::ini::_valid_ns {name} {
  196. variable ::ini::${name}::data
  197. if { ![info exists data] } {
  198. return -code error \
  199. -errorcode {INIFILE HANDLE INVALID} \
  200. "$name is not an open INI file"
  201. }
  202. }
  203. } else {
  204. proc ::ini::_normalize {path} {
  205. file normalize $path
  206. }
  207. proc ::ini::_valid_ns {name} {
  208. if { ![namespace exists ::ini::$name] } {
  209. return -code error \
  210. -errorcode {INIFILE HANDLE INVALID} \
  211. "$name is not an open INI file"
  212. }
  213. }
  214. }
  215. # get and set the ini comment character
  216. proc ::ini::commentchar { {new {}} } {
  217. variable commentchar
  218. if {$new != ""} {
  219. if {[string length $new] > 1} {
  220. return -code error \
  221. -errorcode {INIFILE COMMENT-CHAR INVALID} \
  222. "comment char must be a single character"
  223. }
  224. ::set commentchar $new
  225. }
  226. return $commentchar
  227. }
  228. # return all section names
  229. proc ::ini::sections {fh} {
  230. _valid_ns $fh
  231. variable ::ini::${fh}::sections
  232. return [array names sections]
  233. }
  234. # return boolean indicating existance of section or key in section
  235. proc ::ini::exists {fh sec {key {}}} {
  236. _valid_ns $fh
  237. variable ::ini::${fh}::sections
  238. variable ::ini::${fh}::data
  239. if { $key == "" } {
  240. return [info exists sections($sec)]
  241. }
  242. return [info exists data($sec\000$key)]
  243. }
  244. # return all key names of section
  245. # error if section is nonexistant
  246. proc ::ini::keys {fh sec} {
  247. _valid_ns $fh
  248. _exists $fh $sec
  249. variable ::ini::${fh}::data
  250. ::set keys {}
  251. foreach x [array names data [_globescape $sec]\000*] {
  252. lappend keys [lindex [split $x \000] 1]
  253. }
  254. return $keys
  255. }
  256. # return all key value pairs of section
  257. # error if section is nonexistant
  258. proc ::ini::get {fh sec} {
  259. _valid_ns $fh
  260. _exists $fh $sec
  261. variable ::ini::${fh}::data
  262. ::set r {}
  263. foreach x [array names data [_globescape $sec]\000*] {
  264. lappend r [lindex [split $x \000] 1] $data($x)
  265. }
  266. return $r
  267. }
  268. # return the value of a key
  269. # return default value if key or section is nonexistant otherwise error
  270. proc ::ini::value {fh sec key {default {}}} {
  271. _valid_ns $fh
  272. variable ::ini::${fh}::data
  273. if {$default != "" && ![info exists data($sec\000$key)]} {
  274. return $default
  275. }
  276. _exists $fh $sec $key
  277. return [::set data($sec\000$key)]
  278. }
  279. # set the value of a key
  280. # new section or key names are created
  281. proc ::ini::set {fh sec key value} {
  282. _valid_ns $fh
  283. variable ::ini::${fh}::sections
  284. variable ::ini::${fh}::data
  285. ::set sec [string trim $sec]
  286. ::set key [string trim $key]
  287. if { $sec == "" || $key == "" } {
  288. return -code error \
  289. -errorcode {INIFILE SYNTAX} \
  290. "section or key may not be empty"
  291. }
  292. ::set data($sec\000$key) $value
  293. ::set sections($sec) 1
  294. return $value
  295. }
  296. # delete a key or an entire section
  297. # may delete nonexistant keys and sections
  298. proc ::ini::delete {fh sec {key {}}} {
  299. _valid_ns $fh
  300. variable ::ini::${fh}::sections
  301. variable ::ini::${fh}::data
  302. if { $key == "" } {
  303. array unset data [_globescape $sec]\000*
  304. array unset sections [_globescape $sec]
  305. }
  306. catch {unset data($sec\000$key)}
  307. }
  308. # read and set comments for sections and keys
  309. # may comment nonexistant sections and keys
  310. proc ::ini::comment {fh sec key args} {
  311. _valid_ns $fh
  312. variable ::ini::${fh}::comments
  313. ::set r $sec
  314. if { $key != "" } { append r \000$key }
  315. if { [llength $args] == 0 } {
  316. if { ![info exists comments($r)] } { return {} }
  317. return $comments($r)
  318. }
  319. if { [llength $args] == 1 && [lindex $args 0] == "" } {
  320. unset -nocomplain comments($r)
  321. return {}
  322. }
  323. # take care of any embedded newlines
  324. for {::set i 0} {$i < [llength $args]} {incr i} {
  325. ::set args [eval [list lreplace $args $i $i] [split [lindex $args $i] \n]]
  326. }
  327. eval [list lappend comments($r)] $args
  328. }
  329. # return the physical filename for the handle
  330. proc ::ini::filename {fh} {
  331. _valid_ns $fh
  332. variable ::ini::${fh}::file
  333. return $file
  334. }
  335. # reload the file from disk losing all changes since the last commit
  336. proc ::ini::revert {fh} {
  337. _valid_ns $fh
  338. namespace eval ::ini::$fh {
  339. array set data {}
  340. array set comments {}
  341. array set sections {}
  342. }
  343. if { ![string match "w*" $mode] } {
  344. _loadfile $fh
  345. }
  346. }