Compare commits

...

9 commits

3 changed files with 158 additions and 78 deletions

View file

@ -138,6 +138,10 @@ proc bb*(cli: HwylCliHelp): BbString =
type type
Count* = object ## Count type for an incrementing flag Count* = object ## Count type for an incrementing flag
val*: int val*: int
KV*[X,Y] = object ## basic key value type
key*: X
val*: Y
KVString* = KV[string, string]
type type
CliSetting* = enum CliSetting* = enum
@ -159,6 +163,7 @@ type
ident*: NimNode ident*: NimNode
default*: NimNode default*: NimNode
typeNode*: NimNode typeNode*: NimNode
node*: NimNode
short*: char short*: char
long*: string long*: string
help*: NimNode help*: NimNode
@ -173,21 +178,25 @@ type
CliHelp = object CliHelp = object
header*, footer*, description*, usage*, styles*: NimNode header*, footer*, description*, usage*, styles*: NimNode
CliArg = object
CliCfg = object CliCfg = object
name*: string name*: string
alias*: HashSet[string] alias*: HashSet[string] # only supported in subcommands
version*: NimNode
stopWords*: seq[string] stopWords*: seq[string]
help: CliHelp help: CliHelp
hidden*: seq[string] defaultFlagType: NimNode
subcommands: seq[CliCfg] required*: seq[string]
settings*: set[CliSetting] settings*: set[CliSetting]
subName*: string # used for help generator
subcommands: seq[CliCfg]
preSub*, postSub*, pre*, post*, run*: NimNode preSub*, postSub*, pre*, post*, run*: NimNode
subName*: string # used for help the generator hidden*: seq[string]
version*: NimNode args*: seq[CliArg]
flags*: seq[CliFlag] flags*: seq[CliFlag]
builtinFlags*: seq[BuiltinFlag] builtinFlags*: seq[BuiltinFlag]
flagDefs*: seq[CliFlag] flagDefs*: seq[CliFlag]
required*: seq[string]
inherit*: Inherit inherit*: Inherit
root*: bool root*: bool
@ -256,12 +265,14 @@ func parseFlagParams(f: var CliFlag, node: NimNode) =
f.ident = getFlagParamNode(n).strVal.ident f.ident = getFlagParamNode(n).strVal.ident
of "T": of "T":
f.typeNode = n[1] f.typeNode = n[1]
of "node":
f.node = n[1]
else: else:
error "unexpected setting: " & n[0].strVal error "unexpected setting: " & n[0].strVal
else: else:
bad(n, "flag params") bad(n, "flag params")
func startFlag(f: var CliFlag, n: NimNode) = func newFlag(f: var CliFlag, n: NimNode) =
f.name = f.name =
case n[0].kind case n[0].kind
of nnkIdent, nnkStrLit: n[0].strVal of nnkIdent, nnkStrLit: n[0].strVal
@ -280,11 +291,12 @@ func parseCliFlag(n: NimNode): CliFlag =
if n.kind notin [nnkCommand, nnkCall]: if n.kind notin [nnkCommand, nnkCall]:
bad(n, "flags") bad(n, "flags")
startFlag(result, n) newFlag(result, n)
# option "some help desc" # option "some help desc"
if n.kind == nnkCommand: if n.kind == nnkCommand:
result.help = n[1] result.help = n[1]
# option: # option:
# T string
# help "some help description" # help "some help description"
else: else:
parseFlagParams(result, n[1]) parseFlagParams(result, n[1])
@ -294,7 +306,14 @@ func parseCliFlag(n: NimNode): CliFlag =
if result.typeNode == nil: if result.typeNode == nil:
result.typeNode = ident"bool" 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) = func parseCliFlags(cfg: var CliCfg, node: NimNode) =
var group: string var group: string
expectKind node, nnkStmtList expectKind node, nnkStmtList
@ -323,10 +342,8 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) =
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: bad(n, "flag")
else: bad(n, "flag") else: bad(n, "flag")
cfg.flags = cfg.flagDefs.filterIt(it.group in ["", "global"])
func parseCliSetting(s: string): CliSetting = func parseCliSetting(s: string): CliSetting =
try: parseEnum[CliSetting](s) try: parseEnum[CliSetting](s)
@ -504,8 +521,6 @@ func propagate(c: var CliCfg) =
child.inheritFrom(c) child.inheritFrom(c)
propagate(child) propagate(child)
func parseCliHelp(c: var CliCfg, node: NimNode) = func parseCliHelp(c: var CliCfg, node: NimNode) =
## some possible DSL inputs: ## some possible DSL inputs:
## ##
@ -600,13 +615,16 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
result.preSub = node[1] result.preSub = node[1]
of "postSub": of "postSub":
result.postSub = node[1] result.postSub = node[1]
of "defaultFlagType":
result.defaultFlagType = node[1]
else: else:
error "unknown hwylCli setting: " & name error "unknown hwylCli setting: " & name
if result.name == "": if result.name == "":
error "missing required option: name" error "missing required option: name"
# TODO: validate "required" flags exist here postParse result
# TODO: validate "required" flags exist here?
result.addBuiltinFlags() result.addBuiltinFlags()
if root: if root:
@ -661,36 +679,40 @@ func generateCliHelpProc(cfg: CliCfg, printHelpName: NimNode): NimNode =
result = quote do: result = quote do:
proc `printHelpName`() = proc `printHelpName`() =
echo bb(render(newHwylCliHelp( echo bb(
header = `header`, render(
footer = `footer`, newHwylCliHelp(
usage = `usage`, header = `header`,
description = `description`, footer = `footer`,
subcmds = `subcmds`, usage = `usage`,
flags = `helpFlags`, description = `description`,
styles = `styles`, subcmds = `subcmds`,
))) flags = `helpFlags`,
styles = `styles`,
)
)
)
proc preParseCheck(key: string, val: string) = proc checkVal(p: OptParser) =
if val == "": if p.val == "":
hwylCliError( 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 target = true
proc parse*(p: OptParser, key: string, val: string, target: var string) = proc parse*(p: OptParser, target: var string) =
preParseCheck(key, val) checkVal p
target = val target = p.val
proc parse*(p: OptParser, key: string, val: string, target: var int) = proc parse*(p: OptParser, target: var int) =
preParseCheck(key, val) checkVal p
try: try:
target = parseInt(val) target = parseInt(p.val)
except: except:
hwylCliError( 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 = macro enumNames(a: typed): untyped =
@ -700,40 +722,75 @@ macro enumNames(a: typed): untyped =
assert ai.kind == nnkSym assert ai.kind == nnkSym
result.add newLit ai.strVal result.add newLit ai.strVal
proc parse*[E: enum](p: OptParser, key: string, val: string, target: var E) = proc parse*[E: enum](p: OptParser, target: var E) =
preParseCheck(key, val) checkVal p
try: try:
target = parseEnum[E](val) target = parseEnum[E](p.val)
except: except:
let choices = enumNames(E).join(",") let choices = enumNames(E).join(",")
hwylCliError( 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) = proc parse*(p: OptParser, target: var float) =
preParseCheck(key, val) checkVal p
try: try:
target = parseFloat(val) target = parseFloat(p.val)
except: except:
hwylCliError( 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]) = proc parse*[T](p: var OptParser, target: var seq[T]) =
preParseCheck(key, val) checkVal p
var parsed: T case p.sep
parse(p, key, val, parsed) of ",=":
target.add parsed 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 value set to that otherwise increment
if val != "": if p.val != "":
var num: int var num: int
parse(p, key, val, num) parse(p, num)
target.val = num target.val = num
else: else:
inc target.val 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 = func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): NimNode =
var caseStmt = nnkCaseStmt.newTree() var caseStmt = nnkCaseStmt.newTree()
if NoNormalize notin cfg.settings: if NoNormalize notin cfg.settings:
@ -761,9 +818,11 @@ func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): N
if f.short != '\x00': branch.add(newLit($f.short)) if f.short != '\x00': branch.add(newLit($f.short))
let varName = f.ident let varName = f.ident
let name = newLit(f.name) let name = newLit(f.name)
branch.add quote do: branch.add nnkStmtList.newTree(
flagSet.incl `name` nnkCall.newTree(ident("incl"),ident("flagSet"),name),
parse(p, key, val, `varName`) if f.node == nil: nnkCall.newTree(ident"parse", ident"p", varName)
else: f.node
)
caseStmt.add branch caseStmt.add branch
@ -797,8 +856,8 @@ func setFlagVars(cfg: CliCfg): NimNode =
else: cfg.flags.filterIt(it.group != "global") else: cfg.flags.filterIt(it.group != "global")
result.add flags.mapIt( result.add flags.mapIt(
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode()) 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]
@ -863,6 +922,23 @@ func genSubcommandHandler(cfg: CliCfg): NimNode =
result.add subCommandCase 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 = func hwylCliImpl(cfg: CliCfg): NimNode =
@ -875,9 +951,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
optParser = ident("p") optParser = ident("p")
cmdLine = ident"cmdLine" cmdLine = ident"cmdLine"
flagSet = ident"flagSet" flagSet = ident"flagSet"
kind = ident"kind" nArgs = ident"nargs"
key = ident"key"
val = ident"val"
(longNoVal, shortNoVal) = cfg.getNoVals() (longNoVal, shortNoVal) = cfg.getNoVals()
printHelpProc = generateCliHelpProc(cfg, printHelpName) printHelpProc = generateCliHelpProc(cfg, printHelpName)
flagVars = setFlagVars(cfg) flagVars = setFlagVars(cfg)
@ -893,9 +967,9 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
stopWords = nnkPrefix.newTree(ident"@", stopWords) stopWords = nnkPrefix.newTree(ident"@", stopWords)
# should this a CritBitTree?
parserBody.add quote do: parserBody.add quote do:
var `flagSet` {.used.}: HashSet[string] var `flagSet`: HashSet[string]
var `nArgs`: int
parserBody.add( parserBody.add(
quote do: quote do:
@ -907,24 +981,23 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
opChars = {','} 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( parserBody.add nnkForStmt.newTree(
kind, key, val, ident"kind", ident"key", ident"val",
nnkCall.newTree(nnkDotExpr.newTree(optParser,ident("getopt"))), # nnkCall.newTree(nnkDotExpr.newTree(optParser,ident("getopt"))),
nnkCall.newTree(ident"getopt", optParser),
nnkStmtList.newTree( nnkStmtList.newTree(
# # for debugging.. # # for debugging..
# quote do: # quote do:
# echo `kind`,"|",`key`,"|",`val` # echo `kind`,"|",`key`,"|",`val`
# , # ,
nnkCaseStmt.newTree( nnkCaseStmt.newTree(
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: assert false),
# TODO: add nArgs to change how cmdArgument is handled ... argOfBranch(cfg),
nnkOfBranch.newTree(ident("cmdArgument"),
quote do:
result.add `key`
),
nnkOfBranch.newTree( nnkOfBranch.newTree(
ident("cmdShortOption"), ident("cmdLongOption"), ident("cmdShortOption"), ident("cmdLongOption"),
shortLongCaseStmt(cfg, printHelpName, version) shortLongCaseStmt(cfg, printHelpName, version)
@ -971,6 +1044,7 @@ 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)

View file

@ -6,7 +6,6 @@ type
Color = enum Color = enum
red, blue, green red, blue, green
hwylCli: hwylCli:
name "example" name "example"
V "0.1.0" V "0.1.0"
@ -72,14 +71,17 @@ hwylCli:
""" """
flags: flags:
^something ^something
auto: thing:
- a T KV[string, Color]
? "some help" ? "some key value string"
b: b:
T seq[float] T seq[float]
? "multiple floats" ? "multiple floats"
h "this will override the builtin 'h' for help" h "this will override the builtin 'h' for help"
def:
? "a flag with a string default"
* "the value"
run: run:
echo "hello from `example b` command" echo "hello from `example b` command"
echo fmt"{auto=}, {b=}" echo fmt"{thing=}, {b=}, {h=}, {def=}"

10
todo.md
View file

@ -8,10 +8,10 @@
- [ ] addJoinStyle(); works like join except wraps each argument in a style - [ ] 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 - [ ] consider reducing illwill surface to only relevant IO (input) features
- [ ] revamp spinner api (new threads?) - [ ] revamp spinner api (new threads?)
- [x] add Bbstring ~~indexing operations~~ strutils, that are span aware - [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 - [ ] console object with customizable options to apply formatting
### cli generator ### cli generator
@ -22,8 +22,10 @@
- [x] add support to either (lengthen commands) or provide an alias for a subcommand - [x] add support to either (lengthen commands) or provide an alias for a subcommand
- [x] add command aliases to hwylcli help with switch - [x] add command aliases to hwylcli help with switch
- [x] don't recreate "global"" variables in var section - [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 ## features
@ -35,8 +37,10 @@
- [ ] support for rgb colors - [ ] support for rgb colors
- [ ] modify 256 colors w/parser changes to be `"[color(9)]red"` instead of `[9]red` - [ ] 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) - [x] improve color detection [ref](https://github.com/Textualize/rich/blob/4101991898ee7a09fe1706daca24af5e1e054862/rich/console.py#L791)
## testing ## testing
- [ ] make proper test suite for cli generator
- [ ] investigate [cap10](https://github.com/crashappsec/cap10) as a means of scripting the testing - [ ] investigate [cap10](https://github.com/crashappsec/cap10) as a means of scripting the testing
<!-- generated with <3 by daylinmorgan/todo --> <!-- generated with <3 by daylinmorgan/todo -->