From 097d963b2a4897a4facf4e7c2c7e3621e2517dd6 Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Wed, 29 Jan 2025 16:11:16 -0600 Subject: [PATCH] handle required at the flag level --- src/hwylterm/hwylcli.nim | 106 +++++++++++++++++++++++--------- tests/cli/clis/helpSettings.nim | 25 ++++++++ tests/cli/clis/inheritFlags.nim | 2 +- tests/cli/clis/required.nim | 13 ++++ tests/cli/tester.nim | 26 ++++++++ todo.md | 2 +- 6 files changed, 143 insertions(+), 31 deletions(-) create mode 100644 tests/cli/clis/helpSettings.nim create mode 100644 tests/cli/clis/required.nim diff --git a/src/hwylterm/hwylcli.nim b/src/hwylterm/hwylcli.nim index 234fa72..a927de8 100644 --- a/src/hwylterm/hwylcli.nim +++ b/src/hwylterm/hwylcli.nim @@ -37,12 +37,14 @@ import ./[bbansi, parseopt3] export parseopt3, sets, bbansi type - HwylFlagHelp* = tuple - short, long, description, defaultVal: string - HwylSubCmdHelp* = tuple + HwylFlagHelp* = tuple[ + short, long, description, defaultVal: string; required: bool + ] + HwylSubCmdHelp* = tuple[ name, aliases, desc: string + ] HwylCliStyleSetting = enum - Aliases + Aliases, Required, Defaults HwylCliStyles* = object header* = "bold cyan" flagShort* = "yellow" @@ -51,7 +53,9 @@ type default* = "faint" required* = "red" cmd* = "bold" - settings*: set[HwylCliStyleSetting] = {Aliases} + minCmdLen* = 8 + settings*: set[HwylCliStyleSetting] = {Aliases, Required, Defaults} + HwylCliLengths = object subcmd*, subcmdDesc*, shortArg*, longArg*, descArg*, defaultVal*: int @@ -87,7 +91,7 @@ func newHwylCliHelp*( result.usage = dedent(usage).strip() result.flags = @flags result.styles = styles - result.lengths.subcmd = 8 # TODO: incorporate into "styles?" + result.lengths.subcmd = styles.minCmdLen for f in flags: result.lengths.shortArg = max(result.lengths.shortArg, f.short.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 f.description result.add "[/" & cli.styles.flagDesc & "]" - if f.defaultVal != "": - result.add " " - result.add "[" & cli.styles.default & "]" - result.add "(" & f.defaultVal & ")" - result.add "[/" & cli.styles.default & "]" + + if f.defaultVal != "" and Defaults in cli.styles.settings: + result.add " " + 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 = @@ -190,13 +202,13 @@ type NoHelpFlag, ## Remove the builtin help flag ShowHelp, ## If cmdline empty show help NoNormalize, ## Don't normalize flags and commands - NoPositional, ## Raise error if any remaing positional arguments DEPRECATED HideDefault, ## Don't show default values InferShort ## Autodefine short flags CliFlagSetting* = enum 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 name*: string @@ -240,7 +252,6 @@ type stopWords*: seq[string] help: CliHelp defaultFlagType: NimNode - required*: seq[string] settings*: set[CliSetting] subName*: string # used for help generator subcommands: seq[CliCfg] @@ -735,7 +746,6 @@ func parseCliHelp(c: var CliCfg, node: NimNode) = if node[1].kind != nnkStmtList: c.err node, "expected list of arguments for help" for n in node[1]: - <<< n c.expectLen n, 2 let id = n[0].strVal var val: NimNode @@ -853,8 +863,6 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg = parseHiddenFlags(result, node) of "run": result.run = node[1] - of "required": - result.required = result.parseIdentLikeList(node) of "preSub": result.preSub = node[1] of "postSub": @@ -874,7 +882,45 @@ func parseCliBody(body: NimNode, name = "", root = false): CliCfg = if root: 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 short = if f.short != '\x00': newLit($f.short) @@ -889,14 +935,22 @@ func flagToTuple(c: CliCfg, f: CliFlag | BuiltinFlag): NimNode = else: f.defaultVal or newLit"" + required = newLit(c.isRequiredFlag(f)) + # 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`)) - + result = nnkTupleConstr.newTree( + short, + newLit(f.long), + f.help, + quote do: bbEscape($`defaultVal`), + required, + ) + # quote do: + # (`short`, `long`, `help`, bbEscape($`defaultVal`), `required`) + # func flagsArray(cfg: CliCfg): NimNode = result = newTree(nnkBracket) for f in cfg.flags: @@ -1099,12 +1153,6 @@ func shortLongCaseStmt(cfg: CliCfg, printHelpName: NimNode, version: NimNode): N 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] = 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] for f in cfg.flags: - if f.name in cfg.required and f.defaultVal == nil: + if cfg.isRequiredFlag(f): required.add f elif f.defaultVal != nil: default.add f diff --git a/tests/cli/clis/helpSettings.nim b/tests/cli/clis/helpSettings.nim new file mode 100644 index 0000000..2a7023e --- /dev/null +++ b/tests/cli/clis/helpSettings.nim @@ -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=}" diff --git a/tests/cli/clis/inheritFlags.nim b/tests/cli/clis/inheritFlags.nim index bdbf411..e9a145c 100644 --- a/tests/cli/clis/inheritFlags.nim +++ b/tests/cli/clis/inheritFlags.nim @@ -2,7 +2,7 @@ import std/strformat import hwylterm, hwylterm/hwylcli hwylCli: - name "base" + name "inherit-flags" flags: [global] always "in all subcommands" diff --git a/tests/cli/clis/required.nim b/tests/cli/clis/required.nim new file mode 100644 index 0000000..eeb280d --- /dev/null +++ b/tests/cli/clis/required.nim @@ -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=}" diff --git a/tests/cli/tester.nim b/tests/cli/tester.nim index 0b35c5b..e3bfd96 100644 --- a/tests/cli/tester.nim +++ b/tests/cli/tester.nim @@ -93,6 +93,32 @@ flags: 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": okWithArgs( diff --git a/todo.md b/todo.md index a541de9..2872057 100644 --- a/todo.md +++ b/todo.md @@ -21,7 +21,7 @@ - [ ] 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)) 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" > app help (show all help?) > app help same as app --help