basic support for git-style interfaces

This commit is contained in:
Zahary Karadjov 2018-12-19 12:52:32 +02:00
parent af24b62e80
commit 734368010d
2 changed files with 166 additions and 52 deletions

View File

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

View File

@ -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.}