Produce basic help messages; Couple of new APIs for simpler CLI utilities

This commit is contained in:
Zahary Karadjov 2019-01-23 13:49:10 +02:00
parent 3f20eba5a8
commit 62e53ff1c1

View File

@ -1,10 +1,67 @@
import
os, parseopt, strutils, options, std_shims/macros_shim, typetraits,
os, parseopt, strutils, options, std_shims/macros_shim, typetraits, terminal,
confutils/defs
export
defs
type
CommandDesc = object
name: string
options: seq[OptionDesc]
subCommands: seq[CommandDesc]
fieldIdx: int
argumentsFieldIdx: int
OptionDesc = object
name, typename, shortform: string
hasDefault: bool
rejectNext: bool
fieldIdx: int
desc: string
template appName: string =
getAppFilename().splitFile.name
when not defined(confutils_no_colors):
template write(args: varargs[untyped]) =
stdout.styledWrite(args)
else:
const
styleBright = ""
template write(args: varargs[untyped]) =
stdout.write(args)
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"
proc showHelp(cmd: CommandDesc) =
let app = appName
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)
if cmd.subCommands.len > 0:
write "\nAvailable sub-commands:\n\n"
for subcmd in cmd.subCommands:
write " ", styleBright, app, " ", subcmd.name, "\n\n"
describeCmdOptions(subcmd)
write "\n"
quit(0)
proc parseCmdArg*(T: type DirPath, p: TaintedString): T =
# TODO: check existence
result = DirPath(p)
@ -54,6 +111,16 @@ proc makeDefaultValue*(T: type): T =
proc requiresInput*(T: type): bool =
result = not ((T is seq) or (T is Option))
# 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
proc load*(Configuration: type,
cmdLine = commandLineParams(),
printUsage = true,
@ -75,19 +142,6 @@ proc load*(Configuration: type,
type
FieldSetter = proc (cfg: var Configuration, val: TaintedString): bool {.nimcall.}
CommandDesc = object
name: string
options: seq[OptionDesc]
subCommands: seq[CommandDesc]
fieldIdx: int
argumentsFieldIdx: int
OptionDesc = object
name, typename, shortform: string
hasDefault: bool
rejectNext: bool
fieldIdx: int
template readPragma(field, name): NimNode =
let p = field.pragmas.findPragma bindSym(name)
if p != nil and p.len == 2: p[1] else: p
@ -110,9 +164,13 @@ proc load*(Configuration: type,
if defaultValue == nil:
defaultValue = newCall(makeDefaultValue, newTree(nnkTypeOfExpr, recordField))
# 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)
settersArray.add newTree(nnkTupleConstr,
newCall(bindSym"FieldSetter", setterName),
newCall(bindSym"requiresInput", newTree(nnkTypeOfExpr, field.typ)))
newCall(bindSym"requiresInput", fixedFieldType))
result.add quote do:
proc `setterName`(`recordVar`: var `RecordType`, val: TaintedString): bool {.nimcall.} =
@ -125,6 +183,7 @@ proc load*(Configuration: type,
return setField(`recordField`, val, `defaultValue`)
result.add settersArray
debugMacroResult "Field Setters"
macro buildCommandTree(RecordType: type): untyped =
var recordDef = RecordType.getType[1].getImpl
@ -154,6 +213,7 @@ proc load*(Configuration: type,
option.name = $field.name
option.hasDefault = hasDefault
option.typename = field.typ.repr
if desc != nil: option.desc = desc.strVal
if longform != nil: option.name = longform.strVal
if shortform != nil: option.shortform = shortform.strVal
@ -172,6 +232,7 @@ proc load*(Configuration: type,
inc fieldIdx
result = newLitFixed(res)
debugMacroResult "Command Tree"
let fieldSetters = generateFieldSetters(Configuration)
var rootCmd = buildCommandTree(Configuration)
@ -179,6 +240,7 @@ proc load*(Configuration: type,
proc fail(msg: string) =
if quitOnFailure:
stderr.writeLine(msg)
stderr.writeLine("Try '{1} --help' for more information" % appName)
quit 1
else:
raise newException(ConfigurationError, msg)
@ -201,10 +263,13 @@ proc load*(Configuration: type,
template required(opt: OptionDesc): bool =
fieldSetters[opt.fieldIdx][1] and not opt.hasDefault
proc checkForMissingOptions(cmd: ptr CommandDesc) =
proc processMissingOptions(conf: var Configuration, 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]
if o.rejectNext == false:
if o.required:
fail "The required option '$1' was not specified" % [o.name]
elif o.hasDefault:
discard fieldSetters[o.fieldIdx][0](conf, TaintedString(""))
var currentCmd = addr rootCmd
var rejectNextArgument = currentCmd.argumentsFieldIdx == -1
@ -212,6 +277,9 @@ proc load*(Configuration: type,
for kind, key, val in getopt(cmdLine):
case kind
of cmdLongOption, cmdShortOption:
if string(key) == "help":
showHelp currentCmd[]
let option = currentCmd.findOption(key)
if option != nil:
if option.rejectNext:
@ -221,6 +289,9 @@ proc load*(Configuration: type,
fail "Unrecognized option '$1'" % [string(key)]
of cmdArgument:
if string(key) == "help" and currentCmd.subCommands.len > 0:
showHelp currentCmd[]
let subCmd = currentCmd.findSubcommand(key)
if subCmd != nil:
discard fieldSetters[subCmd.fieldIdx][0](result, key)
@ -236,5 +307,81 @@ proc load*(Configuration: type,
else:
discard
currentCmd.checkForMissingOptions()
result.processMissingOptions(currentCmd)
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
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:
arg[0] = newTree(nnkPragmaExpr, arg[0])
arg[0].add newTree(nnkPragma,
newColonExpr(bindSym"defaultValue", arg[2]))
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))
debugMacroResult "CLI Code"