Restore compilation with nimscript; More colorful help output

This commit is contained in:
Zahary Karadjov 2019-10-23 03:30:40 +03:00
parent 5cda9a1fa3
commit aa5ccdd57f
No known key found for this signature in database
GPG Key ID: C8936F8A3073D609
3 changed files with 291 additions and 244 deletions

View File

@ -1,19 +1,21 @@
import
std/[strutils, wordwrap, options, typetraits],
std/[options, strutils, wordwrap],
stew/shims/macros,
confutils/[defs, cli_parser, shell_completion]
confutils/[defs, cli_parser]
export
defs
const
useBufferedOutput = true # defined(nimscript)
useBufferedOutput = defined(nimscript)
noColors = useBufferedOutput or defined(confutils_no_colors)
descriptionPadding = 6
minLongformsWidth = 24 - descriptionPadding
when not defined(nimscript):
import os, terminal
import
os, terminal,
confutils/shell_completion
type
HelpAppInfo = ref object
@ -47,6 +49,9 @@ type
else:
discard
FieldSetter[Configuration] = proc (cfg: var Configuration, val: TaintedString) {.nimcall, gcsafe.}
FieldCompleter = proc (val: TaintedString): seq[string] {.nimcall, gcsafe.}
proc newLit*(arg: ref): NimNode {.compileTime.} =
result = nnkObjConstr.newTree(arg.type.getTypeInst[1])
for a, b in fieldPairs(arg[]):
@ -83,7 +88,13 @@ else:
getAppFilename().splitFile.name
when noColors:
const styleBright = ""
const
styleBright = ""
fgYellow = ""
fgWhite = ""
fgGreen = ""
fgCyan = ""
fgBlue = ""
when useBufferedOutput:
template helpOutput(args: varargs[string]) =
@ -100,6 +111,13 @@ else:
template flushHelp =
discard
const
fgSection = fgYellow
fgCommand = fgCyan
fgOption = fgBlue
fgValue = fgGreen
fgType = fgYellow
func isCliSwitch(opt: OptInfo): bool =
opt.kind == CliSwitch or
(opt.kind == Discriminator and opt.isCommand == false)
@ -173,8 +191,8 @@ func humaneName(opt: OptInfo): string =
if opt.longform.len > 0: opt.longform
else: opt.shortform
proc paddedOutput(help: var string, output: string, desiredWidth: int) =
helpOutput output, spaces(max(desiredWidth - output.len, 0))
template padding(output: string, desiredWidth: int): string =
spaces(max(desiredWidth - output.len, 0))
proc writeDesc(help: var string, appInfo: HelpAppInfo, desc: string) =
let
@ -188,9 +206,10 @@ proc writeDesc(help: var string, appInfo: HelpAppInfo, desc: string) =
helpOutput wrapWords(desc, remainingColumns,
newLine = "\p" & spaces(nonDescColumns))
proc describeInvocation(cmd: CmdInfo, cmdInvocation: string,
appInfo: HelpAppInfo, help: var string) =
helpOutput styleBright, cmdInvocation
proc describeInvocation(help: var string,
cmd: CmdInfo, cmdInvocation: string,
appInfo: HelpAppInfo) =
helpOutput styleBright, "\p", fgCommand, cmdInvocation
var longestArg = 0
if cmd.opts.len > 0:
@ -203,30 +222,41 @@ proc describeInvocation(cmd: CmdInfo, cmdInvocation: string,
helpOutput " <", arg.longform, ">"
longestArg = max(longestArg, arg.longform.len)
helpOutput "\p\p"
helpOutput "\p"
for arg in cmd.args:
if arg.desc.len > 0:
help.paddedOutput("<" & arg.humaneName & ">",
6 + appInfo.longformsWidth)
let cliArg = "<" & arg.humaneName & ">"
helpOutput cliArg, padding(cliArg, 6 + appInfo.longformsWidth)
help.writeDesc appInfo, arg.desc
proc describeOptions(cmd: CmdInfo, cmdInvocation: string,
appInfo: HelpAppInfo, help: var string) =
proc describeOptions(help: var string,
cmd: CmdInfo, cmdInvocation: string,
appInfo: HelpAppInfo, isSubOptions = false) =
if cmd.hasOpts:
helpOutput "The following options are available:\p\p"
if isSubOptions:
helpOutput ", the following additional options are available:\p\p"
else:
helpOutput "\pThe following options are available:\p\p"
for opt in cmd.opts:
if opt.kind == Arg: continue
if opt.kind == Discriminator:
if opt.isCommand: continue
# Indent all command-line switches
helpOutput " "
if opt.shortform.len > 0:
helpOutput styleBright, " -", opt.shortform, " "
helpOutput fgOption, styleBright, "-", opt.shortform, ", "
elif appInfo.hasShortforms:
# Add additional indentatition, so all longforms are aligned
helpOutput " "
if opt.longform.len > 0:
help.paddedOutput("--" & opt.longform, appInfo.longformsWidth)
let switch = "--" & opt.longform
helpOutput fgOption, styleBright,
switch, padding(switch, appInfo.longformsWidth)
else:
helpOutput spaces(2 + appInfo.longformsWidth)
@ -237,27 +267,27 @@ proc describeOptions(cmd: CmdInfo, cmdInvocation: string,
if opt.kind == Discriminator:
for i, subCmd in opt.subCmds:
helpOutput "\pWhen ", opt.humaneName, "=", subCmd.name
if i == opt.defaultSubCmd: helpOutput " (default)"
helpOutput ":\p\p"
subCmd.describeOptions cmdInvocation, appInfo, help
if not subCmd.hasOpts: continue
helpOutput "\p"
helpOutput "\pWhen ", styleBright, fgBlue, opt.humaneName, fgWhite, " = ", fgGreen, subCmd.name
if i == opt.defaultSubCmd: helpOutput " (default)"
help.describeOptions subCmd, cmdInvocation, appInfo, isSubOptions = true
let subCmdDiscriminator = cmd.getSubCmdDiscriminator
if subCmdDiscriminator != nil:
let defaultCmdIdx = subCmdDiscriminator.defaultSubCmd
if defaultCmdIdx != -1:
let defaultCmd = subCmdDiscriminator.subCmds[defaultCmdIdx]
defaultCmd.describeOptions cmdInvocation, appInfo, help
help.describeOptions defaultCmd, cmdInvocation, appInfo
helpOutput "Available sub-commands:\p\p"
helpOutput fgSection, "\pAvailable sub-commands:\p"
for i, subCmd in subCmdDiscriminator.subCmds:
if i != subCmdDiscriminator.defaultSubCmd:
let subCmdInvocation = cmdInvocation & " " & subCmd.name
subCmd.describeInvocation subCmdInvocation, appInfo, help
subCmd.describeOptions subCmdInvocation, appInfo, help
help.describeInvocation subCmd, subCmdInvocation, appInfo
help.describeOptions subCmd, subCmdInvocation, appInfo
proc showHelp(appInfo: HelpAppInfo, activeCmds: openarray[CmdInfo]) =
var help = ""
@ -277,9 +307,10 @@ proc showHelp(appInfo: HelpAppInfo, activeCmds: openarray[CmdInfo]) =
cmdInvocation.add activeCmds[i].name
# Write out the app or script name
helpOutput "Usage: "
cmd.describeInvocation cmdInvocation, appInfo, help
cmd.describeOptions cmdInvocation, appInfo, help
helpOutput fgSection, "Usage: \p"
help.describeInvocation cmd, cmdInvocation, appInfo
help.describeOptions cmd, cmdInvocation, appInfo
helpOutput "\p"
flushHelp
quit 1
@ -447,6 +478,7 @@ proc completeCmdArg(T: type string, val: TaintedString): seq[string] =
proc completeCmdArg*(T: type[InputFile|TypedInputFile|InputDir|OutFile|OutDir|OutPath],
val: TaintedString): seq[string] =
when not defined(nimscript):
let (dir, name, ext) = splitFile(val)
let tail = name & ext
# Expand the directory component for the directory walker routine
@ -507,28 +539,7 @@ template debugMacroResult(macroName: string) {.dirty.} =
echo "\n-------- ", macroName, " ----------------------"
echo result.repr
proc load*(Configuration: type,
cmdLine = commandLineParams(),
version = "",
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
FieldSetter = proc (cfg: var Configuration, val: TaintedString) {.nimcall.}
FieldCompleter = proc (val: TaintedString): seq[string] {.nimcall.}
macro generateFieldSetters(RecordType: type): untyped =
macro generateFieldSetters(RecordType: type): untyped =
var recordDef = RecordType.getType[1].getImpl
let makeDefaultValue = bindSym"makeDefaultValue"
@ -553,16 +564,15 @@ proc load*(Configuration: type,
settersArray.add newTree(nnkTupleConstr,
newLit($fieldName),
newCall(bindSym"FieldSetter", setterName),
newCall(bindSym"FieldCompleter", completerName),
setterName, completerName,
newCall(bindSym"requiresInput", fixedFieldType),
newCall(bindSym"acceptsMultipleValues", fixedFieldType))
result.add quote do:
proc `completerName`(val: TaintedString): seq[string] {.nimcall.} =
proc `completerName`(val: TaintedString): seq[string] {.nimcall, gcsafe.} =
return completeCmdArgAux(`fixedFieldType`, val)
proc `setterName`(`configVar`: var `RecordType`, val: TaintedString) {.nimcall.} =
proc `setterName`(`configVar`: var `RecordType`, val: TaintedString) {.nimcall, gcsafe.} =
when `configField` is enum:
# TODO: For some reason, the normal `setField` rejects enum fields
# when they are used as case discriminators. File this as a bug.
@ -576,7 +586,7 @@ proc load*(Configuration: type,
result.add settersArray
debugMacroResult "Field Setters"
macro buildCommandTree(RecordType: type): untyped =
macro buildCommandTree(RecordType: type): untyped =
var
recordDef = RecordType.getType[1].getImpl
res = CmdInfo()
@ -585,14 +595,13 @@ proc load*(Configuration: type,
for field in recordFields(recordDef):
let
isDiscriminator = field.caseField != nil and field.caseBranch == nil
isImplicitlySelectable = field.readPragma"implicitlySelectable" != nil
defaultValue = field.readPragma"defaultValue"
shortform = field.readPragma"shortform"
longform = field.readPragma"longform"
desc = field.readPragma"desc"
var opt = OptInfo(kind: if isDiscriminator: Discriminator else: CliSwitch,
var opt = OptInfo(kind: if field.isDiscriminator: Discriminator else: CliSwitch,
idx: fieldIdx,
longform: $field.name,
hasDefault: defaultValue != nil,
@ -604,7 +613,7 @@ proc load*(Configuration: type,
inc fieldIdx
if isDiscriminator:
if field.isDiscriminator:
discriminatorFields.add opt
let cmdType = field.typ.getImpl[^1]
if cmdType.kind != nnkEnumTy:
@ -625,8 +634,7 @@ proc load*(Configuration: type,
if opt.defaultSubCmd == -1:
error "The default value is not a valid enum value", defaultValue
else:
if field.caseField != nil:
if field.caseField != nil and field.caseBranch != nil:
let fieldName = field.caseField.getFieldName
var discriminator = findOpt(discriminatorFields, $fieldName)
if discriminator == nil:
@ -640,6 +648,23 @@ proc load*(Configuration: type,
result = newLit(res)
debugMacroResult "Command Tree"
proc load*(Configuration: type,
cmdLine = commandLineParams(),
version = "",
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.
let fieldSetters = generateFieldSetters(Configuration)
var fieldCounters: array[fieldSetters.len, int]
@ -695,6 +720,7 @@ proc load*(Configuration: type,
longForm
shortForm
when not defined(nimscript):
proc showMatchingOptions(cmd: CmdInfo, prefix: string, filterKind: set[ArgKindFilter]) =
var matchingOptions: seq[OptInfo]

View File

@ -1,9 +1,6 @@
# Copyright 2018 Status Research & Development GmbH
# Parts taken from Nim's Runtime Library (c) Copyright 2015 Andreas Rumpf
import
strutils
type
CmdLineKind* = enum ## The detected command line token.
cmdEnd, ## End of command line reached

View File

@ -10,15 +10,39 @@ type
innerCmd1
innerCmd2
OuterOpt = enum
outerOpt1
outerOpt2
InnerOpt = enum
innerOpt1
innerOpt2
Conf = object
commonOptional: Option[string]
commonMandatory: int
case cmd: OuterCmd
commonMandatory {.
desc: "A mandatory option"
shortform: "m" .}: int
case opt: OuterOpt
of outerOpt1:
case innerOpt: InnerOpt
of innerOpt1:
io1Mandatory: string
io1Optional: Option[int]
else:
discard
of outerOpt2:
ooMandatory: string
case cmd {.command.}: OuterCmd
of outerCmd1:
case innerCmd: InnerCmd
of innerCmd1:
ic1Mandatory: string
ic1Optional: Option[int]
ic1Optional {.
desc: "Delay in seconds"
shortform: "s" .}: Option[int]
else:
discard
of outerCmd2: