2018-11-21 13:25:53 +00:00
|
|
|
import
|
2019-07-07 09:41:15 +00:00
|
|
|
strutils, options, stew/shims/macros, typetraits,
|
2019-06-29 11:22:46 +00:00
|
|
|
confutils/[defs, cli_parser, shell_completion]
|
2018-11-21 13:25:53 +00:00
|
|
|
|
|
|
|
export
|
|
|
|
defs
|
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
when not defined(nimscript):
|
|
|
|
import os, terminal
|
|
|
|
|
2019-01-23 11:49:10 +00:00
|
|
|
type
|
2019-06-14 16:51:40 +00:00
|
|
|
CommandDesc = ref object
|
2019-01-23 11:49:10 +00:00
|
|
|
name: string
|
|
|
|
options: seq[OptionDesc]
|
|
|
|
subCommands: seq[CommandDesc]
|
2019-03-25 23:18:02 +00:00
|
|
|
defaultSubCommand: int
|
2019-01-23 11:49:10 +00:00
|
|
|
fieldIdx: int
|
|
|
|
argumentsFieldIdx: int
|
|
|
|
|
2019-06-14 16:51:40 +00:00
|
|
|
OptionDesc = ref object
|
2019-01-23 11:49:10 +00:00
|
|
|
name, typename, shortform: string
|
|
|
|
hasDefault: bool
|
|
|
|
rejectNext: bool
|
|
|
|
fieldIdx: int
|
|
|
|
desc: string
|
|
|
|
|
2019-06-14 16:51:40 +00:00
|
|
|
CommandPtr = CommandDesc
|
|
|
|
OptionPtr = OptionDesc
|
|
|
|
|
|
|
|
proc newLitFixed*(arg: ref): NimNode {.compileTime.} =
|
|
|
|
result = nnkObjConstr.newTree(arg.type.getTypeInst[1])
|
|
|
|
for a, b in fieldPairs(arg[]):
|
|
|
|
result.add nnkExprColonExpr.newTree( newIdentNode(a), newLitFixed(b) )
|
2019-06-14 16:33:59 +00:00
|
|
|
|
|
|
|
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 =
|
|
|
|
""
|
2019-01-23 11:49:10 +00:00
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
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):
|
2019-01-23 11:49:10 +00:00
|
|
|
template write(args: varargs[untyped]) =
|
|
|
|
stdout.styledWrite(args)
|
2019-06-14 16:33:59 +00:00
|
|
|
|
2019-01-23 11:49:10 +00:00
|
|
|
else:
|
2019-06-14 16:33:59 +00:00
|
|
|
const styleBright = ""
|
2019-01-23 11:49:10 +00:00
|
|
|
|
|
|
|
template write(args: varargs[untyped]) =
|
|
|
|
stdout.write(args)
|
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
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"
|
2019-03-25 23:18:02 +00:00
|
|
|
|
2019-01-23 11:49:10 +00:00
|
|
|
proc describeCmdOptions(cmd: CommandDesc) =
|
|
|
|
for opt in cmd.options:
|
|
|
|
write " --", opt.name, "=", opt.typename
|
|
|
|
if opt.desc.len > 0:
|
|
|
|
write repeat(" ", max(0, 40 - opt.name.len - opt.typename.len)), ": ", opt.desc
|
|
|
|
write "\n"
|
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
proc showHelp(version: string, cmd: CommandDesc) =
|
2019-06-14 16:33:59 +00:00
|
|
|
let app = appInvocation()
|
2019-01-23 11:49:10 +00:00
|
|
|
|
|
|
|
write "Usage: ", styleBright, app
|
|
|
|
if cmd.name.len > 0: write " ", cmd.name
|
|
|
|
if cmd.options.len > 0: write " [OPTIONS]"
|
|
|
|
if cmd.subCommands.len > 0: write " <command>"
|
|
|
|
if cmd.argumentsFieldIdx != -1: write " [<args>]"
|
|
|
|
|
|
|
|
if cmd.options.len > 0:
|
|
|
|
write "\n\nThe following options are supported:\n\n"
|
|
|
|
describeCmdOptions(cmd)
|
|
|
|
|
2019-03-26 08:55:27 +00:00
|
|
|
if cmd.defaultSubCommand != -1:
|
|
|
|
describeCmdOptions(cmd.subCommands[cmd.defaultSubCommand])
|
|
|
|
|
2019-01-23 11:49:10 +00:00
|
|
|
if cmd.subCommands.len > 0:
|
2019-03-26 08:55:27 +00:00
|
|
|
write "\nAvailable sub-commands:\n"
|
|
|
|
for i in 0 ..< cmd.subCommands.len:
|
|
|
|
if i != cmd.defaultSubCommand:
|
|
|
|
write "\n ", styleBright, app, " ", cmd.subCommands[i].name, "\n\n"
|
|
|
|
describeCmdOptions(cmd.subCommands[i])
|
2019-01-23 11:49:10 +00:00
|
|
|
|
|
|
|
write "\n"
|
|
|
|
quit(0)
|
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
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:
|
2019-06-14 16:51:40 +00:00
|
|
|
return o
|
2019-06-14 16:33:59 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
proc findSubcommand(cmd: CommandPtr, name: TaintedString): CommandPtr =
|
|
|
|
for subCmd in cmd.subCommands.mitems:
|
|
|
|
if cmpIgnoreStyle(subCmd.name, string(name)) == 0:
|
2019-06-14 16:51:40 +00:00
|
|
|
return subCmd
|
2019-06-14 16:33:59 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
|
2019-07-03 15:58:09 +00:00
|
|
|
proc startsWithIgnoreStyle(s: string, prefix: string): bool =
|
|
|
|
# Similar in spirit to cmpIgnoreStyle, but compare only the prefix.
|
|
|
|
var i = 0
|
|
|
|
var j = 0
|
|
|
|
|
|
|
|
while true:
|
|
|
|
# Skip any underscore
|
|
|
|
while i < s.len and s[i] == '_': inc i
|
|
|
|
while j < prefix.len and prefix[j] == '_': inc j
|
|
|
|
|
|
|
|
if j == prefix.len:
|
|
|
|
# The whole prefix matches
|
|
|
|
return true
|
|
|
|
elif i == s.len:
|
|
|
|
# We've reached the end of `s` without matching the prefix
|
|
|
|
return false
|
|
|
|
elif toLowerAscii(s[i]) != toLowerAscii(prefix[j]):
|
|
|
|
return false
|
|
|
|
|
|
|
|
inc i
|
|
|
|
inc j
|
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
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
|
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
# 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):
|
|
|
|
raise newException(ValueError, "Directory doesn't exist")
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
result = T(p)
|
2018-12-19 10:52:32 +00:00
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
proc parseCmdArg*(T: type InputFile, p: TaintedString): T =
|
|
|
|
# TODO this is needed only because InputFile cannot be made
|
|
|
|
# an alias of TypedInputFile at the moment, because of a generics
|
|
|
|
# caching issue
|
|
|
|
if not fileExists(p.string):
|
|
|
|
raise newException(ValueError, "File doesn't exist")
|
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
when not defined(nimscript):
|
|
|
|
try:
|
|
|
|
let f = open(p.string, fmRead)
|
|
|
|
close f
|
|
|
|
except IOError:
|
|
|
|
raise newException(ValueError, "File not accessible")
|
2019-03-18 02:01:07 +00:00
|
|
|
|
|
|
|
result = T(p.string)
|
|
|
|
|
|
|
|
proc parseCmdArg*(T: type TypedInputFile, p: TaintedString): T =
|
|
|
|
var path = p.string
|
|
|
|
when T.defaultExt.len > 0:
|
|
|
|
path = path.addFileExt(T.defaultExt)
|
|
|
|
|
|
|
|
if not fileExists(path):
|
|
|
|
raise newException(ValueError, "File doesn't exist")
|
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
when not defined(nimscript):
|
|
|
|
try:
|
|
|
|
let f = open(path, fmRead)
|
|
|
|
close f
|
|
|
|
except IOError:
|
|
|
|
raise newException(ValueError, "File not accessible")
|
2019-03-18 02:01:07 +00:00
|
|
|
|
|
|
|
result = T(path)
|
|
|
|
|
|
|
|
proc parseCmdArg*(T: type[OutDir|OutFile|OutPath], p: TaintedString): T =
|
|
|
|
result = T(p)
|
2018-12-19 12:51:00 +00:00
|
|
|
|
|
|
|
proc parseCmdArg*[T](_: type Option[T], s: TaintedString): Option[T] =
|
|
|
|
return some(parseCmdArg(T, s))
|
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
template parseCmdArg*(T: type string, s: TaintedString): string =
|
2018-11-22 21:15:50 +00:00
|
|
|
string s
|
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
proc parseCmdArg*(T: type SomeSignedInt, s: TaintedString): T =
|
2018-11-22 21:15:50 +00:00
|
|
|
T parseInt(string s)
|
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
proc parseCmdArg*(T: type SomeUnsignedInt, s: TaintedString): T =
|
2018-11-22 21:15:50 +00:00
|
|
|
T parseUInt(string s)
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2019-01-23 16:11:24 +00:00
|
|
|
proc parseCmdArg*(T: type SomeFloat, p: TaintedString): T =
|
|
|
|
result = parseFloat(p)
|
|
|
|
|
|
|
|
proc parseCmdArg*(T: type bool, p: TaintedString): T =
|
|
|
|
result = parseBool(p)
|
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
proc parseCmdArg*(T: type enum, s: TaintedString): T =
|
|
|
|
parseEnum[T](string(s))
|
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
proc parseCmdArgAux(T: type, s: TaintedString): T = # {.raises: [ValueError].} =
|
|
|
|
# The parseCmdArg procs are allowed to raise only `ValueError`.
|
|
|
|
# If you have provided your own specializations, please handle
|
|
|
|
# all other exception types.
|
2018-12-19 10:52:32 +00:00
|
|
|
mixin parseCmdArg
|
2019-03-18 02:01:07 +00:00
|
|
|
parseCmdArg(T, s)
|
2018-12-19 10:52:32 +00:00
|
|
|
|
2019-07-03 15:37:59 +00:00
|
|
|
proc completeCmdArg(T: type enum, val: TaintedString): seq[string] =
|
|
|
|
for e in low(T)..high(T):
|
|
|
|
let as_str = $e
|
2019-07-03 15:58:09 +00:00
|
|
|
if startsWithIgnoreStyle(as_str, val):
|
2019-07-03 15:37:59 +00:00
|
|
|
result.add($e)
|
2019-07-01 15:41:35 +00:00
|
|
|
|
2019-07-03 15:37:59 +00:00
|
|
|
proc completeCmdArg(T: type SomeNumber, val: TaintedString): seq[string] =
|
2019-07-01 15:41:35 +00:00
|
|
|
return @[]
|
|
|
|
|
2019-07-03 15:37:59 +00:00
|
|
|
proc completeCmdArg(T: type bool, val: TaintedString): seq[string] =
|
2019-07-01 15:41:35 +00:00
|
|
|
return @[]
|
|
|
|
|
2019-07-03 15:37:59 +00:00
|
|
|
proc completeCmdArg(T: type string, val: TaintedString): seq[string] =
|
2019-07-01 15:41:35 +00:00
|
|
|
return @[]
|
|
|
|
|
2019-07-04 12:25:15 +00:00
|
|
|
proc completeCmdArg*(T: type[InputFile|InputDir|OutFile|OutDir|OutPath], val: TaintedString): seq[string] =
|
2019-07-03 15:37:59 +00:00
|
|
|
let (dir, name, ext) = splitFile(val)
|
|
|
|
let tail = name & ext
|
2019-07-03 16:58:22 +00:00
|
|
|
# Expand the directory component for the directory walker routine
|
2019-07-03 15:37:59 +00:00
|
|
|
let dir_path = if dir == "": "." else: expandTilde(dir)
|
|
|
|
# Dotfiles are hidden unless the user entered a dot as prefix
|
|
|
|
let show_dotfiles = len(name) > 0 and name[0] == '.'
|
|
|
|
|
|
|
|
for kind, path in walkDir(dir_path, relative=true):
|
|
|
|
if not show_dotfiles and path[0] == '.':
|
|
|
|
continue
|
|
|
|
|
2019-07-04 07:56:09 +00:00
|
|
|
# Do not show files if asked for directories, on the other hand we must show
|
|
|
|
# directories even if a file is requested to allow the user to select a file
|
|
|
|
# inside those
|
2019-07-08 15:09:08 +00:00
|
|
|
if type(T) is (InputDir or OutDir) and kind notin {pcDir, pcLinkToDir}:
|
2019-07-04 07:56:09 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# Note, no normalization is needed here
|
|
|
|
if path.startsWith(tail):
|
2019-07-04 12:25:15 +00:00
|
|
|
var match = dir_path / path
|
|
|
|
# Add a trailing slash so that completions can be chained
|
|
|
|
if kind in {pcDir, pcLinkToDir}:
|
|
|
|
match &= DirSep
|
2019-07-04 07:56:09 +00:00
|
|
|
|
|
|
|
result.add(shellPathEscape(match))
|
2019-07-03 15:37:59 +00:00
|
|
|
|
|
|
|
proc completeCmdArg[T](_: type seq[T], val: TaintedString): seq[string] =
|
2019-07-01 15:41:35 +00:00
|
|
|
return @[]
|
|
|
|
|
2019-07-03 15:37:59 +00:00
|
|
|
proc completeCmdArg[T](_: type Option[T], val: TaintedString): seq[string] =
|
|
|
|
return completeCmdArg(type(T), val)
|
2019-07-01 15:41:35 +00:00
|
|
|
|
2019-07-03 15:37:59 +00:00
|
|
|
proc completeCmdArgAux(T: type, val: TaintedString): seq[string] =
|
|
|
|
return completeCmdArg(T, val)
|
2019-07-01 15:41:35 +00:00
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
template setField[T](loc: var T, val: TaintedString, defaultVal: untyped): bool =
|
|
|
|
type FieldType = type(loc)
|
|
|
|
loc = if len(val) > 0: parseCmdArgAux(FieldType, val)
|
2018-12-19 10:52:32 +00:00
|
|
|
else: FieldType(defaultVal)
|
|
|
|
true
|
|
|
|
|
|
|
|
template setField[T](loc: var seq[T], val: TaintedString, defaultVal: untyped): bool =
|
2019-03-18 02:01:07 +00:00
|
|
|
loc.add parseCmdArgAux(type(loc[0]), val)
|
2018-12-19 10:52:32 +00:00
|
|
|
false
|
|
|
|
|
|
|
|
template simpleSet(loc: var auto) =
|
|
|
|
discard
|
|
|
|
|
|
|
|
proc makeDefaultValue*(T: type): T =
|
|
|
|
discard
|
|
|
|
|
2018-12-19 12:51:00 +00:00
|
|
|
proc requiresInput*(T: type): bool =
|
|
|
|
result = not ((T is seq) or (T is Option))
|
|
|
|
|
2019-01-23 11:49:10 +00:00
|
|
|
# TODO: The usage of this should be replacable with just `type(x)`,
|
|
|
|
# but Nim is not able to handle it at the moment.
|
|
|
|
macro typeof(x: typed): untyped =
|
|
|
|
result = x.getType
|
|
|
|
|
|
|
|
template debugMacroResult(macroName: string) {.dirty.} =
|
|
|
|
when defined(debugMacros) or defined(debugConfutils):
|
|
|
|
echo "\n-------- ", macroName, " ----------------------"
|
|
|
|
echo result.repr
|
|
|
|
|
2018-11-21 13:25:53 +00:00
|
|
|
proc load*(Configuration: type,
|
|
|
|
cmdLine = commandLineParams(),
|
2019-03-18 02:01:07 +00:00
|
|
|
version = "",
|
2018-11-21 13:25:53 +00:00
|
|
|
printUsage = true,
|
|
|
|
quitOnFailure = true): Configuration =
|
|
|
|
## Loads a program configuration by parsing command-line arguments
|
|
|
|
## and a standard set of config files that can specify:
|
|
|
|
##
|
|
|
|
## - working directory settings
|
|
|
|
## - user settings
|
|
|
|
## - system-wide setttings
|
|
|
|
##
|
|
|
|
## Supports multiple config files format (INI/TOML, YAML, JSON).
|
|
|
|
|
|
|
|
# This is an initial naive implementation that will be improved
|
|
|
|
# over time.
|
|
|
|
|
|
|
|
type
|
2018-12-19 10:52:32 +00:00
|
|
|
FieldSetter = proc (cfg: var Configuration, val: TaintedString): bool {.nimcall.}
|
2019-07-01 15:41:35 +00:00
|
|
|
FieldCompleter = proc (val: TaintedString): seq[string] {.nimcall.}
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
macro generateFieldSetters(RecordType: type): untyped =
|
|
|
|
var recordDef = RecordType.getType[1].getImpl
|
|
|
|
let makeDefaultValue = bindSym"makeDefaultValue"
|
|
|
|
|
|
|
|
result = newTree(nnkStmtListExpr)
|
|
|
|
var settersArray = newTree(nnkBracket)
|
|
|
|
|
|
|
|
for field in recordFields(recordDef):
|
|
|
|
var
|
|
|
|
setterName = ident($field.name & "Setter")
|
|
|
|
fieldName = field.name
|
2019-03-18 02:01:07 +00:00
|
|
|
configVar = ident "config"
|
|
|
|
configField = newTree(nnkDotExpr, configVar, fieldName)
|
2018-12-19 10:52:32 +00:00
|
|
|
defaultValue = field.readPragma"defaultValue"
|
2019-07-01 15:41:35 +00:00
|
|
|
completerName = ident($field.name & "Complete")
|
2018-12-19 10:52:32 +00:00
|
|
|
|
|
|
|
if defaultValue == nil:
|
2019-03-18 02:01:07 +00:00
|
|
|
defaultValue = newCall(makeDefaultValue, newTree(nnkTypeOfExpr, configField))
|
2018-12-19 10:52:32 +00:00
|
|
|
|
2019-01-23 11:49:10 +00:00
|
|
|
# TODO: This shouldn't be necessary. The type symbol returned from Nim should
|
|
|
|
# be typed as a tyTypeDesc[tyString] instead of just `tyString`. To be filed.
|
|
|
|
var fixedFieldType = newTree(nnkTypeOfExpr, field.typ)
|
|
|
|
|
2018-12-19 12:51:00 +00:00
|
|
|
settersArray.add newTree(nnkTupleConstr,
|
2019-03-18 02:01:07 +00:00
|
|
|
newLit($fieldName),
|
2018-12-19 12:51:00 +00:00
|
|
|
newCall(bindSym"FieldSetter", setterName),
|
2019-07-01 15:41:35 +00:00
|
|
|
newCall(bindSym"requiresInput", fixedFieldType),
|
|
|
|
newCall(bindSym"FieldCompleter", completerName))
|
2018-12-19 10:52:32 +00:00
|
|
|
|
|
|
|
result.add quote do:
|
2019-07-01 15:41:35 +00:00
|
|
|
proc `completerName`(val: TaintedString): seq[string] {.nimcall.} =
|
2019-07-03 15:37:59 +00:00
|
|
|
return completeCmdArgAux(`fixedFieldType`, val)
|
2019-07-01 15:41:35 +00:00
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
proc `setterName`(`configVar`: var `RecordType`, val: TaintedString): bool {.nimcall.} =
|
|
|
|
when `configField` is enum:
|
2018-12-19 10:52:32 +00:00
|
|
|
# TODO: For some reason, the normal `setField` rejects enum fields
|
|
|
|
# when they are used as case discriminators. File this as a bug.
|
2019-01-23 20:20:25 +00:00
|
|
|
if len(val) > 0:
|
2019-03-18 02:01:07 +00:00
|
|
|
`configField` = parseEnum[type(`configField`)](string(val))
|
2019-01-23 20:20:25 +00:00
|
|
|
else:
|
2019-03-18 02:01:07 +00:00
|
|
|
`configField` = `defaultValue`
|
2018-12-19 10:52:32 +00:00
|
|
|
return true
|
|
|
|
else:
|
2019-03-18 02:01:07 +00:00
|
|
|
return setField(`configField`, val, `defaultValue`)
|
2018-12-19 10:52:32 +00:00
|
|
|
|
|
|
|
result.add settersArray
|
2019-01-23 11:49:10 +00:00
|
|
|
debugMacroResult "Field Setters"
|
2018-12-19 10:52:32 +00:00
|
|
|
|
|
|
|
macro buildCommandTree(RecordType: type): untyped =
|
|
|
|
var recordDef = RecordType.getType[1].getImpl
|
2019-03-25 23:18:02 +00:00
|
|
|
var fieldIdx = 0
|
|
|
|
# TODO Handle arbitrary sub-command trees more properly
|
|
|
|
# var cmdStack = newSeq[(NimNode, CommandDesc)]()
|
2019-06-14 16:51:40 +00:00
|
|
|
var res = CommandDesc()
|
2018-12-19 10:52:32 +00:00
|
|
|
res.argumentsFieldIdx = -1
|
2019-03-25 23:18:02 +00:00
|
|
|
res.defaultSubCommand = -1
|
2018-12-19 10:52:32 +00:00
|
|
|
|
|
|
|
for field in recordFields(recordDef):
|
|
|
|
let
|
|
|
|
isCommand = field.readPragma"command" != nil
|
2019-03-25 23:18:02 +00:00
|
|
|
isDiscriminator = field.caseField != nil and field.caseBranch == nil
|
|
|
|
defaultValue = field.readPragma"defaultValue"
|
2018-12-19 10:52:32 +00:00
|
|
|
shortform = field.readPragma"shortform"
|
|
|
|
longform = field.readPragma"longform"
|
|
|
|
desc = field.readPragma"desc"
|
|
|
|
|
2019-03-25 23:18:02 +00:00
|
|
|
if isDiscriminator:
|
|
|
|
# TODO Handle
|
2018-12-19 10:52:32 +00:00
|
|
|
let cmdType = field.typ.getImpl[^1]
|
|
|
|
if cmdType.kind != nnkEnumTy:
|
2019-03-25 23:18:02 +00:00
|
|
|
error "Only enums are supported as case object discriminators", field.name
|
|
|
|
for i in 1 ..< cmdType.len:
|
|
|
|
let name = $cmdType[i]
|
|
|
|
if defaultValue != nil and $name == $defaultValue:
|
|
|
|
res.defaultSubCommand = res.subCommands.len
|
|
|
|
res.subCommands.add CommandDesc(name: name,
|
2018-12-19 10:52:32 +00:00
|
|
|
fieldIdx: fieldIdx,
|
2019-03-25 23:18:02 +00:00
|
|
|
argumentsFieldIdx: -1,
|
|
|
|
defaultSubCommand: -1)
|
|
|
|
elif isCommand:
|
|
|
|
# TODO Handle string commands
|
|
|
|
# (But perhaps these are no different than arguments)
|
|
|
|
discard
|
2018-12-19 10:52:32 +00:00
|
|
|
else:
|
2019-06-14 16:51:40 +00:00
|
|
|
var option = OptionDesc()
|
2018-12-19 10:52:32 +00:00
|
|
|
option.fieldIdx = fieldIdx
|
|
|
|
option.name = $field.name
|
2019-03-25 23:18:02 +00:00
|
|
|
option.hasDefault = defaultValue != nil
|
2018-12-19 10:52:32 +00:00
|
|
|
option.typename = field.typ.repr
|
2019-01-23 11:49:10 +00:00
|
|
|
if desc != nil: option.desc = desc.strVal
|
2018-12-19 10:52:32 +00:00
|
|
|
if longform != nil: option.name = longform.strVal
|
|
|
|
if shortform != nil: option.shortform = shortform.strVal
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
var isSubcommandOption = false
|
|
|
|
if field.caseBranch != nil:
|
|
|
|
let branchCmd = $field.caseBranch[0]
|
|
|
|
for cmd in mitems(res.subCommands):
|
|
|
|
if cmd.name == branchCmd:
|
|
|
|
cmd.options.add option
|
|
|
|
isSubcommandOption = true
|
|
|
|
break
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
if not isSubcommandOption:
|
|
|
|
res.options.add option
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
inc fieldIdx
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
result = newLitFixed(res)
|
2019-01-23 11:49:10 +00:00
|
|
|
debugMacroResult "Command Tree"
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
let fieldSetters = generateFieldSetters(Configuration)
|
|
|
|
var rootCmd = buildCommandTree(Configuration)
|
2019-03-25 23:18:02 +00:00
|
|
|
printCmdTree rootCmd
|
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
let confAddr = addr result
|
2019-06-14 16:51:40 +00:00
|
|
|
var activeCmds = @[rootCmd]
|
2019-03-26 08:45:19 +00:00
|
|
|
template lastCmd: auto = activeCmds[^1]
|
|
|
|
var rejectNextArgument = lastCmd.argumentsFieldIdx == -1
|
2018-11-21 13:25:53 +00:00
|
|
|
|
|
|
|
proc fail(msg: string) =
|
|
|
|
if quitOnFailure:
|
|
|
|
stderr.writeLine(msg)
|
2019-06-14 16:33:59 +00:00
|
|
|
stderr.writeLine("Try '$1 --help' for more information" % appInvocation())
|
2018-11-21 13:25:53 +00:00
|
|
|
quit 1
|
|
|
|
else:
|
|
|
|
raise newException(ConfigurationError, msg)
|
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
template applySetter(setterIdx: int, cmdLineVal: TaintedString): bool =
|
|
|
|
var r: bool
|
|
|
|
try:
|
|
|
|
r = fieldSetters[setterIdx][1](confAddr[], cmdLineVal)
|
|
|
|
except:
|
|
|
|
fail("Invalid value for " & fieldSetters[setterIdx][0] & ": " &
|
|
|
|
getCurrentExceptionMsg())
|
|
|
|
r
|
|
|
|
|
2018-12-19 12:51:00 +00:00
|
|
|
template required(opt: OptionDesc): bool =
|
2019-03-18 02:01:07 +00:00
|
|
|
fieldSetters[opt.fieldIdx][2] and not opt.hasDefault
|
2018-12-19 12:51:00 +00:00
|
|
|
|
2019-07-03 15:37:59 +00:00
|
|
|
template getArgCompletions(opt: OptionDesc, prefix: TaintedString): seq[string] =
|
|
|
|
fieldSetters[opt.fieldIdx][3](prefix)
|
2019-07-01 15:41:35 +00:00
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
proc processMissingOptions(conf: var Configuration, cmd: CommandPtr) =
|
2018-12-19 10:52:32 +00:00
|
|
|
for o in cmd.options:
|
2019-01-23 11:49:10 +00:00
|
|
|
if o.rejectNext == false:
|
|
|
|
if o.required:
|
|
|
|
fail "The required option '$1' was not specified" % [o.name]
|
|
|
|
elif o.hasDefault:
|
2019-03-18 02:01:07 +00:00
|
|
|
discard fieldSetters[o.fieldIdx][1](conf, TaintedString(""))
|
2018-12-19 10:52:32 +00:00
|
|
|
|
2019-06-14 16:33:59 +00:00
|
|
|
template activateCmd(activatedCmd: CommandPtr, key: TaintedString) =
|
2019-03-26 08:45:19 +00:00
|
|
|
let cmd = activatedCmd
|
|
|
|
discard applySetter(cmd.fieldIdx, key)
|
|
|
|
lastCmd.defaultSubCommand = -1
|
|
|
|
activeCmds.add cmd
|
2019-06-14 16:33:59 +00:00
|
|
|
rejectNextArgument = not cmd.hasArguments
|
2018-12-19 10:52:32 +00:00
|
|
|
|
2019-06-29 11:22:46 +00:00
|
|
|
type
|
|
|
|
ArgKindFilter = enum
|
|
|
|
longForm
|
|
|
|
shortForm
|
|
|
|
|
|
|
|
proc showMatchingOptions(cmd: CommandPtr, prefix: string, filterKind: set[ArgKindFilter]) =
|
|
|
|
var matchingOptions: seq[OptionDesc]
|
|
|
|
|
|
|
|
if len(prefix) > 0:
|
|
|
|
# Filter the options according to the input prefix
|
|
|
|
for opt in cmd.options:
|
2019-07-04 12:31:27 +00:00
|
|
|
if longForm in filterKind and len(opt.name) > 0:
|
|
|
|
if startsWithIgnoreStyle(opt.name, prefix):
|
2019-06-29 11:22:46 +00:00
|
|
|
matchingOptions.add(opt)
|
2019-07-04 12:31:27 +00:00
|
|
|
if shortForm in filterKind and len(opt.shortform) > 0:
|
|
|
|
if startsWithIgnoreStyle(opt.shortform, prefix):
|
2019-06-29 11:22:46 +00:00
|
|
|
matchingOptions.add(opt)
|
|
|
|
else:
|
|
|
|
matchingOptions = cmd.options
|
|
|
|
|
|
|
|
for opt in matchingOptions:
|
|
|
|
# The trailing '=' means the switch accepts an argument
|
2019-06-30 11:48:13 +00:00
|
|
|
let trailing = if opt.typename != "bool": "=" else: ""
|
2019-06-29 11:22:46 +00:00
|
|
|
|
2019-07-04 12:31:27 +00:00
|
|
|
if longForm in filterKind and len(opt.name) > 0:
|
2019-06-29 11:22:46 +00:00
|
|
|
stdout.writeLine("--", opt.name, trailing)
|
2019-07-04 12:31:27 +00:00
|
|
|
if shortForm in filterKind and len(opt.shortform) > 0:
|
2019-06-29 11:22:46 +00:00
|
|
|
stdout.writeLine('-', opt.shortform, trailing)
|
|
|
|
|
|
|
|
let completion = splitCompletionLine()
|
|
|
|
# If we're not asked to complete a command line the result is an empty list
|
|
|
|
if len(completion) != 0:
|
|
|
|
var cmdStack = @[rootCmd]
|
|
|
|
# Try to understand what the active chain of commands is without parsing the
|
|
|
|
# whole command line
|
|
|
|
for tok in completion[1..^1]:
|
|
|
|
if not tok.startsWith('-'):
|
|
|
|
let subCmd = findSubcommand(cmdStack[^1], tok)
|
|
|
|
if subCmd != nil: cmdStack.add(subCmd)
|
|
|
|
|
2019-07-03 15:37:59 +00:00
|
|
|
let cur_word = completion[^1]
|
|
|
|
let prev_word = if len(completion) > 2: completion[^2] else: ""
|
|
|
|
let prev_prev_word = if len(completion) > 3: completion[^3] else: ""
|
2019-06-29 11:22:46 +00:00
|
|
|
|
|
|
|
if cur_word.startsWith('-'):
|
|
|
|
# Show all the options matching the prefix input by the user
|
|
|
|
let isLong = cur_word.startsWith("--")
|
|
|
|
var option_word = cur_word
|
|
|
|
option_word.removePrefix('-')
|
|
|
|
|
|
|
|
for i in countdown(cmdStack.len - 1, 0):
|
|
|
|
let argFilter =
|
|
|
|
if isLong:
|
|
|
|
{longForm}
|
|
|
|
elif len(cur_word) > 1:
|
|
|
|
# If the user entered a single hypen then we show both long & short
|
|
|
|
# variants
|
|
|
|
{shortForm}
|
|
|
|
else:
|
|
|
|
{longForm, shortForm}
|
|
|
|
|
|
|
|
showMatchingOptions(cmdStack[i], option_word, argFilter)
|
|
|
|
elif (prev_word.startsWith('-') or
|
|
|
|
(prev_word == "=" and prev_prev_word.startsWith('-'))):
|
|
|
|
# Handle cases where we want to complete a switch choice
|
|
|
|
# -switch
|
|
|
|
# -switch=
|
|
|
|
var option_word = if len(prev_word) == 1: prev_prev_word else: prev_word
|
|
|
|
option_word.removePrefix('-')
|
|
|
|
|
2019-07-01 15:41:35 +00:00
|
|
|
let option = findOption(cmdStack, option_word)
|
|
|
|
if option != nil:
|
2019-07-03 15:37:59 +00:00
|
|
|
for arg in getArgCompletions(option, cur_word):
|
|
|
|
stdout.writeLine(arg)
|
2019-06-29 11:22:46 +00:00
|
|
|
elif len(cmdStack[^1].subCommands) != 0:
|
|
|
|
# Show all the available subcommands
|
|
|
|
for subCmd in cmdStack[^1].subCommands:
|
2019-07-03 15:58:09 +00:00
|
|
|
if startsWithIgnoreStyle(subCmd.name, cur_word):
|
2019-06-29 11:22:46 +00:00
|
|
|
stdout.writeLine(subCmd.name)
|
|
|
|
else:
|
|
|
|
# Full options listing
|
|
|
|
for i in countdown(cmdStack.len - 1, 0):
|
|
|
|
showMatchingOptions(cmdStack[i], "", {longForm, shortForm})
|
|
|
|
|
|
|
|
stdout.flushFile()
|
|
|
|
|
|
|
|
return
|
|
|
|
|
2018-11-21 13:25:53 +00:00
|
|
|
for kind, key, val in getopt(cmdLine):
|
2018-12-19 10:52:32 +00:00
|
|
|
case kind
|
|
|
|
of cmdLongOption, cmdShortOption:
|
2019-01-23 11:49:10 +00:00
|
|
|
if string(key) == "help":
|
2019-06-14 16:51:40 +00:00
|
|
|
showHelp version, lastCmd
|
2019-03-26 08:45:19 +00:00
|
|
|
|
|
|
|
var option = findOption(activeCmds, key)
|
|
|
|
if option == nil:
|
|
|
|
# We didn't find the option.
|
|
|
|
# Check if it's from the default command and activate it if necessary:
|
|
|
|
if lastCmd.defaultSubCommand != -1:
|
2019-06-14 16:51:40 +00:00
|
|
|
let defaultSubCmd = lastCmd.subCommands[lastCmd.defaultSubCommand]
|
2019-03-26 08:45:19 +00:00
|
|
|
option = findOption(@[defaultSubCmd], key)
|
|
|
|
if option != nil:
|
|
|
|
activateCmd(defaultSubCmd, TaintedString(""))
|
2019-01-23 11:49:10 +00:00
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
if option != nil:
|
|
|
|
if option.rejectNext:
|
2018-11-21 13:25:53 +00:00
|
|
|
fail "The options '$1' should not be specified more than once" % [string(key)]
|
2019-03-18 02:01:07 +00:00
|
|
|
option.rejectNext = applySetter(option.fieldIdx, val)
|
2018-11-21 13:25:53 +00:00
|
|
|
else:
|
|
|
|
fail "Unrecognized option '$1'" % [string(key)]
|
|
|
|
|
2018-12-19 10:52:32 +00:00
|
|
|
of cmdArgument:
|
2019-03-26 08:45:19 +00:00
|
|
|
if string(key) == "help" and lastCmd.subCommands.len > 0:
|
2019-06-14 16:51:40 +00:00
|
|
|
showHelp version, lastCmd
|
2019-01-23 11:49:10 +00:00
|
|
|
|
2019-03-26 08:45:19 +00:00
|
|
|
let subCmd = lastCmd.findSubcommand(key)
|
2018-12-19 10:52:32 +00:00
|
|
|
if subCmd != nil:
|
2019-03-26 08:45:19 +00:00
|
|
|
activateCmd(subCmd, key)
|
2018-12-19 10:52:32 +00:00
|
|
|
else:
|
|
|
|
if rejectNextArgument:
|
2019-06-14 16:33:59 +00:00
|
|
|
fail lastCmd.noMoreArgumentsError
|
|
|
|
|
2019-03-26 08:45:19 +00:00
|
|
|
let argumentIdx = lastCmd.argumentsFieldIdx
|
2018-12-19 10:52:32 +00:00
|
|
|
doAssert argumentIdx != -1
|
2019-03-18 02:01:07 +00:00
|
|
|
rejectNextArgument = applySetter(argumentIdx, key)
|
2018-12-19 10:52:32 +00:00
|
|
|
|
|
|
|
else:
|
|
|
|
discard
|
|
|
|
|
2019-03-25 23:18:02 +00:00
|
|
|
for cmd in activeCmds:
|
|
|
|
result.processMissingOptions(cmd)
|
|
|
|
if cmd.defaultSubCommand != -1:
|
2019-06-14 16:51:40 +00:00
|
|
|
result.processMissingOptions(cmd.subCommands[cmd.defaultSubCommand])
|
2019-01-23 11:49:10 +00:00
|
|
|
|
2019-06-10 19:42:42 +00:00
|
|
|
proc defaults*(Configuration: type): Configuration =
|
|
|
|
load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false)
|
|
|
|
|
2019-01-23 11:49:10 +00:00
|
|
|
proc dispatchImpl(cliProcSym, cliArgs, loadArgs: NimNode): NimNode =
|
|
|
|
# Here, we'll create a configuration object with fields matching
|
2019-06-14 16:33:59 +00:00
|
|
|
# the CLI proc params. We'll also generate a call to the designated proc
|
2019-01-23 11:49:10 +00:00
|
|
|
let configType = genSym(nskType, "CliConfig")
|
|
|
|
let configFields = newTree(nnkRecList)
|
|
|
|
let configVar = genSym(nskLet, "config")
|
|
|
|
var dispatchCall = newCall(cliProcSym)
|
|
|
|
|
|
|
|
# The return type of the proc is skipped over
|
|
|
|
for i in 1 ..< cliArgs.len:
|
|
|
|
var arg = copy cliArgs[i]
|
|
|
|
|
|
|
|
# If an argument doesn't specify a type, we infer it from the default value
|
|
|
|
if arg[1].kind == nnkEmpty:
|
|
|
|
if arg[2].kind == nnkEmpty:
|
|
|
|
error "Please provide either a default value or type of the parameter", arg
|
|
|
|
arg[1] = newCall(bindSym"typeof", arg[2])
|
|
|
|
|
|
|
|
# Turn any default parameters into the confutils's `defaultValue` pragma
|
|
|
|
if arg[2].kind != nnkEmpty:
|
|
|
|
if arg[0].kind != nnkPragmaExpr:
|
2019-01-23 16:11:24 +00:00
|
|
|
arg[0] = newTree(nnkPragmaExpr, arg[0], newTree(nnkPragma))
|
|
|
|
arg[0][1].add newColonExpr(bindSym"defaultValue", arg[2])
|
2019-01-23 11:49:10 +00:00
|
|
|
arg[2] = newEmptyNode()
|
|
|
|
|
|
|
|
configFields.add arg
|
|
|
|
dispatchCall.add newTree(nnkDotExpr, configVar, skipPragma arg[0])
|
|
|
|
|
|
|
|
let cliConfigType = nnkTypeSection.newTree(
|
|
|
|
nnkTypeDef.newTree(
|
|
|
|
configType,
|
|
|
|
newEmptyNode(),
|
|
|
|
nnkObjectTy.newTree(
|
|
|
|
newEmptyNode(),
|
|
|
|
newEmptyNode(),
|
|
|
|
configFields)))
|
|
|
|
|
|
|
|
var loadConfigCall = newCall(bindSym"load", configType)
|
|
|
|
for p in loadArgs: loadConfigCall.add p
|
|
|
|
|
|
|
|
result = quote do:
|
|
|
|
`cliConfigType`
|
|
|
|
let `configVar` = `loadConfigCall`
|
|
|
|
`dispatchCall`
|
|
|
|
|
|
|
|
macro dispatch*(fn: typed, args: varargs[untyped]): untyped =
|
|
|
|
if fn.kind != nnkSym or
|
|
|
|
fn.symKind notin {nskProc, nskFunc, nskMacro, nskTemplate}:
|
|
|
|
error "The first argument to `confutils.dispatch` should be a callable symbol"
|
|
|
|
|
|
|
|
let fnImpl = fn.getImpl
|
|
|
|
result = dispatchImpl(fnImpl.name, fnImpl.params, args)
|
|
|
|
debugMacroResult "Dispatch Code"
|
|
|
|
|
|
|
|
macro cli*(args: varargs[untyped]): untyped =
|
|
|
|
if args.len == 0:
|
|
|
|
error "The cli macro expects a do block", args
|
|
|
|
|
|
|
|
let doBlock = args[^1]
|
|
|
|
if doBlock.kind notin {nnkDo, nnkLambda}:
|
|
|
|
error "The last argument to `confutils.cli` should be a do block", doBlock
|
|
|
|
|
|
|
|
args.del(args.len - 1)
|
|
|
|
|
|
|
|
# Create a new anonymous proc we'll dispatch to
|
|
|
|
let cliProcName = genSym(nskProc, "CLI")
|
|
|
|
var cliProc = newTree(nnkProcDef, cliProcName)
|
|
|
|
# Copy everything but the name from the do block:
|
|
|
|
for i in 1 ..< doBlock.len: cliProc.add doBlock[i]
|
|
|
|
|
|
|
|
# Generate the final code
|
|
|
|
result = newStmtList(cliProc, dispatchImpl(cliProcName, cliProc.params, args))
|
2019-01-23 16:11:24 +00:00
|
|
|
|
|
|
|
# TODO: remove this once Nim supports custom pragmas on proc params
|
|
|
|
for p in cliProc.params:
|
|
|
|
if p.kind == nnkEmpty: continue
|
|
|
|
p[0] = skipPragma p[0]
|
|
|
|
|
2019-01-23 11:49:10 +00:00
|
|
|
debugMacroResult "CLI Code"
|
2018-11-21 13:25:53 +00:00
|
|
|
|
2019-03-18 02:01:07 +00:00
|
|
|
proc load*(f: TypedInputFile): f.ContentType =
|
|
|
|
when f.Format is Unspecified or f.ContentType is Unspecified:
|
|
|
|
{.fatal: "To use `InputFile.load`, please specify the Format and ContentType of the file".}
|
|
|
|
|
|
|
|
when f.Format is Txt:
|
|
|
|
# TODO: implement a proper Txt serialization format
|
|
|
|
mixin init
|
|
|
|
f.ContentType.init readFile(f.string).string
|
|
|
|
else:
|
|
|
|
mixin loadFile
|
|
|
|
loadFile(f.Format, f.string, f.ContentType)
|
|
|
|
|