improve hwylcli error handling

This commit is contained in:
Daylin Morgan 2025-01-28 15:58:25 -06:00
parent 2811c05a21
commit a7593561ee
Signed by: daylin
GPG key ID: 950D13E9719334AD
5 changed files with 227 additions and 149 deletions

View file

@ -30,7 +30,7 @@
import std/[ import std/[
algorithm, algorithm,
macros, os, sequtils, macros, os, sequtils,
sets, strutils, tables, sets, strutils, strformat, tables,
sugar sugar
] ]
import ./[bbansi, parseopt3] import ./[bbansi, parseopt3]
@ -255,9 +255,6 @@ type
func hasSubcommands(c: CliCfg): bool = c.subcommands.len > 0 func hasSubcommands(c: CliCfg): bool = c.subcommands.len > 0
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()
@ -288,48 +285,78 @@ func `<<<`(f: CliFlag) {.used.}=
s.add ")" s.add ")"
<<< s <<< s
# -- error reporting
func bad(n: NimNode, argument: string = "") = func err(c: CliCfg, msg: string) =
var msg = "unexpected node kind: " & $n.kind ## quit with error while generating cli
if argument != "": error "hwylcli: \nfailed to generate '" & c.name & "' cli: \n" & msg
msg &= " for argument: " & argument
error msg
# could I deduplicate these somehow? template? func prettyRepr(n: NimNode): string =
let r = n.repr
let maxWidth = r.splitlines().mapIt(it.len).max()
const padding = ""
result.add padding & "\n"
result.add indent(r, 1,padding)
result.add "\n" & padding & "\n"
result.add ""
result.add "".repeat(maxWidth + 2)
func parseCliSetting(s: string): CliSetting = func err(c: CliCfg, node: NimNode, msg: string = "") =
try: parseEnum[CliSetting](s) var fullMsg: string
except: error "unknown cli setting: " & s fullMsg.add node.prettyRepr() & "\n"
fullMsg.add "parsing error"
if msg != "":
fullMsg.add ": " & msg
c.err fullMsg
func expectLen(c: CliCfg, node: NimNode, length: Natural) =
if node.len != length:
c.err node, fmt"expected node to be length {length} not {node.len}"
func expectKind(c: CliCfg, node: NimNode, kinds: varargs[NimNodeKind]) =
if node.kind notin kinds:
c.err node, fmt"expected node kind to be one of: $1 but got $2" % [$kinds, $node.kind]
func unexpectedKind(c: CliCfg, node: NimNode) =
c.err node, fmt"unexpected node kind: $1" & $node.kind
template parseCliSetting(s: string) =
try:
cfg.settings.incl parseEnum[CliSetting](s)
except:
cfg.err "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:
for n in node[1..^1]: for n in node[1..^1]:
cfg.settings.incl parseCliSetting(n.strVal) parseCliSetting n.strVal
of nnkCall: of nnkCall:
expectKind node[1], nnkStmtList cfg.expectKind node[1], nnkStmtList
for n in node[1]: for n in node[1]:
cfg.settings.incl parseCliSetting(n.strVal) parseCliSetting(n.strval)
else: assert false else: cfg.unexpectedKind node
func parseCliFlagSetting(s: string): CliFlagSetting = template parseCliFlagSetting(s: string)=
try: parseEnum[CliFlagSetting](s) try:
except: error "unknown cli flag setting: " & s f.settings.incl parseEnum[CliFlagSetting](s)
except:
c.err "unknown cli flag setting: " & s
func parseCliFlagSettings(c: CliCfg, f: var CliFlag, node: NimNode) =
func parseCliFlagSettings(f: var CliFlag, node: NimNode) =
case node.kind case node.kind
of nnkCommand: of nnkCommand:
for n in node[1..^1]: for n in node[1..^1]:
f.settings.incl parseCliFlagSetting(n.strVal) parseCliFlagSetting(n.strVal)
of nnkCall: of nnkCall:
expectKind node[1], nnkStmtList c.expectKind node[1], nnkStmtList
for n in node[1]: for n in node[1]:
f.settings.incl parseCliFlagSetting(n.strVal) parseCliFlagSetting(n.strVal)
else: assert false else: c.unexpectedKind node
func getFlagParamNode(node: NimNode): NimNode =
func getFlagParamNode(c: CliCfg, node: NimNode): NimNode =
case node.kind case node.kind
of nnkStrLit: of nnkStrLit:
result = node result = node
@ -339,73 +366,81 @@ func getFlagParamNode(node: NimNode): NimNode =
result = node[1] result = node[1]
of nnkPrefix: # NOTE: should i double check prefix value? of nnkPrefix: # NOTE: should i double check prefix value?
result = node[1] result = node[1]
else: bad(node, "flag param") else: c.unexpectedKind node
# TODO: also accept the form `flag: "help"` # TODO: also accept the form `flag: "help"`
func parseFlagParams(f: var CliFlag, node: NimNode) = func parseFlagParams(c: CliCfg, f: var CliFlag, node: NimNode) =
expectKind node, nnkStmtList c.expectKind node, nnkStmtList
for n in node: for n in node:
case n.kind case n.kind
of nnkCall, nnkCommand, nnkPrefix: of nnkCall, nnkCommand, nnkPrefix:
case n[0].strVal let id = n[0].strVal
case id
of "help","?": of "help","?":
f.help = getFlagParamNode(n[1]) f.help = c.getFlagParamNode(n[1])
of "short", "-": of "short", "-":
let val = getFlagParamNode(n).strVal let val = c.getFlagParamNode(n).strVal
if val.len > 1: if val.len > 1:
error "short flag must be a char" c.err "short flag must be a char"
f.short = val[0].char f.short = val[0].char
of "*", "default": of "*", "default":
f.defaultVal = getFlagParamNode(n) f.defaultVal = c.getFlagParamNode(n)
of "i", "ident": of "i", "ident":
f.ident = getFlagParamNode(n).strVal.ident f.ident = c.getFlagParamNode(n).strVal.ident
of "T": of "T":
f.typeNode = n[1] f.typeNode = n[1]
of "node": of "node":
f.node = n[1] f.node = n[1]
of "settings", "S": of "settings", "S":
parseCliFlagSettings(f, n) parseCliFlagSettings c, f, n
else: else:
error "unexpected setting: " & n[0].strVal c.err "unexpected setting: " & id
else: else:
bad(n, "flag params") c.unexpectedKind n
func newFlag(f: var CliFlag, n: NimNode) = func newFlag(cfg: var CliCfg , n: NimNode): CliFlag =
f.name = cfg.expectKind n[0], nnkIdent, nnkStrLit, nnkAccQuoted
case n[0].kind
of nnkIdent, nnkStrLit: n[0].strVal
of nnkAccQuoted: collect(for c in n[0]: c.strVal).join("")
else: error "unexpected node kind for option"
f.help = newLit("") # by default no string case n[0].kind:
of nnkIdent, nnkStrLit:
result.name = n[0].strVal
of nnkAccQuoted:
result.name = collect(for c in n[0]: c.strVal).join("")
else: cfg.unexpectedKind n[0]
result.help = newLit("") # by default no string
# assume a single character is a short flag # assume a single character is a short flag
if f.name.len == 1: if result.name.len == 1:
f.short = f.name[0].char result.short = result.name[0].char
else: else:
f.long = f.name result.long = result.name
func parseCliFlag(n: NimNode): CliFlag =
if n.kind notin [nnkCommand, nnkCall]:
bad(n, "flags")
newFlag(result, n) func parseCliFlag(c: var CliCfg, n: NimNode, group: string) =
c.expectKind n, [nnkCommand, nnkCall]
var f = c.newFlag n
# option "some help desc" # option "some help desc"
if n.kind == nnkCommand: if n.kind == nnkCommand:
result.help = n[1] f.help = n[1]
# option: # option:
# T string # T string
# help "some help description" # help "some help description"
else: else:
parseFlagParams(result, n[1]) parseFlagParams c, f, n[1]
if result.ident == nil: f.ident = f.ident or f.name.ident
result.ident = result.name.ident
f.group = group
c.flagDefs.add f
func inferShortFlags(cfg: var CliCfg) = func inferShortFlags(cfg: var CliCfg) =
## supplement existing short flags based on initial characters of long flags ## supplement existing short flags based on initial characters of long flags
let taken = cfg.flags.mapIt(it.short).toHashSet() - toHashSet(['\x00']) let taken = cfg.flags.mapIt(it.short).toHashSet() - toHashSet(['\x00'])
var candidates = cfg.flags.mapIt(it.long[0]).toHashSet() - taken var candidates = cfg.flags.mapIt(it.long[0]).toHashSet() - taken
for f in cfg.flags.mitems: for f in cfg.flags.mitems:
if f.short != '\x00' or NoShort in f.settings: continue if f.short != '\x00' or NoShort in f.settings: continue
@ -415,74 +450,82 @@ func inferShortFlags(cfg: var CliCfg) =
candidates.excl c candidates.excl c
func postParse(cfg: var CliCfg) = func postParse(c: var CliCfg) =
if cfg.name == "": if c.name == "": # this should be unreachable
error "missing required option: name" c.err "missing required option: name"
if cfg.args.len != 0 and cfg.subcommands.len != 0: if c.args.len != 0 and c.subcommands.len != 0:
error "args and subcommands are mutually exclusive" c.err "args and subcommands are mutually exclusive"
let defaultTypeNode = cfg.defaultFlagType or ident"bool" let defaultTypeNode = c.defaultFlagType or ident"bool"
for f in cfg.flagDefs.mitems: for f in c.flagDefs.mitems:
if f.typeNode == nil: if f.typeNode == nil:
f.typeNode = defaultTypeNode f.typeNode = defaultTypeNode
if f.group in ["", "global"]: if f.group in ["", "global"]:
cfg.flags.add f c.flags.add f
if cfg.args.len > 0: if c.args.len > 0:
let count = cfg.args.filterIt(it.typeNode.kind == nnkBracketExpr).len let count = c.args.filterIt(it.typeNode.kind == nnkBracketExpr).len
if count > 1: if count > 1:
cfg.err "more than one positional argument is variadic" c.err "more than one positional argument is variadic"
if InferShort in cfg.settings: if InferShort in c.settings:
inferShortFlags cfg inferShortFlags c
func parseCliFlags(cfg: var CliCfg, node: NimNode) = func parseCliFlags(cfg: var CliCfg, node: NimNode) =
var group: string var group: string
expectKind node, nnkStmtList cfg.expectKind node, nnkStmtList
for n in node: for n in node:
var flag: CliFlag
case n.kind case n.kind
# flags:
# input "some input"
# count:
# T int
# ? "a number"
of nnkCall, nnkCommand: of nnkCall, nnkCommand:
flag = parseCliFlag(n) cfg.parseCliFlag n, group
flag.group = group
cfg.flagDefs.add flag # start a new flag group
# flags:
# [category]
# input "input flag"
of nnkBracket: of nnkBracket:
group = n[0].strVal group = n[0].strVal
continue continue
# inherit parent flag or group
# flags:
# ^config
# ^[category]
of nnkPrefix: of nnkPrefix:
if if
n[0].kind != nnkIdent or n[0].kind != nnkIdent or
n[0].strVal != "^" or n[0].strVal != "^" or
n.len != 2 or n.len != 2 or
n[1].kind notin [nnkBracket, nnkIdent, nnkStrLit]: n[1].kind notin [nnkBracket, nnkIdent, nnkStrLit]:
error "unexpected node in flags: " & $n.kind cfg.err n, "unable to determine inherited flag/group"
case n[1].kind case n[1].kind
of nnkBracket: of nnkBracket:
cfg.inherit.groups.add n[1][0].strVal cfg.inherit.groups.add n[1][0].strVal
# cfg.inheritFlags.add n[1][0].strVal
of nnkIdent, nnkStrLit: of nnkIdent, nnkStrLit:
cfg.inherit.flags.add n[1].strval cfg.inherit.flags.add n[1].strval
else: bad(n, "flag") else: cfg.unexpectedKind n
else: bad(n, "flag") else: cfg.unexpectedKind n
func parseIdentLikeList(node: NimNode): seq[string] = func parseIdentLikeList(c: CliCfg, node: NimNode): seq[string] =
template check =
if n.kind notin [nnkStrLit,nnkIdent]:
error "expected StrLit or Ident, got:" & $n.kind
case node.kind case node.kind
of nnkCommand: of nnkCommand:
for n in node[1..^1]: for n in node[1..^1]:
check c.expectKind n, nnkStrLit, nnkIdent
result.add n.strVal result.add n.strVal
of nnkCall: of nnkCall:
expectKind node[1], nnkStmtList expectKind node[1], nnkStmtList
for n in node[1]: for n in node[1]:
check c.expectKind n, nnkStrLit, nnkIdent
result.add n.strVal result.add n.strVal
else: assert false else: c.unexpectedKind node
func parseCliBody(body: NimNode, name: string = "", root: bool= false): CliCfg func parseCliBody(body: NimNode, name: string = "", root: bool= false): CliCfg
@ -493,12 +536,12 @@ func isSubMarker(node: NimNode): bool =
return false return false
result = true result = true
func sliceStmts(node: NimNode): seq[ func sliceStmts(c: CliCfg, node: NimNode): seq[
tuple[name: string, slice: Slice[int]] tuple[name: string, slice: Slice[int]]
] = ] =
if not isSubMarker(node[0]): if not isSubMarker(node[0]):
error "expected a subcommand delimiting line" c.err "expected a subcommand delimiting line"
var var
name: string = node[0][0].strVal name: string = node[0][0].strVal
@ -514,6 +557,7 @@ func sliceStmts(node: NimNode): seq[
start = i + 1 start = i + 1
# TODO: swap error stmts
func inheritFrom(child: var CliCfg, parent: CliCfg) = func inheritFrom(child: var CliCfg, parent: CliCfg) =
## inherit settings from parent command ## inherit settings from parent command
var var
@ -537,7 +581,7 @@ func inheritFrom(child: var CliCfg, parent: CliCfg) =
for f in flags: for f in flags:
if f notin pflags: if f notin pflags:
error "expected parent command to have flag: " & f child.err "expected parent command to have flag: " & f
else: else:
child.flags.add pflags[f] child.flags.add pflags[f]
# so subcommands can continue the inheritance # so subcommands can continue the inheritance
@ -545,15 +589,16 @@ func inheritFrom(child: var CliCfg, parent: CliCfg) =
for g in groups: for g in groups:
if g notin pgroups: if g notin pgroups:
error "expected parent command to have flag group " & g child.err "expected parent command to have flag group " & g
else: else:
child.flags.add pgroups[g] child.flags.add pgroups[g]
# so subcommands can continue the inheritance # so subcommands can continue the inheritance
child.flagDefs.add pgroups[g] child.flagDefs.add pgroups[g]
func parseCliSubcommands(cfg: var CliCfg, node: NimNode) = func parseCliSubcommands(cfg: var CliCfg, node: NimNode) =
expectKind node[1], nnkStmtList cfg.expectKind node[1], nnkStmtList
for (name, s) in sliceStmts(node[1]):
for (name, s) in cfg.sliceStmts(node[1]):
var subCfg = parseCliBody( var subCfg = parseCliBody(
nnkStmtList.newTree(node[1][s]), cfg.name & " " & name nnkStmtList.newTree(node[1][s]), cfg.name & " " & name
) )
@ -562,34 +607,34 @@ func parseCliSubcommands(cfg: var CliCfg, node: NimNode) =
cfg.stopWords.add subCfg.alias.toSeq() cfg.stopWords.add subCfg.alias.toSeq()
cfg.subcommands.add subCfg cfg.subcommands.add subCfg
func parseHiddenFlags(cfg: var CliCfg, node: NimNode) = func parseHiddenFlags(c: var CliCfg, node: NimNode) =
template check = template check =
if n.kind notin [nnkStrLit, nnkIdent]: if n.kind notin [nnkStrLit, nnkIdent]:
error "expected string literal or ident" c.err "expected string literal or ident"
case node.kind case node.kind
of nnkCommand: of nnkCommand:
for n in node[1..^1]: for n in node[1..^1]:
check check
cfg.hidden.add n.strVal c.hidden.add n.strVal
of nnkCall: of nnkCall:
expectKind node[1], nnkStmtList expectKind node[1], nnkStmtList
for n in node[1]: for n in node[1]:
check check
cfg.hidden.add n.strVal c.hidden.add n.strVal
else: assert false else: assert false
func addBuiltinFlags(cfg: var CliCfg) = func addBuiltinFlags(c: var CliCfg) =
# duplicated with below :/ # duplicated with below :/
let shorts = cfg.flags.mapIt(it.short).toHashSet() let shorts = c.flags.mapIt(it.short).toHashSet()
let let
name = cfg.name.replace(" ", "") name = c.name.replace(" ", "")
printHelpName = ident("print" & name & "Help") printHelpName = ident("print" & name & "Help")
if NoHelpFlag notin cfg.settings: if NoHelpFlag notin c.settings:
let helpNode = quote do: let helpNode = quote do:
`printHelpName`(); quit 0 `printHelpName`(); quit 0
cfg.builtinFlags.add BuiltinFlag( c.builtinFlags.add BuiltinFlag(
name: "help", name: "help",
long: "help", long: "help",
help: newLit("show this help"), help: newLit("show this help"),
@ -597,12 +642,12 @@ func addBuiltinFlags(cfg: var CliCfg) =
node: helpNode node: helpNode
) )
if cfg.version != nil: if c.version != nil:
let version = cfg.version let version = c.version
let versionNode = quote do: let versionNode = quote do:
echo `version`; quit 0 echo `version`; quit 0
cfg.builtinFlags.add BuiltinFlag( c.builtinFlags.add BuiltinFlag(
name:"version", name:"version",
long: "version", long: "version",
help: newLit("print version"), help: newLit("print version"),
@ -611,7 +656,7 @@ func addBuiltinFlags(cfg: var CliCfg) =
) )
func pasrseCliAlias(cfg: var CliCfg, node: NimNode) = func parseCliAlias(cfg: var CliCfg, node: NimNode) =
# node[0] is "alias" # node[0] is "alias"
for n in node[1..^1]: for n in node[1..^1]:
case n.kind case n.kind
@ -620,7 +665,7 @@ func pasrseCliAlias(cfg: var CliCfg, node: NimNode) =
of nnkAccQuoted: of nnkAccQuoted:
let s = n.mapIt(it.strVal).join("") let s = n.mapIt(it.strVal).join("")
cfg.alias.incl s cfg.alias.incl s
else: bad(n, "alias") else: cfg.unexpectedKind n
func postPropagateCheck(c: CliCfg) = func postPropagateCheck(c: CliCfg) =
## verify the cli is valid ## verify the cli is valid
@ -675,9 +720,9 @@ func parseCliHelp(c: var CliCfg, node: NimNode) =
## ##
## ``` ## ```
## ... NimNode ## ... NimNode
## ` ## ```
expectLen node, 2 c.expectLen(node, 2)
var help: CliHelp = c.help var help: CliHelp = c.help
case node.kind: case node.kind:
# help NimNode or ... NimNode # help NimNode or ... NimNode
@ -688,36 +733,49 @@ func parseCliHelp(c: var CliCfg, node: NimNode) =
# usage: NimNode # usage: NimNode
of nnkCall: of nnkCall:
if node[1].kind != nnkStmtList: if node[1].kind != nnkStmtList:
error "expected list of arguments for help" c.err node, "expected list of arguments for help"
for n in node[1]: for n in node[1]:
expectLen n, 2 <<< n
c.expectLen n, 2
let id = n[0].strVal let id = n[0].strVal
var val: NimNode var val: NimNode
case n.kind case n.kind
of nnkCommand: of nnkCommand:
val =n[1] val = n[1]
of nnKCall: of nnKCall:
val = n[1][0] val = n[1][0]
else: bad(n, id) else: c.err n, "unexpected node for help: " & id & ", expected ident"
case id: case id:
of "usage": help.usage = val of "usage": help.usage = val
of "description": help.description = val of "description": help.description = val
of "header": help.header = val of "header": help.header = val
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: c.err n, "unknown help option: " & id
else: bad(node, "help") else: c.err node, "unexpected node for help, expected nnkCommand/nnkCall"
c.help = help
func badNode(c: CliCfg, node: NimNode, msg: string) = c.help = help
c.err "unexpected node kind: " & $node.kind & "\n" & msg
func isSeq(arg: CliArg): bool = func isSeq(arg: CliArg): bool =
# NOTE: does this need to be more rigorous? # NOTE: does this need to be more rigorous?
arg.typeNode.kind == nnkBracketExpr arg.typeNode.kind == nnkBracketExpr
func parseCliArg(c: CliCfg, node: NimNode): CliArg = func parseCliArg(c: CliCfg, node: NimNode): CliArg =
expectLen node, 2 ## parse a single positional arg
## supported formats:
##
## ```
## input seq[string]
## ```
## ```
## other:
## T string
## ident notOther
## ```
##
c.expectLen node, 2
result.name = node[0].strVal result.name = node[0].strVal
case node[1].kind case node[1].kind
of nnkStmtList: of nnkStmtList:
@ -732,38 +790,51 @@ func parseCliArg(c: CliCfg, node: NimNode): CliArg =
if n[1].len == 2: if n[1].len == 2:
result.typeNode = n[1][1] result.typeNode = n[1][1]
val = n[1][0] val = n[1][0]
else: bad(n, id) # else: bad(n, id)
else: c.err n, "unexpected node for positional '$1'" & id
case id: case id:
of "T": result.typeNode = val of "T": result.typeNode = val
of "ident": result.ident = val of "ident": result.ident = val
else: c.err("unknown cli param: " & id & "provided for arg: " & result.name) else: c.err n, "unknown positional parameter for $1: $2" % [result.name, id]
of nnkIdent, nnkBracketExpr: of nnkIdent, nnkBracketExpr:
result.typeNode = node[1] result.typeNode = node[1]
else: else:
c.badNode(node[1], "parsing cli arg: " & result.name) c.err node, "as positional"
if result.ident == nil: if result.ident == nil:
result.ident = ident(result.name) result.ident = ident(result.name)
func parseCliArgs(c: var CliCfg, node: NimNode) = func parseCliArgs(c: var CliCfg, node: NimNode) =
if node.kind != nnkStmtList: if node.kind != nnkStmtList:
bad(node, "expected node kind nnkStmtList") c.err node, "expected node kind nnkStmtList"
for n in node: for n in node:
c.args.add parseCliArg(c, n) c.args.add parseCliArg(c, n)
func isNameNode(n: NimNode): bool =
if n.kind notin [nnkCall, nnkCommand] or n.len != 2: return false
if n[0].kind != nnKident: return false
if n[0].strVal != "name": return false
true
func parseCliBody(body: NimNode, name = "", root = false): CliCfg = func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
# Try to grab name first for better error messages
if name == "":
let n = body.findChild(it.isNameNode())
if n == nil: error "name is a required property"
result.name = $n[1]
else:
result.name = name result.name = name
result.root = root result.root = root
for node in body: for node in body:
if node.kind notin [nnkCall, nnkCommand, nnkPrefix]: if node.kind notin [nnkCall, nnkCommand, nnkPrefix]:
error "unexpected node kind: " & $node.kind result.err node, "unexpected node kind: " & $node.kind
let name = node[0].strVal let name = node[0].strVal
case name: case name:
of "name": of "name": discard # should have been handled above
expectKind node[1], nnkStrLit
result.name = node[1].strVal
of "alias": of "alias":
if root: error "alias not supported for root command" if root: result.err "alias not supported for root command"
pasrseCliAlias(result, node) parseCliAlias(result, node)
of "version", "V": of "version", "V":
result.version = node[1] result.version = node[1]
of "usage", "?": of "usage", "?":
@ -775,7 +846,7 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
of "settings", "S": of "settings", "S":
parseCliSettings(result, node) parseCliSettings(result, node)
of "stopWords": of "stopWords":
result.stopWords = parseIdentLikeList(node) result.stopWords = result.parseIdentLikeList(node)
of "subcommands": of "subcommands":
parseCliSubcommands(result, node) parseCliSubcommands(result, node)
of "hidden": of "hidden":
@ -783,7 +854,7 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
of "run": of "run":
result.run = node[1] result.run = node[1]
of "required": of "required":
result.required = parseIdentLikeList(node) result.required = result.parseIdentLikeList(node)
of "preSub": of "preSub":
result.preSub = node[1] result.preSub = node[1]
of "postSub": of "postSub":
@ -793,7 +864,7 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
of "positionals": of "positionals":
parseCliArgs result, node[1] parseCliArgs result, node[1]
else: else:
error "unknown hwylCli setting: " & name result.err "unknown hwylCli setting: " & name
postParse result postParse result

View file

@ -1,10 +0,0 @@
import hwylterm, hwylterm/hwylcli
proc hwylCliError*(msg: string) =
stderr.write "override the default error\n"
quit $(bb("error ", "red") & bb(msg))
hwylCli:
name "base"
run:
echo "a base cli"

View file

@ -4,6 +4,9 @@ import hwylterm, hwylterm/hwylcli
hwylCli: hwylCli:
name "posLast" name "posLast"
positionals: positionals:
args seq[string] first:
T string
ident notFirst
rest seq[string]
run: run:
echo fmt"{args=}" echo fmt"{notFirst=} {rest=}"

View file

@ -11,6 +11,7 @@ hwylCli:
outputs seq[string] outputs seq[string]
run: run:
echo fmt"{input=} {outputs=}" echo fmt"{input=} {outputs=}"
[b] [b]
... "a subcommand with flags" ... "a subcommand with flags"
flags: flags:
@ -22,6 +23,7 @@ hwylCli:
T seq[string] T seq[string]
run: run:
echo fmt"{input=} {outputs=}" echo fmt"{input=} {outputs=}"
[ccccc] [ccccc]
... "a subcommand with an alias" ... "a subcommand with an alias"
alias c alias c

View file

@ -4,8 +4,10 @@ import ./lib
if commandLineParams().len == 0: if commandLineParams().len == 0:
preCompileTestModules() preCompileTestModules()
suite "hwylcli":
okWithArgs("posBasic", "a b c d e", """args=@["a", "b", "c", "d", "e"]""") suite "positional":
okWithArgs("posBasic", "a b c d e", """notFirst=a rest=@["b", "c", "d", "e"]""")
okWithArgs("posFirst", "a b c d e", """first=@["a", "b", "c"], second=d, third=e""") okWithArgs("posFirst", "a b c d e", """first=@["a", "b", "c"], second=d, third=e""")
failWithArgs( failWithArgs(
"posFirst", "a b", "error missing positional args, got: 2, expected at least: 3" "posFirst", "a b", "error missing positional args, got: 2, expected at least: 3"
@ -17,12 +19,16 @@ suite "hwylcli":
"""error unexepected positional args, got: 4, expected: 3""", """error unexepected positional args, got: 4, expected: 3""",
) )
suite "flags":
okWithArgs("enumFlag", "--color red", "color=red") okWithArgs("enumFlag", "--color red", "color=red")
failWithArgs( failWithArgs(
"enumFlag", "--color black", "enumFlag", "--color black",
"error failed to parse value for color as enum: black expected one of: red,blue,green", "error failed to parse value for color as enum: black expected one of: red,blue,green",
) )
suite "subcommands":
okWithArgs("subcommands", "a b c", """input=b outputs=@["c"]""") okWithArgs("subcommands", "a b c", """input=b outputs=@["c"]""")
failWithArgs("subcommands", "b b c", """error got unexpected positionals args: b c""") failWithArgs("subcommands", "b b c", """error got unexpected positionals args: b c""")
okWithArgs( okWithArgs(
@ -33,6 +39,8 @@ suite "hwylcli":
okWithArgs("subcommands", "ccccc", """no flags :)""") okWithArgs("subcommands", "ccccc", """no flags :)""")
okWithArgs("subcommands", "c", """no flags :)""") okWithArgs("subcommands", "c", """no flags :)""")
suite "help":
okWithArgs( okWithArgs(
"posFirst", "--help", "posFirst", "--help",
""" """
@ -85,6 +93,8 @@ flags:
show this help show this help
""", """,
) )
suite "hooks":
okWithArgs( okWithArgs(
"subHooks", "a", "subHooks", "a",
""" """
@ -111,6 +121,8 @@ postSub from root!
inside sub c inside sub c
""", """,
) )
suite "settings":
okWithArgs( okWithArgs(
"inferShort", "-i input -o output","""input=input, output=output, count=0, nancy=false, ignore=false""" "inferShort", "-i input -o output","""input=input, output=output, count=0, nancy=false, ignore=false"""
) )