mirror of
https://github.com/daylinmorgan/hwylterm.git
synced 2025-02-23 01:35:50 -06:00
positional arg parsing revamp
This commit is contained in:
parent
938f9d411d
commit
8f98b53b91
11 changed files with 273 additions and 46 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,6 +1,3 @@
|
||||||
tests/*
|
|
||||||
!tests/*.nim
|
|
||||||
!tests/*.nims
|
|
||||||
nimble.develop
|
nimble.develop
|
||||||
nimble.paths
|
nimble.paths
|
||||||
nimbledeps
|
nimbledeps
|
||||||
|
|
|
@ -3,6 +3,7 @@ import std/[os, strformat, strutils]
|
||||||
task test, "run tests":
|
task test, "run tests":
|
||||||
selfExec "r tests/tbbansi.nim"
|
selfExec "r tests/tbbansi.nim"
|
||||||
selfExec "r tests/tcli.nim"
|
selfExec "r tests/tcli.nim"
|
||||||
|
selfExec "r tests/cli/tcli.nim"
|
||||||
|
|
||||||
task develop, "install cligen for development":
|
task develop, "install cligen for development":
|
||||||
exec "nimble install -l 'cligen@1.7.5'"
|
exec "nimble install -l 'cligen@1.7.5'"
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
]##
|
]##
|
||||||
|
|
||||||
import std/[
|
import std/[
|
||||||
|
algorithm,
|
||||||
macros, os, sequtils,
|
macros, os, sequtils,
|
||||||
sets, strutils, tables,
|
sets, strutils, tables,
|
||||||
sugar
|
sugar
|
||||||
|
@ -149,7 +150,9 @@ type
|
||||||
GenerateOnly, ## Don't attach root `runProc()` node
|
GenerateOnly, ## Don't attach root `runProc()` node
|
||||||
NoHelpFlag, ## Remove the builtin help flag
|
NoHelpFlag, ## Remove the builtin help flag
|
||||||
ShowHelp, ## If cmdline empty show help
|
ShowHelp, ## If cmdline empty show help
|
||||||
NoNormalize ## Don't normalize flags and commands
|
NoNormalize, ## Don't normalize flags and commands
|
||||||
|
NoPositional ## Raise error if any remaing positional arguments
|
||||||
|
ExactArgs, ## Raise error if missing positional argument
|
||||||
|
|
||||||
BuiltinFlag = object
|
BuiltinFlag = object
|
||||||
name*: string
|
name*: string
|
||||||
|
@ -179,6 +182,9 @@ type
|
||||||
header*, footer*, description*, usage*, styles*: NimNode
|
header*, footer*, description*, usage*, styles*: NimNode
|
||||||
|
|
||||||
CliArg = object
|
CliArg = object
|
||||||
|
name: string
|
||||||
|
ident: NimNode
|
||||||
|
typeNode: NimNode
|
||||||
|
|
||||||
CliCfg = object
|
CliCfg = object
|
||||||
name*: string
|
name*: string
|
||||||
|
@ -200,6 +206,11 @@ type
|
||||||
inherit*: Inherit
|
inherit*: Inherit
|
||||||
root*: bool
|
root*: bool
|
||||||
|
|
||||||
|
|
||||||
|
func err(c: CliCfg, msg: string) =
|
||||||
|
## quit with error while generating cli
|
||||||
|
error "\nfailed to generate '" & c.name & "' hwylcli: \n" & msg
|
||||||
|
|
||||||
template `<<<`(s: string) {.used.} =
|
template `<<<`(s: string) {.used.} =
|
||||||
let pos = instantiationInfo()
|
let pos = instantiationInfo()
|
||||||
debugEcho "$1:$2" % [pos.filename, $pos.line]
|
debugEcho "$1:$2" % [pos.filename, $pos.line]
|
||||||
|
@ -211,7 +222,6 @@ template `<<<`(n: NimNode) {.used.} =
|
||||||
## for debugging macros
|
## for debugging macros
|
||||||
<<< treeRepr n
|
<<< treeRepr n
|
||||||
|
|
||||||
|
|
||||||
func `<<<`(f: CliFlag) {.used.}=
|
func `<<<`(f: CliFlag) {.used.}=
|
||||||
var s: string
|
var s: string
|
||||||
let fields = [
|
let fields = [
|
||||||
|
@ -246,6 +256,7 @@ func getFlagParamNode(node: NimNode): NimNode =
|
||||||
result = node[1]
|
result = node[1]
|
||||||
else: bad(node, "flag param")
|
else: bad(node, "flag param")
|
||||||
|
|
||||||
|
# TODO: also accept the form `flag: "help"`
|
||||||
func parseFlagParams(f: var CliFlag, node: NimNode) =
|
func parseFlagParams(f: var CliFlag, node: NimNode) =
|
||||||
expectKind node, nnkStmtList
|
expectKind node, nnkStmtList
|
||||||
for n in node:
|
for n in node:
|
||||||
|
@ -307,6 +318,12 @@ func parseCliFlag(n: NimNode): CliFlag =
|
||||||
result.typeNode = ident"bool"
|
result.typeNode = ident"bool"
|
||||||
|
|
||||||
func postParse(cfg: var CliCfg) =
|
func postParse(cfg: var CliCfg) =
|
||||||
|
if cfg.name == "":
|
||||||
|
error "missing required option: name"
|
||||||
|
|
||||||
|
if cfg.args.len != 0 and cfg.subcommands.len != 0:
|
||||||
|
error "args and subcommands are mutually exclusive"
|
||||||
|
|
||||||
let defaultTypeNode = cfg.defaultFlagType or ident"bool"
|
let defaultTypeNode = cfg.defaultFlagType or ident"bool"
|
||||||
for f in cfg.flagDefs.mitems:
|
for f in cfg.flagDefs.mitems:
|
||||||
if f.typeNode == nil:
|
if f.typeNode == nil:
|
||||||
|
@ -314,6 +331,11 @@ func postParse(cfg: var CliCfg) =
|
||||||
if f.group in ["", "global"]:
|
if f.group in ["", "global"]:
|
||||||
cfg.flags.add f
|
cfg.flags.add f
|
||||||
|
|
||||||
|
if cfg.args.len > 0:
|
||||||
|
let count = cfg.args.filterIt(it.typeNode.kind == nnkBracketExpr).len
|
||||||
|
if count > 1:
|
||||||
|
cfg.err "more than one positional argument is variadic"
|
||||||
|
|
||||||
func parseCliFlags(cfg: var CliCfg, node: NimNode) =
|
func parseCliFlags(cfg: var CliCfg, node: NimNode) =
|
||||||
var group: string
|
var group: string
|
||||||
expectKind node, nnkStmtList
|
expectKind node, nnkStmtList
|
||||||
|
@ -514,11 +536,7 @@ func pasrseCliAlias(cfg: var CliCfg, node: NimNode) =
|
||||||
cfg.alias.incl s
|
cfg.alias.incl s
|
||||||
else: bad(n, "alias")
|
else: bad(n, "alias")
|
||||||
|
|
||||||
func err(c: CliCfg, msg: string) =
|
func postPropagateCheck(c: CliCfg) =
|
||||||
## quit with error while generating cli
|
|
||||||
error "failed to generate " & c.name & " hwylcli: \n" & msg
|
|
||||||
|
|
||||||
func check(c: CliCfg) =
|
|
||||||
## verify the cli is valid
|
## verify the cli is valid
|
||||||
var
|
var
|
||||||
short: Table[char, CliFlag]
|
short: Table[char, CliFlag]
|
||||||
|
@ -553,7 +571,7 @@ func propagate(c: var CliCfg) =
|
||||||
child.post = c.postSub
|
child.post = c.postSub
|
||||||
child.inheritFrom(c)
|
child.inheritFrom(c)
|
||||||
propagate child
|
propagate child
|
||||||
check child
|
postPropagateCheck child
|
||||||
|
|
||||||
|
|
||||||
func parseCliHelp(c: var CliCfg, node: NimNode) =
|
func parseCliHelp(c: var CliCfg, node: NimNode) =
|
||||||
|
@ -588,7 +606,6 @@ func parseCliHelp(c: var CliCfg, node: NimNode) =
|
||||||
of nnkCall:
|
of nnkCall:
|
||||||
if node[1].kind != nnkStmtList:
|
if node[1].kind != nnkStmtList:
|
||||||
error "expected list of arguments for help"
|
error "expected list of arguments for help"
|
||||||
|
|
||||||
for n in node[1]:
|
for n in node[1]:
|
||||||
expectLen n, 2
|
expectLen n, 2
|
||||||
let id = n[0].strVal
|
let id = n[0].strVal
|
||||||
|
@ -599,7 +616,6 @@ func parseCliHelp(c: var CliCfg, node: NimNode) =
|
||||||
of nnKCall:
|
of nnKCall:
|
||||||
val = n[1][0]
|
val = n[1][0]
|
||||||
else: bad(n, id)
|
else: bad(n, id)
|
||||||
|
|
||||||
case id:
|
case id:
|
||||||
of "usage": help.usage = val
|
of "usage": help.usage = val
|
||||||
of "description": help.description = val
|
of "description": help.description = val
|
||||||
|
@ -607,11 +623,46 @@ func parseCliHelp(c: var CliCfg, node: NimNode) =
|
||||||
of "footer": help.footer = val
|
of "footer": help.footer = val
|
||||||
of "styles": help.styles = val
|
of "styles": help.styles = val
|
||||||
else: error "unknown help option: " & id
|
else: error "unknown help option: " & id
|
||||||
|
|
||||||
else: bad(node, "help")
|
else: bad(node, "help")
|
||||||
|
|
||||||
c.help = help
|
c.help = help
|
||||||
|
|
||||||
|
func badNode(c: CliCfg, node: NimNode, msg: string) =
|
||||||
|
c.err "unexpected node kind: " & $node.kind & "\n" & msg
|
||||||
|
|
||||||
|
func parseCliArg(c: CliCfg, node: NimNode): CliArg =
|
||||||
|
expectLen node, 2
|
||||||
|
result.name = node[0].strVal
|
||||||
|
case node[1].kind
|
||||||
|
of nnkStmtList:
|
||||||
|
for n in node[1]:
|
||||||
|
let id = n[0].strVal
|
||||||
|
var val: NimNode
|
||||||
|
case n.kind:
|
||||||
|
of nnkCommand:
|
||||||
|
val = n[1]
|
||||||
|
of nnkCall:
|
||||||
|
# input seq[string]
|
||||||
|
if n[1].len == 2:
|
||||||
|
result.typeNode = n[1][1]
|
||||||
|
val = n[1][0]
|
||||||
|
else: bad(n, id)
|
||||||
|
case id:
|
||||||
|
of "T": result.typeNode = val
|
||||||
|
of "ident": result.ident = val
|
||||||
|
else: c.err("unknown cli param: " & id & "provided for arg: " & result.name)
|
||||||
|
of nnkIdent, nnkBracketExpr:
|
||||||
|
result.typeNode = node[1]
|
||||||
|
else:
|
||||||
|
c.badNode(node[1], "parsing cli arg: " & result.name)
|
||||||
|
if result.ident == nil:
|
||||||
|
result.ident = ident(result.name)
|
||||||
|
|
||||||
|
func parseCliArgs(c: var CliCfg, node: NimNode) =
|
||||||
|
if node.kind != nnkStmtList:
|
||||||
|
bad(node, "expected node kind nnkStmtList")
|
||||||
|
for n in node:
|
||||||
|
c.args.add parseCliArg(c, n)
|
||||||
|
|
||||||
func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
|
func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
|
||||||
result.name = name
|
result.name = name
|
||||||
result.root = root
|
result.root = root
|
||||||
|
@ -652,13 +703,13 @@ 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":
|
||||||
|
parseCliArgs result, node[1]
|
||||||
else:
|
else:
|
||||||
error "unknown hwylCli setting: " & name
|
error "unknown hwylCli setting: " & name
|
||||||
|
|
||||||
if result.name == "":
|
|
||||||
error "missing required option: name"
|
|
||||||
|
|
||||||
postParse result
|
postParse result
|
||||||
|
|
||||||
# TODO: validate "required" flags exist here?
|
# TODO: validate "required" flags exist here?
|
||||||
result.addBuiltinFlags()
|
result.addBuiltinFlags()
|
||||||
|
|
||||||
|
@ -700,6 +751,7 @@ proc hwylCliError*(msg: string) =
|
||||||
quit $(bb("error ", "red") & bb(msg))
|
quit $(bb("error ", "red") & bb(msg))
|
||||||
|
|
||||||
func defaultUsage(cfg: CliCfg): NimNode =
|
func defaultUsage(cfg: CliCfg): NimNode =
|
||||||
|
# TODO: attempt to handle pos args
|
||||||
var s = "[b]" & cfg.name & "[/]"
|
var s = "[b]" & cfg.name & "[/]"
|
||||||
if cfg.subcommands.len > 0:
|
if cfg.subcommands.len > 0:
|
||||||
s.add " [bold italic]subcmd[/]"
|
s.add " [bold italic]subcmd[/]"
|
||||||
|
@ -887,8 +939,8 @@ func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] =
|
||||||
)
|
)
|
||||||
result = (nnkPrefix.newTree(ident"@",long), short)
|
result = (nnkPrefix.newTree(ident"@",long), short)
|
||||||
|
|
||||||
func setFlagVars(cfg: CliCfg): NimNode =
|
func setVars(cfg: CliCfg): NimNode =
|
||||||
## generate all variables not covered in global module
|
## generate all positinal variables and flags not covered in global module
|
||||||
result = nnkVarSection.newTree()
|
result = nnkVarSection.newTree()
|
||||||
let flags =
|
let flags =
|
||||||
if cfg.root: cfg.flags
|
if cfg.root: cfg.flags
|
||||||
|
@ -897,6 +949,10 @@ func setFlagVars(cfg: CliCfg): NimNode =
|
||||||
result.add flags.mapIt(
|
result.add flags.mapIt(
|
||||||
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
|
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
|
||||||
)
|
)
|
||||||
|
if cfg.args.len > 0:
|
||||||
|
result.add cfg.args.mapIt(
|
||||||
|
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
|
||||||
|
)
|
||||||
|
|
||||||
func literalFlags(f: CliFlag): NimNode =
|
func literalFlags(f: CliFlag): NimNode =
|
||||||
var flags: seq[string]
|
var flags: seq[string]
|
||||||
|
@ -904,7 +960,116 @@ func literalFlags(f: CliFlag): NimNode =
|
||||||
if f.long != "": flags.add "[b]" & "--" & f.long & "[/]"
|
if f.long != "": flags.add "[b]" & "--" & f.long & "[/]"
|
||||||
result = newLit(flags.join("|"))
|
result = newLit(flags.join("|"))
|
||||||
|
|
||||||
func addPostParseCheck(cfg: CliCfg, body: NimNode) =
|
type
|
||||||
|
MultiArgKind = enum
|
||||||
|
NoMulti, ## No positionals use seq[[T]]
|
||||||
|
First, ## First positional uses seq[[T]]
|
||||||
|
Last, ## Last positional uses seq[[T]]
|
||||||
|
|
||||||
|
func getMultiArgKind(cfg: CliCfg): MultiArgKind =
|
||||||
|
if cfg.args.len == 1:
|
||||||
|
return First
|
||||||
|
if cfg.args[0].typeNode.kind == nnkBracketExpr:
|
||||||
|
return First
|
||||||
|
if cfg.args[^1].typeNode.kind == nnkBracketExpr:
|
||||||
|
return Last
|
||||||
|
|
||||||
|
func parseArgs(p: OptParser, target: var string) =
|
||||||
|
target = p.key
|
||||||
|
|
||||||
|
func parseArgs[T](p: OptParser, target: var seq[T]) =
|
||||||
|
var val: T
|
||||||
|
parseArgs(p, val)
|
||||||
|
target.add val
|
||||||
|
|
||||||
|
proc parseArgs*(arg: string, target: var float) =
|
||||||
|
try: target = parseFloat(arg)
|
||||||
|
except: hwylCliError("failed to parse as float: [b]" & arg)
|
||||||
|
|
||||||
|
func parseArgs*(arg: string, target: var string) =
|
||||||
|
target = arg
|
||||||
|
|
||||||
|
proc parseArgs*(arg: string, target: var int) =
|
||||||
|
try: target = parseInt(arg)
|
||||||
|
except: hwylCliError("failed to parse as integer: [b]" & arg)
|
||||||
|
|
||||||
|
proc parseArgs*[E: enum](arg: string, target: var E) =
|
||||||
|
try: target = parseEnum[E](arg)
|
||||||
|
except:
|
||||||
|
let choices = enumNames(E).join(",")
|
||||||
|
hwylCliError("failed to parse as enum: [b]" & arg & "[/], expected one of: " & choices)
|
||||||
|
|
||||||
|
proc parseArgs*[T](arg: string, target: var seq[T]) =
|
||||||
|
var val: T
|
||||||
|
parseArgs(arg, val)
|
||||||
|
target.add val
|
||||||
|
|
||||||
|
proc parseArgs*[T](args: seq[string], target: var seq[T]) =
|
||||||
|
for arg in args:
|
||||||
|
parseArgs(arg, target)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: rework conditionals and control flow here...
|
||||||
|
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):
|
||||||
|
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:
|
||||||
|
for i, namedArg in cfg.args[1..^1].reversed().mapIt(it.ident):
|
||||||
|
body.add quote do:
|
||||||
|
parseArgs(result[^(1+`i`)], `namedArg`)
|
||||||
|
|
||||||
|
let firstArg = cfg.args[0].ident
|
||||||
|
body.add quote do:
|
||||||
|
parseArgs(result[0..^(`numArgs`)], `firstArg`)
|
||||||
|
|
||||||
|
|
||||||
|
of NoMulti:
|
||||||
|
for i, namedArg in cfg.args.mapIt(it.name.ident):
|
||||||
|
body.add quote do:
|
||||||
|
parseArgs(result[`i`], `namedArg`)
|
||||||
|
|
||||||
|
# clear out 'args'
|
||||||
|
if ExactArgs in cfg.settings:
|
||||||
|
if maKind == NoMulti:
|
||||||
|
body.add quote do:
|
||||||
|
result = @[(`numArgs`)..^1]
|
||||||
|
else:
|
||||||
|
body.add quote do:
|
||||||
|
result = @[`numArgs`..^1]
|
||||||
|
|
||||||
|
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"
|
||||||
var required, default: seq[CliFlag]
|
var required, default: seq[CliFlag]
|
||||||
|
@ -931,6 +1096,10 @@ func addPostParseCheck(cfg: CliCfg, body: NimNode) =
|
||||||
if `name` notin `flagSet`:
|
if `name` notin `flagSet`:
|
||||||
`target` = `default`
|
`target` = `default`
|
||||||
|
|
||||||
|
if 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 =
|
||||||
|
@ -961,16 +1130,11 @@ func genSubcommandHandler(cfg: CliCfg): NimNode =
|
||||||
|
|
||||||
result.add subCommandCase
|
result.add subCommandCase
|
||||||
|
|
||||||
func parseArgs(p: OptParser, target: var string) =
|
# TODO: collect all strings into a seq and handle prior to subcomamnd parsing?
|
||||||
target = p.key
|
# subcommands are really just a special case of positional args handling
|
||||||
|
func positionalArgsOfBranch(cfg: CliCfg): NimNode =
|
||||||
func parseArgs[T](p: OptParser, target: var seq[T]) =
|
|
||||||
var val: T
|
|
||||||
parseArgs(p, val)
|
|
||||||
target.add val
|
|
||||||
|
|
||||||
func argOfBranch(cfg: CliCfg): NimNode =
|
|
||||||
result = nnkOfBranch.newTree(ident"cmdArgument")
|
result = nnkOfBranch.newTree(ident"cmdArgument")
|
||||||
|
# TODO: utilize the NoPositional setting here?
|
||||||
# if cfg.args.len == 0 and cfg.subcommands.len == 0:
|
# if cfg.args.len == 0 and cfg.subcommands.len == 0:
|
||||||
# result.add quote do:
|
# result.add quote do:
|
||||||
# hwylCliError("unexpected positional argument: [b]" & p.key)
|
# hwylCliError("unexpected positional argument: [b]" & p.key)
|
||||||
|
@ -979,7 +1143,6 @@ func argOfBranch(cfg: CliCfg): NimNode =
|
||||||
inc nArgs
|
inc nArgs
|
||||||
parseArgs(p, result)
|
parseArgs(p, result)
|
||||||
|
|
||||||
|
|
||||||
func hwylCliImpl(cfg: CliCfg): NimNode =
|
func hwylCliImpl(cfg: CliCfg): NimNode =
|
||||||
let
|
let
|
||||||
version = cfg.version or newLit("")
|
version = cfg.version or newLit("")
|
||||||
|
@ -993,9 +1156,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
||||||
nArgs = ident"nargs"
|
nArgs = ident"nargs"
|
||||||
(longNoVal, shortNoVal) = cfg.getNoVals()
|
(longNoVal, shortNoVal) = cfg.getNoVals()
|
||||||
printHelpProc = generateCliHelpProc(cfg, printHelpName)
|
printHelpProc = generateCliHelpProc(cfg, printHelpName)
|
||||||
flagVars = setFlagVars(cfg)
|
flagVars = setVars(cfg)
|
||||||
|
|
||||||
result = newTree(nnkStmtList)
|
|
||||||
|
|
||||||
var
|
var
|
||||||
parserBody = nnkStmtList.newTree()
|
parserBody = nnkStmtList.newTree()
|
||||||
|
@ -1035,8 +1196,8 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
||||||
nnkCaseStmt.newTree(
|
nnkCaseStmt.newTree(
|
||||||
ident"kind",
|
ident"kind",
|
||||||
nnkOfBranch.newTree(ident("cmdError"), quote do: hwylCliError(p.message)),
|
nnkOfBranch.newTree(ident("cmdError"), quote do: hwylCliError(p.message)),
|
||||||
nnkOfBranch.newTree(ident("cmdEnd"), quote do: assert false),
|
nnkOfBranch.newTree(ident("cmdEnd"), quote do: hwylCliError("reached cmdEnd unexpectedly.")),
|
||||||
argOfBranch(cfg),
|
positionalArgsOfBranch(cfg),
|
||||||
nnkOfBranch.newTree(
|
nnkOfBranch.newTree(
|
||||||
ident("cmdShortOption"), ident("cmdLongOption"),
|
ident("cmdShortOption"), ident("cmdLongOption"),
|
||||||
shortLongCaseStmt(cfg, printHelpName, version)
|
shortLongCaseStmt(cfg, printHelpName, version)
|
||||||
|
@ -1050,9 +1211,11 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
||||||
if commandLineParams().len == 0:
|
if commandLineParams().len == 0:
|
||||||
`printHelpName`(); quit 1
|
`printHelpName`(); quit 1
|
||||||
|
|
||||||
|
addPostParseHook(cfg, parserBody)
|
||||||
|
|
||||||
let runProcName = ident("run" & name)
|
let runProcName = ident("run" & name)
|
||||||
let runBody = nnkStmtList.newTree()
|
let runBody = nnkStmtList.newTree()
|
||||||
addPostParseCheck(cfg, parserBody)
|
|
||||||
# move to proc?
|
# move to proc?
|
||||||
if cfg.pre != nil:
|
if cfg.pre != nil:
|
||||||
runBody.add cfg.pre
|
runBody.add cfg.pre
|
||||||
|
@ -1061,9 +1224,12 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
||||||
if cfg.post != nil:
|
if cfg.post != nil:
|
||||||
runBody.add cfg.post
|
runBody.add cfg.post
|
||||||
|
|
||||||
|
# args and subcommands need to be mutually exclusive -> implement using a CommandKind?
|
||||||
if cfg.subcommands.len > 0:
|
if cfg.subcommands.len > 0:
|
||||||
runBody.add genSubcommandHandler(cfg)
|
runBody.add genSubcommandHandler(cfg)
|
||||||
|
|
||||||
|
result = newTree(nnkStmtList)
|
||||||
|
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
# block:
|
# block:
|
||||||
`printHelpProc`
|
`printHelpProc`
|
||||||
|
@ -1083,7 +1249,6 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
`runProcName`(`args`[1..^1])
|
`runProcName`(`args`[1..^1])
|
||||||
|
|
||||||
|
|
||||||
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`
|
||||||
var cfg = parseCliBody(body, root = true)
|
var cfg = parseCliBody(body, root = true)
|
||||||
|
|
5
test.nim
Normal file
5
test.nim
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import std/macros
|
||||||
|
|
||||||
|
dumpAstGen:
|
||||||
|
block:
|
||||||
|
echo "hello world"
|
4
tests/.gitignore
vendored
Normal file
4
tests/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*
|
||||||
|
!*.nim
|
||||||
|
!*.nims
|
||||||
|
!cli/
|
4
tests/cli/.gitignore
vendored
Normal file
4
tests/cli/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*
|
||||||
|
!refs/*
|
||||||
|
!*.nim
|
||||||
|
!*.nims
|
1
tests/cli/config.nims
Normal file
1
tests/cli/config.nims
Normal file
|
@ -0,0 +1 @@
|
||||||
|
switch("path", "$projectDir/../../src")
|
29
tests/cli/lib.nim
Normal file
29
tests/cli/lib.nim
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import std/[compilesettings, os, osproc, strutils, times, unittest]
|
||||||
|
|
||||||
|
const pathToSrc = querySetting(SingleValueSetting.projectPath)
|
||||||
|
const binDir = pathToSrc / "bin"
|
||||||
|
const hwylCliSrc = pathToSrc / "../../src/hwylterm/hwylcli.nim"
|
||||||
|
let hwylCliWriteTime = getFileInfo(hwylCliSrc).lastWriteTime
|
||||||
|
|
||||||
|
if not dirExists(binDir):
|
||||||
|
createDir(binDir)
|
||||||
|
|
||||||
|
proc runTestCli(module: string, args: string, code: int = 0): string =
|
||||||
|
let cmd = binDir / module & " " & args
|
||||||
|
let (output, exitCode) = execCmdEx(cmd)
|
||||||
|
check code == exitCode
|
||||||
|
result = output.strip()
|
||||||
|
|
||||||
|
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):
|
||||||
|
let cmd = "nim c -o:$1 $2" % [exe, srcModule]
|
||||||
|
let code = execCmd(cmd)
|
||||||
|
if code != 0:
|
||||||
|
echo "cmd: ", cmd
|
||||||
|
quit "failed to precompile test module"
|
||||||
|
|
||||||
|
proc checkRunWithArgs*(module: string, args = "", output = "", code = 0) =
|
||||||
|
preCompileWorkingModule(module)
|
||||||
|
check output == runTestCli(module, args, code)
|
11
tests/cli/tcli.nim
Normal file
11
tests/cli/tcli.nim
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import std/[unittest]
|
||||||
|
import ./lib
|
||||||
|
|
||||||
|
suite "hwylcli":
|
||||||
|
test "positionals":
|
||||||
|
checkRunWithArgs("posFirst", "a b c d e","""first=@["a", "b", "c"], second=d, third=e""")
|
||||||
|
checkRunWithArgs("posFirst", "a b", "error missing positional args, got: 2, expected at least: 3", code = 1)
|
||||||
|
checkRunWithArgs("posLast", "a b", """first=a, second=b, third=@[]""")
|
||||||
|
checkRunWithArgs("posLastExact", "a b c d e", """first=a, second=b, third=@["c", "d", "e"]""")
|
||||||
|
checkRunWithArgs("posNoMulti", "5 b c", """first=5, second=b, third=c""")
|
||||||
|
checkRunWithArgs("posNoMulti", "5 b c d", """error missing positional args, got: 4, expected: 3""", code = 1)
|
|
@ -70,6 +70,16 @@ hwylCli:
|
||||||
a longer mulitline description that will be visible in the subcommand help
|
a longer mulitline description that will be visible in the subcommand help
|
||||||
and it will automatically be "bb"'ed [bold]this is bold text[/]
|
and it will automatically be "bb"'ed [bold]this is bold text[/]
|
||||||
"""
|
"""
|
||||||
|
# args first, second
|
||||||
|
# or
|
||||||
|
args:
|
||||||
|
# default type is string
|
||||||
|
# only one 'arg' can be the seq[string]
|
||||||
|
# order matters here
|
||||||
|
# by default string
|
||||||
|
inputs:
|
||||||
|
T int
|
||||||
|
second seq[string]
|
||||||
flags:
|
flags:
|
||||||
^something
|
^something
|
||||||
thing:
|
thing:
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
|
# TODO: combine this with tests/cli/
|
||||||
import std/[
|
import std/[
|
||||||
unittest
|
unittest,
|
||||||
|
strutils
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
import hwylterm, hwylterm/hwylcli
|
import hwylterm, hwylterm/hwylcli
|
||||||
|
|
||||||
suite "cli":
|
suite "cli":
|
||||||
test "cli":
|
test "cli":
|
||||||
let expected = """[b]test-program[/] [[args...]
|
let expected = """[b]test-program[/] [[args...]
|
||||||
|
|
||||||
[b cyan]flags[/]:
|
[bold cyan]flags[/]:
|
||||||
[yellow]-h[/] [magenta]--help [/] []show this help[/]
|
[yellow]-h[/yellow] [magenta]--help [/magenta] []show this help[/]
|
||||||
[yellow]-V[/] [magenta]--version[/] []print version[/]
|
[yellow]-V[/yellow] [magenta]--version[/magenta] []print version[/]"""
|
||||||
"""
|
|
||||||
let cli =
|
let cli =
|
||||||
newHwylCliHelp(
|
newHwylCliHelp(
|
||||||
header = "[b]test-program[/] [[args...]",
|
header = "[b]test-program[/] [[args...]",
|
||||||
flags = [("h","help","show this help",),("V","version","print version")]
|
flags = [("h","help","show this help",),("V","version","print version")]
|
||||||
)
|
)
|
||||||
check $bb(cli) == $bb(expected)
|
check render(cli) == expected
|
||||||
|
check $bb(render(cli)) == $bb(expected)
|
||||||
|
|
Loading…
Add table
Reference in a new issue