diff --git a/config.nims b/config.nims index 223d004..858c803 100644 --- a/config.nims +++ b/config.nims @@ -3,7 +3,6 @@ import std/[strformat, strutils] task test, "run tests": selfExec "r tests/tbbansi.nim" - selfExec "r tests/tcli.nim" selfExec "r tests/cli/tester.nim" task develop, "install cligen for development": diff --git a/src/hwylterm/hwylcli.nim b/src/hwylterm/hwylcli.nim index 4295b45..143c831 100644 --- a/src/hwylterm/hwylcli.nim +++ b/src/hwylterm/hwylcli.nim @@ -38,7 +38,7 @@ export parseopt3, sets, bbansi type HwylFlagHelp* = tuple - short, long, description: string + short, long, description, defaultVal: string HwylSubCmdHelp* = tuple name, aliases, desc: string HwylCliStyleSetting = enum @@ -48,6 +48,8 @@ type flagShort* = "yellow" flagLong* = "magenta" flagDesc* = "" + default* = "faint" + required* = "red" cmd* = "bold" settings*: set[HwylCliStyleSetting] = {Aliases} HwylCliHelp* = object @@ -55,7 +57,8 @@ type subcmds*: seq[HwylSubCmdHelp] flags*: seq[HwylFlagHelp] styles*: HwylCliStyles - subcmdLen*, subcmdDescLen*, shortArgLen*, longArgLen*, descArgLen*: int + # make 'lengths' it's own object? + subcmdLen*, subcmdDescLen*, shortArgLen*, longArgLen*, descArgLen*, defaultValLen*: int # NOTE: do i need both strips? func firstLine(s: string): string = @@ -88,6 +91,7 @@ func newHwylCliHelp*( result.shortArgLen = max(result.shortArgLen, f.short.len) result.longArgLen = max(result.longArgLen, f.long.len) result.descArgLen = max(result.descArgLen, f.description.len) + result.defaultValLen = max(result.defaultValLen, f.defaultVal.len) for s in result.subcmds: result.subcmdLen = max(result.subcmdLen, s.name.len) result.subcmdDescLen = max(result.subcmdDescLen, s.desc.len) @@ -109,11 +113,16 @@ func render*(cli: HwylCliHelp, f: HwylFlagHelp): string = result.add " ".repeat(2 + cli.longArgLen) result.add " " - if f.description != "": result.add "[" & cli.styles.flagDesc & "]" result.add f.description result.add "[/" & cli.styles.flagDesc & "]" + if f.defaultVal != "": + result.add " " + result.add "[" & cli.styles.default & "]" + result.add "(" & f.defaultVal & ")" + result.add "[/" & cli.styles.default & "]" + func render*(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string = result.add " " @@ -156,10 +165,9 @@ func render*(cli: HwylCliHelp): string = parts.join("\n\n") -proc bb*(cli: HwylCliHelp): BbString = +proc bb*(cli: HwylCliHelp): BbString = result = bb(render(cli)) -# ---------------------------------------- type Count* = object ## Count type for an incrementing flag @@ -169,6 +177,10 @@ type val*: Y KVString* = KV[string, string] +proc `$`(c: Count): string = $c.val + +# ---------------------------------------- + type CliSetting* = enum # Propagate, ## Include parent command settings in subcommand @@ -176,8 +188,12 @@ type NoHelpFlag, ## Remove the builtin help flag ShowHelp, ## If cmdline empty show help NoNormalize, ## Don't normalize flags and commands - NoPositional ## Raise error if any remaing positional arguments - ExactArgs, ## Raise error if missing positional argument + NoPositional, ## Raise error if any remaing positional arguments DEPRECATED + HideDefault ## Don't show default values + # ExactArgs, ## Raise error if missing positional argument + + CliFlagSetting* = enum + HideDefault ## Don't show default values BuiltinFlag = object name*: string @@ -185,11 +201,13 @@ type long*: string help*: NimNode node: NimNode + defaultVal: NimNode + settings*: set[CliFlagSetting] CliFlag = object name*: string ident*: NimNode - default*: NimNode + defaultVal*: NimNode typeNode*: NimNode node*: NimNode short*: char @@ -197,6 +215,7 @@ type help*: NimNode group*: string inherited*: bool + settings*: set[CliFlagSetting] Inherit = object settings: set[CliSetting] @@ -270,6 +289,40 @@ func bad(n: NimNode, argument: string = "") = msg &= " for argument: " & argument error msg +# could I deduplicate these somehow? template? + +func parseCliSetting(s: string): CliSetting = + try: parseEnum[CliSetting](s) + except: error "unknown cli setting: " & s + + +func parseCliSettings(cfg: var CliCfg, node: NimNode) = + case node.kind + of nnkCommand: + for n in node[1..^1]: + cfg.settings.incl parseCliSetting(n.strVal) + of nnkCall: + expectKind node[1], nnkStmtList + for n in node[1]: + cfg.settings.incl parseCliSetting(n.strVal) + else: assert false + +func parseCliFlagSetting(s: string): CliFlagSetting = + try: parseEnum[CliFlagSetting](s) + except: error "unknown cli flag setting: " & s + + +func parseCliFlagSettings(f: var CliFlag, node: NimNode) = + case node.kind + of nnkCommand: + for n in node[1..^1]: + f.settings.incl parseCliFlagSetting(n.strVal) + of nnkCall: + expectKind node[1], nnkStmtList + for n in node[1]: + f.settings.incl parseCliFlagSetting(n.strVal) + else: assert false + func getFlagParamNode(node: NimNode): NimNode = case node.kind of nnkStrLit: @@ -297,13 +350,15 @@ func parseFlagParams(f: var CliFlag, node: NimNode) = error "short flag must be a char" f.short = val[0].char of "*", "default": - f.default = getFlagParamNode(n) + f.defaultVal = getFlagParamNode(n) of "i", "ident": f.ident = getFlagParamNode(n).strVal.ident of "T": f.typeNode = n[1] of "node": f.node = n[1] + of "settings", "S": + parseCliFlagSettings(f, n) else: error "unexpected setting: " & n[0].strVal else: @@ -393,24 +448,8 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) = else: bad(n, "flag") -func parseCliSetting(s: string): CliSetting = - try: parseEnum[CliSetting](s) - except: error "unknown cli setting: " & s - - -func parseCliSettings(cfg: var CliCfg, node: NimNode) = - case node.kind - of nnkCommand: - for n in node[1..^1]: - cfg.settings.incl parseCliSetting(n.strVal) - of nnkCall: - expectKind node[1], nnkStmtList - for n in node[1]: - cfg.settings.incl parseCliSetting(n.strVal) - else: assert false - func parseIdentLikeList(node: NimNode): seq[string] = - template check = + template check = if n.kind notin [nnkStrLit,nnkIdent]: error "expected StrLit or Ident, got:" & $n.kind case node.kind @@ -716,7 +755,7 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg = parseCliHelp(result, node) of "flags": parseCliFlags(result, node[1]) - of "settings": + of "settings", "S": parseCliSettings(result, node) of "stopWords": result.stopWords = parseIdentLikeList(node) @@ -747,23 +786,30 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg = if root: propagate(result) -func flagToTuple(f: CliFlag | BuiltinFlag): NimNode = +func flagToTuple(c: CliCfg, f: CliFlag | BuiltinFlag): NimNode = let short = if f.short != '\x00': newLit($f.short) else: newLit("") long = newLit(f.long) help = f.help + + defaultVal = + if (HideDefault in f.settings) or + (HideDefault in c.settings): + newLit"" + else: + f.defaultVal or newLit"" quote do: - (`short`, `long`, `help`) + (`short`, `long`, `help`, bbEscape($`defaultVal`)) func flagsArray(cfg: CliCfg): NimNode = result = newTree(nnkBracket) for f in cfg.flags: if f.name in cfg.hidden: continue - result.add f.flagToTuple() + result.add cfg.flagToTuple(f) for f in cfg.builtinFlags: - result.add f.flagToTuple() + result.add cfg.flagToTuple(f) func subCmdsArray(cfg: CliCfg): NimNode = result = newTree(nnkBracket) @@ -963,7 +1009,7 @@ func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): N func isBool(f: CliFlag): bool = f.typeNode == ident"bool" -func isCount(f: CliFlag): bool = +func isCount(f: CliFlag): bool = f.typeNode == ident"Count" @@ -1107,9 +1153,9 @@ func addPostParseHook(cfg: CliCfg, body: NimNode) = var required, default: seq[CliFlag] for f in cfg.flags: - if f.name in cfg.required and f.default == nil: + if f.name in cfg.required and f.defaultVal == nil: required.add f - elif f.default != nil: + elif f.defaultVal != nil: default.add f for f in required: @@ -1123,10 +1169,10 @@ func addPostParseHook(cfg: CliCfg, body: NimNode) = let name = newLit(f.name) target = f.ident - default = f.default + defaultVal = f.defaultVal body.add quote do: if `name` notin `flagSet`: - `target` = `default` + `target` = `defaultVal` if hasSubcommands cfg: diff --git a/tests/cli/clis/cliCfgSettingHideDefault.nim b/tests/cli/clis/cliCfgSettingHideDefault.nim new file mode 100644 index 0000000..95f9e5b --- /dev/null +++ b/tests/cli/clis/cliCfgSettingHideDefault.nim @@ -0,0 +1,16 @@ +import hwylterm, hwylterm/hwylcli + +hwylCli: + name "setting-hide-default" + settings HideDefault + flags: + input: + T string + * "a secret default" + ? "flag with default hidden" + count: + T Count + * Count(val: 0) + ? "a count var with default" + run: + discard diff --git a/tests/cli/clis/defaults.nim b/tests/cli/clis/defaults.nim new file mode 100644 index 0000000..025d9b9 --- /dev/null +++ b/tests/cli/clis/defaults.nim @@ -0,0 +1,18 @@ +import std/[strformat] +import hwylterm, hwylterm/hwylcli + +hwylCli: + name "default-values" + flags: + input: + T string + * "testing" + ? "some help after default" + outputs: + T seq[string] + count: + T int + * 5 + ? "some number" + run: + echo fmt"{input=} {outputs=}" diff --git a/tests/cli/clis/flagSettings.nim b/tests/cli/clis/flagSettings.nim new file mode 100644 index 0000000..b9cd4c3 --- /dev/null +++ b/tests/cli/clis/flagSettings.nim @@ -0,0 +1,16 @@ +import hwylterm, hwylterm/hwylcli + +hwylCli: + name "flag-settings" + flags: + input: + S HideDefault + T string + * "a secret default" + ? "flag with default hidden" + count: + T Count + * Count(val: 0) + ? "a count var with default" + run: + discard diff --git a/tests/cli/clis/subcommands.nim b/tests/cli/clis/subcommands.nim index 3610d53..4ac5fb6 100644 --- a/tests/cli/clis/subcommands.nim +++ b/tests/cli/clis/subcommands.nim @@ -16,6 +16,8 @@ hwylCli: flags: input: T string + * "testing" + ? "some help after default" outputs: T seq[string] run: diff --git a/tests/cli/lib.nim b/tests/cli/lib.nim index 5cdee50..c4eb4b0 100644 --- a/tests/cli/lib.nim +++ b/tests/cli/lib.nim @@ -1,4 +1,4 @@ -import std/[compilesettings, os, osproc, strutils, times, unittest] +import std/[compilesettings, os, osproc, strutils, times, unittest, terminal] const pathToSrc = querySetting(SingleValueSetting.projectPath) const binDir = pathToSrc / "bin" @@ -13,21 +13,36 @@ proc runTestCli(module: string, args: string, code: int = 0): (string, int) = let (output, code) = execCmdEx(cmd) result = (output.strip(), code) +# poor man's progress meter +proc status(s: string) = + eraseLine stdout + stdout.write(s.alignLeft(terminalWidth()).substr(0, terminalWidth()-1)) + flushFile stdout + proc preCompileWorkingModule(module: string) = let exe = binDir / module let srcModule = pathToSrc / "clis" / (module & ".nim") - if not exe.fileExists or getFileInfo(exe).lastWriteTime < max(getFileInfo(srcModule).lastWriteTime, hwylCliWriteTime): + if not exe.fileExists or getFileInfo(exe).lastWriteTime < max(getFileInfo(srcModule).lastWriteTime, hwylCliWriteTime) or defined(forceSetup): let cmd = "nim c -o:$1 $2" % [exe, srcModule] - let code = execCmd(cmd) + let (output, code) = execCmdEx(cmd) if code != 0: echo "cmd: ", cmd - quit "failed to precompile test module" + quit "failed to precompile test module:\n" & output proc preCompileTestModules*() = + var modules: seq[string] for srcModule in walkDirRec(pathToSrc / "clis"): if srcModule.endsWith(".nim"): - let (_, moduleName, _) = srcModule.splitFile - preCompileWorkingModule(moduleName) + modules.add srcModule.splitFile().name + + for i, module in modules: + status "compiling [$2/$3] $1" % [ module, $(i+1), $modules.len] + preCompileWorkingModule(module) + + eraseLine stdout + + #let (_, moduleName, _) = srcModule.splitFile +# preCompileWorkingModule(moduleName) template okWithArgs*(module: string, args = "", output = "") = preCompileWorkingModule(module) diff --git a/tests/cli/tester.nim b/tests/cli/tester.nim index 3b85bc2..a833533 100644 --- a/tests/cli/tester.nim +++ b/tests/cli/tester.nim @@ -1,9 +1,9 @@ import std/[unittest] import ./lib +preCompileTestModules() + suite "hwylcli": - setup: - preCompileTestModules() okWithArgs( "posBasic", @@ -38,3 +38,22 @@ suite "hwylcli": flags: -h --help show this help""") + + okWithArgs("flagSettings", "--help", +"""usage: + flag-settings [flags] + +flags: + --input flag with default hidden + --count a count var with default (0) + -h --help show this help""") + + okWithArgs("cliCfgSettingHideDefault", "--help", +"""usage: + setting-hide-default [flags] + +flags: + --input flag with default hidden + --count a count var with default + -h --help show this help""") + diff --git a/tests/tcli.nim b/tests/tcli.nim deleted file mode 100644 index c879bec..0000000 --- a/tests/tcli.nim +++ /dev/null @@ -1,21 +0,0 @@ -# TODO: combine this with tests/cli/ -import std/[ - unittest, - strutils -] -import hwylterm, hwylterm/hwylcli - -suite "cli": - test "cli": - let expected = """[b]test-program[/] [[args...] - -[bold cyan]flags[/]: - [yellow]-h[/yellow] [magenta]--help [/magenta] []show this help[/] - [yellow]-V[/yellow] [magenta]--version[/magenta] []print version[/]""" - let cli = - newHwylCliHelp( - header = "[b]test-program[/] [[args...]", - flags = [("h","help","show this help",),("V","version","print version")] - ) - check render(cli) == expected - check $bb(render(cli)) == $bb(expected) diff --git a/todo.md b/todo.md index f3d540b..21d975c 100644 --- a/todo.md +++ b/todo.md @@ -16,7 +16,6 @@ ### cli generator - [ ] add support for types(metavars)/defaults/required in help output -- [ ] add nargs to CliCfg - [x] add support for inheriting a single flag from parent (even from a "group") - [x] add support to either (lengthen commands) or provide an alias for a subcommand - [x] add command aliases to hwylcli help with switch