mirror of
https://github.com/daylinmorgan/hwylterm.git
synced 2024-12-21 18:50:44 -06:00
Compare commits
9 commits
f1cc95f86e
...
c5f70cec4e
Author | SHA1 | Date | |
---|---|---|---|
c5f70cec4e | |||
748f7e1bd6 | |||
938131c6cd | |||
ab00305c92 | |||
5a236cd6a5 | |||
88a0bc2ffe | |||
b6a97899fc | |||
fdec798b30 | |||
c80c0d3db9 |
3 changed files with 158 additions and 78 deletions
|
@ -138,6 +138,10 @@ proc bb*(cli: HwylCliHelp): BbString =
|
|||
type
|
||||
Count* = object ## Count type for an incrementing flag
|
||||
val*: int
|
||||
KV*[X,Y] = object ## basic key value type
|
||||
key*: X
|
||||
val*: Y
|
||||
KVString* = KV[string, string]
|
||||
|
||||
type
|
||||
CliSetting* = enum
|
||||
|
@ -159,6 +163,7 @@ type
|
|||
ident*: NimNode
|
||||
default*: NimNode
|
||||
typeNode*: NimNode
|
||||
node*: NimNode
|
||||
short*: char
|
||||
long*: string
|
||||
help*: NimNode
|
||||
|
@ -173,21 +178,25 @@ type
|
|||
CliHelp = object
|
||||
header*, footer*, description*, usage*, styles*: NimNode
|
||||
|
||||
CliArg = object
|
||||
|
||||
CliCfg = object
|
||||
name*: string
|
||||
alias*: HashSet[string]
|
||||
alias*: HashSet[string] # only supported in subcommands
|
||||
version*: NimNode
|
||||
stopWords*: seq[string]
|
||||
help: CliHelp
|
||||
hidden*: seq[string]
|
||||
subcommands: seq[CliCfg]
|
||||
defaultFlagType: NimNode
|
||||
required*: seq[string]
|
||||
settings*: set[CliSetting]
|
||||
subName*: string # used for help generator
|
||||
subcommands: seq[CliCfg]
|
||||
preSub*, postSub*, pre*, post*, run*: NimNode
|
||||
subName*: string # used for help the generator
|
||||
version*: NimNode
|
||||
hidden*: seq[string]
|
||||
args*: seq[CliArg]
|
||||
flags*: seq[CliFlag]
|
||||
builtinFlags*: seq[BuiltinFlag]
|
||||
flagDefs*: seq[CliFlag]
|
||||
required*: seq[string]
|
||||
inherit*: Inherit
|
||||
root*: bool
|
||||
|
||||
|
@ -256,12 +265,14 @@ func parseFlagParams(f: var CliFlag, node: NimNode) =
|
|||
f.ident = getFlagParamNode(n).strVal.ident
|
||||
of "T":
|
||||
f.typeNode = n[1]
|
||||
of "node":
|
||||
f.node = n[1]
|
||||
else:
|
||||
error "unexpected setting: " & n[0].strVal
|
||||
else:
|
||||
bad(n, "flag params")
|
||||
|
||||
func startFlag(f: var CliFlag, n: NimNode) =
|
||||
func newFlag(f: var CliFlag, n: NimNode) =
|
||||
f.name =
|
||||
case n[0].kind
|
||||
of nnkIdent, nnkStrLit: n[0].strVal
|
||||
|
@ -280,11 +291,12 @@ func parseCliFlag(n: NimNode): CliFlag =
|
|||
if n.kind notin [nnkCommand, nnkCall]:
|
||||
bad(n, "flags")
|
||||
|
||||
startFlag(result, n)
|
||||
newFlag(result, n)
|
||||
# option "some help desc"
|
||||
if n.kind == nnkCommand:
|
||||
result.help = n[1]
|
||||
# option:
|
||||
# T string
|
||||
# help "some help description"
|
||||
else:
|
||||
parseFlagParams(result, n[1])
|
||||
|
@ -294,7 +306,14 @@ func parseCliFlag(n: NimNode): CliFlag =
|
|||
if result.typeNode == nil:
|
||||
result.typeNode = ident"bool"
|
||||
|
||||
# TODO: change how this works?
|
||||
func postParse(cfg: var CliCfg) =
|
||||
let defaultTypeNode = cfg.defaultFlagType or ident"bool"
|
||||
for f in cfg.flagDefs.mitems:
|
||||
if f.typeNode == nil:
|
||||
f.typeNode = defaultTypeNode
|
||||
if f.group in ["", "global"]:
|
||||
cfg.flags.add f
|
||||
|
||||
func parseCliFlags(cfg: var CliCfg, node: NimNode) =
|
||||
var group: string
|
||||
expectKind node, nnkStmtList
|
||||
|
@ -323,10 +342,8 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) =
|
|||
of nnkIdent, nnkStrLit:
|
||||
cfg.inherit.flags.add n[1].strval
|
||||
else: bad(n, "flag")
|
||||
|
||||
else: bad(n, "flag")
|
||||
|
||||
cfg.flags = cfg.flagDefs.filterIt(it.group in ["", "global"])
|
||||
|
||||
func parseCliSetting(s: string): CliSetting =
|
||||
try: parseEnum[CliSetting](s)
|
||||
|
@ -504,8 +521,6 @@ func propagate(c: var CliCfg) =
|
|||
child.inheritFrom(c)
|
||||
propagate(child)
|
||||
|
||||
|
||||
|
||||
func parseCliHelp(c: var CliCfg, node: NimNode) =
|
||||
## some possible DSL inputs:
|
||||
##
|
||||
|
@ -600,13 +615,16 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
|
|||
result.preSub = node[1]
|
||||
of "postSub":
|
||||
result.postSub = node[1]
|
||||
of "defaultFlagType":
|
||||
result.defaultFlagType = node[1]
|
||||
else:
|
||||
error "unknown hwylCli setting: " & name
|
||||
|
||||
if result.name == "":
|
||||
error "missing required option: name"
|
||||
|
||||
# TODO: validate "required" flags exist here
|
||||
postParse result
|
||||
# TODO: validate "required" flags exist here?
|
||||
result.addBuiltinFlags()
|
||||
|
||||
if root:
|
||||
|
@ -661,36 +679,40 @@ func generateCliHelpProc(cfg: CliCfg, printHelpName: NimNode): NimNode =
|
|||
|
||||
result = quote do:
|
||||
proc `printHelpName`() =
|
||||
echo bb(render(newHwylCliHelp(
|
||||
header = `header`,
|
||||
footer = `footer`,
|
||||
usage = `usage`,
|
||||
description = `description`,
|
||||
subcmds = `subcmds`,
|
||||
flags = `helpFlags`,
|
||||
styles = `styles`,
|
||||
)))
|
||||
echo bb(
|
||||
render(
|
||||
newHwylCliHelp(
|
||||
header = `header`,
|
||||
footer = `footer`,
|
||||
usage = `usage`,
|
||||
description = `description`,
|
||||
subcmds = `subcmds`,
|
||||
flags = `helpFlags`,
|
||||
styles = `styles`,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
proc preParseCheck(key: string, val: string) =
|
||||
if val == "":
|
||||
proc checkVal(p: OptParser) =
|
||||
if p.val == "":
|
||||
hwylCliError(
|
||||
"expected value for flag: [b]" & key
|
||||
"expected value for flag: [b]" & p.key
|
||||
)
|
||||
|
||||
proc parse*(p: OptParser, key: string, val: string, target: var bool) =
|
||||
proc parse*(p: OptParser, 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, target: var string) =
|
||||
checkVal p
|
||||
target = p.val
|
||||
|
||||
proc parse*(p: OptParser, key: string, val: string, target: var int) =
|
||||
preParseCheck(key, val)
|
||||
proc parse*(p: OptParser, target: var int) =
|
||||
checkVal p
|
||||
try:
|
||||
target = parseInt(val)
|
||||
target = parseInt(p.val)
|
||||
except:
|
||||
hwylCliError(
|
||||
"failed to parse value for [b]" & key & "[/] as integer: [b]" & val
|
||||
"failed to parse value for [b]" & p.key & "[/] as integer: [b]" & p.val
|
||||
)
|
||||
|
||||
macro enumNames(a: typed): untyped =
|
||||
|
@ -700,40 +722,75 @@ macro enumNames(a: typed): untyped =
|
|||
assert ai.kind == nnkSym
|
||||
result.add newLit ai.strVal
|
||||
|
||||
proc parse*[E: enum](p: OptParser, key: string, val: string, target: var E) =
|
||||
preParseCheck(key, val)
|
||||
proc parse*[E: enum](p: OptParser, target: var E) =
|
||||
checkVal p
|
||||
try:
|
||||
target = parseEnum[E](val)
|
||||
target = parseEnum[E](p.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]" & p.key & "[/] as enum: [b]" & p.val & "[/] expected one of: " & choices
|
||||
)
|
||||
|
||||
proc parse*(p: OptParser, key: string, val: string, target: var float) =
|
||||
preParseCheck(key, val)
|
||||
proc parse*(p: OptParser, target: var float) =
|
||||
checkVal p
|
||||
try:
|
||||
target = parseFloat(val)
|
||||
target = parseFloat(p.val)
|
||||
except:
|
||||
hwylCliError(
|
||||
"failed to parse value for [b]" & key & "[/] as float: [b]" & val
|
||||
"failed to parse value for [b]" & p.key & "[/] as float: [b]" & p.val
|
||||
)
|
||||
|
||||
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
|
||||
proc parse*[T](p: var OptParser, target: var seq[T]) =
|
||||
checkVal p
|
||||
case p.sep
|
||||
of ",=":
|
||||
let baseVal = p.val
|
||||
for v in baseVal.split(","):
|
||||
p.val = v.strip()
|
||||
if p.val == "": continue
|
||||
var parsed: T
|
||||
parse(p, parsed)
|
||||
target.add parsed
|
||||
of "=", "":
|
||||
var parsed: T
|
||||
parse(p, parsed)
|
||||
target.add parsed
|
||||
else: assert false
|
||||
|
||||
proc parse*(p: OptParser, key: string, val: string, target: var Count) =
|
||||
|
||||
proc parse*(p: OptParser, target: var Count) =
|
||||
# if value set to that otherwise increment
|
||||
if val != "":
|
||||
if p.val != "":
|
||||
var num: int
|
||||
parse(p, key, val, num)
|
||||
parse(p, num)
|
||||
target.val = num
|
||||
else:
|
||||
inc target.val
|
||||
|
||||
proc extractKey(p: var OptParser): string =
|
||||
var i: int
|
||||
for c in p.val:
|
||||
if c notin {'=',':'}: inc i
|
||||
else: break
|
||||
if i == p.val.len:
|
||||
hwylCliError(
|
||||
"failed to parse key val flag" &
|
||||
"\nkey: " & p.key.bb("bold") &
|
||||
"\nval: " & p.val.bb("bold") &
|
||||
"\ndid you include a separator (= or :)?"
|
||||
)
|
||||
else:
|
||||
result = p.val[0..<i]
|
||||
p.key = p.key & ":" & result
|
||||
p.val = p.val[(i+1) .. ^1]
|
||||
|
||||
proc parse*[T](p: var OptParser, target: var KV[string, T]) =
|
||||
checkVal p
|
||||
let key = extractKey(p)
|
||||
target.key = key
|
||||
parse(p, target.val)
|
||||
|
||||
func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): NimNode =
|
||||
var caseStmt = nnkCaseStmt.newTree()
|
||||
if NoNormalize notin cfg.settings:
|
||||
|
@ -753,7 +810,7 @@ func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): N
|
|||
# add flags
|
||||
for f in cfg.flags:
|
||||
var branch = nnkOfBranch.newTree()
|
||||
if f.long != "":
|
||||
if f.long != "":
|
||||
branch.add newLit(
|
||||
if NoNormalize notin cfg.settings: optionNormalize(f.long)
|
||||
else: f.long
|
||||
|
@ -761,9 +818,11 @@ func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): N
|
|||
if f.short != '\x00': branch.add(newLit($f.short))
|
||||
let varName = f.ident
|
||||
let name = newLit(f.name)
|
||||
branch.add quote do:
|
||||
flagSet.incl `name`
|
||||
parse(p, key, val, `varName`)
|
||||
branch.add nnkStmtList.newTree(
|
||||
nnkCall.newTree(ident("incl"),ident("flagSet"),name),
|
||||
if f.node == nil: nnkCall.newTree(ident"parse", ident"p", varName)
|
||||
else: f.node
|
||||
)
|
||||
|
||||
caseStmt.add branch
|
||||
|
||||
|
@ -797,8 +856,8 @@ func setFlagVars(cfg: CliCfg): NimNode =
|
|||
else: cfg.flags.filterIt(it.group != "global")
|
||||
|
||||
result.add flags.mapIt(
|
||||
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
|
||||
)
|
||||
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
|
||||
)
|
||||
|
||||
func literalFlags(f: CliFlag): NimNode =
|
||||
var flags: seq[string]
|
||||
|
@ -863,6 +922,23 @@ func genSubcommandHandler(cfg: CliCfg): NimNode =
|
|||
|
||||
result.add subCommandCase
|
||||
|
||||
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
|
||||
|
||||
func argOfBranch(cfg: CliCfg): NimNode =
|
||||
result = nnkOfBranch.newTree(ident"cmdArgument")
|
||||
# if cfg.args.len == 0 and cfg.subcommands.len == 0:
|
||||
# result.add quote do:
|
||||
# hwylCliError("unexpected positional argument: [b]" & p.key)
|
||||
# else:
|
||||
result.add quote do:
|
||||
inc nArgs
|
||||
parseArgs(p, result)
|
||||
|
||||
|
||||
func hwylCliImpl(cfg: CliCfg): NimNode =
|
||||
|
@ -875,9 +951,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
|||
optParser = ident("p")
|
||||
cmdLine = ident"cmdLine"
|
||||
flagSet = ident"flagSet"
|
||||
kind = ident"kind"
|
||||
key = ident"key"
|
||||
val = ident"val"
|
||||
nArgs = ident"nargs"
|
||||
(longNoVal, shortNoVal) = cfg.getNoVals()
|
||||
printHelpProc = generateCliHelpProc(cfg, printHelpName)
|
||||
flagVars = setFlagVars(cfg)
|
||||
|
@ -893,9 +967,9 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
|||
|
||||
stopWords = nnkPrefix.newTree(ident"@", stopWords)
|
||||
|
||||
# should this a CritBitTree?
|
||||
parserBody.add quote do:
|
||||
var `flagSet` {.used.}: HashSet[string]
|
||||
var `flagSet`: HashSet[string]
|
||||
var `nArgs`: int
|
||||
|
||||
parserBody.add(
|
||||
quote do:
|
||||
|
@ -907,24 +981,23 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
|||
opChars = {','}
|
||||
)
|
||||
)
|
||||
# TODO: first key needs to be normalized
|
||||
|
||||
# TODO: first key needs to be normalized?
|
||||
# TODO: don't use getopt? use p.next() instead?
|
||||
parserBody.add nnkForStmt.newTree(
|
||||
kind, key, val,
|
||||
nnkCall.newTree(nnkDotExpr.newTree(optParser,ident("getopt"))),
|
||||
ident"kind", ident"key", ident"val",
|
||||
# nnkCall.newTree(nnkDotExpr.newTree(optParser,ident("getopt"))),
|
||||
nnkCall.newTree(ident"getopt", optParser),
|
||||
nnkStmtList.newTree(
|
||||
# # for debugging..
|
||||
# quote do:
|
||||
# echo `kind`,"|",`key`,"|",`val`
|
||||
# ,
|
||||
nnkCaseStmt.newTree(
|
||||
kind,
|
||||
ident"kind",
|
||||
nnkOfBranch.newTree(ident("cmdError"), quote do: hwylCliError(p.message)),
|
||||
nnkOfBranch.newTree(ident("cmdEnd"), quote do: assert false),
|
||||
# TODO: add nArgs to change how cmdArgument is handled ...
|
||||
nnkOfBranch.newTree(ident("cmdArgument"),
|
||||
quote do:
|
||||
result.add `key`
|
||||
),
|
||||
argOfBranch(cfg),
|
||||
nnkOfBranch.newTree(
|
||||
ident("cmdShortOption"), ident("cmdLongOption"),
|
||||
shortLongCaseStmt(cfg, printHelpName, version)
|
||||
|
@ -971,6 +1044,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
|
|||
result.add quote do:
|
||||
`runProcName`(`args`[1..^1])
|
||||
|
||||
|
||||
macro hwylCli*(body: untyped) =
|
||||
## generate a CLI styled by `hwylterm` and parsed by `parseopt3`
|
||||
var cfg = parseCliBody(body, root = true)
|
||||
|
|
|
@ -6,7 +6,6 @@ type
|
|||
Color = enum
|
||||
red, blue, green
|
||||
|
||||
|
||||
hwylCli:
|
||||
name "example"
|
||||
V "0.1.0"
|
||||
|
@ -72,14 +71,17 @@ hwylCli:
|
|||
"""
|
||||
flags:
|
||||
^something
|
||||
auto:
|
||||
- a
|
||||
? "some help"
|
||||
thing:
|
||||
T KV[string, Color]
|
||||
? "some key value string"
|
||||
b:
|
||||
T seq[float]
|
||||
? "multiple floats"
|
||||
h "this will override the builtin 'h' for help"
|
||||
def:
|
||||
? "a flag with a string default"
|
||||
* "the value"
|
||||
run:
|
||||
echo "hello from `example b` command"
|
||||
echo fmt"{auto=}, {b=}"
|
||||
echo fmt"{thing=}, {b=}, {h=}, {def=}"
|
||||
|
||||
|
|
10
todo.md
10
todo.md
|
@ -8,10 +8,10 @@
|
|||
|
||||
|
||||
- [ ] addJoinStyle(); works like join except wraps each argument in a style
|
||||
- [ ] add span aware split/splitlines
|
||||
- [ ] consider reducing illwill surface to only relevant IO (input) features
|
||||
- [ ] revamp spinner api (new threads?)
|
||||
- [x] add Bbstring ~~indexing operations~~ strutils, that are span aware
|
||||
- [ ] add a `commands` option for `newHwylCli` in `hwylterm/cli`
|
||||
- [ ] console object with customizable options to apply formatting
|
||||
|
||||
### cli generator
|
||||
|
@ -22,8 +22,10 @@
|
|||
- [x] add support to either (lengthen commands) or provide an alias for a subcommand
|
||||
- [x] add command aliases to hwylcli help with switch
|
||||
- [x] don't recreate "global"" variables in var section
|
||||
- [ ] make proper test suite for cli generator
|
||||
|
||||
- [ ] add flag overlap check before case statement generation (after parsing?)
|
||||
- [ ] add key-value flag support -> `--setting:a:off`
|
||||
- [x] add defaultFlagType CliCfg setting
|
||||
- [x] add node to flagDef to override builtin `parse(p, varName)`
|
||||
|
||||
## features
|
||||
|
||||
|
@ -35,8 +37,10 @@
|
|||
- [ ] support for rgb colors
|
||||
- [ ] modify 256 colors w/parser changes to be `"[color(9)]red"` instead of `[9]red`
|
||||
- [x] improve color detection [ref](https://github.com/Textualize/rich/blob/4101991898ee7a09fe1706daca24af5e1e054862/rich/console.py#L791)
|
||||
|
||||
## testing
|
||||
|
||||
- [ ] make proper test suite for cli generator
|
||||
- [ ] investigate [cap10](https://github.com/crashappsec/cap10) as a means of scripting the testing
|
||||
|
||||
<!-- generated with <3 by daylinmorgan/todo -->
|
||||
|
|
Loading…
Reference in a new issue