parsecfg.nim 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  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. ##
  22. ## .. code-block:: nim
  23. ##
  24. ## import os, parsecfg, strutils, streams
  25. ##
  26. ## var f = newFileStream(paramStr(1), fmRead)
  27. ## if f != nil:
  28. ## var p: CfgParser
  29. ## open(p, f, paramStr(1))
  30. ## while true:
  31. ## var e = next(p)
  32. ## case e.kind
  33. ## of cfgEof: break
  34. ## of cfgSectionStart: ## a ``[section]`` has been parsed
  35. ## echo("new section: " & e.section)
  36. ## of cfgKeyValuePair:
  37. ## echo("key-value-pair: " & e.key & ": " & e.value)
  38. ## of cfgOption:
  39. ## echo("command: " & e.key & ": " & e.value)
  40. ## of cfgError:
  41. ## echo(e.msg)
  42. ## close(p)
  43. ## else:
  44. ## echo("cannot open: " & paramStr(1))
  45. ##
  46. ##
  47. ## Examples
  48. ## ========
  49. ##
  50. ## Configuration file example
  51. ## --------------------------
  52. ##
  53. ## .. code-block:: nim
  54. ##
  55. ## charset = "utf-8"
  56. ## [Package]
  57. ## name = "hello"
  58. ## --threads:on
  59. ## [Author]
  60. ## name = "lihf8515"
  61. ## qq = "10214028"
  62. ## email = "lihaifeng@wxm.com"
  63. ##
  64. ## Creating a configuration file
  65. ## -----------------------------
  66. ## .. code-block:: nim
  67. ##
  68. ## import parsecfg
  69. ## var dict=newConfig()
  70. ## dict.setSectionKey("","charset","utf-8")
  71. ## dict.setSectionKey("Package","name","hello")
  72. ## dict.setSectionKey("Package","--threads","on")
  73. ## dict.setSectionKey("Author","name","lihf8515")
  74. ## dict.setSectionKey("Author","qq","10214028")
  75. ## dict.setSectionKey("Author","email","lihaifeng@wxm.com")
  76. ## dict.writeConfig("config.ini")
  77. ##
  78. ## Reading a configuration file
  79. ## ----------------------------
  80. ## .. code-block:: nim
  81. ##
  82. ## import parsecfg
  83. ## var dict = loadConfig("config.ini")
  84. ## var charset = dict.getSectionValue("","charset")
  85. ## var threads = dict.getSectionValue("Package","--threads")
  86. ## var pname = dict.getSectionValue("Package","name")
  87. ## var name = dict.getSectionValue("Author","name")
  88. ## var qq = dict.getSectionValue("Author","qq")
  89. ## var email = dict.getSectionValue("Author","email")
  90. ## echo pname & "\n" & name & "\n" & qq & "\n" & email
  91. ##
  92. ## Modifying a configuration file
  93. ## ------------------------------
  94. ## .. code-block:: nim
  95. ##
  96. ## import parsecfg
  97. ## var dict = loadConfig("config.ini")
  98. ## dict.setSectionKey("Author","name","lhf")
  99. ## dict.writeConfig("config.ini")
  100. ##
  101. ## Deleting a section key in a configuration file
  102. ## ----------------------------------------------
  103. ## .. code-block:: nim
  104. ##
  105. ## import parsecfg
  106. ## var dict = loadConfig("config.ini")
  107. ## dict.delSectionKey("Author","email")
  108. ## dict.writeConfig("config.ini")
  109. import
  110. strutils, lexbase, streams, tables
  111. include "system/inclrtl"
  112. type
  113. CfgEventKind* = enum ## enumeration of all events that may occur when parsing
  114. cfgEof, ## end of file reached
  115. cfgSectionStart, ## a ``[section]`` has been parsed
  116. cfgKeyValuePair, ## a ``key=value`` pair has been detected
  117. cfgOption, ## a ``--key=value`` command line option
  118. cfgError ## an error occurred during parsing
  119. CfgEvent* = object of RootObj ## describes a parsing event
  120. case kind*: CfgEventKind ## the kind of the event
  121. of cfgEof: nil
  122. of cfgSectionStart:
  123. section*: string ## `section` contains the name of the
  124. ## parsed section start (syntax: ``[section]``)
  125. of cfgKeyValuePair, cfgOption:
  126. key*, value*: string ## contains the (key, value) pair if an option
  127. ## of the form ``--key: value`` or an ordinary
  128. ## ``key= value`` pair has been parsed.
  129. ## ``value==""`` if it was not specified in the
  130. ## configuration file.
  131. of cfgError: ## the parser encountered an error: `msg`
  132. msg*: string ## contains the error message. No exceptions
  133. ## are thrown if a parse error occurs.
  134. TokKind = enum
  135. tkInvalid, tkEof,
  136. tkSymbol, tkEquals, tkColon, tkBracketLe, tkBracketRi, tkDashDash
  137. Token = object # a token
  138. kind: TokKind # the type of the token
  139. literal: string # the parsed (string) literal
  140. CfgParser* = object of BaseLexer ## the parser object.
  141. tok: Token
  142. filename: string
  143. # implementation
  144. const
  145. SymChars = {'a'..'z', 'A'..'Z', '0'..'9', '_', '\x80'..'\xFF', '.', '/', '\\', '-'}
  146. proc rawGetTok(c: var CfgParser, tok: var Token) {.gcsafe.}
  147. proc open*(c: var CfgParser, input: Stream, filename: string,
  148. lineOffset = 0) {.rtl, extern: "npc$1".} =
  149. ## initializes the parser with an input stream. `Filename` is only used
  150. ## for nice error messages. `lineOffset` can be used to influence the line
  151. ## number information in the generated error messages.
  152. lexbase.open(c, input)
  153. c.filename = filename
  154. c.tok.kind = tkInvalid
  155. c.tok.literal = ""
  156. inc(c.lineNumber, lineOffset)
  157. rawGetTok(c, c.tok)
  158. proc close*(c: var CfgParser) {.rtl, extern: "npc$1".} =
  159. ## closes the parser `c` and its associated input stream.
  160. lexbase.close(c)
  161. proc getColumn*(c: CfgParser): int {.rtl, extern: "npc$1".} =
  162. ## get the current column the parser has arrived at.
  163. result = getColNumber(c, c.bufpos)
  164. proc getLine*(c: CfgParser): int {.rtl, extern: "npc$1".} =
  165. ## get the current line the parser has arrived at.
  166. result = c.lineNumber
  167. proc getFilename*(c: CfgParser): string {.rtl, extern: "npc$1".} =
  168. ## get the filename of the file that the parser processes.
  169. result = c.filename
  170. proc handleHexChar(c: var CfgParser, xi: var int) =
  171. case c.buf[c.bufpos]
  172. of '0'..'9':
  173. xi = (xi shl 4) or (ord(c.buf[c.bufpos]) - ord('0'))
  174. inc(c.bufpos)
  175. of 'a'..'f':
  176. xi = (xi shl 4) or (ord(c.buf[c.bufpos]) - ord('a') + 10)
  177. inc(c.bufpos)
  178. of 'A'..'F':
  179. xi = (xi shl 4) or (ord(c.buf[c.bufpos]) - ord('A') + 10)
  180. inc(c.bufpos)
  181. else:
  182. discard
  183. proc handleDecChars(c: var CfgParser, xi: var int) =
  184. while c.buf[c.bufpos] in {'0'..'9'}:
  185. xi = (xi * 10) + (ord(c.buf[c.bufpos]) - ord('0'))
  186. inc(c.bufpos)
  187. proc getEscapedChar(c: var CfgParser, tok: var Token) =
  188. inc(c.bufpos) # skip '\'
  189. case c.buf[c.bufpos]
  190. of 'n', 'N':
  191. add(tok.literal, "\n")
  192. inc(c.bufpos)
  193. of 'r', 'R', 'c', 'C':
  194. add(tok.literal, '\c')
  195. inc(c.bufpos)
  196. of 'l', 'L':
  197. add(tok.literal, '\L')
  198. inc(c.bufpos)
  199. of 'f', 'F':
  200. add(tok.literal, '\f')
  201. inc(c.bufpos)
  202. of 'e', 'E':
  203. add(tok.literal, '\e')
  204. inc(c.bufpos)
  205. of 'a', 'A':
  206. add(tok.literal, '\a')
  207. inc(c.bufpos)
  208. of 'b', 'B':
  209. add(tok.literal, '\b')
  210. inc(c.bufpos)
  211. of 'v', 'V':
  212. add(tok.literal, '\v')
  213. inc(c.bufpos)
  214. of 't', 'T':
  215. add(tok.literal, '\t')
  216. inc(c.bufpos)
  217. of '\'', '"':
  218. add(tok.literal, c.buf[c.bufpos])
  219. inc(c.bufpos)
  220. of '\\':
  221. add(tok.literal, '\\')
  222. inc(c.bufpos)
  223. of 'x', 'X':
  224. inc(c.bufpos)
  225. var xi = 0
  226. handleHexChar(c, xi)
  227. handleHexChar(c, xi)
  228. add(tok.literal, chr(xi))
  229. of '0'..'9':
  230. var xi = 0
  231. handleDecChars(c, xi)
  232. if (xi <= 255): add(tok.literal, chr(xi))
  233. else: tok.kind = tkInvalid
  234. else: tok.kind = tkInvalid
  235. proc handleCRLF(c: var CfgParser, pos: int): int =
  236. case c.buf[pos]
  237. of '\c': result = lexbase.handleCR(c, pos)
  238. of '\L': result = lexbase.handleLF(c, pos)
  239. else: result = pos
  240. proc getString(c: var CfgParser, tok: var Token, rawMode: bool) =
  241. var pos = c.bufpos + 1 # skip "
  242. tok.kind = tkSymbol
  243. if (c.buf[pos] == '"') and (c.buf[pos + 1] == '"'):
  244. # long string literal:
  245. inc(pos, 2) # skip ""
  246. # skip leading newline:
  247. pos = handleCRLF(c, pos)
  248. while true:
  249. case c.buf[pos]
  250. of '"':
  251. if (c.buf[pos + 1] == '"') and (c.buf[pos + 2] == '"'): break
  252. add(tok.literal, '"')
  253. inc(pos)
  254. of '\c', '\L':
  255. pos = handleCRLF(c, pos)
  256. add(tok.literal, "\n")
  257. of lexbase.EndOfFile:
  258. tok.kind = tkInvalid
  259. break
  260. else:
  261. add(tok.literal, c.buf[pos])
  262. inc(pos)
  263. c.bufpos = pos + 3 # skip the three """
  264. else:
  265. # ordinary string literal
  266. while true:
  267. var ch = c.buf[pos]
  268. if ch == '"':
  269. inc(pos) # skip '"'
  270. break
  271. if ch in {'\c', '\L', lexbase.EndOfFile}:
  272. tok.kind = tkInvalid
  273. break
  274. if (ch == '\\') and not rawMode:
  275. c.bufpos = pos
  276. getEscapedChar(c, tok)
  277. pos = c.bufpos
  278. else:
  279. add(tok.literal, ch)
  280. inc(pos)
  281. c.bufpos = pos
  282. proc getSymbol(c: var CfgParser, tok: var Token) =
  283. var pos = c.bufpos
  284. while true:
  285. add(tok.literal, c.buf[pos])
  286. inc(pos)
  287. if not (c.buf[pos] in SymChars): break
  288. c.bufpos = pos
  289. tok.kind = tkSymbol
  290. proc skip(c: var CfgParser) =
  291. var pos = c.bufpos
  292. while true:
  293. case c.buf[pos]
  294. of ' ', '\t':
  295. inc(pos)
  296. of '#', ';':
  297. while not (c.buf[pos] in {'\c', '\L', lexbase.EndOfFile}): inc(pos)
  298. of '\c', '\L':
  299. pos = handleCRLF(c, pos)
  300. else:
  301. break # EndOfFile also leaves the loop
  302. c.bufpos = pos
  303. proc rawGetTok(c: var CfgParser, tok: var Token) =
  304. tok.kind = tkInvalid
  305. setLen(tok.literal, 0)
  306. skip(c)
  307. case c.buf[c.bufpos]
  308. of '=':
  309. tok.kind = tkEquals
  310. inc(c.bufpos)
  311. tok.literal = "="
  312. of '-':
  313. inc(c.bufpos)
  314. if c.buf[c.bufpos] == '-':
  315. inc(c.bufpos)
  316. tok.kind = tkDashDash
  317. tok.literal = "--"
  318. else:
  319. dec(c.bufpos)
  320. getSymbol(c, tok)
  321. of ':':
  322. tok.kind = tkColon
  323. inc(c.bufpos)
  324. tok.literal = ":"
  325. of 'r', 'R':
  326. if c.buf[c.bufpos + 1] == '\"':
  327. inc(c.bufpos)
  328. getString(c, tok, true)
  329. else:
  330. getSymbol(c, tok)
  331. of '[':
  332. tok.kind = tkBracketLe
  333. inc(c.bufpos)
  334. tok.literal = "]"
  335. of ']':
  336. tok.kind = tkBracketRi
  337. inc(c.bufpos)
  338. tok.literal = "]"
  339. of '"':
  340. getString(c, tok, false)
  341. of lexbase.EndOfFile:
  342. tok.kind = tkEof
  343. tok.literal = "[EOF]"
  344. else: getSymbol(c, tok)
  345. proc errorStr*(c: CfgParser, msg: string): string {.rtl, extern: "npc$1".} =
  346. ## returns a properly formatted error message containing current line and
  347. ## column information.
  348. result = `%`("$1($2, $3) Error: $4",
  349. [c.filename, $getLine(c), $getColumn(c), msg])
  350. proc warningStr*(c: CfgParser, msg: string): string {.rtl, extern: "npc$1".} =
  351. ## returns a properly formatted warning message containing current line and
  352. ## column information.
  353. result = `%`("$1($2, $3) Warning: $4",
  354. [c.filename, $getLine(c), $getColumn(c), msg])
  355. proc ignoreMsg*(c: CfgParser, e: CfgEvent): string {.rtl, extern: "npc$1".} =
  356. ## returns a properly formatted warning message containing that
  357. ## an entry is ignored.
  358. case e.kind
  359. of cfgSectionStart: result = c.warningStr("section ignored: " & e.section)
  360. of cfgKeyValuePair: result = c.warningStr("key ignored: " & e.key)
  361. of cfgOption:
  362. result = c.warningStr("command ignored: " & e.key & ": " & e.value)
  363. of cfgError: result = e.msg
  364. of cfgEof: result = ""
  365. proc getKeyValPair(c: var CfgParser, kind: CfgEventKind): CfgEvent =
  366. if c.tok.kind == tkSymbol:
  367. case kind
  368. of cfgOption, cfgKeyValuePair:
  369. result = CfgEvent(kind: kind, key: c.tok.literal, value: "")
  370. else: discard
  371. rawGetTok(c, c.tok)
  372. if c.tok.kind in {tkEquals, tkColon}:
  373. rawGetTok(c, c.tok)
  374. if c.tok.kind == tkSymbol:
  375. result.value = c.tok.literal
  376. else:
  377. result = CfgEvent(kind: cfgError,
  378. msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
  379. rawGetTok(c, c.tok)
  380. else:
  381. result = CfgEvent(kind: cfgError,
  382. msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
  383. rawGetTok(c, c.tok)
  384. proc next*(c: var CfgParser): CfgEvent {.rtl, extern: "npc$1".} =
  385. ## retrieves the first/next event. This controls the parser.
  386. case c.tok.kind
  387. of tkEof:
  388. result = CfgEvent(kind: cfgEof)
  389. of tkDashDash:
  390. rawGetTok(c, c.tok)
  391. result = getKeyValPair(c, cfgOption)
  392. of tkSymbol:
  393. result = getKeyValPair(c, cfgKeyValuePair)
  394. of tkBracketLe:
  395. rawGetTok(c, c.tok)
  396. if c.tok.kind == tkSymbol:
  397. result = CfgEvent(kind: cfgSectionStart, section: c.tok.literal)
  398. else:
  399. result = CfgEvent(kind: cfgError,
  400. msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
  401. rawGetTok(c, c.tok)
  402. if c.tok.kind == tkBracketRi:
  403. rawGetTok(c, c.tok)
  404. else:
  405. result = CfgEvent(kind: cfgError,
  406. msg: errorStr(c, "']' expected, but found: " & c.tok.literal))
  407. of tkInvalid, tkEquals, tkColon, tkBracketRi:
  408. result = CfgEvent(kind: cfgError,
  409. msg: errorStr(c, "invalid token: " & c.tok.literal))
  410. rawGetTok(c, c.tok)
  411. # ---------------- Configuration file related operations ----------------
  412. type
  413. Config* = OrderedTableRef[string, <//>OrderedTableRef[string, string]]
  414. proc newConfig*(): Config =
  415. ## Create a new configuration table.
  416. ## Useful when wanting to create a configuration file.
  417. result = newOrderedTable[string, <//>OrderedTableRef[string, string]]()
  418. proc loadConfig*(stream: Stream, filename: string = "[stream]"): <//>Config =
  419. ## Load the specified configuration from stream into a new Config instance.
  420. ## `filename` parameter is only used for nicer error messages.
  421. var dict = newOrderedTable[string, <//>OrderedTableRef[string, string]]()
  422. var curSection = "" ## Current section,
  423. ## the default value of the current section is "",
  424. ## which means that the current section is a common
  425. var p: CfgParser
  426. open(p, stream, filename)
  427. while true:
  428. var e = next(p)
  429. case e.kind
  430. of cfgEof:
  431. break
  432. of cfgSectionStart: # Only look for the first time the Section
  433. curSection = e.section
  434. of cfgKeyValuePair:
  435. var t = newOrderedTable[string, string]()
  436. if dict.hasKey(curSection):
  437. t = dict[curSection]
  438. t[e.key] = e.value
  439. dict[curSection] = t
  440. of cfgOption:
  441. var c = newOrderedTable[string, string]()
  442. if dict.hasKey(curSection):
  443. c = dict[curSection]
  444. c["--" & e.key] = e.value
  445. dict[curSection] = c
  446. of cfgError:
  447. break
  448. close(p)
  449. result = dict
  450. proc loadConfig*(filename: string): <//>Config =
  451. ## Load the specified configuration file into a new Config instance.
  452. let file = open(filename, fmRead)
  453. let fileStream = newFileStream(file)
  454. defer: fileStream.close()
  455. result = fileStream.loadConfig(filename)
  456. proc replace(s: string): string =
  457. var d = ""
  458. var i = 0
  459. while i < s.len():
  460. if s[i] == '\\':
  461. d.add(r"\\")
  462. elif s[i] == '\c' and s[i+1] == '\L':
  463. d.add(r"\n")
  464. inc(i)
  465. elif s[i] == '\c':
  466. d.add(r"\n")
  467. elif s[i] == '\L':
  468. d.add(r"\n")
  469. else:
  470. d.add(s[i])
  471. inc(i)
  472. result = d
  473. proc writeConfig*(dict: Config, stream: Stream) =
  474. ## Writes the contents of the table to the specified stream
  475. ##
  476. ## **Note:** Comment statement will be ignored.
  477. for section, sectionData in dict.pairs():
  478. if section != "": ## Not general section
  479. if not allCharsInSet(section, SymChars): ## Non system character
  480. stream.writeLine("[\"" & section & "\"]")
  481. else:
  482. stream.writeLine("[" & section & "]")
  483. for key, value in sectionData.pairs():
  484. var kv, segmentChar: string
  485. if key.len > 1 and key[0] == '-' and key[1] == '-': ## If it is a command key
  486. segmentChar = ":"
  487. if not allCharsInSet(key[2..key.len()-1], SymChars):
  488. kv.add("--\"")
  489. kv.add(key[2..key.len()-1])
  490. kv.add("\"")
  491. else:
  492. kv = key
  493. else:
  494. segmentChar = "="
  495. kv = key
  496. if value != "": ## If the key is not empty
  497. if not allCharsInSet(value, SymChars):
  498. if find(value, '"') == -1:
  499. kv.add(segmentChar)
  500. kv.add("\"")
  501. kv.add(replace(value))
  502. kv.add("\"")
  503. else:
  504. kv.add(segmentChar)
  505. kv.add("\"\"\"")
  506. kv.add(replace(value))
  507. kv.add("\"\"\"")
  508. else:
  509. kv.add(segmentChar)
  510. kv.add(value)
  511. stream.writeLine(kv)
  512. proc `$`*(dict: Config): string =
  513. ## Writes the contents of the table to string.
  514. ## Note: Comment statement will be ignored.
  515. let stream = newStringStream()
  516. defer: stream.close()
  517. dict.writeConfig(stream)
  518. result = stream.data
  519. proc writeConfig*(dict: Config, filename: string) =
  520. ## Writes the contents of the table to the specified configuration file.
  521. ## Note: Comment statement will be ignored.
  522. let file = open(filename, fmWrite)
  523. defer: file.close()
  524. let fileStream = newFileStream(file)
  525. dict.writeConfig(fileStream)
  526. proc getSectionValue*(dict: Config, section, key: string): string =
  527. ## Gets the Key value of the specified Section, returns an empty string if the key does not exist.
  528. if dict.hasKey(section):
  529. if dict[section].hasKey(key):
  530. result = dict[section][key]
  531. else:
  532. result = ""
  533. else:
  534. result = ""
  535. proc setSectionKey*(dict: var Config, section, key, value: string) =
  536. ## Sets the Key value of the specified Section.
  537. var t = newOrderedTable[string, string]()
  538. if dict.hasKey(section):
  539. t = dict[section]
  540. t[key] = value
  541. dict[section] = t
  542. proc delSection*(dict: var Config, section: string) =
  543. ## Deletes the specified section and all of its sub keys.
  544. dict.del(section)
  545. proc delSectionKey*(dict: var Config, section, key: string) =
  546. ## Delete the key of the specified section.
  547. if dict.hasKey(section):
  548. if dict[section].hasKey(key):
  549. if dict[section].len == 1:
  550. dict.del(section)
  551. else:
  552. dict[section].del(key)