Fix `cli` invocation from nimscript (#109)
* 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
This commit is contained in:
parent
9bf293e5d2
commit
cb858a27f4
120
confutils.nim
120
confutils.nim
|
@ -7,23 +7,29 @@
|
||||||
# This file may not be copied, modified, or distributed except according to
|
# This file may not be copied, modified, or distributed except according to
|
||||||
# those terms.
|
# those terms.
|
||||||
|
|
||||||
|
{.push raises: [].}
|
||||||
|
|
||||||
import
|
import
|
||||||
os,
|
os,
|
||||||
std/[enumutils, options, strutils, wordwrap, strformat],
|
std/[enumutils, options, strutils, wordwrap],
|
||||||
stew/shims/macros,
|
stew/shims/macros,
|
||||||
serialization,
|
|
||||||
confutils/[defs, cli_parser, config_file]
|
confutils/[defs, cli_parser, config_file]
|
||||||
|
|
||||||
export
|
export
|
||||||
options, serialization, defs, config_file
|
options, defs, config_file
|
||||||
|
|
||||||
const
|
const
|
||||||
|
hasSerialization = not defined(nimscript)
|
||||||
useBufferedOutput = defined(nimscript)
|
useBufferedOutput = defined(nimscript)
|
||||||
noColors = useBufferedOutput or defined(confutils_no_colors)
|
noColors = useBufferedOutput or defined(confutils_no_colors)
|
||||||
hasCompletions = not defined(nimscript)
|
hasCompletions = not defined(nimscript)
|
||||||
descPadding = 6
|
descPadding = 6
|
||||||
minNameWidth = 24 - descPadding
|
minNameWidth = 24 - descPadding
|
||||||
|
|
||||||
|
when hasSerialization:
|
||||||
|
import serialization
|
||||||
|
export serialization
|
||||||
|
|
||||||
when not defined(nimscript):
|
when not defined(nimscript):
|
||||||
import
|
import
|
||||||
terminal,
|
terminal,
|
||||||
|
@ -420,10 +426,7 @@ func getNextArgIdx(cmd: CmdInfo, consumedArgIdx: int): int =
|
||||||
proc noMoreArgsError(cmd: CmdInfo): string {.raises: [].} =
|
proc noMoreArgsError(cmd: CmdInfo): string {.raises: [].} =
|
||||||
result =
|
result =
|
||||||
if cmd.isSubCommand:
|
if cmd.isSubCommand:
|
||||||
try:
|
"The command '" & cmd.name & "'"
|
||||||
"The command '$1'" % [cmd.name]
|
|
||||||
except ValueError as err:
|
|
||||||
raiseAssert "strutils.`%` failed: " & err.msg
|
|
||||||
else:
|
else:
|
||||||
appInvocation()
|
appInvocation()
|
||||||
result.add " does not accept"
|
result.add " does not accept"
|
||||||
|
@ -630,12 +633,14 @@ proc completeCmdArgAux(T: type, val: string): seq[string] =
|
||||||
mixin completeCmdArg
|
mixin completeCmdArg
|
||||||
return completeCmdArg(T, val)
|
return completeCmdArg(T, val)
|
||||||
|
|
||||||
template setField[T](loc: var T, val: Option[string], defaultVal: untyped) =
|
template setField[T](
|
||||||
|
loc: var T, val: Option[string], defaultVal: untyped): untyped =
|
||||||
type FieldType = type(loc)
|
type FieldType = type(loc)
|
||||||
loc = if isSome(val): parseCmdArgAux(FieldType, val.get)
|
loc = if isSome(val): parseCmdArgAux(FieldType, val.get)
|
||||||
else: FieldType(defaultVal)
|
else: FieldType(defaultVal)
|
||||||
|
|
||||||
template setField[T](loc: var seq[T], val: Option[string], defaultVal: untyped) =
|
template setField[T](
|
||||||
|
loc: var seq[T], val: Option[string], defaultVal: untyped): untyped =
|
||||||
if val.isSome:
|
if val.isSome:
|
||||||
loc.add parseCmdArgAux(type(loc[0]), val.get)
|
loc.add parseCmdArgAux(type(loc[0]), val.get)
|
||||||
else:
|
else:
|
||||||
|
@ -867,36 +872,34 @@ macro configurationRtti(RecordType: type): untyped =
|
||||||
|
|
||||||
result = newTree(nnkPar, newLitFixed cmdInfo, fieldSetters)
|
result = newTree(nnkPar, newLitFixed cmdInfo, fieldSetters)
|
||||||
|
|
||||||
proc addConfigFile*(secondarySources: auto,
|
when hasSerialization:
|
||||||
Format: type,
|
proc addConfigFile*(secondarySources: auto,
|
||||||
path: InputFile) {.raises: [ConfigurationError].} =
|
Format: type,
|
||||||
try:
|
path: InputFile) {.raises: [ConfigurationError].} =
|
||||||
secondarySources.data.add loadFile(Format, string path,
|
try:
|
||||||
type(secondarySources.data[0]))
|
secondarySources.data.add loadFile(Format, string path,
|
||||||
except SerializationError as err:
|
type(secondarySources.data[0]))
|
||||||
raise newException(ConfigurationError, err.formatMsg(string path), err)
|
except SerializationError as err:
|
||||||
except IOError as err:
|
raise newException(ConfigurationError, err.formatMsg(string path), err)
|
||||||
raise newException(ConfigurationError,
|
except IOError as err:
|
||||||
"Failed to read config file at '" & string(path) & "': " & err.msg)
|
raise newException(ConfigurationError,
|
||||||
|
"Failed to read config file at '" & string(path) & "': " & err.msg)
|
||||||
|
|
||||||
proc addConfigFileContent*(secondarySources: auto,
|
proc addConfigFileContent*(secondarySources: auto,
|
||||||
Format: type,
|
Format: type,
|
||||||
content: string) {.raises: [ConfigurationError].} =
|
content: string) {.raises: [ConfigurationError].} =
|
||||||
try:
|
try:
|
||||||
secondarySources.data.add decode(Format, content,
|
secondarySources.data.add decode(Format, content,
|
||||||
type(secondarySources.data[0]))
|
type(secondarySources.data[0]))
|
||||||
except SerializationError as err:
|
except SerializationError as err:
|
||||||
raise newException(ConfigurationError, err.formatMsg("<content>"), err)
|
raise newException(ConfigurationError, err.formatMsg("<content>"), err)
|
||||||
except IOError:
|
except IOError:
|
||||||
raiseAssert "This should not be possible"
|
raiseAssert "This should not be possible"
|
||||||
|
|
||||||
func constructEnvKey*(prefix: string, key: string): string {.raises: [].} =
|
func constructEnvKey*(prefix: string, key: string): string {.raises: [].} =
|
||||||
## Generates env. variable names from keys and prefix following the
|
## 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
|
## IEEE Open Group env. variable spec: https://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html
|
||||||
try:
|
(prefix & "_" & key).toUpperAscii.multiReplace(("-", "_"), (" ", "_"))
|
||||||
(&"{prefix}_{key}").toUpperAscii.multiReplace(("-", "_"), (" ", "_"))
|
|
||||||
except ValueError as err:
|
|
||||||
raiseAssert "strformat.`&` failed: " & err.msg
|
|
||||||
|
|
||||||
# On Posix there is no portable way to get the command
|
# 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.
|
# line from a DLL and thus the proc isn't defined in this environment.
|
||||||
|
@ -915,7 +918,7 @@ proc loadImpl[C, SecondarySources](
|
||||||
secondarySources: proc (
|
secondarySources: proc (
|
||||||
config: Configuration, sources: ref SecondarySources
|
config: Configuration, sources: ref SecondarySources
|
||||||
) {.gcsafe, raises: [ConfigurationError].} = nil,
|
) {.gcsafe, raises: [ConfigurationError].} = nil,
|
||||||
envVarsPrefix = getAppFilename()
|
envVarsPrefix = appInvocation()
|
||||||
): Configuration {.raises: [ConfigurationError].} =
|
): Configuration {.raises: [ConfigurationError].} =
|
||||||
## Loads a program configuration by parsing command-line arguments
|
## Loads a program configuration by parsing command-line arguments
|
||||||
## and a standard set of config files that can specify:
|
## and a standard set of config files that can specify:
|
||||||
|
@ -928,7 +931,6 @@ proc loadImpl[C, SecondarySources](
|
||||||
|
|
||||||
# This is an initial naive implementation that will be improved
|
# This is an initial naive implementation that will be improved
|
||||||
# over time.
|
# over time.
|
||||||
|
|
||||||
let (rootCmd, fieldSetters) = configurationRtti(Configuration)
|
let (rootCmd, fieldSetters) = configurationRtti(Configuration)
|
||||||
var fieldCounters: array[fieldSetters.len, int]
|
var fieldCounters: array[fieldSetters.len, int]
|
||||||
|
|
||||||
|
@ -941,11 +943,11 @@ proc loadImpl[C, SecondarySources](
|
||||||
var help = ""
|
var help = ""
|
||||||
|
|
||||||
proc suggestCallingHelp =
|
proc suggestCallingHelp =
|
||||||
errorOutput "Try ", fgCommand, ("$1 --help" % appInvocation())
|
errorOutput "Try ", fgCommand, appInvocation() & " --help"
|
||||||
errorOutput " for more information.\p"
|
errorOutput " for more information.\p"
|
||||||
flushOutputAndQuit QuitFailure
|
flushOutputAndQuit QuitFailure
|
||||||
|
|
||||||
template fail(args: varargs[untyped]) =
|
template fail(args: varargs[untyped]): untyped =
|
||||||
if quitOnFailure:
|
if quitOnFailure:
|
||||||
errorOutput args
|
errorOutput args
|
||||||
errorOutput "\p"
|
errorOutput "\p"
|
||||||
|
@ -954,14 +956,13 @@ proc loadImpl[C, SecondarySources](
|
||||||
# TODO: populate this string
|
# TODO: populate this string
|
||||||
raise newException(ConfigurationError, "")
|
raise newException(ConfigurationError, "")
|
||||||
|
|
||||||
let confAddr = addr result
|
template applySetter(
|
||||||
|
conf: Configuration, setterIdx: int, cmdLineVal: string): untyped =
|
||||||
template applySetter(setterIdx: int, cmdLineVal: string) =
|
|
||||||
when defined(nimHasWarnBareExcept):
|
when defined(nimHasWarnBareExcept):
|
||||||
{.push warning[BareExcept]:off.}
|
{.push warning[BareExcept]:off.}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fieldSetters[setterIdx][1](confAddr[], some(cmdLineVal))
|
fieldSetters[setterIdx][1](conf, some(cmdLineVal))
|
||||||
inc fieldCounters[setterIdx]
|
inc fieldCounters[setterIdx]
|
||||||
except:
|
except:
|
||||||
fail("Error while processing the ",
|
fail("Error while processing the ",
|
||||||
|
@ -979,10 +980,11 @@ proc loadImpl[C, SecondarySources](
|
||||||
template required(opt: OptInfo): bool =
|
template required(opt: OptInfo): bool =
|
||||||
fieldSetters[opt.idx][3] and not opt.hasDefault
|
fieldSetters[opt.idx][3] and not opt.hasDefault
|
||||||
|
|
||||||
template activateCmd(discriminator: OptInfo, activatedCmd: CmdInfo) =
|
template activateCmd(
|
||||||
|
conf: Configuration, discriminator: OptInfo, activatedCmd: CmdInfo) =
|
||||||
let cmd = activatedCmd
|
let cmd = activatedCmd
|
||||||
applySetter(discriminator.idx, if cmd.desc.len > 0: cmd.desc
|
conf.applySetter(discriminator.idx, if cmd.desc.len > 0: cmd.desc
|
||||||
else: cmd.name)
|
else: cmd.name)
|
||||||
activeCmds.add cmd
|
activeCmds.add cmd
|
||||||
nextArgIdx = cmd.getNextArgIdx(-1)
|
nextArgIdx = cmd.getNextArgIdx(-1)
|
||||||
|
|
||||||
|
@ -1117,14 +1119,14 @@ proc loadImpl[C, SecondarySources](
|
||||||
let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd]
|
let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd]
|
||||||
opt = findOpt(defaultCmd.opts, key)
|
opt = findOpt(defaultCmd.opts, key)
|
||||||
if opt != nil:
|
if opt != nil:
|
||||||
activateCmd(subCmdDiscriminator, defaultCmd)
|
result.activateCmd(subCmdDiscriminator, defaultCmd)
|
||||||
else:
|
else:
|
||||||
discard
|
discard
|
||||||
|
|
||||||
if opt != nil:
|
if opt != nil:
|
||||||
applySetter(opt.idx, val)
|
result.applySetter(opt.idx, val)
|
||||||
else:
|
else:
|
||||||
fail "Unrecognized option '$1'" % [key]
|
fail "Unrecognized option '" & key & "'"
|
||||||
|
|
||||||
of cmdArgument:
|
of cmdArgument:
|
||||||
if lastCmd.hasSubCommands:
|
if lastCmd.hasSubCommands:
|
||||||
|
@ -1135,13 +1137,13 @@ proc loadImpl[C, SecondarySources](
|
||||||
if subCmdDiscriminator != nil:
|
if subCmdDiscriminator != nil:
|
||||||
let subCmd = findCmd(subCmdDiscriminator.subCmds, key)
|
let subCmd = findCmd(subCmdDiscriminator.subCmds, key)
|
||||||
if subCmd != nil:
|
if subCmd != nil:
|
||||||
activateCmd(subCmdDiscriminator, subCmd)
|
result.activateCmd(subCmdDiscriminator, subCmd)
|
||||||
break processArg
|
break processArg
|
||||||
|
|
||||||
if nextArgIdx == -1:
|
if nextArgIdx == -1:
|
||||||
fail lastCmd.noMoreArgsError
|
fail lastCmd.noMoreArgsError
|
||||||
|
|
||||||
applySetter(nextArgIdx, key)
|
result.applySetter(nextArgIdx, key)
|
||||||
|
|
||||||
if not fieldSetters[nextArgIdx][4]:
|
if not fieldSetters[nextArgIdx][4]:
|
||||||
nextArgIdx = lastCmd.getNextArgIdx(nextArgIdx)
|
nextArgIdx = lastCmd.getNextArgIdx(nextArgIdx)
|
||||||
|
@ -1154,13 +1156,14 @@ proc loadImpl[C, SecondarySources](
|
||||||
subCmdDiscriminator.defaultSubCmd != -1 and
|
subCmdDiscriminator.defaultSubCmd != -1 and
|
||||||
fieldCounters[subCmdDiscriminator.idx] == 0:
|
fieldCounters[subCmdDiscriminator.idx] == 0:
|
||||||
let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd]
|
let defaultCmd = subCmdDiscriminator.subCmds[subCmdDiscriminator.defaultSubCmd]
|
||||||
activateCmd(subCmdDiscriminator, defaultCmd)
|
result.activateCmd(subCmdDiscriminator, defaultCmd)
|
||||||
|
|
||||||
if secondarySources != nil:
|
# https://github.com/status-im/nim-confutils/pull/109#discussion_r1820076739
|
||||||
|
if not isNil(secondarySources): # Nim v2.0.10: `!= nil` broken in nimscript
|
||||||
try:
|
try:
|
||||||
secondarySources(result, secondarySourcesRef)
|
secondarySources(result, secondarySourcesRef)
|
||||||
except ConfigurationError as err:
|
except ConfigurationError as err:
|
||||||
fail "Failed to load secondary sources: '$1'" % [err.msg]
|
fail "Failed to load secondary sources: '" & err.msg & "'"
|
||||||
|
|
||||||
proc processMissingOpts(
|
proc processMissingOpts(
|
||||||
conf: var Configuration, cmd: CmdInfo) {.raises: [ConfigurationError].} =
|
conf: var Configuration, cmd: CmdInfo) {.raises: [ConfigurationError].} =
|
||||||
|
@ -1171,7 +1174,7 @@ proc loadImpl[C, SecondarySources](
|
||||||
try:
|
try:
|
||||||
if existsEnv(envKey):
|
if existsEnv(envKey):
|
||||||
let envContent = getEnv(envKey)
|
let envContent = getEnv(envKey)
|
||||||
applySetter(opt.idx, envContent)
|
conf.applySetter(opt.idx, envContent)
|
||||||
elif secondarySourcesRef.setters[opt.idx](conf, secondarySourcesRef):
|
elif secondarySourcesRef.setters[opt.idx](conf, secondarySourcesRef):
|
||||||
# all work is done in the config file setter,
|
# all work is done in the config file setter,
|
||||||
# there is nothing left to do here.
|
# there is nothing left to do here.
|
||||||
|
@ -1179,9 +1182,9 @@ proc loadImpl[C, SecondarySources](
|
||||||
elif opt.hasDefault:
|
elif opt.hasDefault:
|
||||||
fieldSetters[opt.idx][1](conf, none[string]())
|
fieldSetters[opt.idx][1](conf, none[string]())
|
||||||
elif opt.required:
|
elif opt.required:
|
||||||
fail "The required option '$1' was not specified" % [opt.name]
|
fail "The required option '" & opt.name & "' was not specified"
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
fail "Option '$1' failed to parse: '$2'" % [opt.name, err.msg]
|
fail "Option '" & opt.name & "' failed to parse: '" & err.msg & "'"
|
||||||
|
|
||||||
for cmd in activeCmds:
|
for cmd in activeCmds:
|
||||||
result.processMissingOpts(cmd)
|
result.processMissingOpts(cmd)
|
||||||
|
@ -1194,10 +1197,9 @@ template load*(
|
||||||
printUsage = true,
|
printUsage = true,
|
||||||
quitOnFailure = true,
|
quitOnFailure = true,
|
||||||
secondarySources: untyped = nil,
|
secondarySources: untyped = nil,
|
||||||
envVarsPrefix = getAppFilename()): untyped =
|
envVarsPrefix = appInvocation()): untyped =
|
||||||
|
|
||||||
block:
|
block:
|
||||||
var secondarySourcesRef = generateSecondarySources(Configuration)
|
let secondarySourcesRef = generateSecondarySources(Configuration)
|
||||||
loadImpl(Configuration, cmdLine, version,
|
loadImpl(Configuration, cmdLine, version,
|
||||||
copyrightBanner, printUsage, quitOnFailure,
|
copyrightBanner, printUsage, quitOnFailure,
|
||||||
secondarySourcesRef, secondarySources, envVarsPrefix)
|
secondarySourcesRef, secondarySources, envVarsPrefix)
|
||||||
|
|
|
@ -54,3 +54,21 @@ task test, "Run all tests":
|
||||||
else:
|
else:
|
||||||
echo " [FAILED] ", path.split(DirSep)[^1]
|
echo " [FAILED] ", path.split(DirSep)[^1]
|
||||||
quit(QuitFailure)
|
quit(QuitFailure)
|
||||||
|
|
||||||
|
echo "\r\nNimscript test:"
|
||||||
|
let
|
||||||
|
actualOutput = gorgeEx(
|
||||||
|
nimc & " --verbosity:0 e " & flags & " " & "./tests/cli_example.nim " &
|
||||||
|
"--foo=1 --bar=2 --withBaz 42").output
|
||||||
|
expectedOutput = unindent"""
|
||||||
|
foo = 1
|
||||||
|
bar = 2
|
||||||
|
baz = true
|
||||||
|
arg ./tests/cli_example.nim
|
||||||
|
arg 42"""
|
||||||
|
if actualOutput.strip() == expectedOutput:
|
||||||
|
echo " [OK] tests/cli_example.nim"
|
||||||
|
else:
|
||||||
|
echo " [FAILED] tests/cli_example.nim"
|
||||||
|
echo actualOutput
|
||||||
|
quit(QuitFailure)
|
||||||
|
|
Loading…
Reference in New Issue