massive feature in macro cli generator

This commit is contained in:
Daylin Morgan 2024-10-31 21:42:41 -05:00
parent 56bb2bb7c1
commit fed1c03ce8
Signed by: daylin
GPG key ID: 950D13E9719334AD
7 changed files with 836 additions and 204 deletions

View file

@ -26,7 +26,7 @@ task docs, "Deploy doc html + search index to public/ directory":
when defined(clean): when defined(clean):
echo fmt"clearing {deployDir}" echo fmt"clearing {deployDir}"
rmDir deployDir rmDir deployDir
for module in ["cligen", "chooser", "logging", "cli", "parseopt3"]: for module in ["cligen", "chooser", "logging", "hwylcli", "parseopt3"]:
selfExec fmt"{docCmd} --docRoot:{getCurrentDir()}/src/ src/hwylterm/{module}" selfExec fmt"{docCmd} --docRoot:{getCurrentDir()}/src/ src/hwylterm/{module}"
selfExec fmt"{docCmd} --project --project src/{pkgName}.nim" selfExec fmt"{docCmd} --project --project src/{pkgName}.nim"
docFixup(deployDir, pkgName) docFixup(deployDir, pkgName)

View file

@ -3,7 +3,7 @@
see also these utility modules: see also these utility modules:
- [cli](./hwylterm/cli.html) - [hwylcli](./hwylterm/hwylcli.html)
- [cligen adapter](./hwylterm/cligen.html), requires [cligen](https://github.com/c-blake/cligen) - [cligen adapter](./hwylterm/cligen.html), requires [cligen](https://github.com/c-blake/cligen)
- [chooser](./hwylterm/chooser.html) - [chooser](./hwylterm/chooser.html)
- [logging](./hwylterm/logging.html) - [logging](./hwylterm/logging.html)

View file

@ -254,7 +254,6 @@ func bb*(s: string): BbString =
else: else:
next next
result.closeFinalSpan result.closeFinalSpan
proc bb*(s: string, style: string): BbString = proc bb*(s: string, style: string): BbString =
@ -263,7 +262,9 @@ proc bb*(s: string, style: string): BbString =
proc bb*(s: string, style: Color256): BbString = proc bb*(s: string, style: Color256): BbString =
bb(s, $style) bb(s, $style)
proc `&`*(x: BbString, y: string): BbString = proc bb*(s: BbString): BbString = s
func `&`*(x: BbString, y: string): BbString =
result = x result = x
result.plain &= y result.plain &= y
result.spans.add BbSpan(styles: @[], slice: [x.plain.len, result.plain.len - 1]) result.spans.add BbSpan(styles: @[], slice: [x.plain.len, result.plain.len - 1])
@ -343,30 +344,9 @@ proc bbEcho*(args: varargs[string, `$`]) {.raises: [IOError]} =
# NOTE: could move to standlone modules in the tools/ directory # NOTE: could move to standlone modules in the tools/ directory
when isMainModule: when isMainModule:
import ./[cli, parseopt3] import ./[hwylcli]
const version = staticExec "git describe --tags --always --dirty=-dev" const version = staticExec "git describe --tags --always --dirty=-dev"
proc showTestCard() =
proc writeHelp() =
let help = $newHwylCli(
"[bold]bbansi[/] [[[green]args...[/]] [[[faint]-h|-v[/]]",
"""
bbansi "[[yellow] yellow text!"
-> [yellow] yellow text![/]
bbansi "[[bold red] bold red text[[/] plain text..."
-> [bold red] bold red text[/] plain text...
bbansi "[[red]some red[[/red] but all italic" --style:italic
-> [italic][red]some red[/red] but all italic[/italic]
""",
[
("h", "help", "show this help"),
("v", "version", "show version"),
("s", "style", "set style for string"),
]
)
echo help; quit 0
proc testCard() =
for style in [ for style in [
"bold", "faint", "italic", "underline", "blink", "reverse", "conceal", "strike" "bold", "faint", "italic", "underline", "blink", "reverse", "conceal", "strike"
]: ]:
@ -379,52 +359,39 @@ bbansi "[[red]some red[[/red] but all italic" --style:italic
echo "on ", color, " -> ", fmt"[on {color}]****".bb echo "on ", color, " -> ", fmt"[on {color}]****".bb
quit(QuitSuccess) quit(QuitSuccess)
proc debug(bbs: BbString): string = proc debugBb(bbs: BbString): string =
echo "bbString(" echo "bbString("
echo " plain: ", bbs.plain echo " plain: ", bbs.plain
echo " spans: ", bbs.spans echo " spans: ", bbs.spans
echo " escaped: ", escape($bbs) echo " escaped: ", escape($bbs)
echo ")" echo ")"
proc writeVersion() = hwylCli:
echo bbfmt"[yellow]bbansi version[/][red] ->[/] [bold]{version}[/]" name "bbansi"
quit 0 settings NoArgsShowHelp
usage "[bold]bbansi[/] [[[green]args...[/]] [[[faint]-h|-V[/]]"
var description """
strArgs: seq[string] bbansi "[[yellow] yellow text!"
style: string -> [yellow] yellow text![/]
showDebug: bool bbansi "[[bold red] bold red text[[/] plain text..."
var p = initOptParser( -> [bold red] bold red text[/] plain text...
shortNoVal = {'h','v'}, bbansi "[[red]some red[[/red] but all italic" --style:italic
longNoVal = @["help", "version", "testCard"] -> [italic][red]some red[/red] but all italic[/italic]
) """
for kind, key, val in p.getopt(): version bbfmt"[yellow]bbansi version[/][red] ->[/] [bold]{version}[/]"
case kind hidden debug, testCard
of cmdError: quit($(bb"[red]cli error[/]: " & p.message), 1) flags:
of cmdEnd: assert(false) debug:
of cmdShortOption, cmdLongOption: T bool
case key testCard:
of "testCard" : testCard() T bool
of "help" , "h": writeHelp() style:
of "version", "v": writeVersion() ? "set style for string"
of "style" , "s": - "s"
if val == "": run:
bbEcho "[red]ERROR[/]: expected value for -s/--style" if testCard: showTestCard()
quit(QuitFailure) for arg in args:
style = val let styled = arg.bb(style)
of "debug":
showDebug = true
else:
bbEcho "[yellow]warning[/]: unexpected option/value -> ", key, ", ", val
of cmdArgument: strArgs.add key
if strArgs.len == 0:
writeHelp()
for arg in strArgs:
let styled =
if style != "":
arg.bb(style)
else:
arg.bb
echo styled echo styled
if showDebug: if debug:
echo debug(styled) echo debugBb(styled)

View file

@ -127,54 +127,35 @@ proc choose*[T](things: openArray[T], height: Natural = 6): seq[T] =
when isMainModule: when isMainModule:
import ./[cli, parseopt3] import ./[hwylcli]
hwylcli:
proc writeHelp() = name "hwylchoose"
echo newHwylCli( settings NoArgsShowHelp
"[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]", usage "[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]"
description """
hwylchoose a b c d
hwylchoose a,b,c,d -s ,
hwylchoose a,b,c,d --seperator ","
hwylchoose --demo
""" """
hwylchoose a b c d hidden demo
hwylchoose a,b,c,d -s , flags:
hwylchoose a,b,c,d --seperator "," demo:
""", T bool
[ separator:
("h", "help", "show this help"), help "separator to split items"
("s", "seperator", "seperator to split items"), short "s"
] run:
)
var
posArgs: seq[string]
sep: string
var p = initOptParser(
shortNoVal = {'h'}, longNoVal = @["help", "demo"]
)
for kind, key, val in p.getopt():
case kind
of cmdError: quit($(bb"[red]cli error[/]: " & p.message), 1)
of cmdEnd: assert false
of cmdShortOption, cmdLongOption:
case key
of "help", "h":
writeHelp(); quit 0
of "demo":
posArgs &= LowercaseLetters.toSeq().mapIt($it)
of "seperator","s":
if val == "":
echo bb"[red]ERROR[/]: expected value for --seperator"
quit QuitFailure
sep = val
else:
echo bb"[yellow]warning[/]: unexpected option/value -> ", key, ", ", val
of cmdArgument:
posArgs.add key
if posArgs.len == 0: quit "expected values to choose"
var items: seq[string] var items: seq[string]
if sep != "": if demo:
if posArgs.len != 1: quit "only pass one positional arg when using --sep" items &= LowercaseLetters.toSeq().mapIt($it)
items = posArgs[0].split(sep).mapIt(strip(it))
else: else:
items = posArgs if separator != "":
if args.len != 1: quit "only pass one positional arg when using --separator"
items = args[0].split(separator).mapIt(strip(it))
else:
items = args
let item = choose(items) let item = choose(items)
echo "selected: ", item echo "selected: ", item

View file

@ -1,85 +0,0 @@
##[
# Cli
]##
import std/[strutils]
import ./bbansi
type
HwylFlag = tuple
short, long, description = ""
HwylCliStyles* = object
hdr = "b cyan"
shortFlag = "yellow"
longFlag = "magenta"
descFlag = ""
HwylCli* = object
cmd*: string
usage*: string
flags*: seq[HwylFlag]
styles*: HwylCliStyles
shortArgLen, longArgLen, descArgLen: int
func newHwylCli*(
cmd = "",
usage = "",
flags: openArray[HwylFlag] = @[],
styles = HwylCliStyles()
): HwylCli =
result.cmd = cmd
result.usage = usage
result.flags = @flags
result.styles = styles
for f in flags:
result.shortArgLen = max(result.shortArgLen, f.short.len)
result.longArgLen = max(result.longArgLen, f.long.len)
result.descArgLen = max(result.descArgLen, f.description.len)
func flagHelp(cli: HwylCli, f: HwylFlag): string =
result.add " "
if f.short != "":
result.add "[" & cli.styles.shortFlag & "]"
result.add "-" & f.short.alignLeft(cli.shortArgLen)
result.add "[/]"
else:
result.add " ".repeat(1 + cli.shortArgLen)
result.add " "
if f.long != "":
result.add "[" & cli.styles.longFlag & "]"
result.add "--" & f.long.alignLeft(cli.longArgLen)
result.add "[/]"
else:
result.add " ".repeat(2 + cli.longArgLen)
result.add " "
if f.description != "":
result.add "[" & cli.styles.descFlag & "]"
result.add f.description
result.add "[/]"
result.add "\n"
proc bbImpl(cli: HwylCli): string =
if cli.cmd != "":
result.add cli.cmd
result.add "\n"
if cli.usage != "":
result.add "\n"
result.add "[" & cli.styles.hdr & "]"
result.add "usage[/]:\n"
result.add indent(cli.usage, 2 )
result.add "\n"
if cli.flags.len > 0:
result.add "\n"
result.add "[" & cli.styles.hdr & "]"
result.add "flags[/]:\n"
for f in cli.flags:
result.add flagHelp(cli,f)
proc bb*(cli: HwylCli): BbString =
result = bb(bbImpl(cli))
proc `$`*(cli: HwylCli): string =
result = $bb(cli)

768
src/hwylterm/hwylcli.nim Normal file
View file

@ -0,0 +1,768 @@
##[
# HwylCli
]##
import std/[
macros, os, sequtils,
sets, strutils, tables,
sugar
]
import ./[bbansi, parseopt3]
export parseopt3
type
HwylFlagHelp = tuple
short, long, description: string
HwylSubCmdHelp = tuple
name, desc: string
HwylCliStyles* = object
hdr = "bold cyan"
shortFlag = "yellow"
longFlag = "magenta"
descFlag = ""
cmd = "bold"
HwylCliHelp* = object
cmd*: string
usage*: string
desc*: string
subcmds: seq[HwylSubCmdHelp]
flags*: seq[HwylFlagHelp]
styles*: HwylCliStyles
subcmdLen, subcmdDescLen, shortArgLen, longArgLen, descArgLen: int
func newHwylCliHelp*(
cmd = "",
usage = "",
desc = "",
subcmds: openArray[HwylSubCmdHelp] = @[],
flags: openArray[HwylFlagHelp] = @[],
styles = HwylCliStyles()
): HwylCliHelp =
result.cmd = cmd
result.desc = dedent(desc).strip()
result.subcmds = subcmds.mapIt((it.name,it.desc.splitlines()[0]))
result.usage = dedent(usage).strip()
result.flags = @flags
result.styles = styles
# TODO: incorporate into "styles?"
result.subcmdLen = 8
for f in flags:
result.shortArgLen = max(result.shortArgLen, f.short.len)
result.longArgLen = max(result.longArgLen, f.long.len)
result.descArgLen = max(result.descArgLen, f.description.len)
for s in subcmds:
result.subcmdLen = max(result.subcmdLen, s.name.len)
result.subcmdDescLen = max(result.subcmdDescLen, s.desc.len)
func flagHelp(cli: HwylCliHelp, f: HwylFlagHelp): string =
result.add " "
if f.short != "":
result.add "[" & cli.styles.shortFlag & "]"
result.add "-" & f.short.alignLeft(cli.shortArgLen)
result.add "[/]"
else:
result.add " ".repeat(1 + cli.shortArgLen)
result.add " "
if f.long != "":
result.add "[" & cli.styles.longFlag & "]"
result.add "--" & f.long.alignLeft(cli.longArgLen)
result.add "[/]"
else:
result.add " ".repeat(2 + cli.longArgLen)
result.add " "
if f.description != "":
result.add "[" & cli.styles.descFlag & "]"
result.add f.description
result.add "[/]"
result.add "\n"
func subCmdLine(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string =
# NOTE: set some minimum for the subcmdlen?
result.add " "
result.add "[" & cli.styles.cmd & "]"
result.add subcmd.name.alignLeft(cli.subcmdLen)
result.add "[/]"
result.add " "
result.add subcmd.desc.alignLeft(cli.subcmdDescLen)
result.add "\n"
proc bbImpl(cli: HwylCliHelp): string =
if cli.cmd != "":
result.add cli.cmd
result.add "\n"
if cli.usage != "":
result.add "\n"
result.add "[" & cli.styles.hdr & "]"
result.add "usage[/]:\n"
result.add indent(cli.usage, 2 )
if cli.desc != "":
result.add "\n\n"
result.add cli.desc
result.add "\n"
if cli.subcmds.len > 0:
result.add "\n"
result.add "[" & cli.styles.hdr & "]"
result.add "subcommands[/]:\n"
for s in cli.subcmds:
result.add cli.subcmdLine(s)
if cli.flags.len > 0:
result.add "\n"
result.add "[" & cli.styles.hdr & "]"
result.add "flags[/]:\n"
for f in cli.flags:
# NOTE: added to accomate dumb macro below
# if f != ("","",""):
result.add flagHelp(cli,f)
proc bb*(cli: HwylCliHelp): BbString =
result = bb(bbImpl(cli))
proc `$`*(cli: HwylCliHelp): string =
result = $bb(cli)
# ---------------------------
# const supportedbaseTypes = ["string", "bool", "int"]
# const supportedSeqTypes = ["string", "int"]
# #
# proc getObjectFieldTypes(x: NimNode): Table[string, string] =
# let impl = getType(x).getTypeImpl()
# for f in impl[2]:
# let name = f[0].strVal
# template bail = error "unsupported field type for: " & name
# case f[1].kind
# of nnkSym:
# let typeSym = f[1].strVal
# if typeSym notin supportedbaseTypes: bail
# result[name] = typeSym
# of nnkBracketExpr:
# if f[1].len > 2: bail
# if f[1][0].strVal notin "seq": bail
# let typeSym = f[1][1].strVal
# if typeSym notin supportedSeqTypes: bail
# result[name] = f[1][0].strVal & "[" & f[1][1].strVal & "]"
# else: bail
type
CliSetting = enum
NoHelpFlag, NoArgsShowHelp
CliFlag = object
name*: string
ident*: string
default*: NimNode
typeSym*: string
short*: char
long*: string
help*: NimNode
CliCfg = object
stopWords*: seq[string]
styles: NimNode
hidden*: seq[string]
subcommands: seq[CliCfg]
settings*: set[CliSetting]
run*: NimNode
desc*: NimNode
name*: string
subName*: string # used for help the generator
version*, usage*: NimNode
flags*: seq[CliFlag]
required*: seq[string]
globalFlags*: seq[CliFlag]
func `?`(n: NimNode) =
## for debugging macros
debugEcho treeRepr n
# TODO: do i need this?
func newCliFlag(): CliFlag =
result.help = newLit("")
template badNode =
error "unexpected node kind: " & $node.kind
func typeSymFromNode(node: NimNode): string =
case node.kind
of nnkIdent, nnkStrLit:
result = node.strVal
of nnkBracketExpr:
result = node[0].strVal & "[" & node[1].strVal & "]"
else: badNode
func getOptTypeSym(node: NimNode): string =
case node.kind:
of nnkCommand:
result = typeSymFromNode(node[1]) # [0] is T
of nnkCall:
result = typeSymFromNode(node[1][0]) # [1] is stmtlist [0] is the type
else: error "unexpected node kind: " & $node.kind
func getOptOptNode(optOptValue: NimNode): NimNode =
case optOptValue.kind
of nnkStrLit:
result = optOptValue
of nnkStmtList:
result = optOptValue[0]
of nnkCommand:
result = optOptValue[1]
of nnkPrefix: # NOTE: should i double check prefix value?
result = optOptValue[1]
else: error "unexpected node kind: " & $optOptValue.kind
# TODO: don't use the confusing name optOpts here and above
func parseOptOpts(opt: var CliFlag, optOpts: NimNode) =
expectKind optOpts, nnkStmtList
for optOpt in optOpts:
case optOpt.kind
of nnkCall, nnkCommand, nnkPrefix:
case optOpt[0].strVal
of "help","?":
opt.help = getOptOptNode(optOpt[1])
of "short", "-":
let val = getOptOptNode(optOpt).strVal
if val.len > 1:
error "short flag must be a char"
opt.short = val[0].char
of "*", "default":
opt.default = getOptOptNode(optOpt)
of "i", "ident":
opt.ident = getOptOptNode(optOpt).strVal
of "T":
opt.typeSym = getOptTypeSym(optOpt)
else:
error "unexpected option setting: " & optOpt[0].strVal
else:
error "unexpected option node type: " & $optOpt.kind
func startFlag(f: var CliFlag, n: NimNode) =
f.name =
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"
# assume a single character is a short flag
if f.name.len == 1:
f.short = f.name[0].char
else:
f.long = f.name
func parseCliFlag(n: NimNode): CliFlag =
if n.kind notin [nnkCommand, nnkCall]:
error "unexpected node kind: " & $n.kind
# deduplicate these...
result = newCliFlag()
startFlag(result, n)
# option "some help desc"
if n.kind == nnkCommand:
result.help = n[1]
# option:
# help "some help description"
else:
parseOptOpts(result, n[1])
if result.ident == "":
result.ident = result.name
if result.typeSym == "":
result.typeSym = "string"
func parseCliFlags(flags: NimNode): seq[CliFlag] =
expectKind flags, nnkStmtList
for f in flags:
result.add parseCliFlag(f)
func parseCliSetting(s: string): CliSetting =
try: parseEnum[CliSetting](s)
except: error "unknown cli setting: " & s
func parseCliSettings(cfg: var CliCfg, node: NimNode) =
case node.kind
of nnkCommand:
for n in node[1..^1]:
cfg.settings.incl parseCliSetting(n.strVal)
of nnkCall:
expectKind node[1], nnkStmtList
for n in node[1]:
cfg.settings.incl parseCliSetting(n.strVal)
else: assert false
func parseIdentLikeList(node: NimNode): seq[string] =
template check =
if n.kind notin [nnkStrLit,nnkIdent]:
error "expected StrLit or Ident, got:" & $n.kind
case node.kind
of nnkCommand:
for n in node[1..^1]:
check
result.add n.strVal
of nnkCall:
expectKind node[1], nnkStmtList
for n in node[1]:
check
result.add n.strVal
else: assert false
func parseCliBody(body: NimNode, name: string = ""): CliCfg
func isSubMarker(node: NimNode): bool =
if node.kind == nnkPrefix:
if eqIdent(node[0], "---"):
result = true
func sliceStmts(node: NimNode): seq[
tuple[name: string, slice: Slice[int]]
] =
if not isSubMarker(node[0]):
error "expected a subcommand delimiting line"
var
name: string = node[0][1].strVal
start = 1
let nodeLen = node.len()
for i in 1..<nodeLen:
if i == nodeLen - 1:
result.add (name, start..i)
elif isSubMarker(node[i]):
result.add (name, start..(i - 1))
name = node[i][1].strVal
start = i + 1
func addGlobalFlagsFrom(child: var CliCfg, parent: CliCfg) =
let names = child.flags.mapIt(it.name)
for f in parent.globalFlags:
if f.name in names:
error "global flag " & f.name & " conflicts with command flag"
child.flags.add f
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.addGlobalFlagsFrom(cfg)
cfg.subcommands.add subCfg
func parseHiddenFlags(cfg: var CliCfg, node: NimNode) =
template check =
if n.kind notin [nnkStrLit, nnkIdent]:
error "expected string literal or ident"
case node.kind
of nnkCommand:
for n in node[1..^1]:
check
cfg.hidden.add n.strVal
of nnkCall:
expectKind node[1], nnkStmtList
for n in node[1]:
check
cfg.hidden.add n.strVal
else: assert false
func parseCliBody(body: NimNode, name = ""): CliCfg =
result.name = name
for call in body:
if call.kind notin [nnkCall, nnkCommand, nnkPrefix]:
error "unexpected node kind: " & $call.kind
let name = call[0].strVal
case name:
of "name":
expectKind call[1], nnkStrLit
result.name = call[1].strVal
of "version", "V":
result.version = call[1]
of "usage", "?":
result.usage = call[1]
of "description", "...":
result.desc = call[1]
of "globalFlags":
result.globalFlags = parseCliFlags(call[1])
of "flags":
result.flags = parseCliFlags(call[1])
of "settings":
parseCliSettings(result, call)
of "stopWords":
result.stopWords = parseIdentLikeList(call)
of "subcommands":
parseCliSubcommands(result, call)
of "hidden":
parseHiddenFlags(result, call)
of "run":
result.run = call[1]
of "styles":
result.styles = call[1]
of "required":
result.required = parseIdentLikeList(call)
else:
error "unknown hwylCli setting: " & name
result.addGlobalFlagsFrom(result)
if result.name == "":
error "missing required option: name"
func flagsArray(cfg: CliCfg): NimNode =
result = newTree(nnkBracket)
for f in cfg.flags:
if f.name in cfg.hidden: continue
let
help = f.help
long = newLit(f.long)
short =
if f.short != '\x00': newLit($f.short)
else: newLit("")
result.add quote do:
(`short`, `long`, `help`)
if NoHelpFlag notin cfg.settings:
result.add quote do:
("h", "help", "show this help")
if cfg.version != nil:
result.add quote do:
("v", "version", "print version")
func subCmdsArray(cfg: CliCfg): NimNode =
result = newTree(nnkBracket)
for s in cfg.subcommands:
let cmd = newLit(s.subName)
let desc = s.desc
result.add quote do:
(`cmd`, `desc`)
func defaultUsage(cfg: CliCfg): NimNode =
newLit("[b]" & cfg.name & "[/]" & " [[[faint]-h[/]]")
func generateCliHelperProc(cfg: CliCfg, printHelpName: NimNode): NimNode =
let
name = newLit(cfg.name)
desc = cfg.desc or newLit("")
usage = cfg.usage or defaultUsage(cfg)
helpFlags = cfg.flagsArray()
subcmds = cfg.subCmdsArray()
styles = cfg.styles or (quote do: HwylCliStyles())
result = quote do:
proc `printHelpName`() =
echo newHwylCliHelp(
cmd = `name`,
desc = `desc`,
usage = `usage`,
subcmds = `subcmds`,
flags = `helpFlags`,
styles = `styles`,
)
# NOTE: is there a better way to do this?
proc checkVarSet[T](name: string, target: T) =
var default: T
if target == default:
quit($bb("[red]error[/]: missing required flag: [b]" & name))
proc checkDefaultExists[T](target: T, key: string, val: string) =
var default: T
if target == default and val == "":
quit($bb("[red]error[/]: expected value for: [b]" & key))
proc tryParseInt(key: string, val: string): int =
try:
result = parseInt(val)
except:
quit($bb("[red]error[/]: failed to parse value for [b]" & key & "[/] as integer: [b]" & val))
func addOrOverwrite[T](target: var seq[T], default: seq[T], val: T) =
if target != default:
target.add val
else:
target = @[val]
func assignField(f: CliFlag): NimNode =
let key = ident"key"
let varName = ident(f.ident)
case f.typeSym
of "string":
let value = ident"val"
result = quote do:
checkDefaultExists(`varName`, `key`, `value`)
`varName` = `value`
of "bool":
let value = ident"true"
result = quote do:
`varName` = `value`
of "int":
let value = ident"val"
result = quote do:
checkDefaultExists(`varName`, `key`, `value`)
`varName` = tryParseInt(`key`, `value`)
of "seq[string]":
let value = ident"val"
let default = f.default or (quote do: @[])
result = quote do:
`varName`.addOrOverwrite(`default`, `value`)
of "seq[int]":
let value = ident"val"
let default = f.default or (quote do: @[])
result = quote do:
`varName`.addOrOverwrite(`default`, tryParseInt(`value`))
else: error "unable to generate assignment for fion, type: " & f.name & "," & f.typeSym
func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): NimNode =
var caseStmt = nnkCaseStmt.newTree(ident("key"))
caseStmt.add nnkOfBranch.newTree(newLit(""), quote do: quit "empty flag not supported currently")
if NoHelpFlag notin cfg.settings:
caseStmt.add nnkOfBranch.newTree(
newLit("h"), newLit("help"),
quote do:
`printHelpName`(); quit 0
)
if cfg.version != nil:
caseStmt.add nnkOfBranch.newTree(
newLit("V"), newLit("version"),
quote do:
echo `version`; quit 0
)
# add flags
for f in cfg.flags:
var branch = nnkOfBranch.newTree()
if f.long != "": branch.add(newLit(f.long))
if f.short != '\x00': branch.add(newLit($f.short))
branch.add assignField(f)
caseStmt.add branch
caseStmt.add nnkElse.newTree(quote do: quit "unknown flag: " & key)
result = nnkStmtList.newTree(caseStmt)
func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] =
var long = nnkBracket.newTree()
var short = nnkCurly.newTree()
if NoHelpFlag notin cfg.settings:
long.add newLit("help")
short.add newLit('h')
if cfg.version != nil:
long.add newLit("version")
short.add newLit('V')
for f in cfg.flags:
if f.typeSym == "bool":
if f.long != "":
long.add newLit(f.long)
if f.short != '\x00':
short.add newLit(f.short)
result = (nnkPrefix.newTree(ident"@",long), short)
func setFlagVars(cfg: CliCfg): NimNode =
result = nnkVarSection.newTree()
# TODO: generalize this better...
for f in cfg.flags:
let
t =
if f.typeSym == "seq[string]": nnkBracketExpr.newTree(newIdentNode("seq"),newIdentNode("string"))
elif f.typeSym == "seq[int]" : nnkBracketExpr.newTree(newIdentNode("seq"),newIdentNode("string"))
else: ident(f.typeSym)
val =
if f.default == nil: newEmptyNode() # use default here
else: f.default
result.add nnkIdentDefs.newTree(ident(f.ident), t, val)
func addRequiredFlagsCheck(cfg: CliCfg, body: NimNode) =
let requirdFlags = cfg.flags.filterIt(it.name in cfg.required and it.default == nil)
for f in requirdFlags:
let name = newLit(f.name)
let flag = ident(f.ident)
body.add quote do:
checkVarSet(`name`, `flag`)
func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
let
version = cfg.version or newLit("")
name = cfg.name.replace(" ", "")
printHelpName = ident("print" & name & "Help")
parserProcName = ident("parse" & name)
result = newTree(nnkStmtList)
let
printHelperProc = generateCliHelperProc(cfg, printHelpName)
flagVars = setFlagVars(cfg)
# result.add setFlagVars(cfg)
var parserBody = nnkStmtList.newTree()
let
optParser = ident("p")
cmdLine = ident"cmdLine"
(longNoVal, shortNoVal) = cfg.getNoVals()
var stopWords = nnkBracket.newTree(newLit("--"))
for w in cfg.stopWords:
stopWords.add newLit(w)
stopWords = nnkPrefix.newTree(ident"@", stopWords)
parserBody.add(
quote do:
var `optParser` = initOptParser(
@`cmdLine`,
longNoVal = `longNoVal`,
shortNoVal = `shortNoVal`,
stopWords = `stopWords`
)
)
let
kind = ident"kind"
key = ident"key"
val = ident"val"
parserBody.add nnkForStmt.newTree(
kind, key, val,
nnkCall.newTree(nnkDotExpr.newTree(optParser,ident("getopt"))),
nnkStmtList.newTree(
# # for debugging..
# quote do:
# echo `kind`,"|",`key`,"|",`val`
# ,
nnkCaseStmt.newTree(
kind,
nnkOfBranch.newTree(ident("cmdError"), quote do: quit($(bb"[red]cli error[/]: " & p.message), 1)),
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`),
nnkOfBranch.newTree(
ident("cmdShortOption"), ident("cmdLongOption"),
shortLongCaseStmt(cfg, printHelpName, version)
)
)
)
)
if NoArgsShowHelp in cfg.settings:
parserBody.add quote do:
if commandLineParams().len == 0:
`printHelpName`(); quit 1
let runProcName = ident("run" & name)
let runBody = nnkStmtList.newTree()
addRequiredFlagsCheck(cfg, runBody)
# move to proc?
if cfg.run != nil:
runBody.add cfg.run
# let runBody = cfg.run or nnkStmtList.newTree(nnkDiscardStmt.newTree(newEmptyNode()))
let args = ident"args"
if cfg.subcommands.len > 0:
var handleSubCommands = nnkStmtList.newTree()
handleSubCommands.add quote do:
if `args`.len == 0:
quit "expected subcommand"
var subCommandCase = nnkCaseStmt.newTree(
quote do: `args`[0]
)
for sub in cfg.subcommands:
subCommandCase.add nnkOfBranch.newTree(
newLit(sub.subName),
hwylCliImpl(sub)
)
subcommandCase.add nnkElse.newTree(
quote do:
quit $bb"[red]error[/] unknown subcommand: " & `args`[0]
)
runBody.add handleSubCommands.add subCommandCase
result.add quote do:
# block:
`printHelperProc`
`flagVars`
proc `parserProcName`(`cmdLine`: openArray[string] = commandLineParams()): seq[string] =
`parserBody`
proc `runProcName`(`cmdLine`: openArray[string] = commandLineParams()) =
let `args` = `parserProcName`(`cmdLine`)
`runBody`
if root:
result.add quote do:
`runProcName`()
else:
result.add quote do:
`runProcName`(`args`[1..^1])
macro hwylCli*(body: untyped) =
var cfg = parseCliBody(body)
hwylCliImpl(cfg, root = true)
when isMainModule:
import std/strformat
hwylCli:
name "hwylterm"
... "a description of hwylterm"
globalFlags:
config:
T seq[string]
? "path to config file"
flags:
check:
T bool
? "load config and exit"
- c
run:
echo "hello from the main command"
echo fmt"{config=}, {check=}"
subcommands:
--- a
description "the \"a\" subcommand"
flags:
`long-flag` "some help"
flagg "some other help"
run:
echo config
echo "hello from hwylterm sub command!"
echo `long-flag`
echo flagg
--- b
description "the \"b\" subcommand"
flags:
aflag:
T bool
? "some help"
bflag:
? "some other flag?"
* "wow"
run:
echo "hello from hwylterm sub `b` command"
echo aflag, bflag

View file

@ -12,6 +12,7 @@
- [ ] 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` - [ ] add a `commands` option for `newHwylCli` in `hwylterm/cli`
- [ ] console object with customizable options to apply formatting
## features ## features