Compare commits

..

No commits in common. "69ae06fa4765e840403c61f8d34c4b7484b9dc56" and "b6202f4f51edadd041d2b5f2aed1d75abcd4ee51" have entirely different histories.

5 changed files with 340 additions and 425 deletions

View file

@ -368,7 +368,7 @@ when isMainModule:
hwylCli: hwylCli:
name "bbansi" name "bbansi"
settings ShowHelp settings NoArgsShowHelp
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

@ -130,7 +130,7 @@ when isMainModule:
import ./[hwylcli] import ./[hwylcli]
hwylcli: hwylcli:
name "hwylchoose" name "hwylchoose"
settings ShowHelp settings NoArgsShowHelp
usage "[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]" usage "[bold]hwylchoose[/] [[[green]args...[/]] [[[faint]-h[/]]"
description """ description """
hwylchoose a b c d hwylchoose a b c d

View file

@ -8,41 +8,40 @@ import std/[
sugar sugar
] ]
import ./[bbansi, parseopt3] import ./[bbansi, parseopt3]
export parseopt3, sets, bbansi export parseopt3
type type
HwylFlagHelp* = tuple HwylFlagHelp = tuple
short, long, description: string short, long, description: string
HwylSubCmdHelp* = tuple HwylSubCmdHelp = tuple
name, desc: string name, desc: string
HwylCliStyles* = object HwylCliStyles* = object
header* = "bold cyan" hdr = "bold cyan"
flagShort* = "yellow" shortFlag = "yellow"
flagLong* = "magenta" longFlag = "magenta"
flagDesc* = "" descFlag = ""
cmd* = "bold" cmd = "bold"
HwylCliHelp* = object HwylCliHelp* = object
cmd*: string
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?
func firstLine(s: string): string =
s.strip().dedent().strip().splitlines()[0]
func newHwylCliHelp*( func newHwylCliHelp*(
cmd = "",
usage = "", usage = "",
desc = "", desc = "",
subcmds: openArray[HwylSubCmdHelp] = @[], subcmds: openArray[HwylSubCmdHelp] = @[],
flags: openArray[HwylFlagHelp] = @[], flags: openArray[HwylFlagHelp] = @[],
styles = HwylCliStyles() styles = HwylCliStyles()
): HwylCliHelp = ): HwylCliHelp =
result.cmd = cmd
result.desc = dedent(desc).strip() result.desc = dedent(desc).strip()
result.subcmds = result.subcmds = subcmds.mapIt((it.name,it.desc.strip().dedent().strip().splitlines()[0]))
subcmds.mapIt((it.name, it.desc.firstLine))
result.usage = dedent(usage).strip() result.usage = dedent(usage).strip()
result.flags = @flags result.flags = @flags
result.styles = styles result.styles = styles
@ -56,10 +55,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 render*(cli: HwylCliHelp, f: HwylFlagHelp): string = func flagHelp(cli: HwylCliHelp, f: HwylFlagHelp): string =
result.add " " result.add " "
if f.short != "": if f.short != "":
result.add "[" & cli.styles.flagShort & "]" result.add "[" & cli.styles.shortFlag & "]"
result.add "-" & f.short.alignLeft(cli.shortArgLen) result.add "-" & f.short.alignLeft(cli.shortArgLen)
result.add "[/]" result.add "[/]"
else: else:
@ -67,7 +66,7 @@ func render*(cli: HwylCliHelp, f: HwylFlagHelp): string =
result.add " " result.add " "
if f.long != "": if f.long != "":
result.add "[" & cli.styles.flagLong & "]" result.add "[" & cli.styles.longFlag & "]"
result.add "--" & f.long.alignLeft(cli.longArgLen) result.add "--" & f.long.alignLeft(cli.longArgLen)
result.add "[/]" result.add "[/]"
else: else:
@ -76,12 +75,12 @@ func render*(cli: HwylCliHelp, f: HwylFlagHelp): string =
result.add " " result.add " "
if f.description != "": if f.description != "":
result.add "[" & cli.styles.flagDesc & "]" result.add "[" & cli.styles.descFlag & "]"
result.add f.description result.add f.description
result.add "[/]" result.add "[/]"
result.add "\n" result.add "\n"
func render*(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string = func subCmdLine(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)
@ -90,63 +89,49 @@ func render*(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 if cli.cmd != "":
func render*(cli: HwylCliHelp): string = result.add cli.cmd
result.add "\n"
if cli.usage != "": if cli.usage != "":
result.add "[" & cli.styles.header & "]" result.add "\n"
result.add "[" & cli.styles.hdr & "]"
result.add "usage[/]:\n" result.add "usage[/]:\n"
result.add indent(cli.usage, 2 ) result.add indent(cli.usage, 2 )
result.add "\n"
if cli.desc != "": if cli.desc != "":
result.add "\n" result.add "\n\n"
result.add cli.desc result.add cli.desc
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.header & "]" result.add "[" & cli.styles.hdr & "]"
result.add "subcommands[/]:\n" result.add "subcommands[/]:\n"
for s in cli.subcmds: for s in cli.subcmds:
result.add cli.render(s) result.add cli.subcmdLine(s)
if cli.flags.len > 0: if cli.flags.len > 0:
result.add "\n" result.add "\n"
result.add "[" & cli.styles.header & "]" result.add "[" & cli.styles.hdr & "]"
result.add "flags[/]:\n" result.add "flags[/]:\n"
for f in cli.flags: for f in cli.flags:
result.add render(cli,f) result.add flagHelp(cli,f)
proc bb*(cli: HwylCliHelp): BbString = proc bb*(cli: HwylCliHelp): BbString =
result = bb(render(cli)) result = bb(bbImpl(cli))
# ---------------------------------------- proc `$`*(cli: HwylCliHelp): string =
result = $bb(cli)
type type
Count* = object ## Count type for an incrementing flag CliSetting = enum
val*: int NoHelpFlag, NoArgsShowHelp
type
# ----
CliSetting* = enum
NoHelpFlag, ## Remove the builtin help flag
ShowHelp, ## If cmdline empty show help
NoNormalize ## Don't normalize flags and commands
BuiltinFlag = object
name*: string
short*: char
long*: string
help*: NimNode
node: NimNode
CliFlag = object CliFlag = object
name*: string name*: string
ident*: NimNode ident*: string
default*: NimNode default*: NimNode
typeNode*: NimNode typeSym*: string
short*: char short*: char
long*: string long*: string
help*: NimNode help*: NimNode
CliCfg = object CliCfg = object
stopWords*: seq[string] stopWords*: seq[string]
styles: NimNode styles: NimNode
@ -159,60 +144,74 @@ type
subName*: string # used for help the generator subName*: string # used for help the generator
version*, usage*: NimNode version*, usage*: NimNode
flags*: seq[CliFlag] flags*: seq[CliFlag]
builtinFlags*: seq[BuiltinFlag]
flagGroups: Table[string, seq[CliFlag]]
required*: seq[string] required*: seq[string]
inheritFlags*: seq[string] globalFlags*: seq[CliFlag]
root*: bool
# some debug procs I use to wrap my ahead aroung the magic of *macro* {.push hint[XDeclaredButNotUsed]:off .}
func `<<<`(n: NimNode) {.used.} = func peekNode(n: NimNode) =
## for debugging macros ## for debugging macros
debugEcho treeRepr n debugEcho treeRepr n
func `<<<`(s: string) {.used.} = {.pop.}
debugEcho s
func bad(n: NimNode, argument: string = "") = # TODO: do i need this?
var msg = "unexpected node kind: " & $n.kind func newCliFlag(): CliFlag =
if argument != "": result.help = newLit("")
msg &= " for argument: " & argument
error msg
func getFlagParamNode(node: NimNode): NimNode = template badNode =
error "unexpected node kind: " & $node.kind
func typeSymFromNode(node: NimNode): string =
case node.kind case node.kind
of nnkStrLit: of nnkIdent, nnkStrLit:
result = node result = node.strVal
of nnkStmtList: of nnkBracketExpr:
result = node[0] result = node[0].strVal & "[" & node[1].strVal & "]"
of nnkCommand: else: badNode
result = node[1]
of nnkPrefix: # NOTE: should i double check prefix value?
result = node[1]
else: bad(node, "flag param")
func parseFlagParams(f: var CliFlag, node: NimNode) = func getOptTypeSym(node: NimNode): string =
expectKind node, nnkStmtList case node.kind:
for n in node: of nnkCommand:
case n.kind 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: of nnkCall, nnkCommand, nnkPrefix:
case n[0].strVal case optOpt[0].strVal
of "help","?": of "help","?":
f.help = getFlagParamNode(n[1]) opt.help = getOptOptNode(optOpt[1])
of "short", "-": of "short", "-":
let val = getFlagParamNode(n).strVal let val = getOptOptNode(optOpt).strVal
if val.len > 1: if val.len > 1:
error "short flag must be a char" error "short flag must be a char"
f.short = val[0].char opt.short = val[0].char
of "*", "default": of "*", "default":
f.default = getFlagParamNode(n) opt.default = getOptOptNode(optOpt)
of "i", "ident": of "i", "ident":
f.ident = getFlagParamNode(n).strVal.ident opt.ident = getOptOptNode(optOpt).strVal
of "T": of "T":
f.typeNode = n[1] opt.typeSym = getOptTypeSym(optOpt)
else: else:
error "unexpected setting: " & n[0].strVal error "unexpected option setting: " & optOpt[0].strVal
else: else:
bad(n, "flag params") error "unexpected option node type: " & $optOpt.kind
func startFlag(f: var CliFlag, n: NimNode) = func startFlag(f: var CliFlag, n: NimNode) =
f.name = f.name =
@ -221,8 +220,6 @@ func startFlag(f: var CliFlag, n: NimNode) =
of nnkAccQuoted: collect(for c in n[0]: c.strVal).join("") of nnkAccQuoted: collect(for c in n[0]: c.strVal).join("")
else: error "unexpected node kind for option" else: error "unexpected node kind for option"
f.help = newLit("") # by default no string
# assume a single character is a short flag # assume a single character is a short flag
if f.name.len == 1: if f.name.len == 1:
f.short = f.name[0].char f.short = f.name[0].char
@ -231,8 +228,10 @@ func startFlag(f: var CliFlag, n: NimNode) =
func parseCliFlag(n: NimNode): CliFlag = func parseCliFlag(n: NimNode): CliFlag =
if n.kind notin [nnkCommand, nnkCall]: if n.kind notin [nnkCommand, nnkCall]:
bad(n, "flags") error "unexpected node kind: " & $n.kind
# deduplicate these...
result = newCliFlag()
startFlag(result, n) startFlag(result, n)
# option "some help desc" # option "some help desc"
if n.kind == nnkCommand: if n.kind == nnkCommand:
@ -240,36 +239,18 @@ func parseCliFlag(n: NimNode): CliFlag =
# option: # option:
# help "some help description" # help "some help description"
else: else:
parseFlagParams(result, n[1]) parseOptOpts(result, n[1])
if result.ident == nil: if result.ident == "":
result.ident = result.name.ident result.ident = result.name
if result.typeNode == nil: if result.typeSym == "":
result.typeNode = ident"string" result.typeSym = "string"
# TODO: change how this works?
func parseCliFlags(cfg: var CliCfg, node: NimNode) = func parseCliFlags(flags: NimNode): seq[CliFlag] =
var group: string expectKind flags, nnkStmtList
expectKind node, nnkStmtList for f in flags:
for n in node: result.add parseCliFlag(f)
var flag: CliFlag
case n.kind
of nnkCall, nnkCommand:
flag = parseCliFlag(n)
if group == "":
cfg.flags.add flag
else:
if group notin cfg.flagGroups: cfg.flagGroups[group] = @[flag]
else: cfg.flagGroups[group].add flag
of nnkBracket:
group = n[0].strVal
continue
of nnkPrefix:
if n[0].kind != nnkIdent and n[0].strVal != "^":
error "unexpected node in flags: " & $n.kind
expectKind n[1], nnkBracket
cfg.inheritFlags.add n[1][0].strVal
else: bad(n, "flag")
func parseCliSetting(s: string): CliSetting = func parseCliSetting(s: string): CliSetting =
try: parseEnum[CliSetting](s) try: parseEnum[CliSetting](s)
@ -302,7 +283,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 = "", root: bool= false): CliCfg func parseCliBody(body: NimNode, name: string = ""): CliCfg
func isSubMarker(node: NimNode): bool = func isSubMarker(node: NimNode): bool =
if node.kind == nnkPrefix: if node.kind == nnkPrefix:
@ -329,25 +310,13 @@ func sliceStmts(node: NimNode): seq[
start = i + 1 start = i + 1
func addInheritedFlags(child: var CliCfg, parent: CliCfg, self = false) = func addGlobalFlagsFrom(child: var CliCfg, parent: CliCfg) =
let names = child.flags.mapIt(it.name) let names = child.flags.mapIt(it.name)
var groups: seq[string] for f in parent.globalFlags:
if not self: if f.name in names:
groups.add child.inheritFlags error "global flag " & f.name & " conflicts with command flag"
child.flags.add f
# autoinherit the "global" flags
if "global" in parent.flagGroups:
groups.add "global"
for g in groups:
if g notin parent.flagGroups:
debugEcho parent.flagGroups.keys().toSeq()
error "expected flag group: " & g & " to exist in parent command"
for f in parent.flagGroups[g]:
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) = func parseCliSubcommands(cfg: var CliCfg, node: NimNode) =
expectKind node[1], nnkStmtList expectKind node[1], nnkStmtList
for (name, s) in sliceStmts(node[1]): for (name, s) in sliceStmts(node[1]):
@ -356,7 +325,8 @@ func parseCliSubcommands(cfg: var CliCfg, node: NimNode) =
nnkStmtList.newTree(node[1][s]), cfg.name & " " & name nnkStmtList.newTree(node[1][s]), cfg.name & " " & name
) )
subCfg.subName = name subCfg.subName = name
subCfg.addInheritedFlags(cfg) subCfg.addGlobalFlagsFrom(cfg)
cfg.subcommands.add subCfg cfg.subcommands.add subCfg
func parseHiddenFlags(cfg: var CliCfg, node: NimNode) = func parseHiddenFlags(cfg: var CliCfg, node: NimNode) =
@ -375,41 +345,8 @@ func parseHiddenFlags(cfg: var CliCfg, node: NimNode) =
cfg.hidden.add n.strVal cfg.hidden.add n.strVal
else: assert false else: assert false
func addBuiltinFlags(cfg: var CliCfg) = func parseCliBody(body: NimNode, name = ""): CliCfg =
# duplicated with below :/
let shorts = cfg.flags.mapIt(it.short).toHashSet()
let
name = cfg.name.replace(" ", "")
printHelpName = ident("print" & name & "Help")
if NoHelpFlag notin cfg.settings:
let helpNode = quote do:
`printHelpName`(); quit 0
cfg.builtinFlags.add BuiltinFlag(
name: "help",
long: "help",
help: newLit("show this help"),
short: if 'h' notin shorts: 'h' else: '\x00',
node: helpNode
)
if cfg.version != nil:
let version = cfg.version
let versionNode = quote do:
echo `version`; quit 0
cfg.builtinFlags.add BuiltinFlag(
name:"version",
long: "version",
help: newLit("print version"),
short: if 'V' notin shorts: 'V' else: '\x00',
node: versionNode
)
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
@ -424,8 +361,10 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
result.usage = call[1] result.usage = call[1]
of "description", "...": of "description", "...":
result.desc = call[1] result.desc = call[1]
of "globalFlags":
result.globalFlags = parseCliFlags(call[1])
of "flags": of "flags":
parseCliFlags(result, call[1]) result.flags = parseCliFlags(call[1])
of "settings": of "settings":
parseCliSettings(result, call) parseCliSettings(result, call)
of "stopWords": of "stopWords":
@ -451,31 +390,32 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
sub.pre = result.preSub sub.pre = result.preSub
sub.post = result.postSub sub.post = result.postSub
result.addInheritedFlags(result, self = true) result.addGlobalFlagsFrom(result)
if result.name == "": if result.name == "":
error "missing required option: name" error "missing required option: name"
# TODO: validate "required" flags exist here
result.addBuiltinFlags()
func flagToTuple(f: CliFlag | BuiltinFlag): NimNode =
let
short =
if f.short != '\x00': newLit($f.short)
else: newLit("")
long = newLit(f.long)
help = f.help
quote do:
(`short`, `long`, `help`)
func flagsArray(cfg: CliCfg): NimNode = func flagsArray(cfg: CliCfg): NimNode =
result = newTree(nnkBracket) result = newTree(nnkBracket)
for f in cfg.flags: for f in cfg.flags:
if f.name in cfg.hidden: continue if f.name in cfg.hidden: continue
result.add f.flagToTuple() let
for f in cfg.builtinFlags: help = f.help
result.add f.flagToTuple() 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 = func subCmdsArray(cfg: CliCfg): NimNode =
result = newTree(nnkBracket) result = newTree(nnkBracket)
@ -485,7 +425,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 =
@ -497,6 +437,7 @@ func defaultUsage(cfg: CliCfg): NimNode =
func generateCliHelperProc(cfg: CliCfg, printHelpName: NimNode): NimNode = func generateCliHelperProc(cfg: CliCfg, printHelpName: NimNode): NimNode =
let let
name = newLit(cfg.name)
desc = cfg.desc or newLit("") desc = cfg.desc or newLit("")
usage = cfg.usage or defaultUsage(cfg) usage = cfg.usage or defaultUsage(cfg)
helpFlags = cfg.flagsArray() helpFlags = cfg.flagsArray()
@ -505,204 +446,193 @@ func generateCliHelperProc(cfg: CliCfg, printHelpName: NimNode): NimNode =
result = quote do: result = quote do:
proc `printHelpName`() = proc `printHelpName`() =
echo bb(render(newHwylCliHelp( echo newHwylCliHelp(
cmd = `name`,
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) = # NOTE: is there a better way to do this?
target = true proc checkVarSet[T](name: string, target: T) =
var default: T
if target == default:
hwylCliError("missing required flag: [b]" & name)
proc parse*(p: OptParser, key: string, val: string, target: var string) = proc checkDefaultExists[T](target: T, key: string, val: string) =
target = val var default: T
if target == default and val == "":
hwylCliError("expected value for: [b]" & key)
proc parse*(p: OptParser, key: string, val: string, target: var int) = proc tryParseInt(key: string, val: string): int =
try: try:
target = parseInt(val) result = parseInt(val)
except: except:
hwylCliError( hwylCliError(
"failed to parse value for [b]" & key & "[/] as integer: [b]" & val "failed to parse value for [b]" & key & "[/] as integer: [b]" & val
) )
macro enumNames(a: typed): untyped = func addOrOverwrite[T](target: var seq[T], default: seq[T], val: T) =
## unexported macro copied from std/enumutils if target != default:
result = newNimNode(nnkBracket) target.add val
for ai in a.getType[1][1..^1]:
assert ai.kind == nnkSym
result.add newLit ai.strVal
proc parse*[E: enum](p: OptParser, key: string, val: string, target: var E) =
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
)
proc parse*(p: OptParser, key: string, val: string, target: var float) =
try:
target = parseFloat(val)
except:
hwylCliError(
"failed to parse value for [b]" & key & "[/] as float: [b]" & val
)
proc parse*[T](p: OptParser, key: string, val: string, target: var seq[T]) =
var parsed: T
parse(p, key, val, parsed)
target.add parsed
proc parse*(p: OptParser, key: string, val: string, target: var Count) =
# if value set to that otherwise increment
if val != "":
var num: int
parse(p, key, val, num)
target.val = num
else: else:
inc target.val target = @[val]
func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): NimNode = func assignField(f: CliFlag): NimNode =
var caseStmt = nnkCaseStmt.newTree() let key = ident"key"
if NoNormalize notin cfg.settings: let varName = ident(f.ident)
caseStmt.add nnkCall.newTree(ident"optionNormalize", ident"key")
else:
caseStmt.add ident"key"
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: hwylCliError("empty flag not supported currently")) caseStmt.add nnkOfBranch.newTree(newLit(""), quote do: hwylCliError("empty flag not supported currently"))
for f in cfg.builtinFlags: if NoHelpFlag notin cfg.settings:
var branch = nnkOfBranch.newTree() caseStmt.add nnkOfBranch.newTree(
if f.long != "": branch.add(newLit(f.long)) newLit("h"), newLit("help"),
if f.short != '\x00': branch.add(newLit($f.short)) quote do:
branch.add f.node `printHelpName`(); quit 0
caseStmt.add branch )
if cfg.version != nil:
caseStmt.add nnkOfBranch.newTree(
newLit("V"), newLit("version"),
quote do:
echo `version`; quit 0
)
# 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 != "": if f.long != "": branch.add(newLit(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 branch.add assignField(f)
let name = newLit(f.name)
branch.add quote do:
flagSet.incl `name`
parse(p, key, val, `varName`)
caseStmt.add branch caseStmt.add branch
caseStmt.add nnkElse.newTree(quote do: hwylCliError("unknown flag: [b]" & key)) caseStmt.add nnkElse.newTree(quote do: hwylCliError("unknown flag: [b]" & key))
result = nnkStmtList.newTree(caseStmt) result = nnkStmtList.newTree(caseStmt)
func isBool(f: CliFlag): bool =
f.typeNode == ident"bool"
func isCount(f: CliFlag): bool =
f.typeNode == ident"Count"
func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] = func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] =
let flagFlags = cfg.flags.filterIt(it.isBool or it.isCount) var long = nnkBracket.newTree()
let long = var short = nnkCurly.newTree()
nnkBracket.newTree(
(flagFlags.mapIt(it.long) & cfg.builtinFlags.mapIt(it.long)).filterIt(it != "").mapIt(newLit(it)) if NoHelpFlag notin cfg.settings:
) long.add newLit("help")
let short = short.add newLit('h')
nnkCurly.newTree(
(flagFlags.mapIt(it.short) & cfg.builtinFlags.mapIt(it.short)).filterIt(it != '\x00').mapIt(newLit(it)) 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) result = (nnkPrefix.newTree(ident"@",long), short)
func setFlagVars(cfg: CliCfg): NimNode = func setFlagVars(cfg: CliCfg): NimNode =
result = nnkVarSection.newTree().add( result = nnkVarSection.newTree()
cfg.flags.mapIt( # TODO: generalize this better...
nnkIdentDefs.newTree(it.ident, it.typeNode, newEmptyNode())
)
)
func literalFlags(f: CliFlag): NimNode =
var flags: seq[string]
if f.short != '\x00': flags.add "[b]" & "-" & $f.short & "[/]"
if f.long != "": flags.add "[b]" & "--" & f.long & "[/]"
result = newLit(flags.join("|"))
func addPostParseCheck(cfg: CliCfg, body: NimNode) =
## generate block to set defaults and check for required flags
let flagSet = ident"flagSet"
var required, default: seq[CliFlag]
for f in cfg.flags: for f in cfg.flags:
if f.name in cfg.required and f.default == nil:
required.add f
elif f.default != nil:
default.add f
for f in required:
let flagLit = f.literalFlags
let name = newLit(f.name)
body.add quote do:
if `name` notin `flagSet`:
hwylCliError("expected a value for flag: " & `flagLit`)
for f in default:
let let
name = newLit(f.name) t =
target = f.ident if f.typeSym == "seq[string]": nnkBracketExpr.newTree(newIdentNode("seq"),newIdentNode("string"))
default = f.default elif f.typeSym == "seq[int]" : nnkBracketExpr.newTree(newIdentNode("seq"),newIdentNode("string"))
body.add quote do: else: ident(f.typeSym)
if `name` notin `flagSet`: val =
`target` = `default` 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 =
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(" ", "")
printHelpName = ident("print" & name & "Help") printHelpName = ident("print" & name & "Help")
parserProcName = ident("parse" & name) parserProcName = ident("parse" & name)
args = ident"args"
optParser = ident("p")
cmdLine = ident"cmdLine"
flagSet = ident"flagSet"
kind = ident"kind"
key = ident"key"
val = ident"val"
(longNoVal, shortNoVal) = cfg.getNoVals()
printHelperProc = generateCliHelperProc(cfg, printHelpName)
flagVars = setFlagVars(cfg)
result = newTree(nnkStmtList) result = newTree(nnkStmtList)
var let
parserBody = nnkStmtList.newTree() printHelperProc = generateCliHelperProc(cfg, printHelpName)
stopWords = nnkBracket.newTree(newLit("--")) 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: for w in cfg.stopWords:
stopWords.add newLit(w) stopWords.add newLit(w)
stopWords = nnkPrefix.newTree(ident"@", stopWords) stopWords = nnkPrefix.newTree(ident"@", stopWords)
# should this a CritBitTree?
parserBody.add quote do:
var `flagSet`: HashSet[string]
parserBody.add( parserBody.add(
quote do: quote do:
var `optParser` = initOptParser( var `optParser` = initOptParser(
@`cmdLine`, @`cmdLine`,
longNoVal = `longNoVal`, longNoVal = `longNoVal`,
shortNoVal = `shortNoVal`, shortNoVal = `shortNoVal`,
stopWords = `stopWords`, stopWords = `stopWords`
opChars = {','}
) )
) )
# TODO: first key needs to be normalized
let
kind = ident"kind"
key = ident"key"
val = ident"val"
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"))),
@ -716,10 +646,7 @@ func hwylCliImpl(cfg: CliCfg): 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: result.add `key`),
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)
@ -728,14 +655,14 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
) )
) )
if ShowHelp in cfg.settings: if NoArgsShowHelp 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
let runProcName = ident("run" & name) let runProcName = ident("run" & name)
let runBody = nnkStmtList.newTree() let runBody = nnkStmtList.newTree()
addPostParseCheck(cfg, parserBody) addRequiredFlagsCheck(cfg, runBody)
# move to proc? # move to proc?
if cfg.pre != nil: if cfg.pre != nil:
runBody.add cfg.pre runBody.add cfg.pre
@ -744,31 +671,32 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
if cfg.post != nil: if cfg.post != nil:
runBody.add cfg.post runBody.add cfg.post
# let runBody = cfg.run or nnkStmtList.newTree(nnkDiscardStmt.newTree(newEmptyNode()))
let args = ident"args"
if cfg.subcommands.len > 0: if cfg.subcommands.len > 0:
var handleSubCommands = nnkStmtList.newTree() var handleSubCommands = nnkStmtList.newTree()
handleSubCommands.add quote do: handleSubCommands.add quote do:
if `args`.len == 0: if `args`.len == 0:
hwylCliError("expected subcommand") hwylCliError("expected subcommand")
var subCommandCase = nnkCaseStmt.newTree() var subCommandCase = nnkCaseStmt.newTree(
if NoNormalize notin cfg.settings: quote do: `args`[0]
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(optionNormalize(sub.subName)), newLit(sub.subName),
hwylCliImpl(sub) hwylCliImpl(sub)
) )
subcommandCase.add nnkElse.newTree( subcommandCase.add nnkElse.newTree(
quote do: quote do:
hwylCliError("unknown subcommand: [b]" & `args`[0]) hwylCliError("unknown subcommand " & `args`[0])
) )
runBody.add handleSubCommands.add subCommandCase runBody.add handleSubCommands.add subCommandCase
result.add quote do: result.add quote do:
# block: # block:
`printHelperProc` `printHelperProc`
@ -780,7 +708,7 @@ func hwylCliImpl(cfg: CliCfg): NimNode =
let `args` = `parserProcName`(`cmdLine`) let `args` = `parserProcName`(`cmdLine`)
`runBody` `runBody`
if cfg.root: if root:
result.add quote do: result.add quote do:
`runProcName`() `runProcName`()
else: else:
@ -789,6 +717,51 @@ func hwylCliImpl(cfg: CliCfg): 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, root = true) var cfg = parseCliBody(body)
hwylCliImpl(cfg) 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"
* @["config.yml"]
flags:
check:
T bool
? "load config and exit"
- c
run:
echo "hello from the main command"
echo fmt"{config=}, {check=}"
subcommands:
--- a
... "the \"a\" subcommand"
flags:
`long-flag` "some help"
flagg "some other help"
run:
echo "hello from hwylterm sub command!"
--- b
... """
some "B" command
a longer mulitline description that will be visibil in the subcommand help
it will automatically be "bb"'ed [bold]this is bold text[/]
"""
flags:
aflag:
T bool
? "some help"
bflag:
? "some other flag?"
* "wow"
run:
echo "hello from hwylterm sub `b` command"
echo aflag, bflag

View file

@ -1,74 +0,0 @@
import std/[strformat, strutils]
import hwylterm, hwylterm/hwylcli
type
Color = enum
red, blue, green
hwylCli:
name "example"
V "0.1.0"
... "a description of hwylterm"
flags:
[global]
yes:
T bool
? "set flag to yes"
[config]
confiG:
T seq[string]
? "path to config file"
* @["config.yml"]
preSub:
echo "this is run after subcommand parsing but before its run block"
run:
echo "this is always run prior to subcommand parsing"
subcommands:
--- "onelonger"
... """
the first subcommand
this command features both an enum flag and a Count flag
it also inherits the `[[config]` flag group
"""
flags:
color:
T Color
? "a color (red, green, blue)"
verbose:
T Count
? "a count flag"
- v
^[config]
run:
echo "hello from `example one` command!"
echo fmt"{color=}"
echo fmt"{verbose=}"
echo fmt"{config=}"
--- "two-longer"
... """
some second subcommand
a longer mulitline description that will be visible in the subcommand help
and it will automatically be "bb"'ed [bold]this is bold text[/]
"""
flags:
a:
T bool
? "some help"
b:
T seq[float]
? "multiple floats"
h "this will override the builtin 'h' for help"
run:
echo "hello from `example b` command"
echo fmt"{a=}, {b=}"

18
todo.md
View file

@ -14,9 +14,25 @@
- [ ] 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 - [ ] console object with customizable options to apply formatting
### cli generator ### cli generato
- [ ] add support for types(metavars)/defaults/required in help output - [ ] add support for types(metavars)/defaults/required in help output
- [ ] abstract the `globalFlags` argument to a `flagGroups` section with a builtin `global`
this would allow users to "inherit" flag groups in subcommands
```nim
flags:
# global flag group auto propagated down
--- global
config "path to config"
--- shared
shared:
`a-flag` "some shared flag"
-- sub
flags:
^shared
unique "some unique flag"
```
## features ## features