Created
February 6, 2026 08:56
-
-
Save planetis-m/fc832bcfa35c7356281c12730e11e3d4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # | |
| # | |
| # 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: | |
| runnableExamples("-r:off"): | |
| import std/strutils | |
| import jsonx/streams | |
| let configFile = "example.ini" | |
| var f: File | |
| doAssert open(f, configFile), "cannot open " & configFile | |
| let s = streams.open(f) | |
| var p: CfgParser | |
| open(p, s, configFile) | |
| 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) | |
| ##[ | |
| ## Configuration file example | |
| ]## | |
| ## ```none | |
| ## charset = "utf-8" | |
| ## [Package] | |
| ## name = "hello" | |
| ## --threads:on | |
| ## [Author] | |
| ## name = "nim-lang" | |
| ## website = "nim-lang.org" | |
| ## ``` | |
| ##[ | |
| ## Supported INI File structure | |
| ]## | |
| import jsonx/[lexbase, streams] | |
| import std/strformat | |
| import std/private/decode_helpers | |
| when defined(nimPreviewSlimSystem): | |
| import std/syncio | |
| 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: discard | |
| 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, raises: [ValueError, OSError, IOError].} | |
| proc open*(c: var CfgParser, input: Stream, filename: string, | |
| lineOffset = 0) = | |
| ## 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) = | |
| ## Closes the parser `c` and its associated input stream. | |
| lexbase.close(c) | |
| proc getColumn*(c: CfgParser): int = | |
| ## Gets the current column the parser has arrived at. | |
| result = getColNumber(c, c.bufpos) | |
| proc getLine*(c: CfgParser): int = | |
| ## Gets the current line the parser has arrived at. | |
| result = c.lineNumber | |
| proc getFilename*(c: CfgParser): string = | |
| ## 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 = | |
| ## Returns a properly formatted error message containing current line and | |
| ## column information. | |
| &"{c.filename}({getLine(c)}, {getColumn(c)}) Error: {msg}" | |
| proc warningStr*(c: CfgParser, msg: string): string = | |
| ## Returns a properly formatted warning message containing current line and | |
| ## column information. | |
| &"{c.filename}({getLine(c)}, {getColumn(c)}) Warning: {msg}" | |
| proc ignoreMsg*(c: CfgParser, e: CfgEvent): string = | |
| ## 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.move, value: "") | |
| else: result = CfgEvent() | |
| 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 = | |
| ## 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.move) | |
| 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment