mirror of
https://github.com/daylinmorgan/hwylterm.git
synced 2025-02-23 01:35:50 -06:00
improve hwylcli error handling
This commit is contained in:
parent
2811c05a21
commit
a7593561ee
5 changed files with 227 additions and 149 deletions
|
@ -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 =
|
||||||
result.name = name
|
# 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.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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
|
@ -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=}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"""
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue