mirror of
https://github.com/status-im/nim-confutils.git
synced 2025-03-03 21:30:50 +00:00
* Fix `cli` invocation from nimscript When calling `cli` macro from nimscript, there are compilation issues: - `nim-faststreams` is not available, therefore, `nim-serialization` does not work, due to `equalMem` being gated behind `notJSnotNims`. Dropping support for config files in nimscript contexts fixes that. - `std/strformat` raises `ValueError` for invalid format strings, but does so at runtime rather than checking types at compiletime. As it is only used for simple string concatenation in error cases, changing to simple concatenation avoids verbose error handling. - `getAppFilename()` is unavailable in `nimscript`. This was already fixed by replacing it with `appInvocation()` but two instances of direct `getAppFilename()` calls remained in default arguments. This is fixed by changing those default arguments as well. - The `!= nil` check on the `proc` in `loadImpl` does not work when called from nimscript. This is fixed by changing to `isNil`. - Passing `addr result` around to internal templates correctly creates the config, but the object ultimately being returned is not the same. Passing `var result` directly through the templates ensures that the correct `result` gets modified and is clearer than implicit capture. Applying these fixes fixes running `.nims` files with `cli` macro. * Add debugging output on failure * Update confutils.nimble * Update confutils.nim
1303 lines
41 KiB
Nim
1303 lines
41 KiB
Nim
# confutils
|
|
# Copyright (c) 2018-2024 Status Research & Development GmbH
|
|
# Licensed under either of
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
# at your option.
|
|
# This file may not be copied, modified, or distributed except according to
|
|
# those terms.
|
|
|
|
{.push raises: [].}
|
|
|
|
import
|
|
os,
|
|
std/[enumutils, options, strutils, wordwrap],
|
|
stew/shims/macros,
|
|
confutils/[defs, cli_parser, config_file]
|
|
|
|
export
|
|
options, defs, config_file
|
|
|
|
const
|
|
hasSerialization = not defined(nimscript)
|
|
useBufferedOutput = defined(nimscript)
|
|
noColors = useBufferedOutput or defined(confutils_no_colors)
|
|
hasCompletions = not defined(nimscript)
|
|
descPadding = 6
|
|
minNameWidth = 24 - descPadding
|
|
|
|
when hasSerialization:
|
|
import serialization
|
|
export serialization
|
|
|
|
when not defined(nimscript):
|
|
import
|
|
terminal,
|
|
confutils/shell_completion
|
|
|
|
type
|
|
HelpAppInfo = ref object
|
|
appInvocation: string
|
|
copyrightBanner: string
|
|
hasAbbrs: bool
|
|
maxNameLen: int
|
|
terminalWidth: int
|
|
namesWidth: int
|
|
|
|
CmdInfo = ref object
|
|
name: string
|
|
desc: string
|
|
isHidden: bool
|
|
opts: seq[OptInfo]
|
|
|
|
OptKind = enum
|
|
Discriminator
|
|
CliSwitch
|
|
Arg
|
|
|
|
OptInfo = ref object
|
|
name, abbr, desc, typename: string
|
|
separator: string
|
|
longDesc: string
|
|
idx: int
|
|
isHidden: bool
|
|
hasDefault: bool
|
|
defaultInHelpText: string
|
|
case kind: OptKind
|
|
of Discriminator:
|
|
isCommand: bool
|
|
isImplicitlySelectable: bool
|
|
subCmds: seq[CmdInfo]
|
|
defaultSubCmd: int
|
|
else:
|
|
discard
|
|
|
|
const
|
|
confutils_description_width {.intdefine.} = 80
|
|
confutils_narrow_terminal_width {.intdefine.} = 36
|
|
|
|
{.push gcsafe, raises: [].}
|
|
|
|
func getFieldName(caseField: NimNode): NimNode =
|
|
result = caseField
|
|
if result.kind == nnkIdentDefs: result = result[0]
|
|
if result.kind == nnkPragmaExpr: result = result[0]
|
|
if result.kind == nnkPostfix: result = result[1]
|
|
|
|
when defined(nimscript):
|
|
func scriptNameParamIdx: int =
|
|
for i in 1 ..< paramCount():
|
|
var param = paramStr(i)
|
|
if param.len > 0 and param[0] != '-':
|
|
return i
|
|
|
|
proc appInvocation: string =
|
|
let scriptNameIdx = scriptNameParamIdx()
|
|
"nim " & (if paramCount() > scriptNameIdx: paramStr(scriptNameIdx) else: "<nims-script>")
|
|
|
|
type stderr = object
|
|
|
|
template writeLine(T: type stderr, msg: string) =
|
|
echo msg
|
|
|
|
proc commandLineParams(): seq[string] =
|
|
for i in scriptNameParamIdx() + 1 .. paramCount():
|
|
result.add paramStr(i)
|
|
|
|
# TODO: Why isn't this available in NimScript?
|
|
proc getCurrentExceptionMsg(): string =
|
|
""
|
|
|
|
template terminalWidth: int =
|
|
100000
|
|
|
|
else:
|
|
template appInvocation: string =
|
|
try:
|
|
getAppFilename().splitFile.name
|
|
except OSError:
|
|
""
|
|
|
|
when noColors:
|
|
const
|
|
styleBright = ""
|
|
fgYellow = ""
|
|
fgWhite = ""
|
|
fgGreen = ""
|
|
fgCyan = ""
|
|
fgBlue = ""
|
|
resetStyle = ""
|
|
|
|
when useBufferedOutput:
|
|
template helpOutput(args: varargs[string]) =
|
|
for arg in args:
|
|
help.add arg
|
|
|
|
template errorOutput(args: varargs[string]) =
|
|
helpOutput(args)
|
|
|
|
template flushOutput =
|
|
echo help
|
|
|
|
else:
|
|
template errorOutput(args: varargs[untyped]) =
|
|
try:
|
|
styledWrite stderr, args
|
|
except IOError, ValueError:
|
|
discard
|
|
|
|
template helpOutput(args: varargs[untyped]) =
|
|
try:
|
|
styledWrite stdout, args
|
|
except IOError, ValueError:
|
|
discard
|
|
|
|
template flushOutput =
|
|
discard
|
|
|
|
const
|
|
fgSection = fgYellow
|
|
fgDefault = fgWhite
|
|
fgCommand = fgCyan
|
|
fgOption = fgBlue
|
|
fgArg = fgBlue
|
|
|
|
# TODO: Start using these:
|
|
# fgValue = fgGreen
|
|
# fgType = fgYellow
|
|
|
|
template flushOutputAndQuit(exitCode: int) =
|
|
flushOutput
|
|
quit exitCode
|
|
|
|
func isCliSwitch(opt: OptInfo): bool =
|
|
opt.kind == CliSwitch or
|
|
(opt.kind == Discriminator and opt.isCommand == false)
|
|
|
|
func hasOpts(cmd: CmdInfo): bool =
|
|
for opt in cmd.opts:
|
|
if opt.isCliSwitch and not opt.isHidden:
|
|
return true
|
|
|
|
return false
|
|
|
|
func hasArgs(cmd: CmdInfo): bool =
|
|
cmd.opts.len > 0 and cmd.opts[^1].kind == Arg
|
|
|
|
func firstArgIdx(cmd: CmdInfo): int =
|
|
# This will work correctly only if the command has arguments.
|
|
result = cmd.opts.len - 1
|
|
while result > 0:
|
|
if cmd.opts[result - 1].kind != Arg:
|
|
return
|
|
dec result
|
|
|
|
iterator args(cmd: CmdInfo): OptInfo =
|
|
if cmd.hasArgs:
|
|
for i in cmd.firstArgIdx ..< cmd.opts.len:
|
|
yield cmd.opts[i]
|
|
|
|
func getSubCmdDiscriminator(cmd: CmdInfo): OptInfo =
|
|
for i in countdown(cmd.opts.len - 1, 0):
|
|
let opt = cmd.opts[i]
|
|
if opt.kind != Arg:
|
|
if opt.kind == Discriminator and opt.isCommand:
|
|
return opt
|
|
else:
|
|
return nil
|
|
|
|
template hasSubCommands(cmd: CmdInfo): bool =
|
|
getSubCmdDiscriminator(cmd) != nil
|
|
|
|
iterator subCmds(cmd: CmdInfo): CmdInfo =
|
|
let subCmdDiscriminator = cmd.getSubCmdDiscriminator
|
|
if subCmdDiscriminator != nil:
|
|
for cmd in subCmdDiscriminator.subCmds:
|
|
yield cmd
|
|
|
|
template isSubCommand(cmd: CmdInfo): bool =
|
|
cmd.name.len > 0
|
|
|
|
func maxNameLen(cmd: CmdInfo): int =
|
|
result = 0
|
|
for opt in cmd.opts:
|
|
if opt.kind == Arg or opt.kind == Discriminator and opt.isCommand:
|
|
continue
|
|
result = max(result, opt.name.len)
|
|
if opt.kind == Discriminator:
|
|
for subCmd in opt.subCmds:
|
|
result = max(result, subCmd.maxNameLen)
|
|
|
|
func hasAbbrs(cmd: CmdInfo): bool =
|
|
for opt in cmd.opts:
|
|
if opt.kind == Arg or opt.kind == Discriminator and opt.isCommand:
|
|
continue
|
|
if opt.abbr.len > 0:
|
|
return true
|
|
if opt.kind == Discriminator:
|
|
for subCmd in opt.subCmds:
|
|
if hasAbbrs(subCmd):
|
|
return true
|
|
|
|
func humaneName(opt: OptInfo): string =
|
|
if opt.name.len > 0: opt.name
|
|
else: opt.abbr
|
|
|
|
template padding(output: string, desiredWidth: int): string =
|
|
spaces(max(desiredWidth - output.len, 0))
|
|
|
|
proc writeDesc(help: var string,
|
|
appInfo: HelpAppInfo,
|
|
desc, defaultValue: string) =
|
|
const descSpacing = " "
|
|
let
|
|
descIndent = (5 + appInfo.namesWidth + descSpacing.len)
|
|
remainingColumns = appInfo.terminalWidth - descIndent
|
|
defaultValSuffix = if defaultValue.len == 0: ""
|
|
else: " [=" & defaultValue & "]"
|
|
fullDesc = desc & defaultValSuffix & "."
|
|
|
|
if remainingColumns < confutils_narrow_terminal_width:
|
|
helpOutput "\p ", wrapWords(fullDesc, appInfo.terminalWidth - 2,
|
|
newLine = "\p ")
|
|
else:
|
|
let wrappingWidth = min(remainingColumns, confutils_description_width)
|
|
helpOutput descSpacing, wrapWords(fullDesc, wrappingWidth,
|
|
newLine = "\p" & spaces(descIndent))
|
|
|
|
proc writeLongDesc(help: var string,
|
|
appInfo: HelpAppInfo,
|
|
desc: string) =
|
|
let lines = split(desc, {'\n', '\r'})
|
|
for line in lines:
|
|
if line.len > 0:
|
|
helpOutput "\p"
|
|
helpOutput padding("", 5 + appInfo.namesWidth)
|
|
help.writeDesc appInfo, line, ""
|
|
|
|
proc describeInvocation(help: var string,
|
|
cmd: CmdInfo, cmdInvocation: string,
|
|
appInfo: HelpAppInfo) =
|
|
helpOutput styleBright, "\p", fgCommand, cmdInvocation
|
|
var longestArg = 0
|
|
|
|
if cmd.opts.len > 0:
|
|
if cmd.hasOpts: helpOutput " [OPTIONS]..."
|
|
|
|
let subCmdDiscriminator = cmd.getSubCmdDiscriminator
|
|
if subCmdDiscriminator != nil: helpOutput " command"
|
|
|
|
for arg in cmd.args:
|
|
helpOutput " <", arg.name, ">"
|
|
longestArg = max(longestArg, arg.name.len)
|
|
|
|
helpOutput "\p"
|
|
|
|
if cmd.desc.len > 0:
|
|
helpOutput "\p", cmd.desc, ".\p"
|
|
|
|
var argsSectionStarted = false
|
|
|
|
for arg in cmd.args:
|
|
if arg.desc.len > 0:
|
|
if not argsSectionStarted:
|
|
helpOutput "\p"
|
|
argsSectionStarted = true
|
|
|
|
let cliArg = " <" & arg.humaneName & ">"
|
|
helpOutput fgArg, styleBright, cliArg
|
|
helpOutput padding(cliArg, 6 + appInfo.namesWidth)
|
|
help.writeDesc appInfo, arg.desc, arg.defaultInHelpText
|
|
help.writeLongDesc appInfo, arg.longDesc
|
|
helpOutput "\p"
|
|
|
|
type
|
|
OptionsType = enum
|
|
normalOpts
|
|
defaultCmdOpts
|
|
conditionalOpts
|
|
|
|
proc describeOptions(help: var string,
|
|
cmd: CmdInfo, cmdInvocation: string,
|
|
appInfo: HelpAppInfo, optionsType = normalOpts) =
|
|
if cmd.hasOpts:
|
|
case optionsType
|
|
of normalOpts:
|
|
helpOutput "\pThe following options are available:\p\p"
|
|
of conditionalOpts:
|
|
helpOutput ", the following additional options are available:\p\p"
|
|
of defaultCmdOpts:
|
|
discard
|
|
|
|
for opt in cmd.opts:
|
|
if opt.kind == Arg or
|
|
opt.kind == Discriminator or
|
|
opt.isHidden: continue
|
|
|
|
if opt.separator.len > 0:
|
|
helpOutput opt.separator
|
|
helpOutput "\p"
|
|
|
|
# Indent all command-line switches
|
|
helpOutput " "
|
|
|
|
if opt.abbr.len > 0:
|
|
helpOutput fgOption, styleBright, "-", opt.abbr, ", "
|
|
elif appInfo.hasAbbrs:
|
|
# Add additional indentatition, so all names are aligned
|
|
helpOutput " "
|
|
|
|
if opt.name.len > 0:
|
|
let switch = "--" & opt.name
|
|
helpOutput fgOption, styleBright,
|
|
switch, padding(switch, appInfo.namesWidth)
|
|
else:
|
|
helpOutput spaces(2 + appInfo.namesWidth)
|
|
|
|
if opt.desc.len > 0:
|
|
help.writeDesc appInfo,
|
|
opt.desc.replace("%t", opt.typename),
|
|
opt.defaultInHelpText
|
|
help.writeLongDesc appInfo, opt.longDesc
|
|
|
|
helpOutput "\p"
|
|
|
|
if opt.kind == Discriminator:
|
|
for i, subCmd in opt.subCmds:
|
|
if not subCmd.hasOpts: continue
|
|
|
|
helpOutput "\pWhen ", styleBright, fgBlue, opt.humaneName, resetStyle, " = ", fgGreen, subCmd.name
|
|
|
|
if i == opt.defaultSubCmd: helpOutput " (default)"
|
|
help.describeOptions subCmd, cmdInvocation, appInfo, conditionalOpts
|
|
|
|
let subCmdDiscriminator = cmd.getSubCmdDiscriminator
|
|
if subCmdDiscriminator != nil:
|
|
let defaultCmdIdx = subCmdDiscriminator.defaultSubCmd
|
|
if defaultCmdIdx != -1:
|
|
let defaultCmd = subCmdDiscriminator.subCmds[defaultCmdIdx]
|
|
help.describeOptions defaultCmd, cmdInvocation, appInfo, defaultCmdOpts
|
|
|
|
helpOutput fgSection, "\pAvailable sub-commands:\p"
|
|
|
|
for i, subCmd in subCmdDiscriminator.subCmds:
|
|
if i != subCmdDiscriminator.defaultSubCmd:
|
|
let subCmdInvocation = cmdInvocation & " " & subCmd.name
|
|
help.describeInvocation subCmd, subCmdInvocation, appInfo
|
|
help.describeOptions subCmd, subCmdInvocation, appInfo
|
|
|
|
proc showHelp(help: var string,
|
|
appInfo: HelpAppInfo,
|
|
activeCmds: openArray[CmdInfo]) =
|
|
if appInfo.copyrightBanner.len > 0:
|
|
helpOutput appInfo.copyrightBanner, "\p\p"
|
|
|
|
let cmd = activeCmds[^1]
|
|
|
|
appInfo.maxNameLen = cmd.maxNameLen
|
|
appInfo.hasAbbrs = cmd.hasAbbrs
|
|
appInfo.terminalWidth =
|
|
try:
|
|
terminalWidth()
|
|
except ValueError:
|
|
int.high # https://github.com/nim-lang/Nim/pull/21968
|
|
appInfo.namesWidth = min(minNameWidth, appInfo.maxNameLen) + descPadding
|
|
|
|
var cmdInvocation = appInfo.appInvocation
|
|
for i in 1 ..< activeCmds.len:
|
|
cmdInvocation.add " "
|
|
cmdInvocation.add activeCmds[i].name
|
|
|
|
# Write out the app or script name
|
|
helpOutput fgSection, "Usage: \p"
|
|
help.describeInvocation cmd, cmdInvocation, appInfo
|
|
help.describeOptions cmd, cmdInvocation, appInfo
|
|
helpOutput "\p"
|
|
|
|
flushOutputAndQuit QuitSuccess
|
|
|
|
func getNextArgIdx(cmd: CmdInfo, consumedArgIdx: int): int =
|
|
for i in 0 ..< cmd.opts.len:
|
|
if cmd.opts[i].kind == Arg and cmd.opts[i].idx > consumedArgIdx:
|
|
return cmd.opts[i].idx
|
|
|
|
-1
|
|
|
|
proc noMoreArgsError(cmd: CmdInfo): string {.raises: [].} =
|
|
result =
|
|
if cmd.isSubCommand:
|
|
"The command '" & cmd.name & "'"
|
|
else:
|
|
appInvocation()
|
|
result.add " does not accept"
|
|
if cmd.hasArgs: result.add " additional"
|
|
result.add " arguments"
|
|
|
|
func findOpt(opts: openArray[OptInfo], name: string): OptInfo =
|
|
for opt in opts:
|
|
if cmpIgnoreStyle(opt.name, name) == 0 or
|
|
cmpIgnoreStyle(opt.abbr, name) == 0:
|
|
return opt
|
|
|
|
func findOpt(activeCmds: openArray[CmdInfo], name: string): OptInfo =
|
|
for i in countdown(activeCmds.len - 1, 0):
|
|
let found = findOpt(activeCmds[i].opts, name)
|
|
if found != nil: return found
|
|
|
|
func findCmd(cmds: openArray[CmdInfo], name: string): CmdInfo =
|
|
for cmd in cmds:
|
|
if cmpIgnoreStyle(cmd.name, name) == 0:
|
|
return cmd
|
|
|
|
func findSubCmd(cmd: CmdInfo, name: string): CmdInfo =
|
|
let subCmdDiscriminator = cmd.getSubCmdDiscriminator
|
|
if subCmdDiscriminator != nil:
|
|
let cmd = findCmd(subCmdDiscriminator.subCmds, name)
|
|
if cmd != nil: return cmd
|
|
|
|
return nil
|
|
|
|
func startsWithIgnoreStyle(s: string, prefix: string): bool =
|
|
# Similar in spirit to cmpIgnoreStyle, but compare only the prefix.
|
|
var i = 0
|
|
var j = 0
|
|
|
|
while true:
|
|
# Skip any underscore
|
|
while i < s.len and s[i] == '_': inc i
|
|
while j < prefix.len and prefix[j] == '_': inc j
|
|
|
|
if j == prefix.len:
|
|
# The whole prefix matches
|
|
return true
|
|
elif i == s.len:
|
|
# We've reached the end of `s` without matching the prefix
|
|
return false
|
|
elif toLowerAscii(s[i]) != toLowerAscii(prefix[j]):
|
|
return false
|
|
|
|
inc i
|
|
inc j
|
|
|
|
when defined(debugCmdTree):
|
|
proc printCmdTree(cmd: CmdInfo, indent = 0) =
|
|
let blanks = spaces(indent)
|
|
echo blanks, "> ", cmd.name
|
|
|
|
for opt in cmd.opts:
|
|
if opt.kind == Discriminator:
|
|
for subcmd in opt.subCmds:
|
|
printCmdTree(subcmd, indent + 2)
|
|
else:
|
|
echo blanks, " - ", opt.name, ": ", opt.typename
|
|
|
|
else:
|
|
template printCmdTree(cmd: CmdInfo) = discard
|
|
|
|
# TODO remove the overloads here to get better "missing overload" error message
|
|
proc parseCmdArg*(T: type InputDir, p: string): T {.raises: [ValueError].} =
|
|
if not dirExists(p):
|
|
raise newException(ValueError, "Directory doesn't exist")
|
|
|
|
T(p)
|
|
|
|
proc parseCmdArg*(T: type InputFile, p: string): T {.raises: [ValueError].} =
|
|
# TODO this is needed only because InputFile cannot be made
|
|
# an alias of TypedInputFile at the moment, because of a generics
|
|
# caching issue
|
|
if not fileExists(p):
|
|
raise newException(ValueError, "File doesn't exist")
|
|
|
|
when not defined(nimscript):
|
|
try:
|
|
let f = system.open(p, fmRead)
|
|
close f
|
|
except IOError:
|
|
raise newException(ValueError, "File not accessible")
|
|
|
|
T(p)
|
|
|
|
proc parseCmdArg*(
|
|
T: type TypedInputFile, p: string): T {.raises: [ValueError].} =
|
|
var path = p
|
|
when T.defaultExt.len > 0:
|
|
path = path.addFileExt(T.defaultExt)
|
|
|
|
if not fileExists(path):
|
|
raise newException(ValueError, "File doesn't exist")
|
|
|
|
when not defined(nimscript):
|
|
try:
|
|
let f = system.open(path, fmRead)
|
|
close f
|
|
except IOError:
|
|
raise newException(ValueError, "File not accessible")
|
|
|
|
T(path)
|
|
|
|
func parseCmdArg*(T: type[OutDir|OutFile|OutPath], p: string): T =
|
|
T(p)
|
|
|
|
proc parseCmdArg*[T](
|
|
_: type Option[T], s: string): Option[T] {.raises: [ValueError].} =
|
|
some(parseCmdArg(T, s))
|
|
|
|
template parseCmdArg*(T: type string, s: string): string =
|
|
s
|
|
|
|
func parseCmdArg*(
|
|
T: type SomeSignedInt, s: string): T {.raises: [ValueError].} =
|
|
T parseBiggestInt(s)
|
|
|
|
func parseCmdArg*(
|
|
T: type SomeUnsignedInt, s: string): T {.raises: [ValueError].} =
|
|
T parseBiggestUInt(s)
|
|
|
|
func parseCmdArg*(T: type SomeFloat, p: string): T {.raises: [ValueError].} =
|
|
parseFloat(p)
|
|
|
|
func parseCmdArg*(T: type bool, p: string): T {.raises: [ValueError].} =
|
|
try:
|
|
p.len == 0 or parseBool(p)
|
|
except CatchableError:
|
|
raise newException(ValueError, "'" & p & "' is not a valid boolean value. Supported values are on/off, yes/no, true/false or 1/0")
|
|
|
|
func parseCmdArg*(T: type enum, s: string): T {.raises: [ValueError].} =
|
|
parseEnum[T](s)
|
|
|
|
proc parseCmdArgAux(T: type, s: string): T {.raises: [ValueError].} =
|
|
# The parseCmdArg procs are allowed to raise only `ValueError`.
|
|
# If you have provided your own specializations, please handle
|
|
# all other exception types.
|
|
mixin parseCmdArg
|
|
try:
|
|
parseCmdArg(T, s)
|
|
except CatchableError as exc:
|
|
raise newException(ValueError, exc.msg)
|
|
|
|
func completeCmdArg*(T: type enum, val: string): seq[string] =
|
|
for e in low(T)..high(T):
|
|
let as_str = $e
|
|
if startsWithIgnoreStyle(as_str, val):
|
|
result.add($e)
|
|
|
|
func completeCmdArg*(T: type SomeNumber, val: string): seq[string] =
|
|
@[]
|
|
|
|
func completeCmdArg*(T: type bool, val: string): seq[string] =
|
|
@[]
|
|
|
|
func completeCmdArg*(T: type string, val: string): seq[string] =
|
|
@[]
|
|
|
|
proc completeCmdArg*(T: type[InputFile|TypedInputFile|InputDir|OutFile|OutDir|OutPath],
|
|
val: string): 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
|
|
let dir_path = if dir == "": "." else: expandTilde(dir)
|
|
# Dotfiles are hidden unless the user entered a dot as prefix
|
|
let show_dotfiles = len(name) > 0 and name[0] == '.'
|
|
|
|
try:
|
|
for kind, path in walkDir(dir_path, relative=true):
|
|
if not show_dotfiles and path[0] == '.':
|
|
continue
|
|
|
|
# Do not show files if asked for directories, on the other hand we must show
|
|
# directories even if a file is requested to allow the user to select a file
|
|
# inside those
|
|
if type(T) is (InputDir or OutDir) and kind notin {pcDir, pcLinkToDir}:
|
|
continue
|
|
|
|
# Note, no normalization is needed here
|
|
if path.startsWith(tail):
|
|
var match = dir_path / path
|
|
# Add a trailing slash so that completions can be chained
|
|
if kind in {pcDir, pcLinkToDir}:
|
|
match &= DirSep
|
|
|
|
result.add(shellPathEscape(match))
|
|
except OSError:
|
|
discard
|
|
|
|
func completeCmdArg*[T](_: type seq[T], val: string): seq[string] =
|
|
@[]
|
|
|
|
proc completeCmdArg*[T](_: type Option[T], val: string): seq[string] =
|
|
mixin completeCmdArg
|
|
return completeCmdArg(type(T), val)
|
|
|
|
proc completeCmdArgAux(T: type, val: string): seq[string] =
|
|
mixin completeCmdArg
|
|
return completeCmdArg(T, val)
|
|
|
|
template setField[T](
|
|
loc: var T, val: Option[string], defaultVal: untyped): untyped =
|
|
type FieldType = type(loc)
|
|
loc = if isSome(val): parseCmdArgAux(FieldType, val.get)
|
|
else: FieldType(defaultVal)
|
|
|
|
template setField[T](
|
|
loc: var seq[T], val: Option[string], defaultVal: untyped): untyped =
|
|
if val.isSome:
|
|
loc.add parseCmdArgAux(type(loc[0]), val.get)
|
|
else:
|
|
type FieldType = type(loc)
|
|
loc = FieldType(defaultVal)
|
|
|
|
func makeDefaultValue*(T: type): T =
|
|
default(T)
|
|
|
|
func requiresInput*(T: type): bool =
|
|
not ((T is seq) or (T is Option) or (T is bool))
|
|
|
|
func acceptsMultipleValues*(T: type): bool =
|
|
T is seq
|
|
|
|
template debugMacroResult(macroName: string) {.dirty.} =
|
|
when defined(debugMacros) or defined(debugConfutils):
|
|
echo "\n-------- ", macroName, " ----------------------"
|
|
echo result.repr
|
|
|
|
func parseEnumNormalized[T: enum](s: string): T {.raises: [ValueError].} =
|
|
# Note: In Nim 1.6 `parseEnum` normalizes the string except for the first
|
|
# character. Nim 1.2 would normalize for all characters. In config options
|
|
# the latter behaviour is required so this custom function is needed.
|
|
genEnumCaseStmt(T, s, default = nil, ord(low(T)), ord(high(T)), normalize)
|
|
|
|
proc generateFieldSetters(RecordType: NimNode): NimNode =
|
|
var recordDef = getImpl(RecordType)
|
|
let makeDefaultValue = bindSym"makeDefaultValue"
|
|
|
|
result = newTree(nnkStmtListExpr)
|
|
var settersArray = newTree(nnkBracket)
|
|
|
|
for field in recordFields(recordDef):
|
|
var
|
|
setterName = ident($field.name & "Setter")
|
|
fieldName = field.name
|
|
namePragma = field.readPragma"name"
|
|
paramName = if namePragma != nil: namePragma
|
|
else: fieldName
|
|
configVar = ident "config"
|
|
configField = newTree(nnkDotExpr, configVar, fieldName)
|
|
defaultValue = field.readPragma"defaultValue"
|
|
completerName = ident($field.name & "Complete")
|
|
|
|
if defaultValue == nil:
|
|
defaultValue = newCall(makeDefaultValue, newTree(nnkTypeOfExpr, configField))
|
|
|
|
# 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,
|
|
newLit($paramName),
|
|
setterName, completerName,
|
|
newCall(bindSym"requiresInput", fixedFieldType),
|
|
newCall(bindSym"acceptsMultipleValues", fixedFieldType))
|
|
|
|
result.add quote do:
|
|
{.push hint[XCannotRaiseY]: off.}
|
|
|
|
result.add quote do:
|
|
proc `completerName`(val: string): seq[string] {.
|
|
nimcall
|
|
gcsafe
|
|
sideEffect
|
|
raises: []
|
|
.} =
|
|
return completeCmdArgAux(`fixedFieldType`, val)
|
|
|
|
proc `setterName`(`configVar`: var `RecordType`, val: Option[string]) {.
|
|
nimcall
|
|
gcsafe
|
|
sideEffect
|
|
raises: [ValueError]
|
|
.} =
|
|
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.
|
|
if isSome(val):
|
|
`configField` = parseEnumNormalized[type(`configField`)](val.get)
|
|
else:
|
|
`configField` = `defaultValue`
|
|
else:
|
|
setField(`configField`, val, `defaultValue`)
|
|
|
|
result.add quote do:
|
|
{.pop.}
|
|
|
|
result.add settersArray
|
|
debugMacroResult "Field Setters"
|
|
|
|
func checkDuplicate(cmd: CmdInfo, opt: OptInfo, fieldName: NimNode) =
|
|
for x in cmd.opts:
|
|
if opt.name == x.name:
|
|
error "duplicate name detected: " & opt.name, fieldName
|
|
if opt.abbr.len > 0 and opt.abbr == x.abbr:
|
|
error "duplicate abbr detected: " & opt.abbr, fieldName
|
|
|
|
func validPath(path: var seq[CmdInfo], parent, node: CmdInfo): bool =
|
|
for x in parent.opts:
|
|
if x.kind != Discriminator: continue
|
|
for y in x.subCmds:
|
|
if y == node:
|
|
path.add y
|
|
return true
|
|
if validPath(path, y, node):
|
|
path.add y
|
|
return true
|
|
false
|
|
|
|
func findPath(parent, node: CmdInfo): seq[CmdInfo] =
|
|
# find valid path from parent to node
|
|
result = newSeq[CmdInfo]()
|
|
doAssert validPath(result, parent, node)
|
|
result.add parent
|
|
|
|
func toText(n: NimNode): string =
|
|
if n == nil: ""
|
|
elif n.kind in {nnkStrLit..nnkTripleStrLit}: n.strVal
|
|
else: repr(n)
|
|
|
|
proc cmdInfoFromType(T: NimNode): CmdInfo =
|
|
result = CmdInfo()
|
|
|
|
var
|
|
recordDef = getImpl(T)
|
|
discriminatorFields = newSeq[OptInfo]()
|
|
fieldIdx = 0
|
|
|
|
for field in recordFields(recordDef):
|
|
let
|
|
isImplicitlySelectable = field.readPragma"implicitlySelectable" != nil
|
|
defaultValue = field.readPragma"defaultValue"
|
|
defaultValueDesc = field.readPragma"defaultValueDesc"
|
|
defaultInHelp = if defaultValueDesc != nil: defaultValueDesc
|
|
else: defaultValue
|
|
defaultInHelpText = toText(defaultInHelp)
|
|
separator = field.readPragma"separator"
|
|
longDesc = field.readPragma"longDesc"
|
|
|
|
isHidden = field.readPragma("hidden") != nil
|
|
abbr = field.readPragma"abbr"
|
|
name = field.readPragma"name"
|
|
desc = field.readPragma"desc"
|
|
optKind = if field.isDiscriminator: Discriminator
|
|
elif field.readPragma("argument") != nil: Arg
|
|
else: CliSwitch
|
|
|
|
var opt = OptInfo(kind: optKind,
|
|
idx: fieldIdx,
|
|
name: $field.name,
|
|
isHidden: isHidden,
|
|
hasDefault: defaultValue != nil,
|
|
defaultInHelpText: defaultInHelpText,
|
|
typename: field.typ.repr)
|
|
|
|
if desc != nil: opt.desc = desc.strVal
|
|
if name != nil: opt.name = name.strVal
|
|
if abbr != nil: opt.abbr = abbr.strVal
|
|
if separator != nil: opt.separator = separator.strVal
|
|
if longDesc != nil: opt.longDesc = longDesc.strVal
|
|
|
|
inc fieldIdx
|
|
|
|
if field.isDiscriminator:
|
|
discriminatorFields.add opt
|
|
let cmdType = field.typ.getImpl[^1]
|
|
if cmdType.kind != nnkEnumTy:
|
|
error "Only enums are supported as case object discriminators", field.name
|
|
|
|
opt.isImplicitlySelectable = isImplicitlySelectable
|
|
opt.isCommand = field.readPragma"command" != nil
|
|
|
|
for i in 1 ..< cmdType.len:
|
|
let enumVal = cmdType[i]
|
|
var name, desc: string
|
|
if enumVal.kind == nnkEnumFieldDef:
|
|
name = $enumVal[0]
|
|
desc = $enumVal[1]
|
|
else:
|
|
name = $enumVal
|
|
if defaultValue != nil and eqIdent(name, defaultValue):
|
|
opt.defaultSubCmd = i - 1
|
|
opt.subCmds.add CmdInfo(name: name, desc: desc)
|
|
|
|
if defaultValue == nil:
|
|
opt.defaultSubCmd = -1
|
|
else:
|
|
if opt.defaultSubCmd == -1:
|
|
error "The default value is not a valid enum value", defaultValue
|
|
|
|
if field.caseField != nil and field.caseBranch != nil:
|
|
let fieldName = field.caseField.getFieldName
|
|
var discriminator = findOpt(discriminatorFields, $fieldName)
|
|
|
|
if discriminator == nil:
|
|
error "Unable to find " & $fieldName
|
|
|
|
if field.caseBranch.kind == nnkElse:
|
|
error "Sub-command parameters cannot appear in an else branch. " &
|
|
"Please specify the sub-command branch precisely", field.caseBranch[0]
|
|
|
|
var branchEnumVal = field.caseBranch[0]
|
|
if branchEnumVal.kind == nnkDotExpr:
|
|
branchEnumVal = branchEnumVal[1]
|
|
var cmd = findCmd(discriminator.subCmds, $branchEnumVal)
|
|
# we respect subcommand hierarchy when looking for duplicate
|
|
let path = findPath(result, cmd)
|
|
for n in path:
|
|
checkDuplicate(n, opt, field.name)
|
|
|
|
# the reason we check for `ignore` pragma here and not using `continue` statement
|
|
# is we do respect option hierarchy of subcommands
|
|
if field.readPragma("ignore") == nil:
|
|
cmd.opts.add opt
|
|
|
|
else:
|
|
checkDuplicate(result, opt, field.name)
|
|
|
|
if field.readPragma("ignore") == nil:
|
|
result.opts.add opt
|
|
|
|
macro configurationRtti(RecordType: type): untyped =
|
|
let
|
|
T = RecordType.getType[1]
|
|
cmdInfo = cmdInfoFromType T
|
|
fieldSetters = generateFieldSetters T
|
|
|
|
result = newTree(nnkPar, newLitFixed cmdInfo, fieldSetters)
|
|
|
|
when hasSerialization:
|
|
proc addConfigFile*(secondarySources: auto,
|
|
Format: type,
|
|
path: InputFile) {.raises: [ConfigurationError].} =
|
|
try:
|
|
secondarySources.data.add loadFile(Format, string path,
|
|
type(secondarySources.data[0]))
|
|
except SerializationError as err:
|
|
raise newException(ConfigurationError, err.formatMsg(string path), err)
|
|
except IOError as err:
|
|
raise newException(ConfigurationError,
|
|
"Failed to read config file at '" & string(path) & "': " & err.msg)
|
|
|
|
proc addConfigFileContent*(secondarySources: auto,
|
|
Format: type,
|
|
content: string) {.raises: [ConfigurationError].} =
|
|
try:
|
|
secondarySources.data.add decode(Format, content,
|
|
type(secondarySources.data[0]))
|
|
except SerializationError as err:
|
|
raise newException(ConfigurationError, err.formatMsg("<content>"), err)
|
|
except IOError:
|
|
raiseAssert "This should not be possible"
|
|
|
|
func constructEnvKey*(prefix: string, key: string): string {.raises: [].} =
|
|
## Generates env. variable names from keys and prefix following the
|
|
## IEEE Open Group env. variable spec: https://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
|
|
(prefix & "_" & key).toUpperAscii.multiReplace(("-", "_"), (" ", "_"))
|
|
|
|
# On Posix there is no portable way to get the command
|
|
# line from a DLL and thus the proc isn't defined in this environment.
|
|
# See https://nim-lang.org/docs/os.html#commandLineParams
|
|
when not declared(commandLineParams):
|
|
proc commandLineParams(): seq[string] = discard
|
|
|
|
proc loadImpl[C, SecondarySources](
|
|
Configuration: typedesc[C],
|
|
cmdLine = commandLineParams(),
|
|
version = "",
|
|
copyrightBanner = "",
|
|
printUsage = true,
|
|
quitOnFailure = true,
|
|
secondarySourcesRef: ref SecondarySources,
|
|
secondarySources: proc (
|
|
config: Configuration, sources: ref SecondarySources
|
|
) {.gcsafe, raises: [ConfigurationError].} = nil,
|
|
envVarsPrefix = appInvocation()
|
|
): Configuration {.raises: [ConfigurationError].} =
|
|
## 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 (rootCmd, fieldSetters) = configurationRtti(Configuration)
|
|
var fieldCounters: array[fieldSetters.len, int]
|
|
|
|
printCmdTree rootCmd
|
|
|
|
var activeCmds = @[rootCmd]
|
|
template lastCmd: auto = activeCmds[^1]
|
|
var nextArgIdx = lastCmd.getNextArgIdx(-1)
|
|
|
|
var help = ""
|
|
|
|
proc suggestCallingHelp =
|
|
errorOutput "Try ", fgCommand, appInvocation() & " --help"
|
|
errorOutput " for more information.\p"
|
|
flushOutputAndQuit QuitFailure
|
|
|
|
template fail(args: varargs[untyped]): untyped =
|
|
if quitOnFailure:
|
|
errorOutput args
|
|
errorOutput "\p"
|
|
suggestCallingHelp()
|
|
else:
|
|
# TODO: populate this string
|
|
raise newException(ConfigurationError, "")
|
|
|
|
template applySetter(
|
|
conf: Configuration, setterIdx: int, cmdLineVal: string): untyped =
|
|
when defined(nimHasWarnBareExcept):
|
|
{.push warning[BareExcept]:off.}
|
|
|
|
try:
|
|
fieldSetters[setterIdx][1](conf, some(cmdLineVal))
|
|
inc fieldCounters[setterIdx]
|
|
except:
|
|
fail("Error while processing the ",
|
|
fgOption, fieldSetters[setterIdx][0],
|
|
"=", cmdLineVal, resetStyle, " parameter: ",
|
|
getCurrentExceptionMsg())
|
|
|
|
when defined(nimHasWarnBareExcept):
|
|
{.pop.}
|
|
|
|
when hasCompletions:
|
|
template getArgCompletions(opt: OptInfo, prefix: string): seq[string] =
|
|
fieldSetters[opt.idx][2](prefix)
|
|
|
|
template required(opt: OptInfo): bool =
|
|
fieldSetters[opt.idx][3] and not opt.hasDefault
|
|
|
|
template activateCmd(
|
|
conf: Configuration, discriminator: OptInfo, activatedCmd: CmdInfo) =
|
|
let cmd = activatedCmd
|
|
conf.applySetter(discriminator.idx, if cmd.desc.len > 0: cmd.desc
|
|
else: cmd.name)
|
|
activeCmds.add cmd
|
|
nextArgIdx = cmd.getNextArgIdx(-1)
|
|
|
|
when hasCompletions:
|
|
type
|
|
ArgKindFilter = enum
|
|
argName
|
|
argAbbr
|
|
|
|
proc showMatchingOptions(cmd: CmdInfo, prefix: string, filterKind: set[ArgKindFilter]) =
|
|
var matchingOptions: seq[OptInfo]
|
|
|
|
if len(prefix) > 0:
|
|
# Filter the options according to the input prefix
|
|
for opt in cmd.opts:
|
|
if argName in filterKind and len(opt.name) > 0:
|
|
if startsWithIgnoreStyle(opt.name, prefix):
|
|
matchingOptions.add(opt)
|
|
if argAbbr in filterKind and len(opt.abbr) > 0:
|
|
if startsWithIgnoreStyle(opt.abbr, prefix):
|
|
matchingOptions.add(opt)
|
|
else:
|
|
matchingOptions = cmd.opts
|
|
|
|
for opt in matchingOptions:
|
|
# The trailing '=' means the switch accepts an argument
|
|
let trailing = if opt.typename != "bool": "=" else: ""
|
|
|
|
if argName in filterKind and len(opt.name) > 0:
|
|
try:
|
|
stdout.writeLine("--", opt.name, trailing)
|
|
except IOError:
|
|
discard
|
|
if argAbbr in filterKind and len(opt.abbr) > 0:
|
|
try:
|
|
stdout.writeLine('-', opt.abbr, trailing)
|
|
except IOError:
|
|
discard
|
|
|
|
let completion = splitCompletionLine()
|
|
# If we're not asked to complete a command line the result is an empty list
|
|
if len(completion) != 0:
|
|
var cmdStack = @[rootCmd]
|
|
# Try to understand what the active chain of commands is without parsing the
|
|
# whole command line
|
|
for tok in completion[1..^1]:
|
|
if not tok.startsWith('-'):
|
|
let subCmd = findSubCmd(cmdStack[^1], tok)
|
|
if subCmd != nil: cmdStack.add(subCmd)
|
|
|
|
let cur_word = completion[^1]
|
|
let prev_word = if len(completion) > 2: completion[^2] else: ""
|
|
let prev_prev_word = if len(completion) > 3: completion[^3] else: ""
|
|
|
|
if cur_word.startsWith('-'):
|
|
# Show all the options matching the prefix input by the user
|
|
let isFullName = cur_word.startsWith("--")
|
|
var option_word = cur_word
|
|
option_word.removePrefix('-')
|
|
|
|
for i in countdown(cmdStack.len - 1, 0):
|
|
let argFilter =
|
|
if isFullName:
|
|
{argName}
|
|
elif len(cur_word) > 1:
|
|
# If the user entered a single hypen then we show both long & short
|
|
# variants
|
|
{argAbbr}
|
|
else:
|
|
{argName, argAbbr}
|
|
|
|
showMatchingOptions(cmdStack[i], option_word, argFilter)
|
|
elif (prev_word.startsWith('-') or
|
|
(prev_word == "=" and prev_prev_word.startsWith('-'))):
|
|
# Handle cases where we want to complete a switch choice
|
|
# -switch
|
|
# -switch=
|
|
var option_word = if len(prev_word) == 1: prev_prev_word else: prev_word
|
|
option_word.removePrefix('-')
|
|
|
|
let opt = findOpt(cmdStack, option_word)
|
|
if opt != nil:
|
|
for arg in getArgCompletions(opt, cur_word):
|
|
try:
|
|
stdout.writeLine(arg)
|
|
except IOError:
|
|
discard
|
|
elif cmdStack[^1].hasSubCommands:
|
|
# Show all the available subcommands
|
|
for subCmd in subCmds(cmdStack[^1]):
|
|
if startsWithIgnoreStyle(subCmd.name, cur_word):
|
|
try:
|
|
stdout.writeLine(subCmd.name)
|
|
except IOError:
|
|
discard
|
|
else:
|
|
# Full options listing
|
|
for i in countdown(cmdStack.len - 1, 0):
|
|
showMatchingOptions(cmdStack[i], "", {argName, argAbbr})
|
|
|
|
stdout.flushFile()
|
|
|
|
return
|
|
|
|
proc lazyHelpAppInfo: HelpAppInfo =
|
|
HelpAppInfo(
|
|
copyrightBanner: copyrightBanner,
|
|
appInvocation: appInvocation())
|
|
|
|
template processHelpAndVersionOptions(optKey: string) =
|
|
let key = optKey
|
|
if cmpIgnoreStyle(key, "help") == 0:
|
|
help.showHelp lazyHelpAppInfo(), activeCmds
|
|
elif version.len > 0 and cmpIgnoreStyle(key, "version") == 0:
|
|
help.helpOutput version, "\p"
|
|
flushOutputAndQuit QuitSuccess
|
|
|
|
for kind, key, val in getopt(cmdLine):
|
|
when key isnot string:
|
|
let key = string(key)
|
|
case kind
|
|
of cmdLongOption, cmdShortOption:
|
|
processHelpAndVersionOptions key
|
|
|
|
var opt = findOpt(activeCmds, key)
|
|
if opt == nil:
|
|
# We didn't find the option.
|
|
# Check if it's from the default command and activate it if necessary:
|
|
let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator
|
|
if subCmdDiscriminator != nil:
|
|
if subCmdDiscriminator.defaultSubCmd != -1:
|
|
let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd]
|
|
opt = findOpt(defaultCmd.opts, key)
|
|
if opt != nil:
|
|
result.activateCmd(subCmdDiscriminator, defaultCmd)
|
|
else:
|
|
discard
|
|
|
|
if opt != nil:
|
|
result.applySetter(opt.idx, val)
|
|
else:
|
|
fail "Unrecognized option '" & key & "'"
|
|
|
|
of cmdArgument:
|
|
if lastCmd.hasSubCommands:
|
|
processHelpAndVersionOptions key
|
|
|
|
block processArg:
|
|
let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator
|
|
if subCmdDiscriminator != nil:
|
|
let subCmd = findCmd(subCmdDiscriminator.subCmds, key)
|
|
if subCmd != nil:
|
|
result.activateCmd(subCmdDiscriminator, subCmd)
|
|
break processArg
|
|
|
|
if nextArgIdx == -1:
|
|
fail lastCmd.noMoreArgsError
|
|
|
|
result.applySetter(nextArgIdx, key)
|
|
|
|
if not fieldSetters[nextArgIdx][4]:
|
|
nextArgIdx = lastCmd.getNextArgIdx(nextArgIdx)
|
|
|
|
else:
|
|
discard
|
|
|
|
let subCmdDiscriminator = lastCmd.getSubCmdDiscriminator
|
|
if subCmdDiscriminator != nil and
|
|
subCmdDiscriminator.defaultSubCmd != -1 and
|
|
fieldCounters[subCmdDiscriminator.idx] == 0:
|
|
let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd]
|
|
result.activateCmd(subCmdDiscriminator, defaultCmd)
|
|
|
|
# https://github.com/status-im/nim-confutils/pull/109#discussion_r1820076739
|
|
if not isNil(secondarySources): # Nim v2.0.10: `!= nil` broken in nimscript
|
|
try:
|
|
secondarySources(result, secondarySourcesRef)
|
|
except ConfigurationError as err:
|
|
fail "Failed to load secondary sources: '" & err.msg & "'"
|
|
|
|
proc processMissingOpts(
|
|
conf: var Configuration, cmd: CmdInfo) {.raises: [ConfigurationError].} =
|
|
for opt in cmd.opts:
|
|
if fieldCounters[opt.idx] == 0:
|
|
let envKey = constructEnvKey(envVarsPrefix, opt.name)
|
|
|
|
try:
|
|
if existsEnv(envKey):
|
|
let envContent = getEnv(envKey)
|
|
conf.applySetter(opt.idx, envContent)
|
|
elif secondarySourcesRef.setters[opt.idx](conf, secondarySourcesRef):
|
|
# all work is done in the config file setter,
|
|
# there is nothing left to do here.
|
|
discard
|
|
elif opt.hasDefault:
|
|
fieldSetters[opt.idx][1](conf, none[string]())
|
|
elif opt.required:
|
|
fail "The required option '" & opt.name & "' was not specified"
|
|
except ValueError as err:
|
|
fail "Option '" & opt.name & "' failed to parse: '" & err.msg & "'"
|
|
|
|
for cmd in activeCmds:
|
|
result.processMissingOpts(cmd)
|
|
|
|
template load*(
|
|
Configuration: type,
|
|
cmdLine = commandLineParams(),
|
|
version = "",
|
|
copyrightBanner = "",
|
|
printUsage = true,
|
|
quitOnFailure = true,
|
|
secondarySources: untyped = nil,
|
|
envVarsPrefix = appInvocation()): untyped =
|
|
block:
|
|
let secondarySourcesRef = generateSecondarySources(Configuration)
|
|
loadImpl(Configuration, cmdLine, version,
|
|
copyrightBanner, printUsage, quitOnFailure,
|
|
secondarySourcesRef, secondarySources, envVarsPrefix)
|
|
|
|
func defaults*(Configuration: type): Configuration =
|
|
load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false)
|
|
|
|
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 proc
|
|
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], newTree(nnkPragma))
|
|
arg[0][1].add 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))
|
|
|
|
# TODO: remove this once Nim supports custom pragmas on proc params
|
|
for p in cliProc.params:
|
|
if p.kind == nnkEmpty: continue
|
|
p[0] = skipPragma p[0]
|
|
|
|
debugMacroResult "CLI Code"
|
|
|
|
func load*(f: TypedInputFile): f.ContentType =
|
|
when f.Format is Unspecified or f.ContentType is Unspecified:
|
|
{.fatal: "To use `InputFile.load`, please specify the Format and ContentType of the file".}
|
|
|
|
when f.Format is Txt:
|
|
# TODO: implement a proper Txt serialization format
|
|
mixin init
|
|
f.ContentType.init readFile(f.string).string
|
|
else:
|
|
mixin loadFile
|
|
loadFile(f.Format, f.string, f.ContentType)
|
|
|
|
{.pop.}
|