From c40a0a2038338b6bbaa4179c76452f6f0d1ac87c Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Thu, 23 Jan 2025 19:25:09 -0600 Subject: [PATCH] args -> positionals --- src/hwylterm/bbansi.nim | 3 +- src/hwylterm/chooser.nim | 3 +- src/hwylterm/hwylcli.nim | 119 +++++++++++++++----------------- tests/cli/clis/enumFlag.nim | 3 - tests/cli/clis/posBasic.nim | 9 +++ tests/cli/clis/posFirst.nim | 3 +- tests/cli/clis/posLast.nim | 3 +- tests/cli/clis/posLastExact.nim | 13 ---- tests/cli/clis/posNoMulti.nim | 4 +- tests/cli/clis/subcommands.nim | 22 ++++++ tests/cli/lib.nim | 10 ++- tests/cli/tester.nim | 52 ++++++++------ 12 files changed, 132 insertions(+), 112 deletions(-) create mode 100644 tests/cli/clis/posBasic.nim delete mode 100644 tests/cli/clis/posLastExact.nim create mode 100644 tests/cli/clis/subcommands.nim diff --git a/src/hwylterm/bbansi.nim b/src/hwylterm/bbansi.nim index 2145df5..fba6cdc 100644 --- a/src/hwylterm/bbansi.nim +++ b/src/hwylterm/bbansi.nim @@ -389,8 +389,9 @@ when isMainModule: hwylCli: name "bbansi" settings ShowHelp + positionals: + args seq[string] help: - usage "[bold]bbansi[/] [[[green]args...[/]] [[[faint]-h|-V[/]]" description """ bbansi "[[yellow] yellow text!" -> [yellow] yellow text![/] diff --git a/src/hwylterm/chooser.nim b/src/hwylterm/chooser.nim index 3438748..b77f8d8 100644 --- a/src/hwylterm/chooser.nim +++ b/src/hwylterm/chooser.nim @@ -132,7 +132,6 @@ when isMainModule: name "hwylchoose" settings ShowHelp help: - usage "[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]" description """ hwylchoose a b c d hwylchoose a,b,c,d -s , @@ -140,6 +139,8 @@ when isMainModule: hwylchoose --demo """ hidden demo + positionals: + args seq[string] flags: demo "show demo" separator: diff --git a/src/hwylterm/hwylcli.nim b/src/hwylterm/hwylcli.nim index 6e038f9..4295b45 100644 --- a/src/hwylterm/hwylcli.nim +++ b/src/hwylterm/hwylcli.nim @@ -231,6 +231,7 @@ type inherit*: Inherit root*: bool +func hasSubcommands(c: CliCfg): bool = c.subcommands.len > 0 func err(c: CliCfg, msg: string) = ## quit with error while generating cli @@ -396,6 +397,7 @@ 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: @@ -732,7 +734,7 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg = result.postSub = node[1] of "defaultFlagType": result.defaultFlagType = node[1] - of "args": + of "positionals": parseCliArgs result, node[1] else: error "unknown hwylCli setting: " & name @@ -964,6 +966,7 @@ func isBool(f: CliFlag): bool = func isCount(f: CliFlag): bool = f.typeNode == ident"Count" + func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] = let flagFlags = cfg.flags.filterIt(it.isBool or it.isCount) let long = @@ -976,6 +979,7 @@ func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] = ) result = (nnkPrefix.newTree(ident"@",long), short) + func setVars(cfg: CliCfg): NimNode = ## generate all positinal variables and flags not covered in global module result = nnkVarSection.newTree() @@ -990,6 +994,8 @@ func setVars(cfg: CliCfg): NimNode = result.add cfg.args.mapIt( nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode()) ) + if hasSubcommands cfg: + result.add nnkIdentDefs.newTree(ident"subcmd", ident"string", newEmptyNode()) func literalFlags(f: CliFlag): NimNode = var flags: seq[string] @@ -1007,7 +1013,7 @@ type func getMultiArgKind(cfg: CliCfg): MultiArgKind = if cfg.args.len == 1: if cfg.args[0].isSeq: - return First + return Last else: return NoMulti if cfg.args[0].isSeq: @@ -1055,38 +1061,21 @@ func genPosArgHandler(cfg: CliCfg, body: NimNode) = ## generate code to handle positional arguments let numArgs = cfg.args.len let maKind = cfg.getMultiArgKind() - if ExactArgs in cfg.settings: - case maKind: - of NoMulti: - body.add quote do: - if result.len != `numArgs`: - hwylCliError("missing positional args, got: " & $result.len & ", expected: " & $`numArgs`) - else: - body.add quote do: - if result.len < `numArgs`: - hwylCliError("missing positional args, got: " & $result.len & ", expected: " & $`numArgs`) - elif maKind == First: - body.add quote do: - if result.len < `numArgs`: - hwylCliError("missing positional args, got: " & $result.len & ", expected at least: " & $`numArgs`) - elif maKind == Last: - body.add quote do: - if result.len < (`numArgs` - 1): - hwylCliError("missing positional args, got: " & $result.len & ", expected at least: " & $(`numArgs` - 1)) - case maKind: - # BUG: this may create index defects, - # if not coupled with ExactArgs or result length checks - of Last: - for i, namedArg in cfg.args[0..^2].mapIt(it.ident): + of NoMulti: + body.add quote do: + if result.len > `numArgs`: + hwylCliError("unexepected positional args, got: " & $result.len & ", expected: " & $`numArgs`) + elif result.len < `numArgs`: + hwylCliError("missing positional args, got: " & $result.len & ", expected: " & $`numArgs`) + for i, namedArg in cfg.args.mapIt(it.name.ident): body.add quote do: parseArgs(result[`i`], `namedArg`) - let lastArg = cfg.args[^1].ident - body.add quote do: - parseArgs(result[(`numArgs`-1).. ^1],`lastArg`) - of First: + body.add quote do: + if result.len < `numArgs`: + hwylCliError("missing positional args, got: " & $result.len & ", expected at least: " & $`numArgs`) for i, namedArg in cfg.args[1..^1].reversed().mapIt(it.ident): body.add quote do: parseArgs(result[^(1+`i`)], `namedArg`) @@ -1095,33 +1084,26 @@ func genPosArgHandler(cfg: CliCfg, body: NimNode) = body.add quote do: parseArgs(result[0..^(`numArgs`)], `firstArg`) - - of NoMulti: - for i, namedArg in cfg.args.mapIt(it.name.ident): + of Last: + body.add quote do: + if result.len < (`numArgs` - 1): + hwylCliError("missing positional args, got: " & $result.len & ", expected at least: " & $(`numArgs` - 1)) + for i, namedArg in cfg.args[0..^2].mapIt(it.ident): body.add quote do: parseArgs(result[`i`], `namedArg`) - if ExactArgs in cfg.settings: - if maKind == NoMulti: - body.add quote do: - result = result[(`numArgs`)..^1] - if result.len > 0: - hwylCliError("unexpected positional arguments: " & $result) - # # here if result.len > 1 then it should error? - # # really if we pass ExactArgs the parse function should return nothing... - - # first and last already absorbed the remaining args - # so ExactArgs is a NOOP use a compile Hint? - - if maKind in [First, Last]: - if ExactArgs in cfg.settings: - hint "Exact args is a No-op when one of the positional args is seq[T]" + let lastArg = cfg.args[^1].ident body.add quote do: - result = @[] + if result.len > `numArgs` - 1: + parseArgs(result[(`numArgs`-1).. ^1],`lastArg`) + + body.add quote do: + result = @[] func addPostParseHook(cfg: CliCfg, body: NimNode) = ## generate block to set defaults and check for required flags let flagSet = ident"flagSet" + let subcmd = ident"subcmd" var required, default: seq[CliFlag] for f in cfg.flags: @@ -1146,24 +1128,34 @@ func addPostParseHook(cfg: CliCfg, body: NimNode) = if `name` notin `flagSet`: `target` = `default` - if cfg.args.len > 0: - genPosArgHandler cfg, body + if hasSubcommands cfg: + body.add quote do: + if result.len == 0: + hwylCliError("expected subcommand") + `subcmd` = result[0] + result = result[1..^1] + + + elif cfg.args.len == 0: + body.add quote do: + if result.len > 0: + hwylCliError("got unexpected positionals args: [b]" & result.join(" ")) + + elif cfg.args.len > 0: + genPosArgHandler cfg, body func hwylCliImpl(cfg: CliCfg): NimNode func genSubcommandHandler(cfg: CliCfg): NimNode = - let args = ident"args" + let subcmd = ident"subcmd" result = nnkStmtList.newTree() - result.add quote do: - if `args`.len == 0: - hwylCliError("expected subcommand") var subCommandCase = nnkCaseStmt.newTree() if NoNormalize notin cfg.settings: - subCommandCase.add(quote do: optionNormalize(`args`[0])) + subCommandCase.add(quote do: optionNormalize(`subcmd`)) else: - subCommandCase.add(quote do: `args`[0]) + subCommandCase.add(quote do: `subcmd`) for sub in cfg.subcommands: var branch = nnkOfBranch.newTree() @@ -1175,7 +1167,7 @@ func genSubcommandHandler(cfg: CliCfg): NimNode = subcommandCase.add nnkElse.newTree( quote do: - hwylCliError("unknown subcommand: [b]" & `args`[0]) + hwylCliError("unknown subcommand: [b]" & `subcmd`) ) result.add subCommandCase @@ -1199,14 +1191,14 @@ func hwylCliImpl(cfg: CliCfg): NimNode = name = cfg.name.replace(" ", "") printHelpName = ident("print" & name & "Help") parserProcName = ident("parse" & name) - args = ident"args" + posArgs = ident"posArgs" optParser = ident("p") cmdLine = ident"cmdLine" flagSet = ident"flagSet" nArgs = ident"nargs" (longNoVal, shortNoVal) = cfg.getNoVals() printHelpProc = generateCliHelpProc(cfg, printHelpName) - flagVars = setVars(cfg) + varBlock= setVars(cfg) var parserBody = nnkStmtList.newTree() @@ -1259,6 +1251,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode = ) ) + if ShowHelp in cfg.settings: parserBody.add quote do: if commandLineParams().len == 0: @@ -1278,7 +1271,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode = runBody.add cfg.post # args and subcommands need to be mutually exclusive -> implement using a CommandKind? - if cfg.subcommands.len > 0: + if hasSubcommands cfg: runBody.add genSubcommandHandler(cfg) result = newTree(nnkStmtList) @@ -1286,12 +1279,12 @@ func hwylCliImpl(cfg: CliCfg): NimNode = result.add quote do: # block: `printHelpProc` - `flagVars` + `varBlock` proc `parserProcName`(`cmdLine`: openArray[string] = commandLineParams()): seq[string] = `parserBody` proc `runProcName`(`cmdLine`: openArray[string] = commandLineParams()) = - let `args` {.used.} = `parserProcName`(`cmdLine`) + let `posArgs` {.used.} = `parserProcName`(`cmdLine`) `runBody` if cfg.root: @@ -1300,7 +1293,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode = `runProcName`() else: result.add quote do: - `runProcName`(`args`[1..^1]) + `runProcName`(`posArgs`) macro hwylCli*(body: untyped) = ## generate a CLI styled by `hwylterm` and parsed by `parseopt3` diff --git a/tests/cli/clis/enumFlag.nim b/tests/cli/clis/enumFlag.nim index adad2f4..f3c1b3f 100644 --- a/tests/cli/clis/enumFlag.nim +++ b/tests/cli/clis/enumFlag.nim @@ -5,8 +5,6 @@ type Color = enum red, blue, green -# TODO: color should be a required flag by default? - hwylCli: name "enumFlag" flags: @@ -14,4 +12,3 @@ hwylCli: T Color run: echo fmt"{color=}" - assert args.len == 0 diff --git a/tests/cli/clis/posBasic.nim b/tests/cli/clis/posBasic.nim new file mode 100644 index 0000000..1cccde1 --- /dev/null +++ b/tests/cli/clis/posBasic.nim @@ -0,0 +1,9 @@ +import std/[strformat] +import hwylterm, hwylterm/hwylcli + +hwylCli: + name "posLast" + positionals: + args seq[string] + run: + echo fmt"{args=}" diff --git a/tests/cli/clis/posFirst.nim b/tests/cli/clis/posFirst.nim index 0dc9e95..4b404c6 100644 --- a/tests/cli/clis/posFirst.nim +++ b/tests/cli/clis/posFirst.nim @@ -3,10 +3,9 @@ import hwylterm, hwylterm/hwylcli hwylCli: name "positionals" - args: + positionals: first seq[string] second string third string run: echo fmt"{first=}, {second=}, {third=}" - echo fmt"{args=}" diff --git a/tests/cli/clis/posLast.nim b/tests/cli/clis/posLast.nim index beb61ed..841c9f3 100644 --- a/tests/cli/clis/posLast.nim +++ b/tests/cli/clis/posLast.nim @@ -3,10 +3,9 @@ import hwylterm, hwylterm/hwylcli hwylCli: name "posLast" - args: + positionals: first string second string third seq[string] run: echo fmt"{first=}, {second=}, {third=}" - assert args.len == 0 diff --git a/tests/cli/clis/posLastExact.nim b/tests/cli/clis/posLastExact.nim deleted file mode 100644 index 027a6b2..0000000 --- a/tests/cli/clis/posLastExact.nim +++ /dev/null @@ -1,13 +0,0 @@ -import std/[strformat] -import hwylterm, hwylterm/hwylcli - -hwylCli: - name "posLast" - settings: ExactArgs - args: - first string - second string - third seq[string] - run: - echo fmt"{first=}, {second=}, {third=}" - assert args.len == 0 diff --git a/tests/cli/clis/posNoMulti.nim b/tests/cli/clis/posNoMulti.nim index bba4c45..b92d664 100644 --- a/tests/cli/clis/posNoMulti.nim +++ b/tests/cli/clis/posNoMulti.nim @@ -3,11 +3,9 @@ import hwylterm, hwylterm/hwylcli hwylCli: name "positionals" - settings: ExactArgs - args: + positionals: first int second string third string run: echo fmt"{first=}, {second=}, {third=}" - assert args.len == 0 diff --git a/tests/cli/clis/subcommands.nim b/tests/cli/clis/subcommands.nim new file mode 100644 index 0000000..3610d53 --- /dev/null +++ b/tests/cli/clis/subcommands.nim @@ -0,0 +1,22 @@ +import std/[strformat] +import hwylterm, hwylterm/hwylcli + +hwylCli: + name "subcommands" + subcommands: + [a] + ... "a subcommand with positionals" + positionals: + input string + outputs seq[string] + run: + echo fmt"{input=} {outputs=}" + [b] + ... "a subcommand with flags" + flags: + input: + T string + outputs: + T seq[string] + run: + echo fmt"{input=} {outputs=}" diff --git a/tests/cli/lib.nim b/tests/cli/lib.nim index dbb72ad..5cdee50 100644 --- a/tests/cli/lib.nim +++ b/tests/cli/lib.nim @@ -23,16 +23,22 @@ proc preCompileWorkingModule(module: string) = echo "cmd: ", cmd quit "failed to precompile test module" +proc preCompileTestModules*() = + for srcModule in walkDirRec(pathToSrc / "clis"): + if srcModule.endsWith(".nim"): + let (_, moduleName, _) = srcModule.splitFile + preCompileWorkingModule(moduleName) + template okWithArgs*(module: string, args = "", output = "") = preCompileWorkingModule(module) - test module: + test (module & "|" & args): let (actualOutput, code) = runTestCli(module, args) check code == 0 check output == actualOutput template failWithArgs*(module: string, args = "", output = "") = preCompileWorkingModule(module) - test module: + test (module & "|" & args): let (actualOutput, code) = runTestCli(module, args) check code == 1 check output == actualOutput diff --git a/tests/cli/tester.nim b/tests/cli/tester.nim index e0ab503..3b85bc2 100644 --- a/tests/cli/tester.nim +++ b/tests/cli/tester.nim @@ -2,31 +2,39 @@ import std/[unittest] import ./lib suite "hwylcli": - test "positionals": - okWithArgs( - "posFirst", - "a b c d e", - """ -first=@["a", "b", "c"], second=d, third=e -args=@[]""" - ) - failWithArgs( - "posFirst", - "a b", - "error missing positional args, got: 2, expected at least: 3", - ) - okWithArgs("posLast", "a b", """first=a, second=b, third=@[]""") - okWithArgs("posLastExact", "a b c d e", """first=a, second=b, third=@["c", "d", "e"]""") - okWithArgs("posNoMulti", "5 b c", """first=5, second=b, third=c""") - failWithArgs("posNoMulti", "5 b c d", """error missing positional args, got: 4, expected: 3""") - test "special flag types": - okWithArgs("enumFlag","--color red", "color=red") - failWithArgs("enumFlag","--color black", "error failed to parse value for color as enum: black expected one of: red,blue,green") + setup: + preCompileTestModules() - test "help": - okWithArgs("posFirst", "--help", + okWithArgs( + "posBasic", + "a b c d e", + """args=@["a", "b", "c", "d", "e"]""" + ) + okWithArgs( + "posFirst", + "a b c d e", + """first=@["a", "b", "c"], second=d, third=e""" + ) + failWithArgs( + "posFirst", + "a b", + "error missing positional args, got: 2, expected at least: 3", + ) + okWithArgs("posLast", "a b", """first=a, second=b, third=@[]""") + okWithArgs("posNoMulti", "5 b c", """first=5, second=b, third=c""") + failWithArgs("posNoMulti", "5 b c d", """error unexepected positional args, got: 4, expected: 3""") + + okWithArgs("enumFlag","--color red", "color=red") + failWithArgs("enumFlag","--color black", "error failed to parse value for color as enum: black expected one of: red,blue,green") + + okWithArgs("subcommands", "a b c","""input=b outputs=@["c"]""") + failWithArgs("subcommands", "b b c","""error got unexpected positionals args: b c""") + okWithArgs("subcommands","b --input in --outputs out1 --outputs out2", """input=in outputs=@["out1", "out2"]""") + + okWithArgs("posFirst", "--help", """usage: positionals first... second third [flags] flags: -h --help show this help""") +