From 30309748a09081704e873ae33f76ad243843873d Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Fri, 14 Jun 2019 19:33:59 +0300 Subject: [PATCH] Some steps towards enabling the use of confutils in nimscript So far, a show-stopped Nim bug was discovered: https://github.com/nim-lang/Nim/issues/11502 --- confutils.nim | 154 +++++++++++++++++++++++++------------- confutils/cli_parser.nim | 157 +++++++++++++++++++++++++++++++++++++++ confutils/defs.nim | 2 - 3 files changed, 260 insertions(+), 53 deletions(-) create mode 100644 confutils/cli_parser.nim diff --git a/confutils.nim b/confutils.nim index 0e9707b..a404167 100644 --- a/confutils.nim +++ b/confutils.nim @@ -1,10 +1,13 @@ import - os, parseopt, strutils, options, std_shims/macros_shim, typetraits, terminal, - confutils/defs + strutils, options, std_shims/macros_shim, typetraits, + confutils/[defs, cli_parser] export defs +when not defined(nimscript): + import os, terminal + type CommandDesc = object name: string @@ -21,29 +24,65 @@ type fieldIdx: int desc: string -template appName: string = - getAppFilename().splitFile.name + CommandPtr = ptr CommandDesc + OptionPtr = ptr OptionDesc -when not defined(confutils_no_colors): +when defined(nimscript): + proc appInvocation: string = + "nim " & (if paramCount() > 1: paramStr(1) else: "") + + type stderr = object + + template writeLine(T: type stderr, msg: string) = + echo msg + + proc commandLineParams(): seq[string] = + for i in 2 .. paramCount(): + result.add paramStr(i) + + # TODO: Why isn't this available in NimScript? + proc getCurrentExceptionMsg(): string = + "" + +else: + template appInvocation: string = + getAppFilename().splitFile.name + +when defined(nimscript): + const styleBright = "" + + # Deal with the issue that `stdout` is not defined in nimscript + var buffer = "" + proc write(args: varargs[string, `$`]) = + for arg in args: + buffer.add arg + if args[^1][^1] == '\n': + buffer.setLen(buffer.len - 1) + echo buffer + buffer = "" + +elif not defined(confutils_no_colors): template write(args: varargs[untyped]) = stdout.styledWrite(args) + else: - const - styleBright = "" + const styleBright = "" template write(args: varargs[untyped]) = stdout.write(args) -when defined(debugCmdTree): - proc printCmdTree(cmd: CommandDesc, indent = 0) = - let blanks = repeat(' ', indent) - echo blanks, "> ", cmd.name - for opt in cmd.options: - echo blanks, " - ", opt.name, ": ", opt.typename - for subcmd in cmd.subCommands: - printCmdTree(subcmd, indent + 2) -else: - template printCmdTree(cmd: CommandDesc) = discard +template hasArguments(cmd: CommandPtr): bool = + cmd.argumentsFieldIdx != -1 + +template isSubCommand(cmd: CommandPtr): bool = + cmd.name.len > 0 + +proc noMoreArgumentsError(cmd: CommandPtr): string = + result = if cmd.isSubCommand: "The command '$1'" % [cmd.name] + else: appInvocation() + result.add " does not accept" + if cmd.hasArguments: result.add " additional" + result.add " arguments" proc describeCmdOptions(cmd: CommandDesc) = for opt in cmd.options: @@ -53,7 +92,7 @@ proc describeCmdOptions(cmd: CommandDesc) = write "\n" proc showHelp(version: string, cmd: CommandDesc) = - let app = appName + let app = appInvocation() write "Usage: ", styleBright, app if cmd.name.len > 0: write " ", cmd.name @@ -78,6 +117,33 @@ proc showHelp(version: string, cmd: CommandDesc) = write "\n" quit(0) +proc findOption(cmds: seq[CommandPtr], name: TaintedString): OptionPtr = + for i in countdown(cmds.len - 1, 0): + for o in cmds[i].options.mitems: + if cmpIgnoreStyle(o.name, string(name)) == 0 or + cmpIgnoreStyle(o.shortform, string(name)) == 0: + return addr(o) + + return nil + +proc findSubcommand(cmd: CommandPtr, name: TaintedString): CommandPtr = + for subCmd in cmd.subCommands.mitems: + if cmpIgnoreStyle(subCmd.name, string(name)) == 0: + return addr(subCmd) + + return nil + +when defined(debugCmdTree): + proc printCmdTree(cmd: CommandDesc, indent = 0) = + let blanks = repeat(' ', indent) + echo blanks, "> ", cmd.name + for opt in cmd.options: + echo blanks, " - ", opt.name, ": ", opt.typename + for subcmd in cmd.subCommands: + printCmdTree(subcmd, indent + 2) +else: + template printCmdTree(cmd: CommandDesc) = discard + # TODO remove the overloads here to get better "missing overload" error message proc parseCmdArg*(T: type InputDir, p: TaintedString): T = if not dirExists(p.string): @@ -92,11 +158,12 @@ proc parseCmdArg*(T: type InputFile, p: TaintedString): T = if not fileExists(p.string): raise newException(ValueError, "File doesn't exist") - try: - let f = open(p.string, fmRead) - close f - except IOError: - raise newException(ValueError, "File not accessible") + when not defined(nimscript): + try: + let f = open(p.string, fmRead) + close f + except IOError: + raise newException(ValueError, "File not accessible") result = T(p.string) @@ -108,11 +175,12 @@ proc parseCmdArg*(T: type TypedInputFile, p: TaintedString): T = if not fileExists(path): raise newException(ValueError, "File doesn't exist") - try: - let f = open(path, fmRead) - close f - except IOError: - raise newException(ValueError, "File not accessible") + when not defined(nimscript): + try: + let f = open(path, fmRead) + close f + except IOError: + raise newException(ValueError, "File not accessible") result = T(path) @@ -317,27 +385,11 @@ proc load*(Configuration: type, proc fail(msg: string) = if quitOnFailure: stderr.writeLine(msg) - stderr.writeLine("Try '$1 --help' for more information" % appName) + stderr.writeLine("Try '$1 --help' for more information" % appInvocation()) quit 1 else: raise newException(ConfigurationError, msg) - proc findOption(cmds: seq[ptr CommandDesc], name: TaintedString): ptr OptionDesc = - for i in countdown(cmds.len - 1, 0): - for o in cmds[i].options.mitems: - if cmpIgnoreStyle(o.name, string(name)) == 0 or - cmpIgnoreStyle(o.shortform, string(name)) == 0: - return addr(o) - - return nil - - proc findSubcommand(cmd: ptr CommandDesc, name: TaintedString): ptr CommandDesc = - for subCmd in cmd.subCommands.mitems: - if cmpIgnoreStyle(subCmd.name, string(name)) == 0: - return addr(subCmd) - - return nil - template applySetter(setterIdx: int, cmdLineVal: TaintedString): bool = var r: bool try: @@ -350,7 +402,7 @@ proc load*(Configuration: type, template required(opt: OptionDesc): bool = fieldSetters[opt.fieldIdx][2] and not opt.hasDefault - proc processMissingOptions(conf: var Configuration, cmd: ptr CommandDesc) = + proc processMissingOptions(conf: var Configuration, cmd: CommandPtr) = for o in cmd.options: if o.rejectNext == false: if o.required: @@ -358,12 +410,12 @@ proc load*(Configuration: type, elif o.hasDefault: discard fieldSetters[o.fieldIdx][1](conf, TaintedString("")) - template activateCmd(activatedCmd: ptr CommandDesc, key: TaintedString) = + template activateCmd(activatedCmd: CommandPtr, key: TaintedString) = let cmd = activatedCmd discard applySetter(cmd.fieldIdx, key) lastCmd.defaultSubCommand = -1 activeCmds.add cmd - rejectNextArgument = cmd.argumentsFieldIdx == -1 + rejectNextArgument = not cmd.hasArguments for kind, key, val in getopt(cmdLine): case kind @@ -397,7 +449,8 @@ proc load*(Configuration: type, activateCmd(subCmd, key) else: if rejectNextArgument: - fail "The command '$1' does not accept additional arguments" % [lastCmd.name] + fail lastCmd.noMoreArgumentsError + let argumentIdx = lastCmd.argumentsFieldIdx doAssert argumentIdx != -1 rejectNextArgument = applySetter(argumentIdx, key) @@ -415,8 +468,7 @@ proc defaults*(Configuration: type): Configuration = proc dispatchImpl(cliProcSym, cliArgs, loadArgs: NimNode): NimNode = # Here, we'll create a configuration object with fields matching - # the CLI proc params. We'll also generate a call to the designated - # p + # the CLI proc params. We'll also generate a call to the designated proc let configType = genSym(nskType, "CliConfig") let configFields = newTree(nnkRecList) let configVar = genSym(nskLet, "config") diff --git a/confutils/cli_parser.nim b/confutils/cli_parser.nim new file mode 100644 index 0000000..3bdce9e --- /dev/null +++ b/confutils/cli_parser.nim @@ -0,0 +1,157 @@ +# Copyright 2018 Status Research & Development GmbH +# Parts taken from Nim's Runtime Library (c) Copyright 2015 Andreas Rumpf + +import + strutils + +type + CmdLineKind* = enum ## The detected command line token. + cmdEnd, ## End of command line reached + cmdArgument, ## An argument such as a filename + cmdLongOption, ## A long option such as --option + cmdShortOption ## A short option such as -c + + OptParser* = object of RootObj ## Implementation of the command line parser. + pos*: int + inShortState: bool + allowWhitespaceAfterColon: bool + shortNoVal: set[char] + longNoVal: seq[string] + cmds: seq[string] + idx: int + kind*: CmdLineKind ## The detected command line token + key*, val*: TaintedString ## Key and value pair; the key is the option + ## or the argument, and the value is not "" if + ## the option was given a value + +proc parseWord(s: string, i: int, w: var string, + delim: set[char] = {'\t', ' '}): int = + result = i + if result < s.len and s[result] == '\"': + inc(result) + while result < s.len: + if s[result] == '"': + inc result + break + add(w, s[result]) + inc(result) + else: + while result < s.len and s[result] notin delim: + add(w, s[result]) + inc(result) + +proc initOptParser*(cmds: seq[string], shortNoVal: set[char]={}, + longNoVal: seq[string] = @[]; + allowWhitespaceAfterColon = true): OptParser = + result.pos = 0 + result.idx = 0 + result.inShortState = false + result.shortNoVal = shortNoVal + result.longNoVal = longNoVal + result.allowWhitespaceAfterColon = allowWhitespaceAfterColon + result.cmds = cmds + result.kind = cmdEnd + result.key = TaintedString"" + result.val = TaintedString"" + +proc handleShortOption(p: var OptParser; cmd: string) = + var i = p.pos + p.kind = cmdShortOption + if i < cmd.len: + add(p.key.string, cmd[i]) + inc(i) + p.inShortState = true + while i < cmd.len and cmd[i] in {'\t', ' '}: + inc(i) + p.inShortState = false + if i < cmd.len and cmd[i] in {':', '='} or + card(p.shortNoVal) > 0 and p.key.string[0] notin p.shortNoVal: + if i < cmd.len and cmd[i] in {':', '='}: + inc(i) + p.inShortState = false + while i < cmd.len and cmd[i] in {'\t', ' '}: inc(i) + p.val = TaintedString substr(cmd, i) + p.pos = 0 + inc p.idx + else: + p.pos = i + if i >= cmd.len: + p.inShortState = false + p.pos = 0 + inc p.idx + +proc next*(p: var OptParser) = + ## Parses the next token. + ## + ## ``p.kind`` describes what kind of token has been parsed. ``p.key`` and + ## ``p.val`` are set accordingly. + if p.idx >= p.cmds.len: + p.kind = cmdEnd + return + + var i = p.pos + while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i) + p.pos = i + setLen(p.key.string, 0) + setLen(p.val.string, 0) + if p.inShortState: + p.inShortState = false + if i >= p.cmds[p.idx].len: + inc(p.idx) + p.pos = 0 + if p.idx >= p.cmds.len: + p.kind = cmdEnd + return + else: + handleShortOption(p, p.cmds[p.idx]) + return + + if i < p.cmds[p.idx].len and p.cmds[p.idx][i] == '-': + inc(i) + if i < p.cmds[p.idx].len and p.cmds[p.idx][i] == '-': + p.kind = cmdLongOption + inc(i) + i = parseWord(p.cmds[p.idx], i, p.key, {' ', '\t', ':', '='}) + while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i) + if i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {':', '='}: + inc(i) + while i < p.cmds[p.idx].len and p.cmds[p.idx][i] in {'\t', ' '}: inc(i) + # if we're at the end, use the next command line option: + if i >= p.cmds[p.idx].len and p.idx < p.cmds.len and p.allowWhitespaceAfterColon: + inc p.idx + i = 0 + if p.idx < p.cmds.len: + p.val = TaintedString p.cmds[p.idx].substr(i) + elif len(p.longNoVal) > 0 and p.key.string notin p.longNoVal and p.idx+1 < p.cmds.len: + p.val = TaintedString p.cmds[p.idx+1] + inc p.idx + else: + p.val = TaintedString"" + inc p.idx + p.pos = 0 + else: + p.pos = i + handleShortOption(p, p.cmds[p.idx]) + else: + p.kind = cmdArgument + p.key = TaintedString p.cmds[p.idx] + inc p.idx + p.pos = 0 + +iterator getopt*(p: var OptParser): tuple[kind: CmdLineKind, key, val: TaintedString] = + p.pos = 0 + p.idx = 0 + while true: + next(p) + if p.kind == cmdEnd: break + yield (p.kind, p.key, p.val) + +iterator getopt*(cmds: seq[string], + shortNoVal: set[char]={}, longNoVal: seq[string] = @[]): + tuple[kind: CmdLineKind, key, val: TaintedString] = + var p = initOptParser(cmds, shortNoVal=shortNoVal, longNoVal=longNoVal) + while true: + next(p) + if p.kind == cmdEnd: break + yield (p.kind, p.key, p.val) + diff --git a/confutils/defs.nim b/confutils/defs.nim index 7994087..496e924 100644 --- a/confutils/defs.nim +++ b/confutils/defs.nim @@ -1,5 +1,3 @@ -import os - type ConfigurationError* = object of CatchableError