extended flag syntax

This commit is contained in:
Daylin Morgan 2025-02-05 18:40:31 -06:00
parent 2b1c7b607e
commit f4c45d1ae4
Signed by: daylin
GPG key ID: 950D13E9719334AD
5 changed files with 228 additions and 47 deletions

View file

@ -276,19 +276,20 @@ type
func hasSubcommands(c: CliCfg): bool = c.subcommands.len > 0
template `<<<`(s: string) {.used.} =
let pos = instantiationInfo()
debugEcho "$1:$2" % [pos.filename, $pos.line]
debugEcho s
debugEcho "^^^^^^^^^^^^^^^^^^^^^"
# template `<<<`(s: string) {.used.} =
# let pos = instantiationInfo()
# debugEcho "$1:$2" % [pos.filename, $pos.line]
# debugEcho s
# some debug procs I use to wrap my ahead aroung the magic of *macro*
template `<<<`(n: NimNode) {.used.} =
## for debugging macros
<<< treeRepr n
<<< ("TreeRepr:\n" & (treeRepr n))
<<< ("Repr: \n" & (repr n))
template `<<<`(n: untyped) {.used.} =
debugEcho n, "|||", instantiationInfo().line
debugEcho n, "....................", instantiationInfo().line
func `<<<`(f: CliFlag) {.used.}=
var s: string
@ -321,24 +322,27 @@ func prettyRepr(n: NimNode): string =
result.add ""
result.add "".repeat(maxWidth + 2)
func err(c: CliCfg, node: NimNode, msg: string = "") =
func err(c: CliCfg, node: NimNode, msg: string = "", instantiationInfo: tuple[filename: string, line: int, column: int]) =
var fullMsg: string
fullMsg.add node.prettyRepr() & "\n"
fullMsg.add "parsing error"
fullMsg.add "parsing error ($1, $2) " % [instantiationInfo.filename, $instantiationInfo.line]
if msg != "":
fullMsg.add ": " & msg
c.err fullMsg
func expectLen(c: CliCfg, node: NimNode, length: Natural) =
template err(c: CliCfg, node: NimNode, msg: string = "") =
err c, node, msg, instantiationInfo()
template expectLen(c: CliCfg, node: NimNode, length: Natural) =
if node.len != length:
c.err node, fmt"expected node to be length {length} not {node.len}"
c.err node, "expected node to be length $1 not $2" % [$length, $node.len], instantiationInfo()
func expectKind(c: CliCfg, node: NimNode, kinds: varargs[NimNodeKind]) =
template expectKind(c: CliCfg, node: NimNode, kinds: varargs[NimNodeKind]) =
if node.kind notin kinds:
c.err node, fmt"expected node kind to be one of: $1 but got $2" % [$kinds, $node.kind]
c.err node, "expected node kind to be one of: $1 but got $2" % [$kinds, $node.kind], instantiationInfo()
func unexpectedKind(c: CliCfg, node: NimNode) =
c.err node, fmt"unexpected node kind: $1" & $node.kind
template unexpectedKind(c: CliCfg, node: NimNode) =
c.err node, "unexpected node kind: $1" % $node.kind, instantiationInfo()
template parseCliSetting(s: string) =
@ -389,7 +393,7 @@ func getFlagParamNode(c: CliCfg, node: NimNode): NimNode =
else: c.unexpectedKind node
# TODO: also accept the form `flag: "help"`
func parseFlagParams(c: CliCfg, f: var CliFlag, node: NimNode) =
func parseFlagStmtList(c: CliCfg, f: var CliFlag, node: NimNode) =
c.expectKind node, nnkStmtList
for n in node:
case n.kind
@ -418,41 +422,148 @@ func parseFlagParams(c: CliCfg, f: var CliFlag, node: NimNode) =
else:
c.unexpectedKind n
func newFlag(cfg: var CliCfg , n: NimNode): CliFlag =
cfg.expectKind n[0], nnkIdent, nnkStrLit, nnkAccQuoted
func getShortChar(c: CliCfg, n: NimNode): char =
let val = n.strVal
if val.len > 1:
c.err n, "short flag must be a char not: " & val
result = val[0].char
case n[0].kind:
func parseCliFlagCall(c: var CliCfg, f: var CliFlag, nodes: seq[NimNode]) =
# TODO: be more careful here?
# TODO: ignore positionals that are ident'_'
template `<-`(target: untyped, node: NimNode) =
if node != ident"_":
target = node
case nodes.len:
# flag("help string")
of 1:
f.help <- nodes[0]
# flag(T, "help string")
of 2:
f.typeNode <- nodes[0]
f.help <- nodes[1]
# flag(NimNode , T , "help string")
of 3:
f.defaultVal <- nodes[0]
f.typeNode <- nodes[1]
f.help <- nodes[2]
else:
c.err "unexpected number of parameters for flag"
func newFlag(cfg: var CliCfg , n: NimNode): CliFlag =
cfg.expectKind n, nnkIdent, nnkStrLit, nnkAccQuoted
case n.kind:
of nnkIdent, nnkStrLit:
result.name = n[0].strVal
result.name = n.strVal
of nnkAccQuoted:
result.name = collect(for c in n[0]: c.strVal).join("")
else: cfg.unexpectedKind n[0]
result.name = collect(for c in n: c.strVal).join("")
else: cfg.unexpectedKind n
result.help = newLit("") # by default no string
# assume a single character is a short flag
if result.name.len == 1:
result.short = result.name[0].char
else:
result.long = result.name
type FlagKind = enum
Command ## flag "help"
InfixCommand ## a | aflag "help"
InfixCall ## a | aflag("help")
InfixStmt ## a | aflag: ? "help"
InfixCallStmt ## c | count ("some number"): * 5
Stmt ## count: * 5
CallStmt ## count("help"): * 5
Call ## count(5, int, "help")
func parseCliFlag(c: var CliCfg, n: NimNode, group: string) =
c.expectKind n, [nnkCommand, nnkCall]
var f = c.newFlag n
func toFlagKind(c: CliCfg, n: NimNode): FlagKind =
case n.kind:
of nnkInfix:
case n[^1].kind
of nnkCommand:
result = InfixCommand
of nnkStmtList:
case n[2].kind
of nnkIdent:
result = InfixStmt
of nnkCall:
result = InfixCallStmt
else: c.err n, "failed to determine flag kind"
of nnkCall:
result = InfixCall
else:
c.err n, "failed to determine flag kind with short flag"
of nnkCall:
if n.len == 2 and n[1].kind == nnkStmtList:
result = Stmt
elif n[^1].kind == nnkStmtList:
result = CallStmt
else:
result = Call
of nnkCommand:
result = Command
else:
c.unexpectedKind n
# option "some help desc"
if n.kind == nnkCommand:
func parseCliFlag(
c: var CliCfg,
n: NimNode,
group: string,
short: char = '\x00'
) =
var f : CliFlag
let flagKind = c.toFlagKind n
case flagKind
of Stmt:
f = c.newFlag n[0]
parseFlagStmtList c, f, n[1]
of InfixCommand:
f = c.newFlag n[2][0]
f.help = n[2][1]
of InfixStmt:
f = c.newFlag n[2]
parseFlagStmtList c, f, n[^1]
of InfixCallStmt:
f = c.newFlag n[2][0]
parseCliFlagCall c, f, n[^2][1..^1]
parseFlagStmtList c, f, n[^1]
of Call:
f = c.newFlag n[0]
parseCliFlagCall c, f, n[1..^1]
of CallStmt:
f = c.newFlag n[0]
parseCliFlagCall c, f, n[1..^2]
parseFlagStmtList c, f, n[^1]
of InfixCall:
f = c.newFlag n[2][0]
parseCliFlagCall c, f, n[^1][1..^1]
of Command:
f = c.newFlag n[0]
f.help = n[1]
# option:
# T string
# help "some help description"
else:
parseFlagParams c, f, n[1]
if short != '\x00':
f.short = short
f.ident = f.ident or f.name.ident
f.group = group
c.flagDefs.add f
@ -492,15 +603,8 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) =
var group: string
cfg.expectKind node, nnkStmtList
for n in node:
case n.kind
# flags:
# input "some input"
# count:
# T int
# ? "a number"
of nnkCall, nnkCommand:
cfg.parseCliFlag n, group
case n.kind
# start a new flag group
# flags:
# [category]
@ -516,7 +620,6 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) =
of nnkPrefix:
if
n[0].kind != nnkIdent or
n[0].strVal != "^" or
n.len != 2 or
n[1].kind notin [nnkBracket, nnkIdent, nnkStrLit]:
cfg.err n, "unable to determine inherited flag/group"
@ -527,6 +630,29 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) =
of nnkIdent, nnkStrLit:
cfg.inherit.flags.add n[1].strval
else: cfg.unexpectedKind n
# flags:
# input "some input"
# count:
# T int
# ? "a number"
of nnkCall, nnkCommand:
cfg.expectKind n, [nnkCommand, nnkCall]
cfg.parseCliFlag n, group
# flags:
# l | `long-flag` "flag with short using infix"
# n | `dry-run`("set dry-run"):
# ident dry
of nnkInfix:
cfg.expectKind n[0], nnkIdent
if n[0].strVal != "|":
cfg.err n, "unexpected infix operator in flags"
cfg.expectKind n[2], nnkCall, nnkCommand, nnkAccQuoted, nnkIdent
# need to make sure that this node getting passed here is stmt?
cfg.parseCliFlag n, group, cfg.getShortChar(n[1])
else: cfg.unexpectedKind n
@ -701,7 +827,8 @@ func postPropagate(c: var CliCfg) =
else:
short[f.short] = f
if f.long in long:
if f.long == "": discard
elif f.long in long:
let conflict = long[f.long]
c.err "conflicting long flags for: " & f.name & " and " & conflict.name
else:

View file

@ -0,0 +1,21 @@
import std/strformat
import hwylterm, hwylterm/hwylcli
hwylCli:
name "flag-kinds"
flags:
a "kind: Command"
b | bbbb "kind: InfixCommand"
cccc:
? "kind: Stmt"
d | dddd:
? "kind: InfixStmt"
e(string, "kind: Call")
f | ffff("kind: InfixCallStmt"):
ident notffff
gggg(string, "kind: CallStmt"):
* "default"
h | hhhh("kind: InfixCall")
run:
echo fmt"{a=}, {bbbb=}, {cccc=}, {dddd=}"
echo fmt"{e=}, {notffff=}, {gggg=}, {hhhh=}"

View file

@ -0,0 +1,9 @@
import hwylterm, hwylterm/hwylcli
hwylCli:
name "multiple-short-flags"
flags:
a "first short"
b "second short"
run:
echo a, b

View file

@ -1,4 +1,4 @@
import std/[compilesettings, os, osproc, strutils, times, unittest, terminal]
import std/[os, osproc, strutils, times, unittest, terminal]
const pathToSrc = currentSourcePath().parentDir()
const binDir = pathToSrc / "bin"

View file

@ -27,6 +27,30 @@ suite "flags":
"error failed to parse value for color as enum: black expected one of: red,blue,green",
)
okWithArgs("flagKey", "--key", "key set")
okWithArgs("multiShortFlags", "--help","""
usage:
multiple-short-flags [flags]
flags:
-a first short
-b second short
-h --help show this help
""")
okWithArgs("allFlagKinds", "--help", """
usage:
flag-kinds [flags]
flags:
-a kind: Command
-b --bbbb kind: InfixCommand
--cccc kind: Stmt
-d --dddd kind: InfixStmt
-e string kind: Call
-f --ffff kind: InfixCallStmt
--gggg string kind: CallStmt (default: default)
-h --hhhh kind: InfixCall
--help show this help
""")
suite "subcommands":