diff --git a/.gitignore b/.gitignore index fbea0f6..54b1b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ tests/test_nested_cmd tests/test_help tests/test_argument tests/test_parsecmdarg +tests/test_obsolete diff --git a/README.md b/README.md index fd6432a..6e94a2b 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,34 @@ The `defaultValue` won't be set. ----------------- +```nim +template obsolete*(v: string = "") {.pragma.} +``` + +Apply this to a field to emit a deprecation warning when the option +is set by the user through the CLI, an env-var, or a configuration file. + +The warning logger can be customized by overloading +`proc obsoleteCmdOpt*(T: type, opt, msg: string)`. +Where `T` is the config type, `opt` is the option name, and `msg` +is the value of the `obsolete` pragma (which may be empty). + +If the logger requires initialization, it can be set through +the `loggerSetup` parameter, for example: + +```nim +type MyConf = object + # options + +proc myLogger(config: MyConf) {.gcsafe, raises: [ConfigurationError].} = + # set up logger + discard + +let c = MyConf.load(loggerSetup = myLogger) +``` + +----------------- + ```nim template implicitlySelectable* {.pragma.} ``` diff --git a/confutils.nim b/confutils.nim index 9f9a3de..2c8b6f9 100644 --- a/confutils.nim +++ b/confutils.nim @@ -64,6 +64,7 @@ type OptFlag = enum optHidden optDebug + optObsolete OptInfo = ref object name, abbr, desc, typename: string @@ -74,6 +75,7 @@ type flags: set[OptFlag] hasDefault: bool defaultInHelpText: string + obsoleteMsg: string case kind: OptKind of Discriminator: isCommand: bool @@ -270,8 +272,7 @@ func hasAbbrs(cmds: openArray[CmdInfo], excl: set[OptFlag]): bool = return true false -func hasDebugOpts(cmds: openArray[CmdInfo]): bool = - let excl = {optHidden} +func hasDebugOpts(cmds: openArray[CmdInfo], excl: set[OptFlag]): bool = for opt in helpOptsIt(cmds, excl): if optDebug in opt.flags: return true @@ -492,13 +493,13 @@ proc showHelp(help: var string, let cmd = activeCmds[^1] - var excl = {optHidden} + var excl = {optHidden, optObsolete} if hlpDebug notin appInfo.flags: excl.incl optDebug appInfo.maxNameLen = maxNameLen(activeCmds, excl) appInfo.hasAbbrs = hasAbbrs(activeCmds, excl) - appInfo.hasDebugOpts = hasDebugOpts(activeCmds) + appInfo.hasDebugOpts = hasDebugOpts(activeCmds, excl - {optDebug}) let termWidth = try: terminalWidth() @@ -782,6 +783,24 @@ func requiresInput*(T: type): bool = func acceptsMultipleValues*(T: type): bool = T is seq +when defined(nimscript): + template warnOutput(args: varargs[string]) = + writeLine(stderr, "Warning: " & join(@args)) +else: + template warnOutput(args: varargs[untyped]) = + errorOutput(styleBright, fgYellow, "Warning: ", resetStyle, args) + errorOutput("\p") + +proc obsoleteCmdOpt(T: type, opt, msg: string) = + if msg.len > 0: + warnOutput(msg, "; ", opt, " is deprecated") + else: + warnOutput(opt, " is deprecated") + +proc obsoleteCmdOptAux(T: type, opt, msg: string) = + mixin obsoleteCmdOpt + obsoleteCmdOpt(T, opt, msg) + template debugMacroResult(macroName: string) {.dirty.} = when defined(debugMacros) or defined(debugConfutils): echo "\n-------- ", macroName, " ----------------------" @@ -890,6 +909,8 @@ func readPragmaFlags(field: FieldDescription): set[OptFlag] = result.incl optHidden if field.readPragma("debug") != nil: result.incl optDebug + if field.readPragma"obsolete" != nil: + result.incl optObsolete proc cmdInfoFromType(T: NimNode): CmdInfo = result = CmdInfo() @@ -907,6 +928,7 @@ proc cmdInfoFromType(T: NimNode): CmdInfo = defaultInHelp = if defaultValueDesc != nil: defaultValueDesc else: defaultValue defaultInHelpText = toText(defaultInHelp) + obsoleteMsg = field.readPragma"obsolete" separator = field.readPragma"separator" longDesc = field.readPragma"longDesc" envVarValueSep = field.readPragma"envVarValueSep" @@ -932,6 +954,7 @@ proc cmdInfoFromType(T: NimNode): CmdInfo = if separator != nil: opt.separator = separator.strVal if longDesc != nil: opt.longDesc = longDesc.strVal if envVarValueSep != nil: opt.envVarValueSep = envVarValueSep.strVal + if obsoleteMsg != nil: opt.obsoleteMsg = obsoleteMsg.strVal inc fieldIdx @@ -1090,7 +1113,10 @@ proc loadImpl[C, SecondarySources]( config: Configuration, sources: ref SecondarySources ) {.gcsafe, raises: [ConfigurationError].} = nil, envVarsPrefix = appInvocation(), - termWidth = 0 + termWidth = 0, + loggerSetup: proc ( + config: Configuration + ) {.gcsafe, raises: [ConfigurationError].} = nil, ): Configuration {.raises: [ConfigurationError].} = ## Loads a program configuration by parsing command-line arguments ## and a standard set of config files that can specify: @@ -1369,6 +1395,17 @@ proc loadImpl[C, SecondarySources]( for cmd in activeCmds: result.processMissingOpts(cmd) + if not isNil(loggerSetup): + try: + loggerSetup(result) + except ConfigurationError as err: + fail "Failed to setup the logger: '" & err.msg & "'" + + for cmd in activeCmds: + for opt in cmd.opts: + if optObsolete in opt.flags and fieldCounters[opt.idx] != 0: + obsoleteCmdOptAux(typeof(Configuration), opt.humaneName(), opt.obsoleteMsg) + template load*( Configuration: type, cmdLine = commandLineParams(), @@ -1379,12 +1416,14 @@ template load*( ignoreUnknown = false, secondarySources: untyped = nil, envVarsPrefix = appInvocation(), - termWidth = 0): untyped = + termWidth = 0, + loggerSetup: untyped = nil +): untyped = block: let secondarySourcesRef = generateSecondarySources(Configuration) loadImpl(Configuration, cmdLine, version, copyrightBanner, printUsage, quitOnFailure, ignoreUnknown, - secondarySourcesRef, secondarySources, envVarsPrefix, termWidth) + secondarySourcesRef, secondarySources, envVarsPrefix, termWidth, loggerSetup) func defaults*(Configuration: type): Configuration = load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false) diff --git a/confutils/defs.nim b/confutils/defs.nim index d7bac9f..4f9fe8c 100644 --- a/confutils/defs.nim +++ b/confutils/defs.nim @@ -61,6 +61,7 @@ template separator*(v: string) {.pragma.} template defaultValue*(v: untyped) {.pragma.} template defaultValueDesc*(v: string) {.pragma.} template envVarValueSep*(v = "") {.pragma.} +template obsolete*(v = "") {.pragma.} template required* {.pragma.} template command* {.pragma.} template argument* {.pragma.} diff --git a/tests/test_all.nim b/tests/test_all.nim index 607c0c2..5dd5da3 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -16,6 +16,8 @@ import test_ignore, test_multi_case_values, test_nested_cmd, + test_obsolete_overload, + test_obsolete, test_parsecmdarg, test_pragma, test_qualified_ident, diff --git a/tests/test_obsolete.nim b/tests/test_obsolete.nim new file mode 100644 index 0000000..8c8a9f8 --- /dev/null +++ b/tests/test_obsolete.nim @@ -0,0 +1,107 @@ +import + unittest2, + ../confutils + +type + TestConf = object + opt1 {. + obsolete + defaultValue: "opt1 default" + name: "opt1"}: string + +#let conf = TestConf.load() +#echo conf.opt1 + +suite "test obsolete option": + test "obsolete option default": + let conf = TestConf.load() + check conf.opt1 == "opt1 default" + + test "obsolete option set": + let conf = TestConf.load(cmdLine = @[ + "--opt1=foo" + ]) + check conf.opt1 == "foo" + +type + OverloadConf = object + opt1 {. + obsolete + defaultValue: "opt1 default" + name: "opt1"}: string + opt2 {. + obsolete + defaultValue: "opt2 default" + name: "opt2"}: string + opt3 {. + obsolete: "opt3 obsolete msg" + defaultValue: "opt3 default" + name: "opt3"}: string + opt4 {. + defaultValue: "opt4 default" + name: "opt4"}: string + +var registry {.threadvar.}: seq[string] + +proc obsoleteCmdOpt(T: type OverloadConf, opt, msg: string) = + registry.add opt + if msg.len > 0: + registry.add msg + +suite "test obsolete option overload": + test "the overload is called if opt is set": + registry.setLen 0 + let conf = OverloadConf.load(cmdLine = @[ + "--opt1=foo" + ]) + check conf.opt1 == "foo" + check registry == @["opt1"] + + test "the overload is not called if opt not set": + registry.setLen 0 + let conf = OverloadConf.load() + check conf.opt1 == "opt1 default" + check registry.len == 0 + + test "the overload is called for all obsolete opts set": + registry.setLen 0 + let conf = OverloadConf.load(cmdLine = @[ + "--opt1=foo", + "--opt2=bar" + ]) + check conf.opt1 == "foo" + check conf.opt2 == "bar" + check registry == @["opt1", "opt2"] + + test "the overload is called with obsolete msg": + registry.setLen 0 + let conf = OverloadConf.load(cmdLine = @[ + "--opt3=foo" + ]) + check conf.opt3 == "foo" + check registry == @["opt3", "opt3 obsolete msg"] + + test "the logger setup is called": + proc loggerSetup(c: OverloadConf) = + doAssert c.opt1 == "opt1 default" + registry.add "logger" + + registry.setLen 0 + let conf = OverloadConf.load(loggerSetup = loggerSetup) + check conf.opt1 == "opt1 default" + check registry == @["logger"] + + test "the logger setup is called before the overload": + proc loggerSetup(c: OverloadConf) = + doAssert c.opt1 == "foo" + registry.add "logger" + + registry.setLen 0 + let conf = OverloadConf.load( + cmdLine = @[ + "--opt1=foo" + ], + loggerSetup = loggerSetup + ) + check conf.opt1 == "foo" + check registry == @["logger", "opt1"] diff --git a/tests/test_obsolete_overload.nim b/tests/test_obsolete_overload.nim new file mode 100644 index 0000000..fb4741c --- /dev/null +++ b/tests/test_obsolete_overload.nim @@ -0,0 +1,26 @@ +import + unittest2, + ../confutils, + ./test_obsolete_overload_def + +type + TestConf = object + opt1 {. + obsolete + defaultValue: "opt1 default" + name: "opt1"}: string + +suite "test obsolete overload for type": + test "obsolete option default": + registry.setLen 0 + let conf = TestConf.load() + check conf.opt1 == "opt1 default" + check registry.len == 0 + + test "obsolete option set": + registry.setLen 0 + let conf = TestConf.load(cmdLine = @[ + "--opt1=foo" + ]) + check conf.opt1 == "foo" + check registry == @["opt1"] diff --git a/tests/test_obsolete_overload_def.nim b/tests/test_obsolete_overload_def.nim new file mode 100644 index 0000000..62e8083 --- /dev/null +++ b/tests/test_obsolete_overload_def.nim @@ -0,0 +1,8 @@ + +# this is in its own module to test there +# is no ambiguous (import) call error for obsoleteCmdOpt + +var registry* {.threadvar.}: seq[string] + +proc obsoleteCmdOpt*(T: type[object], opt, msg: string) = + registry.add opt