basic support for git-style interfaces
This commit is contained in:
parent
af24b62e80
commit
734368010d
211
confutils.nim
211
confutils.nim
|
@ -1,21 +1,46 @@
|
||||||
import
|
import
|
||||||
os, parseopt, strutils, macros, typetraits, confutils/defs
|
os, parseopt, strutils, std_shims/macros_shim, typetraits, confutils/defs
|
||||||
|
|
||||||
export
|
export
|
||||||
defs
|
defs
|
||||||
|
|
||||||
proc parse*(T: type DirPath, p: TaintedString): T =
|
proc parseCmdArg*(T: type DirPath, p: TaintedString): T =
|
||||||
result = DirPath(p)
|
result = DirPath(p)
|
||||||
|
|
||||||
template parse*(T: type string, s: TaintedString): string =
|
proc parseCmdArg*(T: type OutFilePath, p: TaintedString): T =
|
||||||
|
result = OutFilePath(p)
|
||||||
|
|
||||||
|
template parseCmdArg*(T: type string, s: TaintedString): string =
|
||||||
string s
|
string s
|
||||||
|
|
||||||
proc parse*(T: type SomeSignedInt, s: TaintedString): T =
|
proc parseCmdArg*(T: type SomeSignedInt, s: TaintedString): T =
|
||||||
T parseInt(string s)
|
T parseInt(string s)
|
||||||
|
|
||||||
proc parse*(T: type SomeUnsignedInt, s: TaintedString): T =
|
proc parseCmdArg*(T: type SomeUnsignedInt, s: TaintedString): T =
|
||||||
T parseUInt(string s)
|
T parseUInt(string s)
|
||||||
|
|
||||||
|
proc parseCmdArg*(T: type enum, s: TaintedString): T =
|
||||||
|
parseEnum[T](string(s))
|
||||||
|
|
||||||
|
template setField[T](loc: var T, val: TaintedString, defaultVal: untyped): bool =
|
||||||
|
mixin parseCmdArg
|
||||||
|
type FieldType = type(loc)
|
||||||
|
|
||||||
|
loc = if len(val) > 0: parseCmdArg(FieldType, val)
|
||||||
|
else: FieldType(defaultVal)
|
||||||
|
true
|
||||||
|
|
||||||
|
template setField[T](loc: var seq[T], val: TaintedString, defaultVal: untyped): bool =
|
||||||
|
mixin parseCmdArg
|
||||||
|
loc.add parseCmdArg(type(loc[0]), val)
|
||||||
|
false
|
||||||
|
|
||||||
|
template simpleSet(loc: var auto) =
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc makeDefaultValue*(T: type): T =
|
||||||
|
discard
|
||||||
|
|
||||||
proc load*(Configuration: type,
|
proc load*(Configuration: type,
|
||||||
cmdLine = commandLineParams(),
|
cmdLine = commandLineParams(),
|
||||||
printUsage = true,
|
printUsage = true,
|
||||||
|
@ -32,54 +57,109 @@ proc load*(Configuration: type,
|
||||||
# This is an initial naive implementation that will be improved
|
# This is an initial naive implementation that will be improved
|
||||||
# over time.
|
# over time.
|
||||||
|
|
||||||
mixin parse
|
mixin parseCmdArg
|
||||||
|
|
||||||
type
|
type
|
||||||
FieldSetter = proc (val: TaintedString)
|
FieldSetter = proc (cfg: var Configuration, val: TaintedString): bool {.nimcall.}
|
||||||
|
|
||||||
ParamDesc = object
|
CommandDesc = object
|
||||||
name, shorthand: string
|
name: string
|
||||||
typename: string # this is a human-readable type
|
options: seq[OptionDesc]
|
||||||
|
subCommands: seq[CommandDesc]
|
||||||
|
fieldIdx: int
|
||||||
|
argumentsFieldIdx: int
|
||||||
|
|
||||||
|
OptionDesc = object
|
||||||
|
name, typename, shortform: string
|
||||||
required: bool
|
required: bool
|
||||||
occurances: int
|
rejectNext: bool
|
||||||
isSeq: bool
|
fieldIdx: int
|
||||||
|
|
||||||
setter: FieldSetter
|
template readPragma(field, name): NimNode =
|
||||||
|
let p = field.pragmas.findPragma bindSym(name)
|
||||||
|
if p != nil and p.len == 2: p[1] else: p
|
||||||
|
|
||||||
var
|
macro generateFieldSetters(RecordType: type): untyped =
|
||||||
params = newSeq[ParamDesc]()
|
var recordDef = RecordType.getType[1].getImpl
|
||||||
requiredFields = 0
|
let makeDefaultValue = bindSym"makeDefaultValue"
|
||||||
|
|
||||||
for fieldName, field in fieldPairs(result):
|
result = newTree(nnkStmtListExpr)
|
||||||
var param: ParamDesc
|
var settersArray = newTree(nnkBracket)
|
||||||
param.name = fieldName
|
|
||||||
|
|
||||||
type FieldType = type(field)
|
for field in recordFields(recordDef):
|
||||||
|
var
|
||||||
|
setterName = ident($field.name & "Setter")
|
||||||
|
fieldName = field.name
|
||||||
|
recordVar = ident "record"
|
||||||
|
recordField = newTree(nnkDotExpr, recordVar, fieldName)
|
||||||
|
defaultValue = field.readPragma"defaultValue"
|
||||||
|
|
||||||
when field.hasCustomPragma(defaultValue):
|
if defaultValue == nil:
|
||||||
field = FieldType field.getCustomPragmaVal(defaultValue)
|
defaultValue = newCall(makeDefaultValue, newTree(nnkTypeOfExpr, recordField))
|
||||||
else:
|
|
||||||
param.required = true
|
|
||||||
|
|
||||||
when FieldType is seq:
|
settersArray.add newCall(bindSym"FieldSetter", setterName)
|
||||||
param.isSeq = true
|
|
||||||
param.required = false
|
|
||||||
|
|
||||||
param.typename = FieldType.name
|
result.add quote do:
|
||||||
|
proc `setterName`(`recordVar`: var `RecordType`, val: TaintedString): bool {.nimcall.} =
|
||||||
|
when `recordField` is enum:
|
||||||
|
# TODO: For some reason, the normal `setField` rejects enum fields
|
||||||
|
# when they are used as case discriminators. File this as a bug.
|
||||||
|
`recordField` = parseEnum[type(`recordField`)](string(val))
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
return setField(`recordField`, val, `defaultValue`)
|
||||||
|
|
||||||
var fieldAddr = addr(field)
|
result.add settersArray
|
||||||
param.setter = proc (stringValue: TaintedString) =
|
|
||||||
when FieldType is seq:
|
macro buildCommandTree(RecordType: type): untyped =
|
||||||
type ElemType = type(field[0])
|
var recordDef = RecordType.getType[1].getImpl
|
||||||
fieldAddr[].add parse(ElemType, stringValue)
|
var res: CommandDesc
|
||||||
|
res.argumentsFieldIdx = -1
|
||||||
|
|
||||||
|
var fieldIdx = 0
|
||||||
|
for field in recordFields(recordDef):
|
||||||
|
let
|
||||||
|
isCommand = field.readPragma"command" != nil
|
||||||
|
hasDefault = field.readPragma"defaultValue" != nil
|
||||||
|
shortform = field.readPragma"shortform"
|
||||||
|
longform = field.readPragma"longform"
|
||||||
|
desc = field.readPragma"desc"
|
||||||
|
|
||||||
|
if isCommand:
|
||||||
|
let cmdType = field.typ.getImpl[^1]
|
||||||
|
if cmdType.kind != nnkEnumTy:
|
||||||
|
error "The command pragma should be specified only on enum fields", field.name
|
||||||
|
for i in 2 ..< cmdType.len:
|
||||||
|
res.subCommands.add CommandDesc(name: $cmdType[i],
|
||||||
|
fieldIdx: fieldIdx,
|
||||||
|
argumentsFieldIdx: -1)
|
||||||
else:
|
else:
|
||||||
fieldAddr[] = FieldType.parse(stringValue)
|
var option: OptionDesc
|
||||||
|
option.fieldIdx = fieldIdx
|
||||||
|
option.name = $field.name
|
||||||
|
option.required = not hasDefault
|
||||||
|
option.typename = field.typ.repr
|
||||||
|
if longform != nil: option.name = longform.strVal
|
||||||
|
if shortform != nil: option.shortform = shortform.strVal
|
||||||
|
|
||||||
when field.hasCustomPragma(shorthand):
|
var isSubcommandOption = false
|
||||||
param.shorthand = field.getCustomPragmaVal(shorthand)
|
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
|
||||||
|
|
||||||
params.add param
|
if not isSubcommandOption:
|
||||||
|
res.options.add option
|
||||||
|
|
||||||
|
inc fieldIdx
|
||||||
|
|
||||||
|
result = newLitFixed(res)
|
||||||
|
|
||||||
|
let fieldSetters = generateFieldSetters(Configuration)
|
||||||
|
var rootCmd = buildCommandTree(Configuration)
|
||||||
|
|
||||||
proc fail(msg: string) =
|
proc fail(msg: string) =
|
||||||
if quitOnFailure:
|
if quitOnFailure:
|
||||||
|
@ -88,26 +168,55 @@ proc load*(Configuration: type,
|
||||||
else:
|
else:
|
||||||
raise newException(ConfigurationError, msg)
|
raise newException(ConfigurationError, msg)
|
||||||
|
|
||||||
proc findParam(name: TaintedString): ptr ParamDesc =
|
proc findOption(cmd: ptr CommandDesc, name: TaintedString): ptr OptionDesc =
|
||||||
for p in params.mitems:
|
for o in cmd.options.mitems:
|
||||||
if cmpIgnoreStyle(p.name, string(name)) == 0 or
|
if cmpIgnoreStyle(o.name, string(name)) == 0 or
|
||||||
cmpIgnoreStyle(p.shorthand, string(name)) == 0:
|
cmpIgnoreStyle(o.shortform, string(name)) == 0:
|
||||||
return addr(p)
|
return addr(o)
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
|
||||||
|
proc checkForMissingOptions(cmd: ptr CommandDesc) =
|
||||||
|
for o in cmd.options:
|
||||||
|
if o.required and o.rejectNext == false:
|
||||||
|
fail "The required option '$1' was not specified" % [o.name]
|
||||||
|
|
||||||
|
var currentCmd = addr rootCmd
|
||||||
|
var rejectNextArgument = currentCmd.argumentsFieldIdx == -1
|
||||||
|
|
||||||
for kind, key, val in getopt(cmdLine):
|
for kind, key, val in getopt(cmdLine):
|
||||||
if kind in {cmdLongOption, cmdShortOption}:
|
case kind
|
||||||
let param = findParam(key)
|
of cmdLongOption, cmdShortOption:
|
||||||
if param != nil:
|
let option = currentCmd.findOption(key)
|
||||||
inc param.occurances
|
if option != nil:
|
||||||
if param.occurances > 1 and not param.isSeq:
|
if option.rejectNext:
|
||||||
fail "The options '$1' should not be specified more than once" % [string(key)]
|
fail "The options '$1' should not be specified more than once" % [string(key)]
|
||||||
param.setter(val)
|
option.rejectNext = fieldSetters[option.fieldIdx](result, val)
|
||||||
else:
|
else:
|
||||||
fail "Unrecognized option '$1'" % [string(key)]
|
fail "Unrecognized option '$1'" % [string(key)]
|
||||||
|
|
||||||
for p in params:
|
of cmdArgument:
|
||||||
if p.required and p.occurances == 0:
|
let subCmd = currentCmd.findSubcommand(key)
|
||||||
fail "The required option '$1' was not specified" % [p.name]
|
if subCmd != nil:
|
||||||
|
discard fieldSetters[subCmd.fieldIdx](result, key)
|
||||||
|
currentCmd = subCmd
|
||||||
|
rejectNextArgument = currentCmd.argumentsFieldIdx == -1
|
||||||
|
else:
|
||||||
|
if rejectNextArgument:
|
||||||
|
fail "The command '$1' does not accept additional arguments" % [currentCmd.name]
|
||||||
|
let argumentIdx = currentCmd.argumentsFieldIdx
|
||||||
|
doAssert argumentIdx != -1
|
||||||
|
rejectNextArgument = fieldSetters[argumentIdx](result, key)
|
||||||
|
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
currentCmd.checkForMissingOptions()
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
type
|
type
|
||||||
DirPath* = distinct string
|
DirPath* = distinct string
|
||||||
|
OutFilePath* = distinct string
|
||||||
ConfigurationError* = object of CatchableError
|
ConfigurationError* = object of CatchableError
|
||||||
|
|
||||||
template desc*(v: string) {.pragma.}
|
template desc*(v: string) {.pragma.}
|
||||||
template shorthand*(v: string) {.pragma.}
|
template longform*(v: string) {.pragma.}
|
||||||
|
template shortform*(v: string) {.pragma.}
|
||||||
template defaultValue*(v: untyped) {.pragma.}
|
template defaultValue*(v: untyped) {.pragma.}
|
||||||
|
template required* {.pragma.}
|
||||||
|
template command* {.pragma.}
|
||||||
|
template argument* {.pragma.}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue