args -> positionals

This commit is contained in:
Daylin Morgan 2025-01-23 19:25:09 -06:00
parent 4c63636c24
commit c40a0a2038
Signed by: daylin
GPG key ID: 950D13E9719334AD
12 changed files with 132 additions and 112 deletions

View file

@ -389,8 +389,9 @@ when isMainModule:
hwylCli: hwylCli:
name "bbansi" name "bbansi"
settings ShowHelp settings ShowHelp
positionals:
args seq[string]
help: help:
usage "[bold]bbansi[/] [[[green]args...[/]] [[[faint]-h|-V[/]]"
description """ description """
bbansi "[[yellow] yellow text!" bbansi "[[yellow] yellow text!"
-> [yellow] yellow text![/] -> [yellow] yellow text![/]

View file

@ -132,7 +132,6 @@ when isMainModule:
name "hwylchoose" name "hwylchoose"
settings ShowHelp settings ShowHelp
help: help:
usage "[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]"
description """ description """
hwylchoose a b c d hwylchoose a b c d
hwylchoose a,b,c,d -s , hwylchoose a,b,c,d -s ,
@ -140,6 +139,8 @@ when isMainModule:
hwylchoose --demo hwylchoose --demo
""" """
hidden demo hidden demo
positionals:
args seq[string]
flags: flags:
demo "show demo" demo "show demo"
separator: separator:

View file

@ -231,6 +231,7 @@ type
inherit*: Inherit inherit*: Inherit
root*: bool root*: bool
func hasSubcommands(c: CliCfg): bool = c.subcommands.len > 0
func err(c: CliCfg, msg: string) = func err(c: CliCfg, msg: string) =
## quit with error while generating cli ## quit with error while generating cli
@ -396,6 +397,7 @@ func parseCliSetting(s: string): CliSetting =
try: parseEnum[CliSetting](s) try: parseEnum[CliSetting](s)
except: error "unknown cli setting: " & s except: error "unknown cli setting: " & s
func parseCliSettings(cfg: var CliCfg, node: NimNode) = func parseCliSettings(cfg: var CliCfg, node: NimNode) =
case node.kind case node.kind
of nnkCommand: of nnkCommand:
@ -732,7 +734,7 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
result.postSub = node[1] result.postSub = node[1]
of "defaultFlagType": of "defaultFlagType":
result.defaultFlagType = node[1] result.defaultFlagType = node[1]
of "args": of "positionals":
parseCliArgs result, node[1] parseCliArgs result, node[1]
else: else:
error "unknown hwylCli setting: " & name error "unknown hwylCli setting: " & name
@ -964,6 +966,7 @@ func isBool(f: CliFlag): bool =
func isCount(f: CliFlag): bool = func isCount(f: CliFlag): bool =
f.typeNode == ident"Count" f.typeNode == ident"Count"
func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] = func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] =
let flagFlags = cfg.flags.filterIt(it.isBool or it.isCount) let flagFlags = cfg.flags.filterIt(it.isBool or it.isCount)
let long = let long =
@ -976,6 +979,7 @@ func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] =
) )
result = (nnkPrefix.newTree(ident"@",long), short) result = (nnkPrefix.newTree(ident"@",long), short)
func setVars(cfg: CliCfg): NimNode = func setVars(cfg: CliCfg): NimNode =
## generate all positinal variables and flags not covered in global module ## generate all positinal variables and flags not covered in global module
result = nnkVarSection.newTree() result = nnkVarSection.newTree()
@ -990,6 +994,8 @@ func setVars(cfg: CliCfg): NimNode =
result.add cfg.args.mapIt( result.add cfg.args.mapIt(
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode()) nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
) )
if hasSubcommands cfg:
result.add nnkIdentDefs.newTree(ident"subcmd", ident"string", newEmptyNode())
func literalFlags(f: CliFlag): NimNode = func literalFlags(f: CliFlag): NimNode =
var flags: seq[string] var flags: seq[string]
@ -1007,7 +1013,7 @@ type
func getMultiArgKind(cfg: CliCfg): MultiArgKind = func getMultiArgKind(cfg: CliCfg): MultiArgKind =
if cfg.args.len == 1: if cfg.args.len == 1:
if cfg.args[0].isSeq: if cfg.args[0].isSeq:
return First return Last
else: else:
return NoMulti return NoMulti
if cfg.args[0].isSeq: if cfg.args[0].isSeq:
@ -1055,38 +1061,21 @@ func genPosArgHandler(cfg: CliCfg, body: NimNode) =
## generate code to handle positional arguments ## generate code to handle positional arguments
let numArgs = cfg.args.len let numArgs = cfg.args.len
let maKind = cfg.getMultiArgKind() 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: case maKind:
# BUG: this may create index defects, of NoMulti:
# if not coupled with ExactArgs or result length checks body.add quote do:
of Last: if result.len > `numArgs`:
for i, namedArg in cfg.args[0..^2].mapIt(it.ident): 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: body.add quote do:
parseArgs(result[`i`], `namedArg`) parseArgs(result[`i`], `namedArg`)
let lastArg = cfg.args[^1].ident
body.add quote do:
parseArgs(result[(`numArgs`-1).. ^1],`lastArg`)
of First: 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): for i, namedArg in cfg.args[1..^1].reversed().mapIt(it.ident):
body.add quote do: body.add quote do:
parseArgs(result[^(1+`i`)], `namedArg`) parseArgs(result[^(1+`i`)], `namedArg`)
@ -1095,33 +1084,26 @@ func genPosArgHandler(cfg: CliCfg, body: NimNode) =
body.add quote do: body.add quote do:
parseArgs(result[0..^(`numArgs`)], `firstArg`) parseArgs(result[0..^(`numArgs`)], `firstArg`)
of Last:
of NoMulti: body.add quote do:
for i, namedArg in cfg.args.mapIt(it.name.ident): 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: body.add quote do:
parseArgs(result[`i`], `namedArg`) parseArgs(result[`i`], `namedArg`)
if ExactArgs in cfg.settings: let lastArg = cfg.args[^1].ident
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]"
body.add quote do: 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) = func addPostParseHook(cfg: CliCfg, body: NimNode) =
## generate block to set defaults and check for required flags ## generate block to set defaults and check for required flags
let flagSet = ident"flagSet" let flagSet = ident"flagSet"
let subcmd = ident"subcmd"
var required, default: seq[CliFlag] var required, default: seq[CliFlag]
for f in cfg.flags: for f in cfg.flags:
@ -1146,24 +1128,34 @@ func addPostParseHook(cfg: CliCfg, body: NimNode) =
if `name` notin `flagSet`: if `name` notin `flagSet`:
`target` = `default` `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 hwylCliImpl(cfg: CliCfg): NimNode
func genSubcommandHandler(cfg: CliCfg): NimNode = func genSubcommandHandler(cfg: CliCfg): NimNode =
let args = ident"args" let subcmd = ident"subcmd"
result = nnkStmtList.newTree() result = nnkStmtList.newTree()
result.add quote do:
if `args`.len == 0:
hwylCliError("expected subcommand")
var subCommandCase = nnkCaseStmt.newTree() var subCommandCase = nnkCaseStmt.newTree()
if NoNormalize notin cfg.settings: if NoNormalize notin cfg.settings:
subCommandCase.add(quote do: optionNormalize(`args`[0])) subCommandCase.add(quote do: optionNormalize(`subcmd`))
else: else:
subCommandCase.add(quote do: `args`[0]) subCommandCase.add(quote do: `subcmd`)
for sub in cfg.subcommands: for sub in cfg.subcommands:
var branch = nnkOfBranch.newTree() var branch = nnkOfBranch.newTree()
@ -1175,7 +1167,7 @@ func genSubcommandHandler(cfg: CliCfg): NimNode =
subcommandCase.add nnkElse.newTree( subcommandCase.add nnkElse.newTree(
quote do: quote do:
hwylCliError("unknown subcommand: [b]" & `args`[0]) hwylCliError("unknown subcommand: [b]" & `subcmd`)
) )
result.add subCommandCase result.add subCommandCase
@ -1199,14 +1191,14 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
name = cfg.name.replace(" ", "") name = cfg.name.replace(" ", "")
printHelpName = ident("print" & name & "Help") printHelpName = ident("print" & name & "Help")
parserProcName = ident("parse" & name) parserProcName = ident("parse" & name)
args = ident"args" posArgs = ident"posArgs"
optParser = ident("p") optParser = ident("p")
cmdLine = ident"cmdLine" cmdLine = ident"cmdLine"
flagSet = ident"flagSet" flagSet = ident"flagSet"
nArgs = ident"nargs" nArgs = ident"nargs"
(longNoVal, shortNoVal) = cfg.getNoVals() (longNoVal, shortNoVal) = cfg.getNoVals()
printHelpProc = generateCliHelpProc(cfg, printHelpName) printHelpProc = generateCliHelpProc(cfg, printHelpName)
flagVars = setVars(cfg) varBlock= setVars(cfg)
var var
parserBody = nnkStmtList.newTree() parserBody = nnkStmtList.newTree()
@ -1259,6 +1251,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
) )
) )
if ShowHelp in cfg.settings: if ShowHelp in cfg.settings:
parserBody.add quote do: parserBody.add quote do:
if commandLineParams().len == 0: if commandLineParams().len == 0:
@ -1278,7 +1271,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
runBody.add cfg.post runBody.add cfg.post
# args and subcommands need to be mutually exclusive -> implement using a CommandKind? # args and subcommands need to be mutually exclusive -> implement using a CommandKind?
if cfg.subcommands.len > 0: if hasSubcommands cfg:
runBody.add genSubcommandHandler(cfg) runBody.add genSubcommandHandler(cfg)
result = newTree(nnkStmtList) result = newTree(nnkStmtList)
@ -1286,12 +1279,12 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
result.add quote do: result.add quote do:
# block: # block:
`printHelpProc` `printHelpProc`
`flagVars` `varBlock`
proc `parserProcName`(`cmdLine`: openArray[string] = commandLineParams()): seq[string] = proc `parserProcName`(`cmdLine`: openArray[string] = commandLineParams()): seq[string] =
`parserBody` `parserBody`
proc `runProcName`(`cmdLine`: openArray[string] = commandLineParams()) = proc `runProcName`(`cmdLine`: openArray[string] = commandLineParams()) =
let `args` {.used.} = `parserProcName`(`cmdLine`) let `posArgs` {.used.} = `parserProcName`(`cmdLine`)
`runBody` `runBody`
if cfg.root: if cfg.root:
@ -1300,7 +1293,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
`runProcName`() `runProcName`()
else: else:
result.add quote do: result.add quote do:
`runProcName`(`args`[1..^1]) `runProcName`(`posArgs`)
macro hwylCli*(body: untyped) = macro hwylCli*(body: untyped) =
## generate a CLI styled by `hwylterm` and parsed by `parseopt3` ## generate a CLI styled by `hwylterm` and parsed by `parseopt3`

View file

@ -5,8 +5,6 @@ type
Color = enum Color = enum
red, blue, green red, blue, green
# TODO: color should be a required flag by default?
hwylCli: hwylCli:
name "enumFlag" name "enumFlag"
flags: flags:
@ -14,4 +12,3 @@ hwylCli:
T Color T Color
run: run:
echo fmt"{color=}" echo fmt"{color=}"
assert args.len == 0

View file

@ -0,0 +1,9 @@
import std/[strformat]
import hwylterm, hwylterm/hwylcli
hwylCli:
name "posLast"
positionals:
args seq[string]
run:
echo fmt"{args=}"

View file

@ -3,10 +3,9 @@ import hwylterm, hwylterm/hwylcli
hwylCli: hwylCli:
name "positionals" name "positionals"
args: positionals:
first seq[string] first seq[string]
second string second string
third string third string
run: run:
echo fmt"{first=}, {second=}, {third=}" echo fmt"{first=}, {second=}, {third=}"
echo fmt"{args=}"

View file

@ -3,10 +3,9 @@ import hwylterm, hwylterm/hwylcli
hwylCli: hwylCli:
name "posLast" name "posLast"
args: positionals:
first string first string
second string second string
third seq[string] third seq[string]
run: run:
echo fmt"{first=}, {second=}, {third=}" echo fmt"{first=}, {second=}, {third=}"
assert args.len == 0

View file

@ -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

View file

@ -3,11 +3,9 @@ import hwylterm, hwylterm/hwylcli
hwylCli: hwylCli:
name "positionals" name "positionals"
settings: ExactArgs positionals:
args:
first int first int
second string second string
third string third string
run: run:
echo fmt"{first=}, {second=}, {third=}" echo fmt"{first=}, {second=}, {third=}"
assert args.len == 0

View file

@ -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=}"

View file

@ -23,16 +23,22 @@ proc preCompileWorkingModule(module: string) =
echo "cmd: ", cmd echo "cmd: ", cmd
quit "failed to precompile test module" 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 = "") = template okWithArgs*(module: string, args = "", output = "") =
preCompileWorkingModule(module) preCompileWorkingModule(module)
test module: test (module & "|" & args):
let (actualOutput, code) = runTestCli(module, args) let (actualOutput, code) = runTestCli(module, args)
check code == 0 check code == 0
check output == actualOutput check output == actualOutput
template failWithArgs*(module: string, args = "", output = "") = template failWithArgs*(module: string, args = "", output = "") =
preCompileWorkingModule(module) preCompileWorkingModule(module)
test module: test (module & "|" & args):
let (actualOutput, code) = runTestCli(module, args) let (actualOutput, code) = runTestCli(module, args)
check code == 1 check code == 1
check output == actualOutput check output == actualOutput

View file

@ -2,31 +2,39 @@ import std/[unittest]
import ./lib import ./lib
suite "hwylcli": suite "hwylcli":
test "positionals": setup:
okWithArgs( preCompileTestModules()
"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")
test "help": okWithArgs(
okWithArgs("posFirst", "--help", "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: """usage:
positionals first... second third [flags] positionals first... second third [flags]
flags: flags:
-h --help show this help""") -h --help show this help""")