add default values to help screen

This commit is contained in:
Daylin Morgan 2025-01-24 17:42:29 -06:00
parent c40a0a2038
commit ea49ee81fc
Signed by: daylin
GPG key ID: 950D13E9719334AD
10 changed files with 176 additions and 67 deletions

View file

@ -3,7 +3,6 @@ import std/[strformat, strutils]
task test, "run tests": task test, "run tests":
selfExec "r tests/tbbansi.nim" selfExec "r tests/tbbansi.nim"
selfExec "r tests/tcli.nim"
selfExec "r tests/cli/tester.nim" selfExec "r tests/cli/tester.nim"
task develop, "install cligen for development": task develop, "install cligen for development":

View file

@ -38,7 +38,7 @@ export parseopt3, sets, bbansi
type type
HwylFlagHelp* = tuple HwylFlagHelp* = tuple
short, long, description: string short, long, description, defaultVal: string
HwylSubCmdHelp* = tuple HwylSubCmdHelp* = tuple
name, aliases, desc: string name, aliases, desc: string
HwylCliStyleSetting = enum HwylCliStyleSetting = enum
@ -48,6 +48,8 @@ type
flagShort* = "yellow" flagShort* = "yellow"
flagLong* = "magenta" flagLong* = "magenta"
flagDesc* = "" flagDesc* = ""
default* = "faint"
required* = "red"
cmd* = "bold" cmd* = "bold"
settings*: set[HwylCliStyleSetting] = {Aliases} settings*: set[HwylCliStyleSetting] = {Aliases}
HwylCliHelp* = object HwylCliHelp* = object
@ -55,7 +57,8 @@ type
subcmds*: seq[HwylSubCmdHelp] subcmds*: seq[HwylSubCmdHelp]
flags*: seq[HwylFlagHelp] flags*: seq[HwylFlagHelp]
styles*: HwylCliStyles styles*: HwylCliStyles
subcmdLen*, subcmdDescLen*, shortArgLen*, longArgLen*, descArgLen*: int # make 'lengths' it's own object?
subcmdLen*, subcmdDescLen*, shortArgLen*, longArgLen*, descArgLen*, defaultValLen*: int
# NOTE: do i need both strips? # NOTE: do i need both strips?
func firstLine(s: string): string = func firstLine(s: string): string =
@ -88,6 +91,7 @@ func newHwylCliHelp*(
result.shortArgLen = max(result.shortArgLen, f.short.len) result.shortArgLen = max(result.shortArgLen, f.short.len)
result.longArgLen = max(result.longArgLen, f.long.len) result.longArgLen = max(result.longArgLen, f.long.len)
result.descArgLen = max(result.descArgLen, f.description.len) result.descArgLen = max(result.descArgLen, f.description.len)
result.defaultValLen = max(result.defaultValLen, f.defaultVal.len)
for s in result.subcmds: for s in result.subcmds:
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)
@ -109,11 +113,16 @@ func render*(cli: HwylCliHelp, f: HwylFlagHelp): string =
result.add " ".repeat(2 + cli.longArgLen) result.add " ".repeat(2 + cli.longArgLen)
result.add " " result.add " "
if f.description != "": if f.description != "":
result.add "[" & cli.styles.flagDesc & "]" result.add "[" & cli.styles.flagDesc & "]"
result.add f.description result.add f.description
result.add "[/" & cli.styles.flagDesc & "]" result.add "[/" & cli.styles.flagDesc & "]"
if f.defaultVal != "":
result.add " "
result.add "[" & cli.styles.default & "]"
result.add "(" & f.defaultVal & ")"
result.add "[/" & cli.styles.default & "]"
func render*(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string = func render*(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string =
result.add " " result.add " "
@ -159,7 +168,6 @@ func render*(cli: HwylCliHelp): string =
proc bb*(cli: HwylCliHelp): BbString = proc bb*(cli: HwylCliHelp): BbString =
result = bb(render(cli)) result = bb(render(cli))
# ----------------------------------------
type type
Count* = object ## Count type for an incrementing flag Count* = object ## Count type for an incrementing flag
@ -169,6 +177,10 @@ type
val*: Y val*: Y
KVString* = KV[string, string] KVString* = KV[string, string]
proc `$`(c: Count): string = $c.val
# ----------------------------------------
type type
CliSetting* = enum CliSetting* = enum
# Propagate, ## Include parent command settings in subcommand # Propagate, ## Include parent command settings in subcommand
@ -176,8 +188,12 @@ type
NoHelpFlag, ## Remove the builtin help flag NoHelpFlag, ## Remove the builtin help flag
ShowHelp, ## If cmdline empty show help ShowHelp, ## If cmdline empty show help
NoNormalize, ## Don't normalize flags and commands NoNormalize, ## Don't normalize flags and commands
NoPositional ## Raise error if any remaing positional arguments NoPositional, ## Raise error if any remaing positional arguments DEPRECATED
ExactArgs, ## Raise error if missing positional argument HideDefault ## Don't show default values
# ExactArgs, ## Raise error if missing positional argument
CliFlagSetting* = enum
HideDefault ## Don't show default values
BuiltinFlag = object BuiltinFlag = object
name*: string name*: string
@ -185,11 +201,13 @@ type
long*: string long*: string
help*: NimNode help*: NimNode
node: NimNode node: NimNode
defaultVal: NimNode
settings*: set[CliFlagSetting]
CliFlag = object CliFlag = object
name*: string name*: string
ident*: NimNode ident*: NimNode
default*: NimNode defaultVal*: NimNode
typeNode*: NimNode typeNode*: NimNode
node*: NimNode node*: NimNode
short*: char short*: char
@ -197,6 +215,7 @@ type
help*: NimNode help*: NimNode
group*: string group*: string
inherited*: bool inherited*: bool
settings*: set[CliFlagSetting]
Inherit = object Inherit = object
settings: set[CliSetting] settings: set[CliSetting]
@ -270,6 +289,40 @@ func bad(n: NimNode, argument: string = "") =
msg &= " for argument: " & argument msg &= " for argument: " & argument
error msg error msg
# could I deduplicate these somehow? template?
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 parseCliFlagSetting(s: string): CliFlagSetting =
try: parseEnum[CliFlagSetting](s)
except: error "unknown cli flag setting: " & s
func parseCliFlagSettings(f: var CliFlag, node: NimNode) =
case node.kind
of nnkCommand:
for n in node[1..^1]:
f.settings.incl parseCliFlagSetting(n.strVal)
of nnkCall:
expectKind node[1], nnkStmtList
for n in node[1]:
f.settings.incl parseCliFlagSetting(n.strVal)
else: assert false
func getFlagParamNode(node: NimNode): NimNode = func getFlagParamNode(node: NimNode): NimNode =
case node.kind case node.kind
of nnkStrLit: of nnkStrLit:
@ -297,13 +350,15 @@ func parseFlagParams(f: var CliFlag, node: NimNode) =
error "short flag must be a char" error "short flag must be a char"
f.short = val[0].char f.short = val[0].char
of "*", "default": of "*", "default":
f.default = getFlagParamNode(n) f.defaultVal = getFlagParamNode(n)
of "i", "ident": of "i", "ident":
f.ident = getFlagParamNode(n).strVal.ident f.ident = getFlagParamNode(n).strVal.ident
of "T": of "T":
f.typeNode = n[1] f.typeNode = n[1]
of "node": of "node":
f.node = n[1] f.node = n[1]
of "settings", "S":
parseCliFlagSettings(f, n)
else: else:
error "unexpected setting: " & n[0].strVal error "unexpected setting: " & n[0].strVal
else: else:
@ -393,22 +448,6 @@ func parseCliFlags(cfg: var CliCfg, node: NimNode) =
else: bad(n, "flag") else: bad(n, "flag")
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] = func parseIdentLikeList(node: NimNode): seq[string] =
template check = template check =
if n.kind notin [nnkStrLit,nnkIdent]: if n.kind notin [nnkStrLit,nnkIdent]:
@ -716,7 +755,7 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
parseCliHelp(result, node) parseCliHelp(result, node)
of "flags": of "flags":
parseCliFlags(result, node[1]) parseCliFlags(result, node[1])
of "settings": of "settings", "S":
parseCliSettings(result, node) parseCliSettings(result, node)
of "stopWords": of "stopWords":
result.stopWords = parseIdentLikeList(node) result.stopWords = parseIdentLikeList(node)
@ -747,23 +786,30 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
if root: if root:
propagate(result) propagate(result)
func flagToTuple(f: CliFlag | BuiltinFlag): NimNode = func flagToTuple(c: CliCfg, f: CliFlag | BuiltinFlag): NimNode =
let let
short = short =
if f.short != '\x00': newLit($f.short) if f.short != '\x00': newLit($f.short)
else: newLit("") else: newLit("")
long = newLit(f.long) long = newLit(f.long)
help = f.help help = f.help
defaultVal =
if (HideDefault in f.settings) or
(HideDefault in c.settings):
newLit""
else:
f.defaultVal or newLit""
quote do: quote do:
(`short`, `long`, `help`) (`short`, `long`, `help`, bbEscape($`defaultVal`))
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() result.add cfg.flagToTuple(f)
for f in cfg.builtinFlags: for f in cfg.builtinFlags:
result.add f.flagToTuple() result.add cfg.flagToTuple(f)
func subCmdsArray(cfg: CliCfg): NimNode = func subCmdsArray(cfg: CliCfg): NimNode =
result = newTree(nnkBracket) result = newTree(nnkBracket)
@ -1107,9 +1153,9 @@ func addPostParseHook(cfg: CliCfg, body: NimNode) =
var required, default: seq[CliFlag] 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: if f.name in cfg.required and f.defaultVal == nil:
required.add f required.add f
elif f.default != nil: elif f.defaultVal != nil:
default.add f default.add f
for f in required: for f in required:
@ -1123,10 +1169,10 @@ func addPostParseHook(cfg: CliCfg, body: NimNode) =
let let
name = newLit(f.name) name = newLit(f.name)
target = f.ident target = f.ident
default = f.default defaultVal = f.defaultVal
body.add quote do: body.add quote do:
if `name` notin `flagSet`: if `name` notin `flagSet`:
`target` = `default` `target` = `defaultVal`
if hasSubcommands cfg: if hasSubcommands cfg:

View file

@ -0,0 +1,16 @@
import hwylterm, hwylterm/hwylcli
hwylCli:
name "setting-hide-default"
settings HideDefault
flags:
input:
T string
* "a secret default"
? "flag with default hidden"
count:
T Count
* Count(val: 0)
? "a count var with default"
run:
discard

View file

@ -0,0 +1,18 @@
import std/[strformat]
import hwylterm, hwylterm/hwylcli
hwylCli:
name "default-values"
flags:
input:
T string
* "testing"
? "some help after default"
outputs:
T seq[string]
count:
T int
* 5
? "some number"
run:
echo fmt"{input=} {outputs=}"

View file

@ -0,0 +1,16 @@
import hwylterm, hwylterm/hwylcli
hwylCli:
name "flag-settings"
flags:
input:
S HideDefault
T string
* "a secret default"
? "flag with default hidden"
count:
T Count
* Count(val: 0)
? "a count var with default"
run:
discard

View file

@ -16,6 +16,8 @@ hwylCli:
flags: flags:
input: input:
T string T string
* "testing"
? "some help after default"
outputs: outputs:
T seq[string] T seq[string]
run: run:

View file

@ -1,4 +1,4 @@
import std/[compilesettings, os, osproc, strutils, times, unittest] import std/[compilesettings, os, osproc, strutils, times, unittest, terminal]
const pathToSrc = querySetting(SingleValueSetting.projectPath) const pathToSrc = querySetting(SingleValueSetting.projectPath)
const binDir = pathToSrc / "bin" const binDir = pathToSrc / "bin"
@ -13,21 +13,36 @@ proc runTestCli(module: string, args: string, code: int = 0): (string, int) =
let (output, code) = execCmdEx(cmd) let (output, code) = execCmdEx(cmd)
result = (output.strip(), code) result = (output.strip(), code)
# poor man's progress meter
proc status(s: string) =
eraseLine stdout
stdout.write(s.alignLeft(terminalWidth()).substr(0, terminalWidth()-1))
flushFile stdout
proc preCompileWorkingModule(module: string) = proc preCompileWorkingModule(module: string) =
let exe = binDir / module let exe = binDir / module
let srcModule = pathToSrc / "clis" / (module & ".nim") let srcModule = pathToSrc / "clis" / (module & ".nim")
if not exe.fileExists or getFileInfo(exe).lastWriteTime < max(getFileInfo(srcModule).lastWriteTime, hwylCliWriteTime): if not exe.fileExists or getFileInfo(exe).lastWriteTime < max(getFileInfo(srcModule).lastWriteTime, hwylCliWriteTime) or defined(forceSetup):
let cmd = "nim c -o:$1 $2" % [exe, srcModule] let cmd = "nim c -o:$1 $2" % [exe, srcModule]
let code = execCmd(cmd) let (output, code) = execCmdEx(cmd)
if code != 0: if code != 0:
echo "cmd: ", cmd echo "cmd: ", cmd
quit "failed to precompile test module" quit "failed to precompile test module:\n" & output
proc preCompileTestModules*() = proc preCompileTestModules*() =
var modules: seq[string]
for srcModule in walkDirRec(pathToSrc / "clis"): for srcModule in walkDirRec(pathToSrc / "clis"):
if srcModule.endsWith(".nim"): if srcModule.endsWith(".nim"):
let (_, moduleName, _) = srcModule.splitFile modules.add srcModule.splitFile().name
preCompileWorkingModule(moduleName)
for i, module in modules:
status "compiling [$2/$3] $1" % [ module, $(i+1), $modules.len]
preCompileWorkingModule(module)
eraseLine stdout
#let (_, moduleName, _) = srcModule.splitFile
# preCompileWorkingModule(moduleName)
template okWithArgs*(module: string, args = "", output = "") = template okWithArgs*(module: string, args = "", output = "") =
preCompileWorkingModule(module) preCompileWorkingModule(module)

View file

@ -1,9 +1,9 @@
import std/[unittest] import std/[unittest]
import ./lib import ./lib
preCompileTestModules()
suite "hwylcli": suite "hwylcli":
setup:
preCompileTestModules()
okWithArgs( okWithArgs(
"posBasic", "posBasic",
@ -38,3 +38,22 @@ suite "hwylcli":
flags: flags:
-h --help show this help""") -h --help show this help""")
okWithArgs("flagSettings", "--help",
"""usage:
flag-settings [flags]
flags:
--input flag with default hidden
--count a count var with default (0)
-h --help show this help""")
okWithArgs("cliCfgSettingHideDefault", "--help",
"""usage:
setting-hide-default [flags]
flags:
--input flag with default hidden
--count a count var with default
-h --help show this help""")

View file

@ -1,21 +0,0 @@
# TODO: combine this with tests/cli/
import std/[
unittest,
strutils
]
import hwylterm, hwylterm/hwylcli
suite "cli":
test "cli":
let expected = """[b]test-program[/] [[args...]
[bold cyan]flags[/]:
[yellow]-h[/yellow] [magenta]--help [/magenta] []show this help[/]
[yellow]-V[/yellow] [magenta]--version[/magenta] []print version[/]"""
let cli =
newHwylCliHelp(
header = "[b]test-program[/] [[args...]",
flags = [("h","help","show this help",),("V","version","print version")]
)
check render(cli) == expected
check $bb(render(cli)) == $bb(expected)

View file

@ -16,7 +16,6 @@
### cli generator ### cli generator
- [ ] add support for types(metavars)/defaults/required in help output - [ ] add support for types(metavars)/defaults/required in help output
- [ ] add nargs to CliCfg
- [x] add support for inheriting a single flag from parent (even from a "group") - [x] add support for inheriting a single flag from parent (even from a "group")
- [x] add support to either (lengthen commands) or provide an alias for a subcommand - [x] add support to either (lengthen commands) or provide an alias for a subcommand
- [x] add command aliases to hwylcli help with switch - [x] add command aliases to hwylcli help with switch