docs/cli-examples

hwylterm CLI Examples

CLIDescription
allFlagKindsThe flag DSL supports multiple equivalent syntaxes — this shows all of them side by side.
collectionsVarious collection types (seq/sets) allow for repeated flags or delimited values
customHelpThe help renderer can be replaced entirely by overriding the render procs before the hwylCli macro.
customParsecustomize parsing of typical types and with distinct types
defaultsFlags can carry default values of any supported type, shown in help output automatically.
defaultsSubGlobal flags with defaults and env var inference flow through to subcommands unchanged.
enumFlagNim enum types work directly as flag types, with automatic error messaging for invalid values.
flagKVsKey=value pair flags let you accept structured input like --flag key=value in a single argument.
flagKeyBool flags are the default type — no type annotation needed, just a name and help string.
flagObjectFlags can be objects ,including with their own collection types (seq, set, etc.)
flagSeparatorsString flags accept both --flag value and --flag=value — either separator works.
flagSettingsIndividual flags can opt out of default help display, and Count gives an incrementing integer flag.
generateOnlyGenerateOnly lets you run arbitrary code before the CLI starts, then invoke the parser manually.
helpSettingsBuiltin style presets let you switch the entire help appearance without manual style configuration.
hideDefaultSettingHideDefault on the CLI hides all flag defaults from help at once, without per-flag annotation.
inferEnvInferEnv automatically binds CMDNAME_FLAGNAME environment variables to their flags.
inferShortInferShort generates a short flag from the first letter of each long flag name automatically.
inheritFlagsFlag groups let subcommands selectively pull in flags from the parent using the ^ syntax.
multiShortFlagsSingle-letter flag names are automatically short-only — no long form is generated.
posBasicPositional arguments can bind to a different variable name in the run block via ident.
posFirstA variadic positional can appear first, collecting values until the fixed positionals are satisfied from the right.
posLastA variadic positional in the last position collects all remaining arguments after the fixed ones.
posNoMultiPositionals with non-seq types accept exactly one value each and support type coercion.
propagateSettingSettings flow down to subcommands by default; IgnoreParent opts out and per-subcommand settings extend or replace.
requiredMarking a flag Required causes the CLI to exit with an error if it is not provided.
subHookspreSub and postSub hooks run code around any subcommand dispatch, and can be nested per level.
subcommandsSubcommands each have their own flags, positionals, aliases, and can use hyphenated names.
versionVersions can be set using compile time defines or runtime code

allFlagKinds

## The flag DSL supports multiple equivalent syntaxes — this shows all of them side by side.
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")
    i: "kind: Stmt (but one line)"
  run:
    echo fmt"{a=}, {bbbb=}, {cccc=}, {dddd=}"
    echo fmt"{e=}, {notffff=}, {gggg=}, {hhhh=}, {i=}"

Case 1 (ok)

allFlagKinds --help

collections

## Various collection types (seq/sets) allow for repeated flags or delimited values

import std/strformat
import hwylterm, hwylterm/hwylcli

type
  Mode = enum
    fun, sad, angry, sleepy

hwylCli:
  name "collects"
  flags:
    inputs(seq[string], "sequence of inputs")
    modes(set[Mode], "set of modes")
    modes2(toHashSet([fun]), HashSet[Mode], "hashset of modes")
    outputs(seq[KVstring], "sequence of key/value pairs")
  run:
    echo fmt"{inputs=},{modes=},{modes2=},{outputs=}"

Case 1 (ok)

collections --inputs one --inputs two --inputs three --modes fun --modes sad \
  --outputs:first:first.txt --outputs:second=second.txt

Case 2 (ok)

collections --inputs one --inputs,=two,three --modes,=,fun,sad,fun --modes2 sad --modes2 sleepy \
  --outputs,:first:first.txt,second:second.txt

customHelp

## The help renderer can be replaced entirely by overriding the render procs before the hwylCli macro.
import std/[strutils,strformat, sequtils]
import hwylterm
import hwylterm/hwylcli except renderHeader


# TODO: actually implement this in bbansi
func stripMarkup*(s: string): string =
  result = bb(s).plain

func render*(cli: HwylCliHelp, f: HwylFlagHelp): string =
  result.add "  "
  if f.short != "":
    result.add "[" & cli.styles.flagShort & "]"
    result.add "-" & f.short.alignLeft(cli.lengths.shortArg)
    result.add "[/" & cli.styles.flagShort & "]"
  else:
    result.add " ".repeat(1 + cli.lengths.shortArg)
  result.add " "
  
  let indentSize = stripMarkup(result).len
  if f.long != "":
    result.add "[" & cli.styles.flagLong & "]"
    result.add "--" & f.long.alignLeft(cli.lengths.longArg)
    result.add "[/" & cli.styles.flagLong & "]"
  else:
    result.add " ".repeat(2 + cli.lengths.longArg)
  
  result.add "\n"
  result.add " ".repeat(indentSize + 4)
  if f.description != "":
    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 & "]"

func render*(cli: HwylCliHelp, flags: seq[HwylFlagHelp]): string =
  result.add "flags".bbMarkup(cli.styles.header)
  result.add ":\n"
  result.add flags.mapIt(render(cli, it)).join("\n")

hwylCli:
  name "custom-help"
  defaultFlagType string
  help:
    header """
     a header
    indentations
      indentations more
    """
  flags:
    input:
      ? "input"
      * "input.txt"
    output:
      ? "output"
      * "output.txt"
  run:
    echo fmt"{input=},  {output=}"

Case 1 (ok)

customHelp --help

customParse

## customize parsing of typical types and with distinct types
import std/[os, paths, strformat]
import hwylterm, hwylterm/hwylcli

type
  Input = distinct string
  Data = object
    nums: seq[int]
    names: seq[string]

proc `$`(v: Input): string {.borrow.}
proc parse(p: OptParser, target: var Input) =
  checkVal p
  if not p.val.fileExists:
    hecho "warning input file does not exist: " & p.val
  target = Input(p.val)

proc parse(p: OptParser, target: var Path) =
  checkVal p
  target = Path(p.val)

proc fieldNames(o: object): seq[string] =
  for k, _ in o.fieldPairs: result.add k

proc parse(p: var OptParser, target: var object) =
  let key = extractKey(p)
  if key notin target.fieldNames:
    hecho "ignoring unknown key: " & key
    return
  for name, field in target.fieldPairs:
    if name == key:
      parse(p, field)
      return

hwylCli:
  name "customParse"
  flags:
    input(Input, "some input")
    output(Path, "some output")
    data(Data, "data")
  run:
    echo fmt"{input=},{output=}"
    echo data

Case 1 (ok)

customParse --input input.nim --output new.nim --data:unknown-key:val --data,=nums:1,2,3 \
  --data:names:John --data:names:Jane

defaults

## Flags can carry default values of any supported type, shown in help output automatically.
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=}, {count=}"

Case 1 (ok)

defaults

defaultsSub

## Global flags with defaults and env var inference flow through to subcommands unchanged.
import std/[os, strformat]
import hwylterm, hwylterm/hwylcli

putEnv("DEFAULTS_SUB_COUNT", "15")
putEnv("DEFAULTS_SUB_OUTPUTS", ",a,b,c")

hwylCli:
  name "defaults-sub"
  settings InferEnv
  flags:
    [global]
    input:
      T string
      * "testing"
      ? "some help after default"
    outputs:
      T seq[string]
    count:
      T int
      * 5
      ? "some number"
  subcommands:
    [a]
    run:
      assert count == 15
      echo fmt"{input=}, {outputs=}, {count=}"

Case 1 (ok)

defaultsSub a

enumFlag

## Nim enum types work directly as flag types, with automatic error messaging for invalid values.
import std/[strformat]
import hwylterm, hwylterm/hwylcli

type
  Color = enum
    red, blue, green

hwylCli:
  name "enumFlag"
  flags:
    color:
      T Color
  run:
    echo fmt"{color=}"

Case 1 (ok)

enumFlag --color red

Case 2 (error)

enumFlag --color black

flagKVs

## Key=value pair flags let you accept structured input like --flag key=value in a single argument.
import hwylterm, hwylterm/hwylcli


type Inputs = KVString

proc `$`(_:typedesc[seq[KVString]]): string =
  "k(str):v(str)..."

hwylCli:
  name "flagKVs"
  flags:
    inputs(seq[Inputs], "version with type alias")
    counts(KV[string, int], "key value, custom types")
    builtins(seq[KVstring], "one using provided type")
  run:
    echo counts.key,":", counts.val
    for (k, v) in inputs:
      echo k, ":", v
    for item in builtins:
      echo item

Case 1 (ok)

flagKVs --counts a:5 --inputs "test:a" --inputs "test:b" --builtins,=,"key with space:a,key:value \
  with space"

flagKey

## Bool flags are the default type — no type annotation needed, just a name and help string.
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "base"
  flags:
    key "a flag named 'key'"
  run:
    if key:
      echo "key set"
    else:
      echo "key not set"

Case 1 (ok)

flagKey --key

flagObject

## Flags can be objects ,including with their own collection types (seq, set, etc.)

import hwylterm, hwylterm/hwylcli

type
  Person = object
    name: string
    age: int
    weight: float = 150.5
    roles: seq[string]

hwylCli:
  name "flagObject"
  flags:
    person(Person, "a person")
  run:
    echo person

Case 1 (ok)

flagObject --person:name:John --person,=roles=admin,user

Case 2 (ok)

flagObject --person name:John --person:age=25 --person:weight:160

Case 3 (error)

flagObject --person:nonexistient:error

flagSeparators

## String flags accept both --flag value and --flag=value — either separator works.
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "flag-separators"
  flags:
    input(string, "flag that expects some input")
  run:
    echo "input = ", input

Case 1 (error)

flagSeparators --input --help

Case 2 (ok)

flagSeparators --input=--help

flagSettings

## Individual flags can opt out of default help display, and Count gives an incrementing integer flag.
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

Case 1 (ok)

flagSettings --help

generateOnly

## GenerateOnly lets you run arbitrary code before the CLI starts, then invoke the parser manually.
import std/[os, strformat]
import hwylterm/hwylcli


hwylCLi:
  name "gen only"
  settings GenerateOnly, ShowHelp
  subcommands:
    [one]
    positionals:
      args seq[string]
    run:
      assert args == @["a", "b", "c"]
      echo fmt"{args=}"
    
    [two]
    flags:
      input(string, "someinput")
    run:
      echo "running subcmd 2"
      echo fmt"{input=}"

echo "some code that runs first"

printGenOnlyHelp()
if paramCount() == 0:
  let args = parseCmdLine("two --input test")
  runGenOnly(args)
else:
  # use from parseCommandLine
  runGenOnly()

Case 1 (ok)

generateOnly one a b c

Case 2 (ok)

generateOnly

helpSettings

## Builtin style presets let you switch the entire help appearance without manual style configuration.
import std/[os, strformat]
import hwylterm, hwylterm/hwylcli

putEnv("HWYLCLISTYLES_HEADER","red")
# putEnv("HWYLCLISTYLES_SETTINGS", "Aliases")

const identHelp = """predefined help string

k could be short for key idk
"""

proc helpRuntime(s: string): string =
  result = s & """

some runtime generated help
"""


hwylCli:
  name "help-settings"
  flags:
    [group]
    input:
      T string
      ? """
      required input
      A long help that continues here
      and spans multiple lines
        the lines are dedented then reindented
        [yellow]this is yellow....[/]
      """
      S Required
    key:
      T string
      ? identHelp
      * "value"
    other:
      ? helpRuntime("first line of help")
  subcommands:
    [all]
    ... "show all help styling settings"
    help:
      styles: fromBuiltinHelpStyles(AllSettings)
    flags: ^[group]
    
    [minimal]
    ... "show minimal help with no styling"
    help:
      styles: fromBuiltinHelpStyles(Minimal)
    flags: ^[group]
    
    [noColor]
    ... "show noColor help "
    help:
      styles: fromBuiltinHelpStyles(WithoutColor)
    flags: ^[group]
    
    [noAnsi]
    ... "show noColor help "
    help:
      styles: fromBuiltinHelpStyles(WithoutAnsi)
    flags: ^[group]
    
    [longHelp]
    ... "-h != --help"
    flags: ^[group]
    settings LongHelp
    
    [noEnv]
    ... "ignore env styles"
    flags: ^[group]
    help:
      styles: newHwylCliStyles(settings = defaultStyleSettings + {HwylCliStyleSetting.NoEnv})

Case 1 (ok)

helpSettings all --help

Case 2 (ok)

helpSettings minimal --help

Case 3 (ok)

helpSettings noColor --help

Case 4 (ok)

helpSettings noAnsi --help

Case 5 (ok)

helpSettings longHelp --help

hideDefaultSetting

## HideDefault on the CLI hides all flag defaults from help at once, without per-flag annotation.
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

Case 1 (ok)

hideDefaultSetting --help

inferEnv

## InferEnv automatically binds CMDNAME_FLAGNAME environment variables to their flags.
import std/[strformat, os]
import hwylterm, hwylterm/hwylcli

putEnv("INFERENV_INPUT", "TEST")
putEnv("INFERENV_COUNT", "5")
putEnv("INFERENV_YES", "false")

hwylCli:
  name "inferEnv"
  settings InferEnv
  flags:
    input:
      S HideDefault
      T string
      * "a secret default"
      ? "flag with default hidden"
    count:
      T Count
      * Count(val: 0)
      ? "a count var with default"
    shell:
      S NoEnv
      T string
      * "guess"
    yes:
      ? "a boolean flag"
  run:
    echo fmt"{input=}, {count=}, {shell=}, {yes=}"

Case 1 (ok)

inferEnv --input "command line"

inferShort

## InferShort generates a short flag from the first letter of each long flag name automatically.
import std/strformat
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "inferred short flags"
  settings InferShort
  flags:
    input:
      T string
      ? "the input var"
    output:
      T string
      ? "the output var"
    count:
      T int
      ? "a number"
      - n
    nancy:
      ? "needed a flag that starts with n :)"
    ignore:
      S NoShort
      ? "a flag to not infer"
  run:
    echo fmt"{input=}, {output=}, {count=}, {nancy=}, {ignore=}"

Case 1 (ok)

inferShort -i input -o output

inheritFlags

## Flag groups let subcommands selectively pull in flags from the parent using the ^ syntax.
import std/strformat
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "inherit-flags"
  help:
    styles: newHwylCliStyles(settings = defaultStyleSettings + {FlagGroups})
  flags:
    [global]
    always "in all subcommands"
    [misc]
    misc1 "first misc flag"
    misc2 "second misc flag"
    ["_hidden"]
    other "flag from hidden group"
  subcommands:
    [first]
    ... "command with it's own flag"
    flags:
      # manually defined groups
      first "first first flag":
        group misc
    run:
      echo fmt"{always=},{first=}"
    
    [second]
    ... "command with 'misc' flags"
    flags:
      ^[misc]
    run:
      echo fmt"{always=},{misc1=},{misc2=}"
    
    [third]
    ... "command with only 'misc1' flag"
    flags:
      ^misc1
      ^["_hidden"]
    run:
      echo fmt"{always=},{misc1=}"

Case 1 (ok)

inheritFlags first --help

Case 2 (ok)

inheritFlags third --help

Case 3 (ok)

inheritFlags first --always

Case 4 (ok)

inheritFlags second --always --misc2

Case 5 (ok)

inheritFlags third --misc1

multiShortFlags

## Single-letter flag names are automatically short-only — no long form is generated.
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "multiple-short-flags"
  flags:
    a "first short"
    b "second short"
  run:
    echo a, b

Case 1 (ok)

multiShortFlags --help

posBasic

## Positional arguments can bind to a different variable name in the run block via ident.
import std/[strformat]
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "posBasic"
  positionals:
    input:
      T string
      ? "the \"input\""
      ident i
  run:
    echo fmt"input={i}"

Case 1 (ok)

posBasic --help

Case 2 (ok)

posBasic a

posFirst

## A variadic positional can appear first, collecting values until the fixed positionals are satisfied from the right.
import std/[strformat]
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "positionals"
  positionals:
    first seq[string]
    second string
    third string
  run:
    echo fmt"{first=}, {second=}, {third=}"

Case 1 (ok)

posFirst --help

Case 2 (ok)

posFirst a b c d e

Case 3 (error)

posFirst a b

posLast

## A variadic positional in the last position collects all remaining arguments after the fixed ones.
import std/[strformat]
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "posLast"
  positionals:
    first string
    second string
    third seq[string]
  run:
    echo fmt"{first=}, {second=}, {third=}"

Case 1 (ok)

posLast a b

Case 2 (error)

posLast a

posNoMulti

## Positionals with non-seq types accept exactly one value each and support type coercion.
import std/[strformat]
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "positionals"
  positionals:
    first int
    second string
    third string
  run:
    echo fmt"{first=}, {second=}, {third=}"

Case 1 (ok)

posNoMulti 5 b c

Case 2 (error)

posNoMulti 5 b c d

propagateSetting

## Settings flow down to subcommands by default; IgnoreParent opts out and per-subcommand settings extend or replace.
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "setting-propagate"
  settings InferShort, LongHelp
  flags:
    [misc]
    input:
      T string
      * "the default"
      ? "input flag"
    count:
      T Count
      * Count(val: 0)
      ? "a count var with default"
  subcommands:
    [one]
    settings IgnoreParent
    flags:
      ^[misc]
    
    [two]
    settings HideDefault
    flags:
      ^[misc]

Case 1 (ok)

propagateSetting one -h

Case 2 (ok)

propagateSetting two --help

required

## Marking a flag Required causes the CLI to exit with an error if it is not provided.
import std/strformat
import hwylterm, hwylterm/hwylcli


hwylCli:
  name "required-flag"
  flags:
    input:
      S Required
      T string
      ? "a required flag!"
  run:
    echo fmt"{input=}"

Case 1 (ok)

required --input a

Case 2 (error)

required

Case 3 (ok)

required --help

subHooks

## preSub and postSub hooks run code around any subcommand dispatch, and can be nested per level.
import std/[strformat]
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "subcommands"
  preSub:
    echo "preSub from root!"
  postSub:
    echo "postSub from root!"
  subcommands:
    [a]
    ... "subcommand 'a'"
    run:
      echo "inside sub 'a'"
    [b]
    ... "subcommand 'b'"
    run:
      echo "inside sub 'b'"
    subcommands:
      [a]
      ... "subcommand 'b a'"
      run:
        echo "inside sub 'b a'"
    [c]
    ... "subcommand 'c'"
    preSub:
      echo "preSub from 'c'!"
    run:
      echo "inside sub c"
    subcommands:
      [a]
      ... "subcommand 'c a'"
      run:
        echo "inside sub 'c a'"

Case 1 (ok)

subHooks a

Case 2 (ok)

subHooks b a

Case 3 (ok)

subHooks c a

subcommands

## Subcommands each have their own flags, positionals, aliases, and can use hyphenated names.
import std/[strformat]
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "subcommands"
  V "0.0.0"
  subcommands:
    [help]
    [version]
    [a]
    ... "a subcommand with positionals"
    positionals:
      input string
      outputs seq[string]
    run:
      echo fmt"{input=} {outputs=}"
    
    [b]
    ... "a subcommand with flags"
    flags:
      input:
        T string
        * "testing"
        ? "some help after default"
      outputs:
        T seq[string]
    run:
      echo fmt"{input=} {outputs=}"
    
    [ccccc]
    ... "a subcommand with an alias"
    alias c
    run:
      echo "no flags :)"
    
    [dd-dd]
    ... "subcommand with hyphen"
    run:
      echo "not all hyphens are flags"

Case 1 (ok)

subcommands a b c

Case 2 (error)

subcommands b b c

Case 3 (ok)

subcommands b --input in --outputs out1 --outputs out2

Case 4 (ok)

subcommands ccccc

Case 5 (ok)

subcommands c

Case 6 (ok)

subcommands dd-dd

Case 7 (error)

subcommands e

Case 8 (ok)

subcommands help --help

Case 9 (ok)

subcommands help dd-dd

Case 10 (ok)

subcommands help c

Case 11 (ok)

subcommands help

version

## Versions can be set using compile time defines or runtime code
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "version"
  version "0.1.0"
  run:
    echo "hello world"

Case 1 (ok)

version --help

Case 2 (ok)

version --version