mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-29 17:34:43 +00:00
652 lines
20 KiB
Nim
652 lines
20 KiB
Nim
#
|
|
#
|
|
# Nim's Runtime Library
|
|
# (c) Copyright 2010 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
## The `parsecfg` module implements a high performance configuration file
|
|
## parser. The configuration file's syntax is similar to the Windows `.ini`
|
|
## format, but much more powerful, as it is not a line based parser. String
|
|
## literals, raw string literals and triple quoted string literals are supported
|
|
## as in the Nim programming language.
|
|
##
|
|
## Example of how a configuration file may look like:
|
|
##
|
|
## .. include:: ../../doc/mytest.cfg
|
|
## :literal:
|
|
##
|
|
## Here is an example of how to use the configuration file parser:
|
|
##
|
|
## .. code-block:: nim
|
|
##
|
|
## import std/[os, parsecfg, strutils, streams]
|
|
##
|
|
## var f = newFileStream(paramStr(1), fmRead)
|
|
## if f != nil:
|
|
## var p: CfgParser
|
|
## open(p, f, paramStr(1))
|
|
## while true:
|
|
## var e = next(p)
|
|
## case e.kind
|
|
## of cfgEof: break
|
|
## of cfgSectionStart: ## a `[section]` has been parsed
|
|
## echo("new section: " & e.section)
|
|
## of cfgKeyValuePair:
|
|
## echo("key-value-pair: " & e.key & ": " & e.value)
|
|
## of cfgOption:
|
|
## echo("command: " & e.key & ": " & e.value)
|
|
## of cfgError:
|
|
## echo(e.msg)
|
|
## close(p)
|
|
## else:
|
|
## echo("cannot open: " & paramStr(1))
|
|
##
|
|
##
|
|
## Examples
|
|
## ========
|
|
##
|
|
## Configuration file example
|
|
## --------------------------
|
|
##
|
|
## .. code-block:: nim
|
|
##
|
|
## charset = "utf-8"
|
|
## [Package]
|
|
## name = "hello"
|
|
## --threads:on
|
|
## [Author]
|
|
## name = "lihf8515"
|
|
## qq = "10214028"
|
|
## email = "lihaifeng@wxm.com"
|
|
##
|
|
## Creating a configuration file
|
|
## -----------------------------
|
|
## .. code-block:: nim
|
|
##
|
|
## import std/parsecfg
|
|
## var dict=newConfig()
|
|
## dict.setSectionKey("","charset","utf-8")
|
|
## dict.setSectionKey("Package","name","hello")
|
|
## dict.setSectionKey("Package","--threads","on")
|
|
## dict.setSectionKey("Author","name","lihf8515")
|
|
## dict.setSectionKey("Author","qq","10214028")
|
|
## dict.setSectionKey("Author","email","lihaifeng@wxm.com")
|
|
## dict.writeConfig("config.ini")
|
|
##
|
|
## Reading a configuration file
|
|
## ----------------------------
|
|
## .. code-block:: nim
|
|
##
|
|
## import std/parsecfg
|
|
## var dict = loadConfig("config.ini")
|
|
## var charset = dict.getSectionValue("","charset")
|
|
## var threads = dict.getSectionValue("Package","--threads")
|
|
## var pname = dict.getSectionValue("Package","name")
|
|
## var name = dict.getSectionValue("Author","name")
|
|
## var qq = dict.getSectionValue("Author","qq")
|
|
## var email = dict.getSectionValue("Author","email")
|
|
## echo pname & "\n" & name & "\n" & qq & "\n" & email
|
|
##
|
|
## Modifying a configuration file
|
|
## ------------------------------
|
|
## .. code-block:: nim
|
|
##
|
|
## import std/parsecfg
|
|
## var dict = loadConfig("config.ini")
|
|
## dict.setSectionKey("Author","name","lhf")
|
|
## dict.writeConfig("config.ini")
|
|
##
|
|
## Deleting a section key in a configuration file
|
|
## ----------------------------------------------
|
|
## .. code-block:: nim
|
|
##
|
|
## import std/parsecfg
|
|
## var dict = loadConfig("config.ini")
|
|
## dict.delSectionKey("Author","email")
|
|
## dict.writeConfig("config.ini")
|
|
##
|
|
## Supported INI File structure
|
|
## ----------------------------
|
|
## The examples below are supported:
|
|
##
|
|
|
|
# taken from https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
|
|
runnableExamples:
|
|
import std/streams
|
|
|
|
var dict = loadConfig(newStringStream("""[Simple Values]
|
|
key=value
|
|
spaces in keys=allowed
|
|
spaces in values=allowed as well
|
|
spaces around the delimiter = obviously
|
|
you can also use : to delimit keys from values
|
|
[All Values Are Strings]
|
|
values like this: 19990429
|
|
or this: 3.14159265359
|
|
are they treated as numbers : no
|
|
integers floats and booleans are held as: strings
|
|
can use the API to get converted values directly: true
|
|
[No Values]
|
|
key_without_value
|
|
# empty string value is not allowed =
|
|
[ Seletion A ]
|
|
space around section name will be ignored
|
|
[You can use comments]
|
|
# like this
|
|
; or this
|
|
# By default only in an empty line.
|
|
# Inline comments can be harmful because they prevent users
|
|
# from using the delimiting characters as parts of values.
|
|
# That being said, this can be customized.
|
|
[Sections Can Be Indented]
|
|
can_values_be_as_well = True
|
|
does_that_mean_anything_special = False
|
|
purpose = formatting for readability
|
|
# Did I mention we can indent comments, too?
|
|
""")
|
|
)
|
|
|
|
let section1 = "Simple Values"
|
|
doAssert dict.getSectionValue(section1, "key") == "value"
|
|
doAssert dict.getSectionValue(section1, "spaces in keys") == "allowed"
|
|
doAssert dict.getSectionValue(section1, "spaces in values") == "allowed as well"
|
|
doAssert dict.getSectionValue(section1, "spaces around the delimiter") == "obviously"
|
|
doAssert dict.getSectionValue(section1, "you can also use") == "to delimit keys from values"
|
|
|
|
let section2 = "All Values Are Strings"
|
|
doAssert dict.getSectionValue(section2, "values like this") == "19990429"
|
|
doAssert dict.getSectionValue(section2, "or this") == "3.14159265359"
|
|
doAssert dict.getSectionValue(section2, "are they treated as numbers") == "no"
|
|
doAssert dict.getSectionValue(section2, "integers floats and booleans are held as") == "strings"
|
|
doAssert dict.getSectionValue(section2, "can use the API to get converted values directly") == "true"
|
|
|
|
let section3 = "Seletion A"
|
|
doAssert dict.getSectionValue(section3,
|
|
"space around section name will be ignored", "not an empty value") == ""
|
|
|
|
let section4 = "Sections Can Be Indented"
|
|
doAssert dict.getSectionValue(section4, "can_values_be_as_well") == "True"
|
|
doAssert dict.getSectionValue(section4, "does_that_mean_anything_special") == "False"
|
|
doAssert dict.getSectionValue(section4, "purpose") == "formatting for readability"
|
|
|
|
import strutils, lexbase, streams, tables
|
|
import std/private/decode_helpers
|
|
|
|
include "system/inclrtl"
|
|
|
|
type
|
|
CfgEventKind* = enum ## enumeration of all events that may occur when parsing
|
|
cfgEof, ## end of file reached
|
|
cfgSectionStart, ## a `[section]` has been parsed
|
|
cfgKeyValuePair, ## a `key=value` pair has been detected
|
|
cfgOption, ## a `--key=value` command line option
|
|
cfgError ## an error occurred during parsing
|
|
|
|
CfgEvent* = object of RootObj ## describes a parsing event
|
|
case kind*: CfgEventKind ## the kind of the event
|
|
of cfgEof: nil
|
|
of cfgSectionStart:
|
|
section*: string ## `section` contains the name of the
|
|
## parsed section start (syntax: `[section]`)
|
|
of cfgKeyValuePair, cfgOption:
|
|
key*, value*: string ## contains the (key, value) pair if an option
|
|
## of the form `--key: value` or an ordinary
|
|
## `key= value` pair has been parsed.
|
|
## `value==""` if it was not specified in the
|
|
## configuration file.
|
|
of cfgError: ## the parser encountered an error: `msg`
|
|
msg*: string ## contains the error message. No exceptions
|
|
## are thrown if a parse error occurs.
|
|
|
|
TokKind = enum
|
|
tkInvalid, tkEof,
|
|
tkSymbol, tkEquals, tkColon, tkBracketLe, tkBracketRi, tkDashDash
|
|
Token = object # a token
|
|
kind: TokKind # the type of the token
|
|
literal: string # the parsed (string) literal
|
|
|
|
CfgParser* = object of BaseLexer ## the parser object.
|
|
tok: Token
|
|
filename: string
|
|
|
|
# implementation
|
|
|
|
const
|
|
SymChars = {'a'..'z', 'A'..'Z', '0'..'9', '_', ' ', '\x80'..'\xFF', '.', '/', '\\', '-'}
|
|
|
|
proc rawGetTok(c: var CfgParser, tok: var Token) {.gcsafe.}
|
|
|
|
proc open*(c: var CfgParser, input: Stream, filename: string,
|
|
lineOffset = 0) {.rtl, extern: "npc$1".} =
|
|
## Initializes the parser with an input stream. `Filename` is only used
|
|
## for nice error messages. `lineOffset` can be used to influence the line
|
|
## number information in the generated error messages.
|
|
lexbase.open(c, input)
|
|
c.filename = filename
|
|
c.tok.kind = tkInvalid
|
|
c.tok.literal = ""
|
|
inc(c.lineNumber, lineOffset)
|
|
rawGetTok(c, c.tok)
|
|
|
|
proc close*(c: var CfgParser) {.rtl, extern: "npc$1".} =
|
|
## Closes the parser `c` and its associated input stream.
|
|
lexbase.close(c)
|
|
|
|
proc getColumn*(c: CfgParser): int {.rtl, extern: "npc$1".} =
|
|
## Gets the current column the parser has arrived at.
|
|
result = getColNumber(c, c.bufpos)
|
|
|
|
proc getLine*(c: CfgParser): int {.rtl, extern: "npc$1".} =
|
|
## Gets the current line the parser has arrived at.
|
|
result = c.lineNumber
|
|
|
|
proc getFilename*(c: CfgParser): string {.rtl, extern: "npc$1".} =
|
|
## Gets the filename of the file that the parser processes.
|
|
result = c.filename
|
|
|
|
proc handleDecChars(c: var CfgParser, xi: var int) =
|
|
while c.buf[c.bufpos] in {'0'..'9'}:
|
|
xi = (xi * 10) + (ord(c.buf[c.bufpos]) - ord('0'))
|
|
inc(c.bufpos)
|
|
|
|
proc getEscapedChar(c: var CfgParser, tok: var Token) =
|
|
inc(c.bufpos) # skip '\'
|
|
case c.buf[c.bufpos]
|
|
of 'n', 'N':
|
|
add(tok.literal, "\n")
|
|
inc(c.bufpos)
|
|
of 'r', 'R', 'c', 'C':
|
|
add(tok.literal, '\c')
|
|
inc(c.bufpos)
|
|
of 'l', 'L':
|
|
add(tok.literal, '\L')
|
|
inc(c.bufpos)
|
|
of 'f', 'F':
|
|
add(tok.literal, '\f')
|
|
inc(c.bufpos)
|
|
of 'e', 'E':
|
|
add(tok.literal, '\e')
|
|
inc(c.bufpos)
|
|
of 'a', 'A':
|
|
add(tok.literal, '\a')
|
|
inc(c.bufpos)
|
|
of 'b', 'B':
|
|
add(tok.literal, '\b')
|
|
inc(c.bufpos)
|
|
of 'v', 'V':
|
|
add(tok.literal, '\v')
|
|
inc(c.bufpos)
|
|
of 't', 'T':
|
|
add(tok.literal, '\t')
|
|
inc(c.bufpos)
|
|
of '\'', '"':
|
|
add(tok.literal, c.buf[c.bufpos])
|
|
inc(c.bufpos)
|
|
of '\\':
|
|
add(tok.literal, '\\')
|
|
inc(c.bufpos)
|
|
of 'x', 'X':
|
|
inc(c.bufpos)
|
|
var xi = 0
|
|
if handleHexChar(c.buf[c.bufpos], xi):
|
|
inc(c.bufpos)
|
|
if handleHexChar(c.buf[c.bufpos], xi):
|
|
inc(c.bufpos)
|
|
add(tok.literal, chr(xi))
|
|
of '0'..'9':
|
|
var xi = 0
|
|
handleDecChars(c, xi)
|
|
if (xi <= 255): add(tok.literal, chr(xi))
|
|
else: tok.kind = tkInvalid
|
|
else: tok.kind = tkInvalid
|
|
|
|
proc handleCRLF(c: var CfgParser, pos: int): int =
|
|
case c.buf[pos]
|
|
of '\c': result = lexbase.handleCR(c, pos)
|
|
of '\L': result = lexbase.handleLF(c, pos)
|
|
else: result = pos
|
|
|
|
proc getString(c: var CfgParser, tok: var Token, rawMode: bool) =
|
|
var pos = c.bufpos + 1 # skip "
|
|
tok.kind = tkSymbol
|
|
if (c.buf[pos] == '"') and (c.buf[pos + 1] == '"'):
|
|
# long string literal:
|
|
inc(pos, 2) # skip ""
|
|
# skip leading newline:
|
|
pos = handleCRLF(c, pos)
|
|
while true:
|
|
case c.buf[pos]
|
|
of '"':
|
|
if (c.buf[pos + 1] == '"') and (c.buf[pos + 2] == '"'): break
|
|
add(tok.literal, '"')
|
|
inc(pos)
|
|
of '\c', '\L':
|
|
pos = handleCRLF(c, pos)
|
|
add(tok.literal, "\n")
|
|
of lexbase.EndOfFile:
|
|
tok.kind = tkInvalid
|
|
break
|
|
else:
|
|
add(tok.literal, c.buf[pos])
|
|
inc(pos)
|
|
c.bufpos = pos + 3 # skip the three """
|
|
else:
|
|
# ordinary string literal
|
|
while true:
|
|
var ch = c.buf[pos]
|
|
if ch == '"':
|
|
inc(pos) # skip '"'
|
|
break
|
|
if ch in {'\c', '\L', lexbase.EndOfFile}:
|
|
tok.kind = tkInvalid
|
|
break
|
|
if (ch == '\\') and not rawMode:
|
|
c.bufpos = pos
|
|
getEscapedChar(c, tok)
|
|
pos = c.bufpos
|
|
else:
|
|
add(tok.literal, ch)
|
|
inc(pos)
|
|
c.bufpos = pos
|
|
|
|
proc getSymbol(c: var CfgParser, tok: var Token) =
|
|
var pos = c.bufpos
|
|
while true:
|
|
add(tok.literal, c.buf[pos])
|
|
inc(pos)
|
|
if not (c.buf[pos] in SymChars): break
|
|
|
|
while tok.literal.len > 0 and tok.literal[^1] == ' ':
|
|
tok.literal.setLen(tok.literal.len - 1)
|
|
|
|
c.bufpos = pos
|
|
tok.kind = tkSymbol
|
|
|
|
proc skip(c: var CfgParser) =
|
|
var pos = c.bufpos
|
|
while true:
|
|
case c.buf[pos]
|
|
of ' ', '\t':
|
|
inc(pos)
|
|
of '#', ';':
|
|
while not (c.buf[pos] in {'\c', '\L', lexbase.EndOfFile}): inc(pos)
|
|
of '\c', '\L':
|
|
pos = handleCRLF(c, pos)
|
|
else:
|
|
break # EndOfFile also leaves the loop
|
|
c.bufpos = pos
|
|
|
|
proc rawGetTok(c: var CfgParser, tok: var Token) =
|
|
tok.kind = tkInvalid
|
|
setLen(tok.literal, 0)
|
|
skip(c)
|
|
case c.buf[c.bufpos]
|
|
of '=':
|
|
tok.kind = tkEquals
|
|
inc(c.bufpos)
|
|
tok.literal = "="
|
|
of '-':
|
|
inc(c.bufpos)
|
|
if c.buf[c.bufpos] == '-':
|
|
inc(c.bufpos)
|
|
tok.kind = tkDashDash
|
|
tok.literal = "--"
|
|
else:
|
|
dec(c.bufpos)
|
|
getSymbol(c, tok)
|
|
of ':':
|
|
tok.kind = tkColon
|
|
inc(c.bufpos)
|
|
tok.literal = ":"
|
|
of 'r', 'R':
|
|
if c.buf[c.bufpos + 1] == '\"':
|
|
inc(c.bufpos)
|
|
getString(c, tok, true)
|
|
else:
|
|
getSymbol(c, tok)
|
|
of '[':
|
|
tok.kind = tkBracketLe
|
|
inc(c.bufpos)
|
|
tok.literal = "["
|
|
of ']':
|
|
tok.kind = tkBracketRi
|
|
inc(c.bufpos)
|
|
tok.literal = "]"
|
|
of '"':
|
|
getString(c, tok, false)
|
|
of lexbase.EndOfFile:
|
|
tok.kind = tkEof
|
|
tok.literal = "[EOF]"
|
|
else: getSymbol(c, tok)
|
|
|
|
proc errorStr*(c: CfgParser, msg: string): string {.rtl, extern: "npc$1".} =
|
|
## Returns a properly formatted error message containing current line and
|
|
## column information.
|
|
result = `%`("$1($2, $3) Error: $4",
|
|
[c.filename, $getLine(c), $getColumn(c), msg])
|
|
|
|
proc warningStr*(c: CfgParser, msg: string): string {.rtl, extern: "npc$1".} =
|
|
## Returns a properly formatted warning message containing current line and
|
|
## column information.
|
|
result = `%`("$1($2, $3) Warning: $4",
|
|
[c.filename, $getLine(c), $getColumn(c), msg])
|
|
|
|
proc ignoreMsg*(c: CfgParser, e: CfgEvent): string {.rtl, extern: "npc$1".} =
|
|
## Returns a properly formatted warning message containing that
|
|
## an entry is ignored.
|
|
case e.kind
|
|
of cfgSectionStart: result = c.warningStr("section ignored: " & e.section)
|
|
of cfgKeyValuePair: result = c.warningStr("key ignored: " & e.key)
|
|
of cfgOption:
|
|
result = c.warningStr("command ignored: " & e.key & ": " & e.value)
|
|
of cfgError: result = e.msg
|
|
of cfgEof: result = ""
|
|
|
|
proc getKeyValPair(c: var CfgParser, kind: CfgEventKind): CfgEvent =
|
|
if c.tok.kind == tkSymbol:
|
|
case kind
|
|
of cfgOption, cfgKeyValuePair:
|
|
result = CfgEvent(kind: kind, key: c.tok.literal, value: "")
|
|
else: discard
|
|
rawGetTok(c, c.tok)
|
|
if c.tok.kind in {tkEquals, tkColon}:
|
|
rawGetTok(c, c.tok)
|
|
if c.tok.kind == tkSymbol:
|
|
result.value = c.tok.literal
|
|
else:
|
|
result = CfgEvent(kind: cfgError,
|
|
msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
|
|
rawGetTok(c, c.tok)
|
|
else:
|
|
result = CfgEvent(kind: cfgError,
|
|
msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
|
|
rawGetTok(c, c.tok)
|
|
|
|
proc next*(c: var CfgParser): CfgEvent {.rtl, extern: "npc$1".} =
|
|
## Retrieves the first/next event. This controls the parser.
|
|
case c.tok.kind
|
|
of tkEof:
|
|
result = CfgEvent(kind: cfgEof)
|
|
of tkDashDash:
|
|
rawGetTok(c, c.tok)
|
|
result = getKeyValPair(c, cfgOption)
|
|
of tkSymbol:
|
|
result = getKeyValPair(c, cfgKeyValuePair)
|
|
of tkBracketLe:
|
|
rawGetTok(c, c.tok)
|
|
if c.tok.kind == tkSymbol:
|
|
result = CfgEvent(kind: cfgSectionStart, section: c.tok.literal)
|
|
else:
|
|
result = CfgEvent(kind: cfgError,
|
|
msg: errorStr(c, "symbol expected, but found: " & c.tok.literal))
|
|
rawGetTok(c, c.tok)
|
|
if c.tok.kind == tkBracketRi:
|
|
rawGetTok(c, c.tok)
|
|
else:
|
|
result = CfgEvent(kind: cfgError,
|
|
msg: errorStr(c, "']' expected, but found: " & c.tok.literal))
|
|
of tkInvalid, tkEquals, tkColon, tkBracketRi:
|
|
result = CfgEvent(kind: cfgError,
|
|
msg: errorStr(c, "invalid token: " & c.tok.literal))
|
|
rawGetTok(c, c.tok)
|
|
|
|
# ---------------- Configuration file related operations ----------------
|
|
type
|
|
Config* = OrderedTableRef[string, <//>OrderedTableRef[string, string]]
|
|
|
|
proc newConfig*(): Config =
|
|
## Creates a new configuration table.
|
|
## Useful when wanting to create a configuration file.
|
|
result = newOrderedTable[string, <//>OrderedTableRef[string, string]]()
|
|
|
|
proc loadConfig*(stream: Stream, filename: string = "[stream]"): <//>Config =
|
|
## Loads the specified configuration from stream into a new Config instance.
|
|
## `filename` parameter is only used for nicer error messages.
|
|
var dict = newOrderedTable[string, <//>OrderedTableRef[string, string]]()
|
|
var curSection = "" ## Current section,
|
|
## the default value of the current section is "",
|
|
## which means that the current section is a common
|
|
var p: CfgParser
|
|
open(p, stream, filename)
|
|
while true:
|
|
var e = next(p)
|
|
case e.kind
|
|
of cfgEof:
|
|
break
|
|
of cfgSectionStart: # Only look for the first time the Section
|
|
curSection = e.section
|
|
of cfgKeyValuePair:
|
|
var t = newOrderedTable[string, string]()
|
|
if dict.hasKey(curSection):
|
|
t = dict[curSection]
|
|
t[e.key] = e.value
|
|
dict[curSection] = t
|
|
of cfgOption:
|
|
var c = newOrderedTable[string, string]()
|
|
if dict.hasKey(curSection):
|
|
c = dict[curSection]
|
|
c["--" & e.key] = e.value
|
|
dict[curSection] = c
|
|
of cfgError:
|
|
break
|
|
close(p)
|
|
result = dict
|
|
|
|
proc loadConfig*(filename: string): <//>Config =
|
|
## Loads the specified configuration file into a new Config instance.
|
|
let file = open(filename, fmRead)
|
|
let fileStream = newFileStream(file)
|
|
defer: fileStream.close()
|
|
result = fileStream.loadConfig(filename)
|
|
|
|
proc replace(s: string): string =
|
|
var d = ""
|
|
var i = 0
|
|
while i < s.len():
|
|
if s[i] == '\\':
|
|
d.add(r"\\")
|
|
elif s[i] == '\c' and s[i+1] == '\l':
|
|
d.add(r"\c\l")
|
|
inc(i)
|
|
elif s[i] == '\c':
|
|
d.add(r"\n")
|
|
elif s[i] == '\l':
|
|
d.add(r"\n")
|
|
else:
|
|
d.add(s[i])
|
|
inc(i)
|
|
result = d
|
|
|
|
proc writeConfig*(dict: Config, stream: Stream) =
|
|
## Writes the contents of the table to the specified stream.
|
|
##
|
|
## **Note:** Comment statement will be ignored.
|
|
for section, sectionData in dict.pairs():
|
|
if section != "": ## Not general section
|
|
if not allCharsInSet(section, SymChars): ## Non system character
|
|
stream.writeLine("[\"" & section & "\"]")
|
|
else:
|
|
stream.writeLine("[" & section & "]")
|
|
for key, value in sectionData.pairs():
|
|
var kv, segmentChar: string
|
|
if key.len > 1 and key[0] == '-' and key[1] == '-': ## If it is a command key
|
|
segmentChar = ":"
|
|
if not allCharsInSet(key[2..key.len()-1], SymChars):
|
|
kv.add("--\"")
|
|
kv.add(key[2..key.len()-1])
|
|
kv.add("\"")
|
|
else:
|
|
kv = key
|
|
else:
|
|
segmentChar = "="
|
|
kv = key
|
|
if value != "": ## If the key is not empty
|
|
if not allCharsInSet(value, SymChars):
|
|
if find(value, '"') == -1:
|
|
kv.add(segmentChar)
|
|
kv.add("\"")
|
|
kv.add(replace(value))
|
|
kv.add("\"")
|
|
else:
|
|
kv.add(segmentChar)
|
|
kv.add("\"\"\"")
|
|
kv.add(replace(value))
|
|
kv.add("\"\"\"")
|
|
else:
|
|
kv.add(segmentChar)
|
|
kv.add(value)
|
|
stream.writeLine(kv)
|
|
|
|
proc `$`*(dict: Config): string =
|
|
## Writes the contents of the table to string.
|
|
##
|
|
## **Note:** Comment statement will be ignored.
|
|
let stream = newStringStream()
|
|
defer: stream.close()
|
|
dict.writeConfig(stream)
|
|
result = stream.data
|
|
|
|
proc writeConfig*(dict: Config, filename: string) =
|
|
## Writes the contents of the table to the specified configuration file.
|
|
##
|
|
## **Note:** Comment statement will be ignored.
|
|
let file = open(filename, fmWrite)
|
|
defer: file.close()
|
|
let fileStream = newFileStream(file)
|
|
dict.writeConfig(fileStream)
|
|
|
|
proc getSectionValue*(dict: Config, section, key: string, defaultVal = ""): string =
|
|
## Gets the key value of the specified Section.
|
|
## Returns the specified default value if the specified key does not exist.
|
|
if dict.hasKey(section):
|
|
if dict[section].hasKey(key):
|
|
result = dict[section][key]
|
|
else:
|
|
result = defaultVal
|
|
else:
|
|
result = defaultVal
|
|
|
|
proc setSectionKey*(dict: var Config, section, key, value: string) =
|
|
## Sets the Key value of the specified Section.
|
|
var t = newOrderedTable[string, string]()
|
|
if dict.hasKey(section):
|
|
t = dict[section]
|
|
t[key] = value
|
|
dict[section] = t
|
|
|
|
proc delSection*(dict: var Config, section: string) =
|
|
## Deletes the specified section and all of its sub keys.
|
|
dict.del(section)
|
|
|
|
proc delSectionKey*(dict: var Config, section, key: string) =
|
|
## Deletes the key of the specified section.
|
|
if dict.hasKey(section):
|
|
if dict[section].hasKey(key):
|
|
if dict[section].len == 1:
|
|
dict.del(section)
|
|
else:
|
|
dict[section].del(key)
|