mirror of
https://github.com/status-im/nim-confutils.git
synced 2025-03-03 05:10:33 +00:00
Produce basic help messages; Couple of new APIs for simpler CLI utilities
This commit is contained in:
parent
3f20eba5a8
commit
62e53ff1c1
185
confutils.nim
185
confutils.nim
@ -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"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user