parsecfg.nim 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2010 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## The `parsecfg` module implements a high performance configuration file
  10. ## parser. The configuration file's syntax is similar to the Windows `.ini`
  11. ## format, but much more powerful, as it is not a line based parser. String
  12. ## literals, raw string literals and triple quoted string literals are supported
  13. ## as in the Nim programming language.
  14. ##
  15. ## Example of how a configuration file may look like:
  16. ##
  17. ## .. include:: ../../doc/mytest.cfg
  18. ## :literal:
  19. ##
  20. ## Here is an example of how to use the configuration file parser:
  21. runnableExamples("-r:off"):
  22. import std/[strutils, streams]
  23. let configFile = "example.ini"
  24. var f = newFileStream(configFile, fmRead)
  25. assert f != nil, "cannot open " & configFile
  26. var p: CfgParser
  27. open(p, f, configFile)
  28. while true:
  29. var e = next(p)
  30. case e.kind
  31. of cfgEof: break
  32. of cfgSectionStart: ## a `[section]` has been parsed
  33. echo "new section: " & e.section
  34. of cfgKeyValuePair:
  35. echo "key-value-pair: " & e.key & ": " & e.value
  36. of cfgOption:
  37. echo "command: " & e.key & ": " & e.value
  38. of cfgError:
  39. echo e.msg
  40. close(p)
  41. ##[
  42. ## Configuration file example
  43. ]##
  44. ##
  45. ## .. code-block:: nim
  46. ##
  47. ## charset = "utf-8"
  48. ## [Package]
  49. ## name = "hello"
  50. ## --threads:on
  51. ## [Author]
  52. ## name = "nim-lang"
  53. ## website = "nim-lang.org"
  54. ##[
  55. ## Creating a configuration file
  56. ]##
  57. runnableExamples:
  58. var dict = newConfig()
  59. dict.setSectionKey("","charset", "utf-8")
  60. dict.setSectionKey("Package", "name", "hello")
  61. dict.setSectionKey("Package", "--threads", "on")
  62. dict.setSectionKey("Author", "name", "nim-lang")
  63. dict.setSectionKey("Author", "website", "nim-lang.org")
  64. assert $dict == """
  65. charset=utf-8
  66. [Package]
  67. name=hello
  68. --threads:on
  69. [Author]
  70. name=nim-lang
  71. website=nim-lang.org
  72. """
  73. ##[
  74. ## Reading a configuration file
  75. ]##
  76. runnableExamples("-r:off"):
  77. let dict = loadConfig("config.ini")
  78. let charset = dict.getSectionValue("","charset")
  79. let threads = dict.getSectionValue("Package","--threads")
  80. let pname = dict.getSectionValue("Package","name")
  81. let name = dict.getSectionValue("Author","name")
  82. let website = dict.getSectionValue("Author","website")
  83. echo pname & "\n" & name & "\n" & website
  84. ##[
  85. ## Modifying a configuration file
  86. ]##
  87. runnableExamples("-r:off"):
  88. var dict = loadConfig("config.ini")
  89. dict.setSectionKey("Author", "name", "nim-lang")
  90. dict.writeConfig("config.ini")
  91. ##[
  92. ## Deleting a section key in a configuration file
  93. ]##
  94. runnableExamples("-r:off"):
  95. var dict = loadConfig("config.ini")
  96. dict.delSectionKey("Author", "website")
  97. dict.writeConfig("config.ini")
  98. ##[
  99. ## Supported INI File structure
  100. ]##
  101. # taken from https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
  102. runnableExamples:
  103. import std/streams
  104. var dict = loadConfig(newStringStream("""[Simple Values]
  105. key=value
  106. spaces in keys=allowed
  107. spaces in values=allowed as well
  108. spaces around the delimiter = obviously
  109. you can also use : to delimit keys from values
  110. [All Values Are Strings]
  111. values like this: 19990429
  112. or this: 3.14159265359
  113. are they treated as numbers : no
  114. integers floats and booleans are held as: strings
  115. can use the API to get converted values directly: true
  116. [No Values]
  117. key_without_value
  118. # empty string value is not allowed =
  119. [ Seletion A ]
  120. space around section name will be ignored
  121. [You can use comments]
  122. # like this
  123. ; or this
  124. # By default only in an empty line.
  125. # Inline comments can be harmful because they prevent users
  126. # from using the delimiting characters as parts of values.
  127. # That being said, this can be customized.
  128. [Sections Can Be Indented]
  129. can_values_be_as_well = True
  130. does_that_mean_anything_special = False
  131. purpose = formatting for readability
  132. # Did I mention we can indent comments, too?
  133. """)
  134. )
  135. let section1 = "Simple Values"
  136. assert dict.getSectionValue(section1, "key") == "value"
  137. assert dict.getSectionValue(section1, "spaces in keys") == "allowed"
  138. assert dict.getSectionValue(section1, "spaces in values") == "allowed as well"
  139. assert dict.getSectionValue(section1, "spaces around the delimiter") == "obviously"
  140. assert dict.getSectionValue(section1, "you can also use") == "to delimit keys from values"
  141. let section2 = "All Values Are Strings"
  142. assert dict.getSectionValue(section2, "values like this") == "19990429"
  143. assert dict.getSectionValue(section2, "or this") == "3.14159265359"
  144. assert dict.getSectionValue(section2, "are they treated as numbers") == "no"
  145. assert dict.getSectionValue(section2, "integers floats and booleans are held as") == "strings"
  146. assert dict.getSectionValue(section2, "can use the API to get converted values directly") == "true"
  147. let section3 = "Seletion A"
  148. assert dict.getSectionValue(section3,
  149. "space around section name will be ignored", "not an empty value") == ""
  150. let section4 = "Sections Can Be Indented"
  151. assert dict.getSectionValue(section4, "can_values_be_as_well") == "True"
  152. assert dict.getSectionValue(section4, "does_that_mean_anything_special") == "False"
  153. assert dict.getSectionValue(section4, "purpose") == "formatting for readability"
  154. import strutils, lexbase, streams, tables
  155. import std/private/decode_helpers
  156. import std/private/since
  157. when defined(nimPreviewSlimSystem):
  158. import std/syncio
  159. include "system/inclrtl"
  160. type
  161. CfgEventKind* = enum ## enumeration of all events that may occur when parsing
  162. cfgEof, ## end of file reached
  163. cfgSectionStart, ## a `[section]` has been parsed
  164. cfgKeyValuePair, ## a `key=value` pair has been detected
  165. cfgOption, ## a `--key=value` command line option
  166. cfgError ## an error occurred during parsing
  167. CfgEvent* = object of RootObj ## describes a parsing event
  168. case kind*: CfgEventKind ## the kind of the event
  169. of cfgEof: nil
  170. of cfgSectionStart:
  171. section*: string ## `section` contains the name of the
  172. ## parsed section start (syntax: `[section]`)
  173. of cfgKeyValuePair, cfgOption:
  174. key*, value*: string ## contains the (key, value) pair if an option
  175. ## of the form `--key: value` or an ordinary
  176. ## `key= value` pair has been parsed.
  177. ## `value==""` if it was not specified in the
  178. ## configuration file.
  179. of cfgError: ## the parser encountered an error: `msg`
  180. msg*: string ## contains the error message. No exceptions
  181. ## are thrown if a parse error occurs.
  182. TokKind = enum
  183. tkInvalid, tkEof,
  184. tkSymbol, tkEquals, tkColon, tkBracketLe, tkBracketRi, tkDashDash
  185. Token = object # a token
  186. kind: TokKind # the type of the token
  187. literal: string # the parsed (string) literal
  188. CfgParser* = object of BaseLexer ## the parser object.
  189. tok: Token
  190. filename: string
  191. # implementation
  192. const
  193. SymChars = {'a'..'z', 'A'..'Z', '0'..'9', '_', ' ', '\x80'..'\xFF', '.', '/', '\\', '-'}
  194. proc rawGetTok(c: var CfgParser, tok: var Token) {.gcsafe.}
  195. proc open*(c: var CfgParser, input: Stream, filename: string,
  196. lineOffset = 0) {.rtl, extern: "npc$1".} =
  197. ## Initializes the parser with an input stream. `Filename` is only used
  198. ## for nice error messages. `lineOffset` can be used to influence the line
  199. ## number information in the generated error messages.
  200. lexbase.open(c, input)
  201. c.filename = filename
  202. c.tok.kind = tkInvalid
  203. c.tok.literal = ""
  204. inc(c.lineNumber, lineOffset)
  205. rawGetTok(c, c.tok)
  206. proc close*(c: var CfgParser) {.rtl, extern: "npc$1".} =
  207. ## Closes the parser `c` and its associated input stream.
  208. lexbase.close(c)
  209. proc getColumn*(c: CfgParser): int {.rtl, extern: "npc$1".} =
  210. ## Gets the current column the parser has arrived at.
  211. result = getColNumber(c, c.bufpos)
  212. proc getLine*(c: CfgParser): int {.rtl, extern: "npc$1".} =
  213. ## Gets the current line the parser has arrived at.
  214. result = c.lineNumber
  215. proc getFilename*(c: CfgParser): string {.rtl, extern: "npc$1".} =
  216. ## Gets the filename of the file that the parser processes.
  217. result = c.filename
  218. proc handleDecChars(c: var CfgParser, xi: var int) =
  219. while c.buf[c.bufpos] in {'0'..'9'}:
  220. xi = (xi * 10) + (ord(c.buf[c.bufpos]) - ord('0'))
  221. inc(c.bufpos)
  222. proc getEscapedChar(c: var CfgParser, tok: var Token) =
  223. inc(c.bufpos) # skip '\'
  224. case c.buf[c.bufpos]
  225. of 'n', 'N':
  226. add(tok.literal, "\n")
  227. inc(c.bufpos)
  228. of 'r', 'R', 'c', 'C':
  229. add(tok.literal, '\c')
  230. inc(c.bufpos)
  231. of 'l', 'L':
  232. add(tok.literal, '\L')
  233. inc(c.bufpos)
  234. of 'f', 'F':
  235. add(tok.literal, '\f')
  236. inc(c.bufpos)
  237. of 'e', 'E':
  238. add(tok.literal, '\e')
  239. inc(c.bufpos)
  240. of 'a', 'A':
  241. add(tok.literal, '\a')
  242. inc(c.bufpos)
  243. of 'b', 'B':
  244. add(tok.literal, '\b')
  245. inc(c.bufpos)
  246. of 'v', 'V':
  247. add(tok.literal, '\v')
  248. inc(c.bufpos)
  249. of 't', 'T':
  250. add(tok.literal, '\t')
  251. inc(c.bufpos)
  252. of '\'', '"':
  253. add(tok.literal, c.buf[c.bufpos])
  254. inc(c.bufpos)
  255. of '\\':
  256. add(tok.literal, '\\')
  257. inc(c.bufpos)
  258. of 'x', 'X':
  259. inc(c.bufpos)
  260. var xi = 0
  261. if handleHexChar(c.buf[c.bufpos], xi):
  262. inc(c.bufpos)
  263. if handleHexChar(c.buf[c.bufpos], xi):
  264. inc(c.bufpos)
  265. add(tok.literal, chr(xi))
  266. of '0'..'9':
  267. var xi = 0
  268. handleDecChars(c, xi)
  269. if (xi <= 255): add(tok.literal, chr(xi))
  270. else: tok.kind = tkInvalid
  271. else: tok.kind = tkInvalid
  272. proc handleCRLF(c: var CfgParser, pos: int): int =
  273. case c.buf[pos]
  274. of '\c': result = lexbase.handleCR(c, pos)
  275. of '\L': result = lexbase.handleLF(c, pos)
  276. else: result = pos
  277. proc getString(c: var CfgParser, tok: var Token, rawMode: bool) =
  278. var pos = c.bufpos + 1 # skip "
  279. tok.kind = tkSymbol
  280. if (c.buf[pos] == '"') and (c.buf[pos + 1] == '"'):
  281. # long string literal:
  282. inc(pos, 2) # skip ""
  283. # skip leading newline:
  284. pos = handleCRLF(c, pos)
  285. while true:
  286. case c.buf[pos]
  287. of '"':
  288. if (c.buf[pos + 1] == '"') and (c.buf[pos + 2] == '"'): break
  289. add(tok.literal, '"')
  290. inc(pos)
  291. of '\c', '\L':
  292. pos = handleCRLF(c, pos)
  293. add(tok.literal, "\n")
  294. of lexbase.EndOfFile:
  295. tok.kind = tkInvalid
  296. break
  297. else:
  298. add(tok.literal, c.buf[pos])
  299. inc(pos)
  300. c.bufpos = pos + 3 # skip the three """
  301. else:
  302. # ordinary string literal
  303. while true:
  304. var ch = c.buf[pos]
  305. if ch == '"':
  306. inc(pos) # skip '"'
  307. break
  308. if ch in {'\c', '\L', lexbase.EndOfFile}:
  309. tok.kind = tkInvalid
  310. break
  311. if (ch == '\\') and not rawMode:
  312. c.bufpos = pos
  313. getEscapedChar(c, tok)
  314. pos = c.bufpos
  315. else:
  316. add(tok.literal, ch)
  317. inc(pos)
  318. c.bufpos = pos
  319. proc getSymbol(c: var CfgParser, tok: var Token) =
  320. var pos = c.bufpos
  321. while true:
  322. add(tok.literal, c.buf[pos])
  323. inc(pos)
  324. if not (c.buf[pos] in SymChars): break
  325. while tok.literal.len > 0 and tok.literal[^1] == ' ':
  326. tok.literal.setLen(tok.literal.len - 1)
  327. c.bufpos = pos
  328. tok.kind = tkSymbol
  329. proc skip(c: var CfgParser) =
  330. var pos = c.bufpos
  331. while true:
  332. case c.buf[pos]
  333. of ' ', '\t':
  334. inc(pos)
  335. of '#', ';':
  336. while not (c.buf[pos] in {'\c', '\L', lexbase.EndOfFile}): inc(pos)
  337. of '\c', '\L':
  338. pos = handleCRLF(c, pos)
  339. else:
  340. break # EndOfFile also leaves the loop
  341. c.bufpos = pos
  342. proc rawGetTok(c: var CfgParser, tok: var Token) =
  343. tok.kind = tkInvalid
  344. setLen(tok.literal, 0)
  345. skip(c)
  346. case c.buf[c.bufpos]
  347. of '=':
  348. tok.kind = tkEquals
  349. inc(c.bufpos)
  350. tok.literal = "="
  351. of '-':
  352. inc(c.bufpos)
  353. if c.buf[c.bufpos] == '-':
  354. inc(c.bufpos)
  355. tok.kind = tkDashDash
  356. tok.literal = "--"
  357. else:
  358. dec(c.bufpos)
  359. getSymbol(c, tok)
  360. of ':':
  361. tok.kind = tkColon
  362. inc(c.bufpos)
  363. tok.literal = ":"
  364. of 'r', 'R':
  365. if c.buf[c.bufpos + 1] == '\"':
  366. inc(c.bufpos)
  367. getString(c, tok, true)
  368. else:
  369. getSymbol(c, tok)
  370. of '[':
  371. tok.kind = tkBracketLe
  372. inc(c.bufpos)
  373. tok.literal = "["
  374. of ']':
  375. tok.kind = tkBracketRi
  376. inc(c.bufpos)
  377. tok.literal = "]"
  378. of '"':
  379. getString(c, tok, false)
  380. of lexbase.EndOfFile:
  381. tok.kind = tkEof
  382. tok.literal = "[EOF]"
  383. else: getSymbol(c, tok)
  384. proc errorStr*(c: CfgParser, msg: string): string {.rtl, extern: "npc$1".} =
  385. ## Returns a properly formatted error message containing current line and
  386. ## column information.
  387. result = `%`("$1($2, $3) Error: $4",
  388. [c.filename, $getLine(c), $getColumn(c), msg])
  389. proc warningStr*(c: CfgParser, msg: string): string {.rtl, extern: "npc$1".} =
  390. ## Returns a properly formatted warning message containing current line and
  391. ## column information.
  392. result = `%`("$1($2, $3) Warning: $4",
  393. [c.filename, $getLine(c), $getColumn(c), msg])
  394. proc ignoreMsg*(c: CfgParser, e: CfgEvent): string {.rtl, extern: "npc$1".} =
  395. ## Returns a properly formatted warning message containing that
  396. ## an entry is ignored.
  397. case e.kind
  398. of cfgSectionStart: result = c.warningStr("section ignored: " & e.section)
  399. of cfgKeyValuePair: result = c.warningStr("key ignored: " & e.key)
  400. of cfgOption:
  401. result = c.warningStr("command ignored: " & e.key & ": " & e.value)
  402. of cfgError: result = e.msg
  403. of cfgEof: result = ""
  404. proc getKeyValPair(c: var CfgParser, kind: CfgEventKind): CfgEvent =
  405. if c.tok.kind == tkSymbol:
  406. case kind
  407. of cfgOption, cfgKeyValuePair:
  408. result = CfgEvent(kind: kind, key: c.tok.literal, value: "")
  409. else: discard
  410. rawGetTok(c, c.tok)
  411. if c.tok.kind in {tkEquals, tkColon}:
  412. rawGetTok(c, c.tok)
  413. if c.tok.kind == tkSymbol:
  414. result.value = c.tok.literal
  415. else:
  416. result = CfgEvent(kind: cfgError,
  417. msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
  418. rawGetTok(c, c.tok)
  419. else:
  420. result = CfgEvent(kind: cfgError,
  421. msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
  422. rawGetTok(c, c.tok)
  423. proc next*(c: var CfgParser): CfgEvent {.rtl, extern: "npc$1".} =
  424. ## Retrieves the first/next event. This controls the parser.
  425. case c.tok.kind
  426. of tkEof:
  427. result = CfgEvent(kind: cfgEof)
  428. of tkDashDash:
  429. rawGetTok(c, c.tok)
  430. result = getKeyValPair(c, cfgOption)
  431. of tkSymbol:
  432. result = getKeyValPair(c, cfgKeyValuePair)
  433. of tkBracketLe:
  434. rawGetTok(c, c.tok)
  435. if c.tok.kind == tkSymbol:
  436. result = CfgEvent(kind: cfgSectionStart, section: c.tok.literal)
  437. else:
  438. result = CfgEvent(kind: cfgError,
  439. msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
  440. rawGetTok(c, c.tok)
  441. if c.tok.kind == tkBracketRi:
  442. rawGetTok(c, c.tok)
  443. else:
  444. result = CfgEvent(kind: cfgError,
  445. msg: errorStr(c, "']' expected, but found: " & c.tok.literal))
  446. of tkInvalid, tkEquals, tkColon, tkBracketRi:
  447. result = CfgEvent(kind: cfgError,
  448. msg: errorStr(c, "invalid token: " & c.tok.literal))
  449. rawGetTok(c, c.tok)
  450. # ---------------- Configuration file related operations ----------------
  451. type
  452. Config* = OrderedTableRef[string, OrderedTableRef[string, string]]
  453. proc newConfig*(): Config =
  454. ## Creates a new configuration table.
  455. ## Useful when wanting to create a configuration file.
  456. result = newOrderedTable[string, OrderedTableRef[string, string]]()
  457. proc loadConfig*(stream: Stream, filename: string = "[stream]"): Config =
  458. ## Loads the specified configuration from stream into a new Config instance.
  459. ## `filename` parameter is only used for nicer error messages.
  460. var dict = newOrderedTable[string, OrderedTableRef[string, string]]()
  461. var curSection = "" ## Current section,
  462. ## the default value of the current section is "",
  463. ## which means that the current section is a common
  464. var p: CfgParser
  465. open(p, stream, filename)
  466. while true:
  467. var e = next(p)
  468. case e.kind
  469. of cfgEof:
  470. break
  471. of cfgSectionStart: # Only look for the first time the Section
  472. curSection = e.section
  473. of cfgKeyValuePair:
  474. var t = newOrderedTable[string, string]()
  475. if dict.hasKey(curSection):
  476. t = dict[curSection]
  477. t[e.key] = e.value
  478. dict[curSection] = t
  479. of cfgOption:
  480. var c = newOrderedTable[string, string]()
  481. if dict.hasKey(curSection):
  482. c = dict[curSection]
  483. c["--" & e.key] = e.value
  484. dict[curSection] = c
  485. of cfgError:
  486. break
  487. close(p)
  488. result = dict
  489. proc loadConfig*(filename: string): Config =
  490. ## Loads the specified configuration file into a new Config instance.
  491. let file = open(filename, fmRead)
  492. let fileStream = newFileStream(file)
  493. defer: fileStream.close()
  494. result = fileStream.loadConfig(filename)
  495. proc replace(s: string): string =
  496. var d = ""
  497. var i = 0
  498. while i < s.len():
  499. if s[i] == '\\':
  500. d.add(r"\\")
  501. elif s[i] == '\c' and s[i+1] == '\l':
  502. d.add(r"\c\l")
  503. inc(i)
  504. elif s[i] == '\c':
  505. d.add(r"\n")
  506. elif s[i] == '\l':
  507. d.add(r"\n")
  508. else:
  509. d.add(s[i])
  510. inc(i)
  511. result = d
  512. proc writeConfig*(dict: Config, stream: Stream) =
  513. ## Writes the contents of the table to the specified stream.
  514. ##
  515. ## .. note:: Comment statement will be ignored.
  516. for section, sectionData in dict.pairs():
  517. if section != "": ## Not general section
  518. if not allCharsInSet(section, SymChars): ## Non system character
  519. stream.writeLine("[\"" & section & "\"]")
  520. else:
  521. stream.writeLine("[" & section & "]")
  522. for key, value in sectionData.pairs():
  523. var kv, segmentChar: string
  524. if key.len > 1 and key[0] == '-' and key[1] == '-': ## If it is a command key
  525. segmentChar = ":"
  526. if not allCharsInSet(key[2..key.len()-1], SymChars):
  527. kv.add("--\"")
  528. kv.add(key[2..key.len()-1])
  529. kv.add("\"")
  530. else:
  531. kv = key
  532. else:
  533. segmentChar = "="
  534. kv = key
  535. if value != "": ## If the key is not empty
  536. if not allCharsInSet(value, SymChars):
  537. if find(value, '"') == -1:
  538. kv.add(segmentChar)
  539. kv.add("\"")
  540. kv.add(replace(value))
  541. kv.add("\"")
  542. else:
  543. kv.add(segmentChar)
  544. kv.add("\"\"\"")
  545. kv.add(replace(value))
  546. kv.add("\"\"\"")
  547. else:
  548. kv.add(segmentChar)
  549. kv.add(value)
  550. stream.writeLine(kv)
  551. proc `$`*(dict: Config): string =
  552. ## Writes the contents of the table to string.
  553. ##
  554. ## .. note:: Comment statement will be ignored.
  555. let stream = newStringStream()
  556. defer: stream.close()
  557. dict.writeConfig(stream)
  558. result = stream.data
  559. proc writeConfig*(dict: Config, filename: string) =
  560. ## Writes the contents of the table to the specified configuration file.
  561. ##
  562. ## .. note:: Comment statement will be ignored.
  563. let file = open(filename, fmWrite)
  564. defer: file.close()
  565. let fileStream = newFileStream(file)
  566. dict.writeConfig(fileStream)
  567. proc getSectionValue*(dict: Config, section, key: string, defaultVal = ""): string =
  568. ## Gets the key value of the specified Section.
  569. ## Returns the specified default value if the specified key does not exist.
  570. if dict.hasKey(section):
  571. if dict[section].hasKey(key):
  572. result = dict[section][key]
  573. else:
  574. result = defaultVal
  575. else:
  576. result = defaultVal
  577. proc setSectionKey*(dict: var Config, section, key, value: string) =
  578. ## Sets the Key value of the specified Section.
  579. var t = newOrderedTable[string, string]()
  580. if dict.hasKey(section):
  581. t = dict[section]
  582. t[key] = value
  583. dict[section] = t
  584. proc delSection*(dict: var Config, section: string) =
  585. ## Deletes the specified section and all of its sub keys.
  586. dict.del(section)
  587. proc delSectionKey*(dict: var Config, section, key: string) =
  588. ## Deletes the key of the specified section.
  589. if dict.hasKey(section):
  590. if dict[section].hasKey(key):
  591. if dict[section].len == 1:
  592. dict.del(section)
  593. else:
  594. dict[section].del(key)
  595. iterator sections*(dict: Config): lent string {.since: (1, 5).} =
  596. ## Iterates through the sections in the `dict`.
  597. for section in dict.keys:
  598. yield section