rework default flags

This commit is contained in:
Daylin Morgan 2024-11-10 01:49:31 -06:00
parent cb231dfc94
commit d228123195
Signed by: daylin
GPG key ID: 950D13E9719334AD
2 changed files with 153 additions and 147 deletions

View file

@ -8,7 +8,7 @@ import std/[
sugar sugar
] ]
import ./[bbansi, parseopt3] import ./[bbansi, parseopt3]
export parseopt3 export parseopt3, sets
type type
HwylFlagHelp* = tuple HwylFlagHelp* = tuple
@ -129,9 +129,10 @@ type
node: NimNode node: NimNode
CliFlag = object CliFlag = object
name*: string name*: string
ident*: string ident*: NimNode
default*: NimNode default*: NimNode
typeSym*: string typeSym*: string
typeNode*: NimNode
short*: char short*: char
long*: string long*: string
help*: NimNode help*: NimNode
@ -164,8 +165,14 @@ func `<<<`(s: string) =
func newCliFlag(): CliFlag = func newCliFlag(): CliFlag =
result.help = newLit("") result.help = newLit("")
template badNode = func bad(n: NimNode, argument: string = "") =
error "unexpected node kind: " & $node.kind
var msg = "unexpected node kind: " & $n.kind
if argument != "":
msg &= " for argument: " & argument
# error "unexpected node kind: " & $n.kind
error msg
func typeSymFromNode(node: NimNode): string = func typeSymFromNode(node: NimNode): string =
case node.kind case node.kind
@ -173,7 +180,7 @@ func typeSymFromNode(node: NimNode): string =
result = node.strVal result = node.strVal
of nnkBracketExpr: of nnkBracketExpr:
result = node[0].strVal & "[" & node[1].strVal & "]" result = node[0].strVal & "[" & node[1].strVal & "]"
else: badNode else: bad node
func getOptTypeSym(node: NimNode): string = func getOptTypeSym(node: NimNode): string =
case node.kind: case node.kind:
@ -212,8 +219,10 @@ func parseOptOpts(opt: var CliFlag, optOpts: NimNode) =
of "*", "default": of "*", "default":
opt.default = getOptOptNode(optOpt) opt.default = getOptOptNode(optOpt)
of "i", "ident": of "i", "ident":
opt.ident = getOptOptNode(optOpt).strVal opt.ident = getOptOptNode(optOpt).strVal.ident
of "T": of "T":
opt.typeNode = optOpt[1]
# TODO: remove this...
opt.typeSym = getOptTypeSym(optOpt) opt.typeSym = getOptTypeSym(optOpt)
else: else:
error "unexpected option setting: " & optOpt[0].strVal error "unexpected option setting: " & optOpt[0].strVal
@ -248,8 +257,10 @@ func parseCliFlag(n: NimNode): CliFlag =
else: else:
parseOptOpts(result, n[1]) parseOptOpts(result, n[1])
if result.ident == "": if result.ident == nil:
result.ident = result.name result.ident = result.name.ident
if result.typeNode == nil:
result.typeNode = ident"string"
if result.typeSym == "": if result.typeSym == "":
result.typeSym = "string" result.typeSym = "string"
@ -499,66 +510,32 @@ func generateCliHelperProc(cfg: CliCfg, printHelpName: NimNode): NimNode =
styles = `styles`, styles = `styles`,
) )
# NOTE: is there a better way to do this? proc parse*(p: OptParser, key: string, val: string, target: var bool) =
proc checkVarSet[T](name: string, target: T) = target = true
var default: T
if target == default:
hwylCliError("missing required flag: [b]" & name)
proc checkDefaultExists[T](target: T, key: string, val: string) = proc parse*(p: OptParser, key: string, val: string, target: var string) =
var default: T target = val
if target == default and val == "":
hwylCliError("expected value for: [b]" & key)
proc tryParseInt(key: string, val: string): int = proc parse*(p: OptParser, key: string, val: string, target: var int) =
try: try:
result = parseInt(val) target = 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
) )
func addOrOverwrite[T](target: var seq[T], default: seq[T], val: T) = proc parse*(p: OptParser, key: string, val: string, target: var float) =
if target != default: try:
target.add val target = parseFloat(val)
else: except:
target = @[val] hwylCliError(
"failed to parse value for [b]" & key & "[/] as float: [b]" & val
)
func assignField(f: CliFlag): NimNode = proc parse[T](p: OptParser, key: string, val: string, target: var seq[T]) =
let key = ident"key" var parsed: T
let varName = ident(f.ident) parse(p, key, val, parsed)
target.add parsed
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 = func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): NimNode =
var caseStmt = nnkCaseStmt.newTree(ident("key")) var caseStmt = nnkCaseStmt.newTree(ident("key"))
@ -571,20 +548,28 @@ func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): N
branch.add f.node branch.add f.node
caseStmt.add branch caseStmt.add branch
# 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(f.long))
if f.short != '\x00': branch.add(newLit($f.short)) if f.short != '\x00': branch.add(newLit($f.short))
branch.add assignField(f) let varName = f.ident
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 getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] = func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] =
let boolFlags = cfg.flags.filterIt(it.typeSym == "bool") let boolFlags = cfg.flags.filterIt(it.isBool)
let long = let long =
nnkBracket.newTree( nnkBracket.newTree(
(boolFlags.mapIt(it.long) & cfg.builtinFlags.mapIt(it.long)).filterIt(it != "").mapIt(newLit(it)) (boolFlags.mapIt(it.long) & cfg.builtinFlags.mapIt(it.long)).filterIt(it != "").mapIt(newLit(it))
@ -596,54 +581,77 @@ func getNoVals(cfg: CliCfg): tuple[long: NimNode, short: NimNode] =
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() result = nnkVarSection.newTree().add(
# TODO: generalize this better... cfg.flags.mapIt(
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:
let if f.name in cfg.required and f.default == nil:
t = required.add f
if f.typeSym == "seq[string]": nnkBracketExpr.newTree(newIdentNode("seq"),newIdentNode("string")) elif f.default != nil:
elif f.typeSym == "seq[int]" : nnkBracketExpr.newTree(newIdentNode("seq"),newIdentNode("string")) default.add f
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) for f in required:
let flagLit = f.literalFlags
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 name = newLit(f.name)
let flag = ident(f.ident)
body.add quote do: body.add quote do:
checkVarSet(`name`, `flag`) if `name` notin `flagSet`:
hwylCliError("expected a value for flag: " & `flagLit`)
for f in default:
let
name = newLit(f.name)
target = f.ident
default = f.default
body.add quote do:
if `name` notin `flagSet`:
`target` = `default`
func hwylCliImpl(cfg: CliCfg, root = false): NimNode = func hwylCliImpl(cfg: CliCfg, root = false): 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"
result = newTree(nnkStmtList) optParser = ident("p")
cmdLine = ident"cmdLine"
let flagSet = ident"flagSet"
kind = ident"kind"
key = ident"key"
val = ident"val"
(longNoVal, shortNoVal) = cfg.getNoVals()
printHelperProc = generateCliHelperProc(cfg, printHelpName) printHelperProc = generateCliHelperProc(cfg, printHelpName)
flagVars = setFlagVars(cfg) flagVars = setFlagVars(cfg)
var parserBody = nnkStmtList.newTree() result = newTree(nnkStmtList)
let
optParser = ident("p") var
cmdLine = ident"cmdLine" parserBody = nnkStmtList.newTree()
(longNoVal, shortNoVal) = cfg.getNoVals() stopWords = nnkBracket.newTree(newLit("--"))
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(
@ -654,11 +662,6 @@ func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
) )
) )
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"))),
@ -672,7 +675,10 @@ 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"), quote do: result.add `key`), 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)
@ -688,7 +694,7 @@ func hwylCliImpl(cfg: CliCfg, root = false): NimNode =
let runProcName = ident("run" & name) let runProcName = ident("run" & name)
let runBody = nnkStmtList.newTree() let runBody = nnkStmtList.newTree()
addRequiredFlagsCheck(cfg, runBody) addPostParseCheck(cfg, parserBody)
# move to proc? # move to proc?
if cfg.pre != nil: if cfg.pre != nil:
runBody.add cfg.pre runBody.add cfg.pre
@ -697,10 +703,6 @@ func hwylCliImpl(cfg: CliCfg, root = false): 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:
@ -746,49 +748,3 @@ macro hwylCli*(body: untyped) =
var cfg = parseCliBody(body) var cfg = parseCliBody(body)
hwylCliImpl(cfg, root = true) hwylCliImpl(cfg, root = true)
when isMainModule:
import std/strformat
hwylCli:
name "hwylterm"
version "0.1.0"
... "a description of hwylterm"
flags:
check:
T bool
? "load config and exit"
config:
T seq[string]
? "path to config file"
* @["config.yml"]
run:
echo "hello from the main command"
echo fmt"{config=}, {check=}"
subcommands:
--- a
... "the \"a\" subcommand"
flags:
# ^ other
`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

50
tests/example.nim Normal file
View file

@ -0,0 +1,50 @@
import std/strformat
import hwylterm/hwylcli
hwylCli:
name "example"
V "0.1.0"
... "a description of hwylterm"
flags:
yes:
T bool
? "set flag to yes"
config:
T seq[string]
? "path to config file"
* @["config.yml"]
run:
echo "this is always run prior to subcommand parsing"
echo fmt"{yes=}, {config=}"
subcommands:
--- one
... "the first subcommand"
required flag
flags:
`long-flag` "some help"
flag:
? "some other help"
run:
echo "hello from `example one` command!"
echo "long-flag and flag are: " & `long-flag` & "," & `flag` & " by default strings"
--- two
... """
some second subcommand
a longer mulitline description that will be visible in the subcommand help
it will automatically be "bb"'ed [bold]this is bold text[/]
"""
flags:
aflag:
T bool
? "some help"
bflag:
T seq[float]
? "multiple floats"
run:
echo "hello from `example b` command"
echo fmt"{aflag=}, {bflag=}"