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:
parent
da24be1a9d
commit
30309748a0
154
confutils.nim
154
confutils.nim
|
@ -1,10 +1,13 @@
|
||||||
import
|
import
|
||||||
os, parseopt, strutils, options, std_shims/macros_shim, typetraits, terminal,
|
strutils, options, std_shims/macros_shim, typetraits,
|
||||||
confutils/defs
|
confutils/[defs, cli_parser]
|
||||||
|
|
||||||
export
|
export
|
||||||
defs
|
defs
|
||||||
|
|
||||||
|
when not defined(nimscript):
|
||||||
|
import os, terminal
|
||||||
|
|
||||||
type
|
type
|
||||||
CommandDesc = object
|
CommandDesc = object
|
||||||
name: string
|
name: string
|
||||||
|
@ -21,29 +24,65 @@ type
|
||||||
fieldIdx: int
|
fieldIdx: int
|
||||||
desc: string
|
desc: string
|
||||||
|
|
||||||
template appName: string =
|
CommandPtr = ptr CommandDesc
|
||||||
getAppFilename().splitFile.name
|
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]) =
|
template write(args: varargs[untyped]) =
|
||||||
stdout.styledWrite(args)
|
stdout.styledWrite(args)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
const
|
const styleBright = ""
|
||||||
styleBright = ""
|
|
||||||
|
|
||||||
template write(args: varargs[untyped]) =
|
template write(args: varargs[untyped]) =
|
||||||
stdout.write(args)
|
stdout.write(args)
|
||||||
|
|
||||||
when defined(debugCmdTree):
|
template hasArguments(cmd: CommandPtr): bool =
|
||||||
proc printCmdTree(cmd: CommandDesc, indent = 0) =
|
cmd.argumentsFieldIdx != -1
|
||||||
let blanks = repeat(' ', indent)
|
|
||||||
echo blanks, "> ", cmd.name
|
template isSubCommand(cmd: CommandPtr): bool =
|
||||||
for opt in cmd.options:
|
cmd.name.len > 0
|
||||||
echo blanks, " - ", opt.name, ": ", opt.typename
|
|
||||||
for subcmd in cmd.subCommands:
|
proc noMoreArgumentsError(cmd: CommandPtr): string =
|
||||||
printCmdTree(subcmd, indent + 2)
|
result = if cmd.isSubCommand: "The command '$1'" % [cmd.name]
|
||||||
else:
|
else: appInvocation()
|
||||||
template printCmdTree(cmd: CommandDesc) = discard
|
result.add " does not accept"
|
||||||
|
if cmd.hasArguments: result.add " additional"
|
||||||
|
result.add " arguments"
|
||||||
|
|
||||||
proc describeCmdOptions(cmd: CommandDesc) =
|
proc describeCmdOptions(cmd: CommandDesc) =
|
||||||
for opt in cmd.options:
|
for opt in cmd.options:
|
||||||
|
@ -53,7 +92,7 @@ proc describeCmdOptions(cmd: CommandDesc) =
|
||||||
write "\n"
|
write "\n"
|
||||||
|
|
||||||
proc showHelp(version: string, cmd: CommandDesc) =
|
proc showHelp(version: string, cmd: CommandDesc) =
|
||||||
let app = appName
|
let app = appInvocation()
|
||||||
|
|
||||||
write "Usage: ", styleBright, app
|
write "Usage: ", styleBright, app
|
||||||
if cmd.name.len > 0: write " ", cmd.name
|
if cmd.name.len > 0: write " ", cmd.name
|
||||||
|
@ -78,6 +117,33 @@ proc showHelp(version: string, cmd: CommandDesc) =
|
||||||
write "\n"
|
write "\n"
|
||||||
quit(0)
|
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
|
# TODO remove the overloads here to get better "missing overload" error message
|
||||||
proc parseCmdArg*(T: type InputDir, p: TaintedString): T =
|
proc parseCmdArg*(T: type InputDir, p: TaintedString): T =
|
||||||
if not dirExists(p.string):
|
if not dirExists(p.string):
|
||||||
|
@ -92,11 +158,12 @@ proc parseCmdArg*(T: type InputFile, p: TaintedString): T =
|
||||||
if not fileExists(p.string):
|
if not fileExists(p.string):
|
||||||
raise newException(ValueError, "File doesn't exist")
|
raise newException(ValueError, "File doesn't exist")
|
||||||
|
|
||||||
try:
|
when not defined(nimscript):
|
||||||
let f = open(p.string, fmRead)
|
try:
|
||||||
close f
|
let f = open(p.string, fmRead)
|
||||||
except IOError:
|
close f
|
||||||
raise newException(ValueError, "File not accessible")
|
except IOError:
|
||||||
|
raise newException(ValueError, "File not accessible")
|
||||||
|
|
||||||
result = T(p.string)
|
result = T(p.string)
|
||||||
|
|
||||||
|
@ -108,11 +175,12 @@ proc parseCmdArg*(T: type TypedInputFile, p: TaintedString): T =
|
||||||
if not fileExists(path):
|
if not fileExists(path):
|
||||||
raise newException(ValueError, "File doesn't exist")
|
raise newException(ValueError, "File doesn't exist")
|
||||||
|
|
||||||
try:
|
when not defined(nimscript):
|
||||||
let f = open(path, fmRead)
|
try:
|
||||||
close f
|
let f = open(path, fmRead)
|
||||||
except IOError:
|
close f
|
||||||
raise newException(ValueError, "File not accessible")
|
except IOError:
|
||||||
|
raise newException(ValueError, "File not accessible")
|
||||||
|
|
||||||
result = T(path)
|
result = T(path)
|
||||||
|
|
||||||
|
@ -317,27 +385,11 @@ proc load*(Configuration: type,
|
||||||
proc fail(msg: string) =
|
proc fail(msg: string) =
|
||||||
if quitOnFailure:
|
if quitOnFailure:
|
||||||
stderr.writeLine(msg)
|
stderr.writeLine(msg)
|
||||||
stderr.writeLine("Try '$1 --help' for more information" % appName)
|
stderr.writeLine("Try '$1 --help' for more information" % appInvocation())
|
||||||
quit 1
|
quit 1
|
||||||
else:
|
else:
|
||||||
raise newException(ConfigurationError, msg)
|
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 =
|
template applySetter(setterIdx: int, cmdLineVal: TaintedString): bool =
|
||||||
var r: bool
|
var r: bool
|
||||||
try:
|
try:
|
||||||
|
@ -350,7 +402,7 @@ proc load*(Configuration: type,
|
||||||
template required(opt: OptionDesc): bool =
|
template required(opt: OptionDesc): bool =
|
||||||
fieldSetters[opt.fieldIdx][2] and not opt.hasDefault
|
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:
|
for o in cmd.options:
|
||||||
if o.rejectNext == false:
|
if o.rejectNext == false:
|
||||||
if o.required:
|
if o.required:
|
||||||
|
@ -358,12 +410,12 @@ proc load*(Configuration: type,
|
||||||
elif o.hasDefault:
|
elif o.hasDefault:
|
||||||
discard fieldSetters[o.fieldIdx][1](conf, TaintedString(""))
|
discard fieldSetters[o.fieldIdx][1](conf, TaintedString(""))
|
||||||
|
|
||||||
template activateCmd(activatedCmd: ptr CommandDesc, key: TaintedString) =
|
template activateCmd(activatedCmd: CommandPtr, key: TaintedString) =
|
||||||
let cmd = activatedCmd
|
let cmd = activatedCmd
|
||||||
discard applySetter(cmd.fieldIdx, key)
|
discard applySetter(cmd.fieldIdx, key)
|
||||||
lastCmd.defaultSubCommand = -1
|
lastCmd.defaultSubCommand = -1
|
||||||
activeCmds.add cmd
|
activeCmds.add cmd
|
||||||
rejectNextArgument = cmd.argumentsFieldIdx == -1
|
rejectNextArgument = not cmd.hasArguments
|
||||||
|
|
||||||
for kind, key, val in getopt(cmdLine):
|
for kind, key, val in getopt(cmdLine):
|
||||||
case kind
|
case kind
|
||||||
|
@ -397,7 +449,8 @@ proc load*(Configuration: type,
|
||||||
activateCmd(subCmd, key)
|
activateCmd(subCmd, key)
|
||||||
else:
|
else:
|
||||||
if rejectNextArgument:
|
if rejectNextArgument:
|
||||||
fail "The command '$1' does not accept additional arguments" % [lastCmd.name]
|
fail lastCmd.noMoreArgumentsError
|
||||||
|
|
||||||
let argumentIdx = lastCmd.argumentsFieldIdx
|
let argumentIdx = lastCmd.argumentsFieldIdx
|
||||||
doAssert argumentIdx != -1
|
doAssert argumentIdx != -1
|
||||||
rejectNextArgument = applySetter(argumentIdx, key)
|
rejectNextArgument = applySetter(argumentIdx, key)
|
||||||
|
@ -415,8 +468,7 @@ proc defaults*(Configuration: type): Configuration =
|
||||||
|
|
||||||
proc dispatchImpl(cliProcSym, cliArgs, loadArgs: NimNode): NimNode =
|
proc dispatchImpl(cliProcSym, cliArgs, loadArgs: NimNode): NimNode =
|
||||||
# Here, we'll create a configuration object with fields matching
|
# Here, we'll create a configuration object with fields matching
|
||||||
# the CLI proc params. We'll also generate a call to the designated
|
# the CLI proc params. We'll also generate a call to the designated proc
|
||||||
# p
|
|
||||||
let configType = genSym(nskType, "CliConfig")
|
let configType = genSym(nskType, "CliConfig")
|
||||||
let configFields = newTree(nnkRecList)
|
let configFields = newTree(nnkRecList)
|
||||||
let configVar = genSym(nskLet, "config")
|
let configVar = genSym(nskLet, "config")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import os
|
|
||||||
|
|
||||||
type
|
type
|
||||||
ConfigurationError* = object of CatchableError
|
ConfigurationError* = object of CatchableError
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue