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
This commit is contained in:
Zahary Karadjov 2019-06-14 19:33:59 +03:00
parent da24be1a9d
commit 30309748a0
3 changed files with 260 additions and 53 deletions

View File

@ -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: "<nims-script>")
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")

157
confutils/cli_parser.nim Normal file
View File

@ -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)

View File

@ -1,5 +1,3 @@
import os
type
ConfigurationError* = object of CatchableError