add better normalization

This commit is contained in:
Daylin Morgan 2024-11-10 11:36:59 -06:00
parent 77cfae4d26
commit a57d4ee687
Signed by: daylin
GPG key ID: 950D13E9719334AD
3 changed files with 97 additions and 70 deletions

View file

@ -368,7 +368,7 @@ when isMainModule:
hwylCli: hwylCli:
name "bbansi" name "bbansi"
settings NoArgsShowHelp settings ShowHelp
usage "[bold]bbansi[/] [[[green]args...[/]] [[[faint]-h|-V[/]]" usage "[bold]bbansi[/] [[[green]args...[/]] [[[faint]-h|-V[/]]"
description """ description """
bbansi "[[yellow] yellow text!" bbansi "[[yellow] yellow text!"

View file

@ -8,7 +8,7 @@ import std/[
sugar sugar
] ]
import ./[bbansi, parseopt3] import ./[bbansi, parseopt3]
export parseopt3, sets export parseopt3, sets, bbansi
type type
HwylFlagHelp* = tuple HwylFlagHelp* = tuple
@ -16,22 +16,23 @@ type
HwylSubCmdHelp* = tuple HwylSubCmdHelp* = tuple
name, desc: string name, desc: string
HwylCliStyles* = object HwylCliStyles* = object
hdr = "bold cyan" header* = "bold cyan"
shortFlag = "yellow" flagShort* = "yellow"
longFlag = "magenta" flagLong* = "magenta"
descFlag = "" flagDesc* = ""
cmd = "bold" cmd* = "bold"
HwylCliHelp* = object HwylCliHelp* = object
usage*: string usage*: string
desc*: string desc*: string
subcmds: seq[HwylSubCmdHelp] subcmds*: seq[HwylSubCmdHelp]
flags*: seq[HwylFlagHelp] flags*: seq[HwylFlagHelp]
styles*: HwylCliStyles styles*: HwylCliStyles
subcmdLen, subcmdDescLen, shortArgLen, longArgLen, descArgLen: int subcmdLen*, subcmdDescLen*, shortArgLen*, longArgLen*, descArgLen*: int
# NOTE: do i need both strips? # NOTE: do i need both strips?
func firstLine(s: string): string = func firstLine(s: string): string =
s.strip().dedent().strip().splitlines()[0] s.strip().dedent().strip().splitlines()[0]
func newHwylCliHelp*( func newHwylCliHelp*(
usage = "", usage = "",
desc = "", desc = "",
@ -40,7 +41,7 @@ func newHwylCliHelp*(
styles = HwylCliStyles() styles = HwylCliStyles()
): HwylCliHelp = ): HwylCliHelp =
result.desc = dedent(desc).strip() result.desc = dedent(desc).strip()
result.subcmds = result.subcmds =
subcmds.mapIt((it.name, it.desc.firstLine)) subcmds.mapIt((it.name, it.desc.firstLine))
result.usage = dedent(usage).strip() result.usage = dedent(usage).strip()
result.flags = @flags result.flags = @flags
@ -55,10 +56,10 @@ func newHwylCliHelp*(
result.subcmdLen = max(result.subcmdLen, s.name.len) result.subcmdLen = max(result.subcmdLen, s.name.len)
result.subcmdDescLen = max(result.subcmdDescLen, s.desc.len) result.subcmdDescLen = max(result.subcmdDescLen, s.desc.len)
func flagHelp(cli: HwylCliHelp, f: HwylFlagHelp): string = func render*(cli: HwylCliHelp, f: HwylFlagHelp): string =
result.add " " result.add " "
if f.short != "": if f.short != "":
result.add "[" & cli.styles.shortFlag & "]" result.add "[" & cli.styles.flagShort & "]"
result.add "-" & f.short.alignLeft(cli.shortArgLen) result.add "-" & f.short.alignLeft(cli.shortArgLen)
result.add "[/]" result.add "[/]"
else: else:
@ -66,7 +67,7 @@ func flagHelp(cli: HwylCliHelp, f: HwylFlagHelp): string =
result.add " " result.add " "
if f.long != "": if f.long != "":
result.add "[" & cli.styles.longFlag & "]" result.add "[" & cli.styles.flagLong & "]"
result.add "--" & f.long.alignLeft(cli.longArgLen) result.add "--" & f.long.alignLeft(cli.longArgLen)
result.add "[/]" result.add "[/]"
else: else:
@ -75,12 +76,12 @@ func flagHelp(cli: HwylCliHelp, f: HwylFlagHelp): string =
result.add " " result.add " "
if f.description != "": if f.description != "":
result.add "[" & cli.styles.descFlag & "]" result.add "[" & cli.styles.flagDesc & "]"
result.add f.description result.add f.description
result.add "[/]" result.add "[/]"
result.add "\n" result.add "\n"
func subCmdLine(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string = func render*(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string =
result.add " " result.add " "
result.add "[" & cli.styles.cmd & "]" result.add "[" & cli.styles.cmd & "]"
result.add subcmd.name.alignLeft(cli.subcmdLen) result.add subcmd.name.alignLeft(cli.subcmdLen)
@ -89,9 +90,11 @@ func subCmdLine(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string =
result.add subcmd.desc.alignLeft(cli.subcmdDescLen) result.add subcmd.desc.alignLeft(cli.subcmdDescLen)
result.add "\n" result.add "\n"
proc bbImpl(cli: HwylCliHelp): string =
# TODO: split this into separate procs to make overriding more fluid
func render*(cli: HwylCliHelp): string =
if cli.usage != "": if cli.usage != "":
result.add "[" & cli.styles.hdr & "]" result.add "[" & cli.styles.header & "]"
result.add "usage[/]:\n" result.add "usage[/]:\n"
result.add indent(cli.usage, 2 ) result.add indent(cli.usage, 2 )
result.add "\n" result.add "\n"
@ -101,22 +104,19 @@ proc bbImpl(cli: HwylCliHelp): string =
result.add "\n" result.add "\n"
if cli.subcmds.len > 0: if cli.subcmds.len > 0:
result.add "\n" result.add "\n"
result.add "[" & cli.styles.hdr & "]" result.add "[" & cli.styles.header & "]"
result.add "subcommands[/]:\n" result.add "subcommands[/]:\n"
for s in cli.subcmds: for s in cli.subcmds:
result.add cli.subcmdLine(s) result.add cli.render(s)
if cli.flags.len > 0: if cli.flags.len > 0:
result.add "\n" result.add "\n"
result.add "[" & cli.styles.hdr & "]" result.add "[" & cli.styles.header & "]"
result.add "flags[/]:\n" result.add "flags[/]:\n"
for f in cli.flags: for f in cli.flags:
result.add flagHelp(cli,f) result.add render(cli,f)
proc bb*(cli: HwylCliHelp): BbString = proc bb*(cli: HwylCliHelp): BbString =
result = bb(bbImpl(cli)) result = bb(render(cli))
proc `$`*(cli: HwylCliHelp): string =
result = $bb(cli)
# ---------------------------------------- # ----------------------------------------
@ -125,7 +125,7 @@ type
val*: int val*: int
CliSetting = enum CliSetting = enum
NoHelpFlag, NoArgsShowHelp NoHelpFlag, ShowHelp, NoNormalize
BuiltinFlag = object BuiltinFlag = object
name*: string name*: string
short*: char short*: char
@ -156,6 +156,7 @@ type
flagGroups: Table[string, seq[CliFlag]] flagGroups: Table[string, seq[CliFlag]]
required*: seq[string] required*: seq[string]
inheritFlags*: seq[string] inheritFlags*: seq[string]
root*: bool
{.push hint[XDeclaredButNotUsed]:off .} {.push hint[XDeclaredButNotUsed]:off .}
# some debug procs I use to wrap my ahead aroung the magic of *macro* # some debug procs I use to wrap my ahead aroung the magic of *macro*
@ -263,15 +264,8 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) =
error "unexpected node in flags: " & $n.kind error "unexpected node in flags: " & $n.kind
expectKind n[1], nnkBracket expectKind n[1], nnkBracket
cfg.inheritFlags.add n[1][0].strVal cfg.inheritFlags.add n[1][0].strVal
# of nnkPrefix:
# if n[0].strVal != "---":
# bad(n[0], "flag group prefix")
# group = n[1].strVal
# continue
else: bad(n, "flag") else: bad(n, "flag")
debugEcho cfg.flagGroups.keys().toSeq()
func parseCliSetting(s: string): CliSetting = func parseCliSetting(s: string): CliSetting =
try: parseEnum[CliSetting](s) try: parseEnum[CliSetting](s)
except: error "unknown cli setting: " & s except: error "unknown cli setting: " & s
@ -303,7 +297,7 @@ func parseIdentLikeList(node: NimNode): seq[string] =
result.add n.strVal result.add n.strVal
else: assert false else: assert false
func parseCliBody(body: NimNode, name: string = ""): CliCfg func parseCliBody(body: NimNode, name: string = "", root: bool= false): CliCfg
func isSubMarker(node: NimNode): bool = func isSubMarker(node: NimNode): bool =
if node.kind == nnkPrefix: if node.kind == nnkPrefix:
@ -408,8 +402,9 @@ func addBuiltinFlags(cfg: var CliCfg) =
node: versionNode node: versionNode
) )
func parseCliBody(body: NimNode, name = ""): CliCfg = func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
result.name = name result.name = name
result.root = true
for call in body: for call in body:
if call.kind notin [nnkCall, nnkCommand, nnkPrefix]: if call.kind notin [nnkCall, nnkCommand, nnkPrefix]:
error "unexpected node kind: " & $call.kind error "unexpected node kind: " & $call.kind
@ -485,7 +480,7 @@ func subCmdsArray(cfg: CliCfg): NimNode =
result.add quote do: result.add quote do:
(`cmd`, `desc`) (`cmd`, `desc`)
proc hwylCliError(msg: string | BbString) = proc hwylCliError*(msg: string | BbString) =
quit $(bb("error ", "red") & bb(msg)) quit $(bb("error ", "red") & bb(msg))
func defaultUsage(cfg: CliCfg): NimNode = func defaultUsage(cfg: CliCfg): NimNode =
@ -505,13 +500,13 @@ func generateCliHelperProc(cfg: CliCfg, printHelpName: NimNode): NimNode =
result = quote do: result = quote do:
proc `printHelpName`() = proc `printHelpName`() =
echo newHwylCliHelp( echo bb(render(newHwylCliHelp(
desc = `desc`, desc = `desc`,
usage = `usage`, usage = `usage`,
subcmds = `subcmds`, subcmds = `subcmds`,
flags = `helpFlags`, flags = `helpFlags`,
styles = `styles`, styles = `styles`,
) )))
proc parse*(p: OptParser, key: string, val: string, target: var bool) = proc parse*(p: OptParser, key: string, val: string, target: var bool) =
target = true target = true
@ -557,10 +552,21 @@ proc parse*[T](p: OptParser, key: string, val: string, target: var seq[T]) =
target.add parsed target.add parsed
proc parse*(p: OptParser, key: string, val: string, target: var Count) = proc parse*(p: OptParser, key: string, val: string, target: var Count) =
inc target.val # if value set to that otherwise increment
if val != "":
var num: int
parse(p, key, val, num)
target.val = num
else:
inc target.val
func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): NimNode =
var caseStmt = nnkCaseStmt.newTree()
if NoNormalize notin cfg.settings:
caseStmt.add nnkCall.newTree(ident"optionNormalize", ident"key")
else:
caseStmt.add ident"key"
func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): NimNode =
var caseStmt = nnkCaseStmt.newTree(ident("key"))
caseStmt.add nnkOfBranch.newTree(newLit(""), quote do: hwylCliError("empty flag not supported currently")) caseStmt.add nnkOfBranch.newTree(newLit(""), quote do: hwylCliError("empty flag not supported currently"))
for f in cfg.builtinFlags: for f in cfg.builtinFlags:
@ -573,7 +579,11 @@ func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): N
# add flags # add flags
for f in cfg.flags: for f in cfg.flags:
var branch = nnkOfBranch.newTree() var branch = nnkOfBranch.newTree()
if f.long != "": branch.add(newLit(f.long)) if f.long != "":
branch.add newLit(
if NoNormalize notin cfg.settings: optionNormalize(f.long)
else: f.long
)
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)
@ -645,7 +655,7 @@ func addPostParseCheck(cfg: CliCfg, body: NimNode) =
if `name` notin `flagSet`: if `name` notin `flagSet`:
`target` = `default` `target` = `default`
func hwylCliImpl(cfg: CliCfg, root = false): NimNode = func hwylCliImpl(cfg: CliCfg): NimNode =
let let
version = cfg.version or newLit("") version = cfg.version or newLit("")
name = cfg.name.replace(" ", "") name = cfg.name.replace(" ", "")
@ -683,10 +693,11 @@ func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
@`cmdLine`, @`cmdLine`,
longNoVal = `longNoVal`, longNoVal = `longNoVal`,
shortNoVal = `shortNoVal`, shortNoVal = `shortNoVal`,
stopWords = `stopWords` stopWords = `stopWords`,
opChars = {','}
) )
) )
# TODO: first key needs to be normalized
parserBody.add nnkForStmt.newTree( parserBody.add nnkForStmt.newTree(
kind, key, val, kind, key, val,
nnkCall.newTree(nnkDotExpr.newTree(optParser,ident("getopt"))), nnkCall.newTree(nnkDotExpr.newTree(optParser,ident("getopt"))),
@ -700,7 +711,7 @@ func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
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 ... # TODO: add nArgs to change how cmdArgument is handled ...
nnkOfBranch.newTree(ident("cmdArgument"), nnkOfBranch.newTree(ident("cmdArgument"),
quote do: quote do:
result.add `key` result.add `key`
), ),
@ -712,7 +723,7 @@ func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
) )
) )
if NoArgsShowHelp in cfg.settings: if ShowHelp in cfg.settings:
parserBody.add quote do: parserBody.add quote do:
if commandLineParams().len == 0: if commandLineParams().len == 0:
`printHelpName`(); quit 1 `printHelpName`(); quit 1
@ -734,18 +745,21 @@ func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
if `args`.len == 0: if `args`.len == 0:
hwylCliError("expected subcommand") hwylCliError("expected subcommand")
var subCommandCase = nnkCaseStmt.newTree( var subCommandCase = nnkCaseStmt.newTree()
quote do: `args`[0] if NoNormalize notin cfg.settings:
) subCommandCase.add(quote do: optionNormalize(`args`[0]))
else:
subCommandCase.add(quote do: `args`[0])
for sub in cfg.subcommands: for sub in cfg.subcommands:
subCommandCase.add nnkOfBranch.newTree( subCommandCase.add nnkOfBranch.newTree(
newLit(sub.subName), newLit(optionNormalize(sub.subName)),
hwylCliImpl(sub) hwylCliImpl(sub)
) )
subcommandCase.add nnkElse.newTree( subcommandCase.add nnkElse.newTree(
quote do: quote do:
hwylCliError("unknown subcommand " & `args`[0]) hwylCliError("unknown subcommand: [b]" & `args`[0])
) )
runBody.add handleSubCommands.add subCommandCase runBody.add handleSubCommands.add subCommandCase
@ -761,7 +775,7 @@ func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
let `args` = `parserProcName`(`cmdLine`) let `args` = `parserProcName`(`cmdLine`)
`runBody` `runBody`
if root: if cfg.root:
result.add quote do: result.add quote do:
`runProcName`() `runProcName`()
else: else:
@ -770,6 +784,6 @@ func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
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) var cfg = parseCliBody(body, root = true)
hwylCliImpl(cfg, root = true) hwylCliImpl(cfg)

View file

@ -1,29 +1,39 @@
import std/strformat import std/[strformat, strutils]
import hwylterm/hwylcli import hwylterm, hwylterm/hwylcli
type 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"
... "a description of hwylterm" ... "a description of hwylterm"
flags: flags:
[global]
yes: yes:
T bool T bool
? "set flag to yes" ? "set flag to yes"
[global] [config]
config: confiG:
T seq[string] T seq[string]
? "path to config file" ? "path to config file"
* @["config.yml"] * @["config.yml"]
preSub:
echo "this is run after subcommand parsing but before its run block"
run: run:
echo "this is always run prior to subcommand parsing" echo "this is always run prior to subcommand parsing"
echo fmt"{yes=}, {config=}"
subcommands: subcommands:
--- one --- "onelonger"
... "the first subcommand" ... """
the first subcommand
this command features both an enum flag and a Count flag
it also inherits the `[[config]` flag group
"""
flags: flags:
color: color:
T Color T Color
@ -32,30 +42,33 @@ hwylCli:
T Count T Count
? "a count flag" ? "a count flag"
- v - v
^[config]
run: run:
echo "hello from `example one` command!" echo "hello from `example one` command!"
echo fmt"{color=}" echo fmt"{color=}"
echo fmt"{verbose=}" echo fmt"{verbose=}"
echo fmt"{config=}"
--- two --- "two-longer"
... """ ... """
some second subcommand some second subcommand
a longer mulitline description that will be visible in the subcommand help a longer mulitline description that will be visible in the subcommand help
it will automatically be "bb"'ed [bold]this is bold text[/] and it will automatically be "bb"'ed [bold]this is bold text[/]
""" """
flags: flags:
aflag: a:
T bool T bool
? "some help" ? "some help"
bflag: b:
T seq[float] T seq[float]
? "multiple floats" ? "multiple floats"
c: h "this will override the builtin 'h' for help"
? "this should be a single flag"
h "overwrite the short h from help"
run: run:
echo "hello from `example b` command" echo "hello from `example b` command"
echo fmt"{aflag=}, {bflag=}" echo fmt"{a=}, {b=}"