parsecfg.nim 18 KB

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