Compare commits

...

7 commits

3 changed files with 156 additions and 60 deletions

View file

@ -14,13 +14,16 @@ type
HwylFlagHelp* = tuple
short, long, description: string
HwylSubCmdHelp* = tuple
name, desc: string
name, aliases, desc: string
HwylCliStyleSetting = enum
Aliases
HwylCliStyles* = object
header* = "bold cyan"
flagShort* = "yellow"
flagLong* = "magenta"
flagDesc* = ""
cmd* = "bold"
settings*: set[HwylCliStyleSetting] = {Aliases}
HwylCliHelp* = object
usage*: string
desc*: string
@ -41,8 +44,12 @@ func newHwylCliHelp*(
styles = HwylCliStyles()
): HwylCliHelp =
result.desc = dedent(desc).strip()
result.subcmds =
subcmds.mapIt((it.name, it.desc.firstLine))
if Aliases in styles.settings:
result.subcmds =
subcmds.mapIt((it.name & " " & it.aliases, it.aliases, it.desc.firstLine))
else:
result.subcmds =
subcmds.mapIt((it.name, it.aliases, it.desc.firstLine))
result.usage = dedent(usage).strip()
result.flags = @flags
result.styles = styles
@ -147,6 +154,7 @@ type
long*: string
help*: NimNode
group*: string
inherited*: bool
Inherit = object
settings: set[CliSetting]
@ -154,6 +162,8 @@ type
groups: seq[string]
CliCfg = object
name*: string
alias*: HashSet[string]
stopWords*: seq[string]
styles: NimNode
hidden*: seq[string]
@ -161,7 +171,6 @@ type
settings*: set[CliSetting]
preSub*, postSub*, pre*, post*, run*: NimNode
desc*: NimNode
name*: string
subName*: string # used for help the generator
version*, usage*: NimNode
flags*: seq[CliFlag]
@ -171,12 +180,33 @@ type
inherit*: Inherit
root*: bool
# some debug procs I use to wrap my ahead aroung the magic of *macro*
func `<<<`(n: NimNode) {.used.} =
## for debugging macros
debugEcho treeRepr n
func `<<<`(s: string) {.used.} =
template `<<<`(s: string) {.used.} =
let pos = instantiationInfo()
debugEcho "$1:$2" % [pos.filename, $pos.line]
debugEcho s
debugEcho "^^^^^^^^^^^^^^^^^^^^^"
# some debug procs I use to wrap my ahead aroung the magic of *macro*
template `<<<`(n: NimNode) {.used.} =
## for debugging macros
<<< treeRepr n
func `<<<`(f: CliFlag) {.used.}=
var s: string
let fields = [
("name", f.name),
("long", f.long),
("short", $f.short),
("typeNode", f.typeNode.lispRepr),
("group", f.group)
]
s.add "CliFlag(\n"
for (k,v) in fields:
s.add "$1 = $2\n" % [k,v]
s.add ")"
<<< s
func bad(n: NimNode, argument: string = "") =
var msg = "unexpected node kind: " & $n.kind
@ -258,7 +288,6 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) =
var group: string
expectKind node, nnkStmtList
for n in node:
<<< n
var flag: CliFlag
case n.kind
of nnkCall, nnkCommand:
@ -372,28 +401,30 @@ func inheritFrom(child: var CliCfg, parent: CliCfg) =
for f in flags:
if f notin pflags:
error "expected parent command to define flag: " & f
error "expected parent command to have flag: " & f
else:
child.flags.add pflags[f]
# so subcommands can continue the inheritance
child.flagDefs.add pflags[f]
for g in groups:
if g notin pgroups:
error "expected parent command to define flag group " & g
error "expected parent command to have flag group " & g
else:
child.flags &= pgroups[g]
child.flags.add pgroups[g]
# so subcommands can continue the inheritance
child.flagDefs.add pgroups[g]
func parseCliSubcommands(cfg: var CliCfg, node: NimNode) =
expectKind node[1], nnkStmtList
for (name, s) in sliceStmts(node[1]):
cfg.stopWords.add name
var subCfg = parseCliBody(
nnkStmtList.newTree(node[1][s]), cfg.name & " " & name
)
subCfg.subName = name
subCfg.inheritFrom(cfg)
cfg.subcommands.add subCfg
cfg.stopWords.add name
cfg.stopWords.add subCfg.alias.toSeq()
cfg.subcommands.add subCfg
func parseHiddenFlags(cfg: var CliCfg, node: NimNode) =
template check =
@ -443,9 +474,30 @@ func addBuiltinFlags(cfg: var CliCfg) =
node: versionNode
)
func pasrseCliAlias(cfg: var CliCfg, node: NimNode) =
# node[0] is "alias"
for n in node[1..^1]:
case n.kind
of nnkIdent, nnkStrLit:
cfg.alias.incl n.strVal
of nnkAccQuoted:
let s = n.mapIt(it.strVal).join("")
cfg.alias.incl s
else: bad(n, "alias")
func propagate(c: var CliCfg) =
for child in c.subcommands.mitems:
child.pre = c.preSub
child.post = c.postSub
child.inheritFrom(c)
propagate(child)
func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
result.name = name
result.root = root
# TDOO: change call to n or node?
for call in body:
if call.kind notin [nnkCall, nnkCommand, nnkPrefix]:
error "unexpected node kind: " & $call.kind
@ -454,6 +506,9 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
of "name":
expectKind call[1], nnkStrLit
result.name = call[1].strVal
of "alias":
if root: error "alias not supported for root command"
pasrseCliAlias(result, call)
of "version", "V":
result.version = call[1]
of "usage", "?":
@ -482,16 +537,19 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
result.postSub = call[1]
else:
error "unknown hwylCli setting: " & name
for sub in result.subcommands.mitems:
sub.pre = result.preSub
sub.post = result.postSub
#
# for sub in result.subcommands.mitems:
# # sub.inheritFrom(result)
# sub.pre = result.preSub
# sub.post = result.postSub
if result.name == "":
error "missing required option: name"
# TODO: validate "required" flags exist here
result.addBuiltinFlags()
if root:
propagate(result)
func flagToTuple(f: CliFlag | BuiltinFlag): NimNode =
let
@ -515,9 +573,10 @@ func subCmdsArray(cfg: CliCfg): NimNode =
result = newTree(nnkBracket)
for s in cfg.subcommands:
let cmd = newLit(s.subName)
let aliases = newLit(s.alias.mapIt("($1)" % [it]).join(" "))
let desc = s.desc or newLit("")
result.add quote do:
(`cmd`, `desc`)
(`cmd`, `aliases`, `desc`)
proc hwylCliError*(msg: string | BbString) =
quit $(bb("error ", "red") & bb(msg))
@ -547,13 +606,21 @@ func generateCliHelperProc(cfg: CliCfg, printHelpName: NimNode): NimNode =
styles = `styles`,
)))
proc preParseCheck(key: string, val: string) =
if val == "":
hwylCliError(
"expected value for flag: [b]" & key
)
proc parse*(p: OptParser, key: string, val: string, target: var bool) =
target = true
proc parse*(p: OptParser, key: string, val: string, target: var string) =
preParseCheck(key, val)
target = val
proc parse*(p: OptParser, key: string, val: string, target: var int) =
preParseCheck(key, val)
try:
target = parseInt(val)
except:
@ -569,15 +636,17 @@ macro enumNames(a: typed): untyped =
result.add newLit ai.strVal
proc parse*[E: enum](p: OptParser, key: string, val: string, target: var E) =
preParseCheck(key, val)
try:
target = parseEnum[E](val)
except:
let choices = enumNames(E).join(",")
hwylCliError(
"failed to parse value for [b]" & key & "[/] as enum: [b]" & val & "[/], expected one of: " & choices
"failed to parse value for [b]" & key & "[/] as enum: [b]" & val & "[/] expected one of: " & choices
)
proc parse*(p: OptParser, key: string, val: string, target: var float) =
preParseCheck(key, val)
try:
target = parseFloat(val)
except:
@ -586,6 +655,7 @@ proc parse*(p: OptParser, key: string, val: string, target: var float) =
)
proc parse*[T](p: OptParser, key: string, val: string, target: var seq[T]) =
preParseCheck(key, val)
var parsed: T
parse(p, key, val, parsed)
target.add parsed
@ -655,11 +725,15 @@ func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] =
result = (nnkPrefix.newTree(ident"@",long), short)
func setFlagVars(cfg: CliCfg): NimNode =
result = nnkVarSection.newTree().add(
cfg.flags.mapIt(
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
)
)
## generate all variables not covered in global module
result = nnkVarSection.newTree()
let flags =
if cfg.root: cfg.flags
else: cfg.flags.filterIt(it.group != "global")
result.add flags.mapIt(
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
)
func literalFlags(f: CliFlag): NimNode =
var flags: seq[string]
@ -694,6 +768,38 @@ func addPostParseCheck(cfg: CliCfg, body: NimNode) =
if `name` notin `flagSet`:
`target` = `default`
func hwylCliImpl(cfg: CliCfg): NimNode
func genSubcommandHandler(cfg: CliCfg): NimNode =
let args = ident"args"
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]))
else:
subCommandCase.add(quote do: `args`[0])
for sub in cfg.subcommands:
var branch = nnkOfBranch.newTree()
branch.add newLit(optionNormalize(sub.subName))
for a in sub.alias:
branch.add newLit(optionNormalize(a))
branch.add hwylCliImpl(sub)
subcommandCase.add branch
subcommandCase.add nnkElse.newTree(
quote do:
hwylCliError("unknown subcommand: [b]" & `args`[0])
)
result.add subCommandCase
func hwylCliImpl(cfg: CliCfg): NimNode =
let
version = cfg.version or newLit("")
@ -779,29 +885,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
runBody.add cfg.post
if cfg.subcommands.len > 0:
var handleSubCommands = nnkStmtList.newTree()
handleSubCommands.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]))
else:
subCommandCase.add(quote do: `args`[0])
for sub in cfg.subcommands:
subCommandCase.add nnkOfBranch.newTree(
newLit(optionNormalize(sub.subName)),
hwylCliImpl(sub)
)
subcommandCase.add nnkElse.newTree(
quote do:
hwylCliError("unknown subcommand: [b]" & `args`[0])
)
runBody.add handleSubCommands.add subCommandCase
runBody.add genSubcommandHandler(cfg)
result.add quote do:
# block:
@ -814,7 +898,6 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
let `args` {.used.} = `parserProcName`(`cmdLine`)
`runBody`
# if cfg.root and (GenerateOnly notin cfg.settings):
if cfg.root:
if GenerateOnly notin cfg.settings:
result.add quote do:

View file

@ -13,6 +13,9 @@ hwylCli:
... "a description of hwylterm"
flags:
[global]
color:
T Color
? "a color (red, green, blue)"
yes:
T bool
? "set flag to yes"
@ -25,10 +28,12 @@ hwylCli:
* @["config.yml"]
preSub:
echo "this is run after subcommand parsing but before its run block"
echo fmt"{yes=}, {color=}"
run:
echo "this is always run prior to subcommand parsing"
echo fmt"{yes=}, {color=}"
subcommands:
[onelonger]
[one]
... """
the first subcommand
@ -36,15 +41,21 @@ hwylCli:
it also inherits the `[[shared]` flag group
"""
alias o
flags:
color:
T Color
? "a color (red, green, blue)"
verbose:
T Count
? "a count flag"
- v
^[shared]
subcommands:
[subsub]
... "another level down subcommand"
flags:
^config
run:
echo fmt"{color=}"
run:
echo "hello from `example one` command!"
echo args
@ -52,7 +63,7 @@ hwylCli:
echo fmt"{verbose=}"
echo fmt"{config=}"
["two-longer"]
[two]
... """
some second subcommand

View file

@ -18,8 +18,10 @@
- [ ] add support for types(metavars)/defaults/required in help output
- [ ] add nargs to CliCfg
- [ ] add support for inheriting a single flag from parent (even from a "group")
- [ ] add support to either (lengthen commands) or provide an alias for a subcommand
- [x] add support for inheriting a single flag from parent (even from a "group")
- [x] add support to either (lengthen commands) or provide an alias for a subcommand
- [x] add command aliases to hwylcli help with switch
- [ ] don't recreate "global"" variables in var section
## features