handle required at the flag level

This commit is contained in:
Daylin Morgan 2025-01-29 16:11:16 -06:00
parent 6a93485202
commit 097d963b2a
Signed by: daylin
GPG key ID: 950D13E9719334AD
6 changed files with 143 additions and 31 deletions

View file

@ -37,12 +37,14 @@ import ./[bbansi, parseopt3]
export parseopt3, sets, bbansi export parseopt3, sets, bbansi
type type
HwylFlagHelp* = tuple HwylFlagHelp* = tuple[
short, long, description, defaultVal: string short, long, description, defaultVal: string; required: bool
HwylSubCmdHelp* = tuple ]
HwylSubCmdHelp* = tuple[
name, aliases, desc: string name, aliases, desc: string
]
HwylCliStyleSetting = enum HwylCliStyleSetting = enum
Aliases Aliases, Required, Defaults
HwylCliStyles* = object HwylCliStyles* = object
header* = "bold cyan" header* = "bold cyan"
flagShort* = "yellow" flagShort* = "yellow"
@ -51,7 +53,9 @@ type
default* = "faint" default* = "faint"
required* = "red" required* = "red"
cmd* = "bold" cmd* = "bold"
settings*: set[HwylCliStyleSetting] = {Aliases} minCmdLen* = 8
settings*: set[HwylCliStyleSetting] = {Aliases, Required, Defaults}
HwylCliLengths = object HwylCliLengths = object
subcmd*, subcmdDesc*, shortArg*, longArg*, descArg*, defaultVal*: int subcmd*, subcmdDesc*, shortArg*, longArg*, descArg*, defaultVal*: int
@ -87,7 +91,7 @@ func newHwylCliHelp*(
result.usage = dedent(usage).strip() result.usage = dedent(usage).strip()
result.flags = @flags result.flags = @flags
result.styles = styles result.styles = styles
result.lengths.subcmd = 8 # TODO: incorporate into "styles?" result.lengths.subcmd = styles.minCmdLen
for f in flags: for f in flags:
result.lengths.shortArg = max(result.lengths.shortArg, f.short.len) result.lengths.shortArg = max(result.lengths.shortArg, f.short.len)
result.lengths.longArg = max(result.lengths.longArg, f.long.len) result.lengths.longArg = max(result.lengths.longArg, f.long.len)
@ -119,11 +123,19 @@ func render*(cli: HwylCliHelp, f: HwylFlagHelp): string =
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 " " if f.defaultVal != "" and Defaults in cli.styles.settings:
result.add "[" & cli.styles.default & "]" result.add " "
result.add "(" & f.defaultVal & ")" result.add "[" & cli.styles.default & "]"
result.add "[/" & cli.styles.default & "]" result.add "(" & f.defaultVal & ")"
result.add "[/" & cli.styles.default & "]"
if f.required and Required in cli.styles.settings:
result.add " "
result.add "[" & cli.styles.required & "]"
result.add "(required)"
result.add "[/" & cli.styles.required & "]"
func render*(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string = func render*(cli: HwylCliHelp, subcmd: HwylSubCmdHelp): string =
@ -190,13 +202,13 @@ 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 DEPRECATED
HideDefault, ## Don't show default values HideDefault, ## Don't show default values
InferShort ## Autodefine short flags InferShort ## Autodefine short flags
CliFlagSetting* = enum CliFlagSetting* = enum
HideDefault, ## Don't show default values HideDefault, ## Don't show default values
NoShort ## Counter option to Parent's InferShort NoShort, ## Counter option to Parent's InferShort
Required, ## Flag must be used (or have default value)
BuiltinFlag = object BuiltinFlag = object
name*: string name*: string
@ -240,7 +252,6 @@ type
stopWords*: seq[string] stopWords*: seq[string]
help: CliHelp help: CliHelp
defaultFlagType: NimNode defaultFlagType: NimNode
required*: seq[string]
settings*: set[CliSetting] settings*: set[CliSetting]
subName*: string # used for help generator subName*: string # used for help generator
subcommands: seq[CliCfg] subcommands: seq[CliCfg]
@ -735,7 +746,6 @@ func parseCliHelp(c: var CliCfg, node: NimNode) =
if node[1].kind != nnkStmtList: if node[1].kind != nnkStmtList:
c.err node, "expected list of arguments for help" c.err node, "expected list of arguments for help"
for n in node[1]: for n in node[1]:
<<< n
c.expectLen n, 2 c.expectLen n, 2
let id = n[0].strVal let id = n[0].strVal
var val: NimNode var val: NimNode
@ -853,8 +863,6 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
parseHiddenFlags(result, node) parseHiddenFlags(result, node)
of "run": of "run":
result.run = node[1] result.run = node[1]
of "required":
result.required = result.parseIdentLikeList(node)
of "preSub": of "preSub":
result.preSub = node[1] result.preSub = node[1]
of "postSub": of "postSub":
@ -874,7 +882,45 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg =
if root: if root:
propagate(result) propagate(result)
func flagToTuple(c: CliCfg, f: CliFlag | BuiltinFlag): NimNode =
func isBool(f: CliFlag | BuiltinFlag): bool =
f.typeNode == ident"bool"
func isCount(f: CliFlag): bool =
f.typeNode == ident"Count"
func isRequiredFlag(cfg: CliCfg, f: CliFlag): bool =
result = (Required in f.settings and f.defaultVal == nil)
if result and f.isBool:
cfg.err "boolean flag `$1` can't be a required flag " % [f.long]
# TODO: deprecate builtinflag
func flagToTuple(c: CliCfg, f: BuiltinFlag): NimNode =
let
short =
if f.short != '\x00': newLit($f.short)
else: newLit("")
long = newLit(f.long)
help = f.help
defaultVal =
if (HideDefault in f.settings) or
(HideDefault in c.settings):
newLit""
else:
f.defaultVal or newLit""
required = newLit(false)
# BUG: if f.defaultVal is @[] `$` fails
# but works with `newSeq[T]()`
# could replace "defaultVal" with newSeq[T]()
# under the hood when parsing type/val
quote do:
(`short`, `long`, `help`, bbEscape($`defaultVal`), `required`)
func flagToTuple(c: CliCfg, f: CliFlag): NimNode =
let let
short = short =
if f.short != '\x00': newLit($f.short) if f.short != '\x00': newLit($f.short)
@ -889,14 +935,22 @@ func flagToTuple(c: CliCfg, f: CliFlag | BuiltinFlag): NimNode =
else: else:
f.defaultVal or newLit"" f.defaultVal or newLit""
required = newLit(c.isRequiredFlag(f))
# BUG: if f.defaultVal is @[] `$` fails # BUG: if f.defaultVal is @[] `$` fails
# but works with `newSeq[T]()` # but works with `newSeq[T]()`
# could replace "defaultVal" with newSeq[T]() # could replace "defaultVal" with newSeq[T]()
# under the hood when parsing type/val # under the hood when parsing type/val
result = nnkTupleConstr.newTree(
quote do: short,
(`short`, `long`, `help`, bbEscape($`defaultVal`)) newLit(f.long),
f.help,
quote do: bbEscape($`defaultVal`),
required,
)
# quote do:
# (`short`, `long`, `help`, bbEscape($`defaultVal`), `required`)
#
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:
@ -1099,12 +1153,6 @@ func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): N
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) let flagFlags = cfg.flags.filterIt(it.isBool or it.isCount)
@ -1246,7 +1294,7 @@ 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.defaultVal == nil: if cfg.isRequiredFlag(f):
required.add f required.add f
elif f.defaultVal != nil: elif f.defaultVal != nil:
default.add f default.add f

View file

@ -0,0 +1,25 @@
import std/strformat
import hwylterm, hwylterm/hwylcli
const noExtras = HwylCliStyles(settings: {})
hwylCli:
name "help-switches"
help:
styles: noExtras
subcommands:
[required]
alias r
help:
styles: noExtras
flags:
input:
T string
? "required input"
S Required
k:
T string
? "predefined flag"
* "value"
run:
echo fmt"{input=},{k=}"

View file

@ -2,7 +2,7 @@ import std/strformat
import hwylterm, hwylterm/hwylcli import hwylterm, hwylterm/hwylcli
hwylCli: hwylCli:
name "base" name "inherit-flags"
flags: flags:
[global] [global]
always "in all subcommands" always "in all subcommands"

View file

@ -0,0 +1,13 @@
import std/strformat
import hwylterm, hwylterm/hwylcli
hwylCli:
name "required-flag"
flags:
input:
S Required
T string
? "a required flag!"
run:
echo fmt"{input=}"

View file

@ -93,6 +93,32 @@ flags:
show this help show this help
""", """,
) )
okWithArgs(
"helpSettings", "--help",
"""
usage:
help-switches subcmd [flags]
subcommands:
required
flags:
-h --help show this help
"""
)
okWithArgs(
"helpSettings", "required --help",
"""
usage:
help-switches required [flags]
flags:
--input required input
-k predefined flag
-h --help show this help
"""
)
suite "hooks": suite "hooks":
okWithArgs( okWithArgs(

View file

@ -21,7 +21,7 @@
- [ ] add support for types(metavars)/defaults/required in help output - [ ] add support for types(metavars)/defaults/required in help output
- [ ] add support for E/env param for flags to add custom env_var (in help (env: OPTIONAL_ENV_VAR)) - [ ] add support for E/env param for flags to add custom env_var (in help (env: OPTIONAL_ENV_VAR))
inject this code in the same place as the "default" setting inject this code in the same place as the "default" setting
- [ ] generalize parser error handling - [ ] BUG: flag can't be `key`
- [ ] consider default (or opt in) "help subcmd" - [ ] consider default (or opt in) "help subcmd"
> app help (show all help?) > app help (show all help?)
> app help <subcmd> same as app <subcmd> --help > app help <subcmd> same as app <subcmd> --help