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:
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![/]

View file

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

View file

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

View file

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

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:
name "positionals"
args:
positionals:
first seq[string]
second string
third string
run:
echo fmt"{first=}, {second=}, {third=}"
echo fmt"{args=}"

View file

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

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:
name "positionals"
settings: ExactArgs
args:
positionals:
first int
second string
third string
run:
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
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

View file

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