mirror of
https://github.com/status-im/nim-confutils.git
synced 2025-02-12 12:06:28 +00:00
Restore compilation with nimscript; More colorful help output
This commit is contained in:
parent
5cda9a1fa3
commit
aa5ccdd57f
150
confutils.nim
150
confutils.nim
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user