hwylterm/hwylcli

Search:
Group by:
Source   Edit  

HwylCli

For some example programs and expected outputs see here.

basic program:

import std/[strutils]
import hywlterm/hwylcli

hwylCli:
  name "example"
  flags:
    count:
      T Count
      ? "# of times"
      - n
    input:
      ? "content"
      - i
  run:
    echo (input & " ").repeat(count)

$ example -nn --input "testing"
> testing testing

$ example -n=3 --input "testing"
> testing testing testing

hwylcli

A macro-based DSL for building styled, color-aware CLIs.

Minimal example

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "mytool"
  run:
    echo "hello"

The hwylCli macro generates a parser, a help printer, and runs the command. -h/--help are added automatically.

Flags

Declare flags inside a flags: block. The default type is bool.

Single-character names are treated as short flags only:

import hwylterm, hwylterm/hwylcli

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

Flag DSL syntax

There are several equivalent ways to declare a flag. All forms support the same set of properties:

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "flag-kinds"
  flags:
    a "kind: Command"                       # flagname "help"
    b | bbbb "kind: InfixCommand"           # short | flagname "help"
    cccc:
      ? "kind: Stmt"                        # flagname: block
    d | dddd:
      ? "kind: InfixStmt"                   # short | flagname: block
    e(string, "kind: Call")                 # flagname(type, "help")
    f | ffff("kind: InfixCall"):
      ident notffff                         # custom variable name
    gggg(string, "kind: CallStmt"):
      * "default"                           # flagname(type, "help"): block
    h | hhhh("kind: InfixCall")
    i: "kind: Stmt (but one line)"
  run:
    echo a, bbbb, cccc, dddd, e, notffff, gggg, hhhh, i

Inside a stmt block, the available properties are:

KeyAliasMeaning
?helphelp text
-shortshort flag char
*defaultdefault value
Ttype
ggroupflag group name
Eenvenv var name override
Ssettingsflag settings
identvariable name in run: block

Flag types

Flags use Nim types directly:

import std/strformat
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "default-values"
  flags:
    input:
      T string
      * "testing"
      ? "input file"
    count:
      T int
      * 5
      ? "number of iterations"
  run:
    echo fmt"{input=}, {count=}"

See Count and KV for the incrementing and key-value types. Enum flags are parsed by name with an error listing valid choices.

Enum flag example:

import std/strformat
import hwylterm, hwylterm/hwylcli

type Color = enum
  red, blue, green

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

Count and KV examples:

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "flagKVs"
  flags:
    count(Count, "count")
    input(seq[KV[string, int]], "key=value pairs")
  run:
    echo count
    for (k, v) in input:
      echo k, ":", v

object's can function the same as KV flags with known options

import hwylterm, hwylterm/hwylcli

type
  Person = object
    name: string
    age: int

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

Flag settings

Use S <setting> inside a flag block:

See CliFlagSetting for more info.

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "flag-settings"
  flags:
    input:
      S HideDefault
      T string
      * "default.txt"
      ? "flag with default hidden from help"
  run:
    discard

Help text

Use ... or help to set help content:

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "mytool"
  help:
    header "My Tool v1.0"
    description "does useful things"
    footer "see https://example.com for more"
  flags:
    input:
      T string
      ? "input file"
  run:
    echo input

Available keys inside help::

KeyMeaning
descriptionmain description text
usageusage line
headerprinted above usage
footerprinted below flags
stylesHwylCliStyles value

Short form: ... "description text" sets the description directly.

CLI settings

Use settings to enable different behaviors. See CliSetting for more info.

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "setting-propagate"
  settings InferShort, LongHelp
  flags:
    input:
      T string
      * "the default"
      ? "input flag"
  run:
    echo input

Settings propagate to subcommands by default. Use IgnoreParent on a subcommand to opt out, or add a setting to override for that subcommand only:

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "setting-propagate"
  settings InferShort
  flags:
    input:
      T string
      ? "input flag"
  subcommands:
    [one]
    settings IgnoreParent   # won't inherit InferShort
    run:
      echo input
    
    [two]
    settings HideDefault    # inherits InferShort, adds HideDefault
    run:
      echo input

Versioning

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "mytool"
  version "1.0.0"
  run:
    echo "running"

Specifying a version will adds the -V/--version flag which prints the version string and exits. It's also possible to set the version for the CLI using define switches:

switchdescription
hwylVersionstrdefine
hwylVersionNimblesearches up from directory returned by getProjectPath attempting to extract version from a nimble file

Positional arguments

Positionals are declared in a positionals: block. Each entry is name type:

import std/strformat
import hwylterm, hwylterm/hwylcli

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

Use seq[T] for a variadic positional. It can appear first or last:

# variadic last
import std/strformat
import hwylterm, hwylterm/hwylcli

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

# variadic first
import std/strformat
import hwylterm, hwylterm/hwylcli

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

Use a stmt block to set a custom variable name via ident:

import std/strformat
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "posBasic"
  positionals:
    first:
      T string
      ident notFirst
    rest seq[string]
  run:
    echo fmt"{notFirst=} {rest=}"

Subcommands

Subcommands are delimited by [name] markers inside a subcommands: block:

import std/strformat
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "mytool"
  subcommands:
    [build]
    ... "compile the project"
    flags:
      release "build in release mode"
    run:
      echo fmt"building... {release=}"
    
    [clean]
    ... "remove build artifacts"
    alias c
    run:
      echo "cleaning..."

Subcommands can be nested:

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "subcommands"
  subcommands:
    [b]
    ... "subcommand with nested subcommands"
    run:
      echo "inside sub 'b'"
    subcommands:
      [a]
      ... "subcommand 'b a'"
      run:
        echo "inside sub 'b a'"

Builtin Subcommands

A builtin help subcommand or version subcommand can be added by including the help header in the subcommands definition.

import hwylterm, hwylterm/hwylcli

hwylcli:
  name "subcommans"
  subcommand:
    [help]
    [a]
    ... "command a"
    [b]
    ... "commmand b"
    [version]

Hooks

preSub and postSub run before/after any subcommand at that level:

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "mytool"
  preSub:
    echo "before subcommand"
  postSub:
    echo "after subcommand"
  subcommands:
    [build]
    run:
      echo "building"
    
    [clean]
    run:
      echo "cleaning"

Inheriting flags from parent and flag grouping

Use [groupname] inside flags: to group flags. Requires FlagGroups in help settings to render as separate sections. Flags in a group named global are automatically inherited by all subcommands. Other flags/groups can be pulled in explicitly using ^:

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=}"

Groups prefixed with _ are hidden from help output but can still be inherited with ^.

Env var inference with subcommands

With InferEnv, env vars are named CMDNAME_FLAGNAME and work across subcommands:

import std/strformat
import hwylterm, hwylterm/hwylcli

hwylCli:
  name "mytool"
  settings InferEnv
  flags:
    [global]
    input:
      T string
      * "default.txt"
      ? "input file"
  subcommands:
    [build]
    run:
      echo fmt"{input=}"   # reads MYTOOL_INPUT env var if flag not passed

GenerateOnly — manual invocation

Use GenerateOnly when you need to run code before the CLI or call the parser manually. The macro generates printNameHelp(), parseNameCmdLine(), and runName() procs but does not call runName() automatically:

import hwylterm/hwylcli

hwylCli:
  name "mytool"
  settings GenerateOnly
  flags:
    input(string, "input file")
  run:
    echo input

# code that runs before the CLI
echo "starting up"

runMytool()

Help styling

The HwylCliStyles object controls how help text is rendered:

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "help-settings"
  help:
    styles: newHwylCliStyles(
      header = "bold cyan",
      flagShort = "yellow",
      flagLong = "magenta",
      required = "red",
      settings = defaultStyleSettings + {FlagGroups},
    )
  flags:
    input:
      T string
      S Required
      ? "required input"
  run:
    echo input

Builtin style presets

import hwylterm, hwylterm/hwylcli

hwylCli:
  name "mytool"
  flags:
    input:
      T string
      ? "input file"
  subcommands:
    [styled]
    help:
      styles: fromBuiltinHelpStyles(AllSettings)
    run:
      echo input
    
    [plain]
    help:
      styles: fromBuiltinHelpStyles(WithoutAnsi)
    run:
      echo input

See BuiltinStyleKind for available presets.

Help display settings

Controlled via HwylCliStyleSetting values in styles.settings. The default set is {Aliases, Required, Defaults, Types}.

Environment overrides for styles

Each style field can be overridden at runtime:

HWYLCLISTYLES_HEADER=bold
HWYLCLISTYLES_FLAGSHORT=cyan
HWYLCLISTYLES_SETTINGS=Aliases,Defaults
HWYLCLISTYLES_MINCMDLEN=12

Set NoEnv in the help settings to disable this.

Error reporting

hwylCliError("something went wrong")
hwylCliError(bb("[bold]flag[/] requires a value"))

Prints error <msg> (with error styled red) and exits.

Types

BuiltinStyleKind = enum
  AllSettings,              ## Use all help settings (besides NoEnv)
  Minimal,                  ## Use no extra help settings
  WithoutColor,             ## Default help settings without color
  WithoutAnsi                ## Default help settings without color or styling
Source   Edit  
CliFlagSetting = enum
  HideDefault,              ## Don't show default values
  NoShort,                  ## Counter option to Parent's InferShort
  NoEnv,                    ## Counter option to Parent's InferEnv
  Required                   ## Flag must be used (or have default value)
Source   Edit  
CliSetting = enum
  IgnoreParent,             ## Don't propagate parent settings to subcommands
  GenerateOnly,             ## Don't attach root `runProc()` node
  NoHelpFlag,               ## Don't add the builtin help flag
  NoVersionFlag,            ## Don't add builtin version flag
  ShowHelp,                 ## If cmdline empty show help
  SkipPosCheck,             ## Don't check if not enough positionals were provided
  LongHelp,                 ## Show more info with --help than -h
  NoNormalize,              ## Don't normalize flags and commands
  HideDefault,              ## Don't show default values
  InferShort,               ## Autodefine short flags
  InferEnv                   ## Autodefine env vars for flags
Source   Edit  
Collection[T] = seq[T] | SomeSet[T]
Source   Edit  
Count = object
  val*: int
Count type for an incrementing flag Source   Edit  
HwylCliHelp = object
  header*, footer*, description*, usage*: string
  positionals*: seq[HwylPosArg]
  subcmds*: seq[HwylSubCmdHelp]
  flags*: seq[HwylFlagHelp]
  styles*: HwylCliStyles
  lengths*: HwylCliLengths
  longHelp*: bool
Source   Edit  
HwylCliStyles = object
  name* = "bold"
  header* = "bold cyan"
  flagShort* = "yellow"
  flagLong* = "magenta"
  flagDesc* = ""
  default* = "faint"
  required* = "red"
  subcmd* = "bold"
  args* = "bold italic"
  minCmdLen* = 8
  settings*: HashSet[HwylCliStyleSetting] = (data: [(0, Aliases),
      (-8667131158406858863, Types), (0, Aliases), (0, Aliases),
      (6130242188011939732, Defaults), (0, Aliases), (0, Aliases),
      (0, Aliases), (0, Aliases), (-5940405725068231575, Aliases),
      (0, Aliases), (0, Aliases), (0, Aliases),
      (8641844181895329213, Required), (0, Aliases), (0, Aliases)], counter: 4)
Source   Edit  
HwylCliStyleSetting = enum
  Aliases,                  ## show aliases, example "show (s)"
  Required,                 ## indicate if flag is required
  Defaults,                 ## show default value
  Types,                    ## show expected type for flag
  FlagGroups,               ## group flags together
  NoEnv                      ## ignore env settings for style
Source   Edit  
HwylFlagHelp = tuple[short, long, description, typeRepr, defaultVal, group: string,
                     required: bool]
Source   Edit  
HwylSubCmdHelp = tuple[name, aliases, desc: string]
Source   Edit  
KV[X; Y] = object
  key*: X
  val*: Y
basic key value type Source   Edit  
KVString = KV[string, string]
Source   Edit  
SomeSet[T] = set[T] | SomeSet[T]
Source   Edit  

Vars

hwylCliCurrentCmd: string
Source   Edit  

Consts

defaultStyleSettings = (data: [(0, Aliases), (-8667131158406858863, Types),
                               (0, Aliases), (0, Aliases),
                               (6130242188011939732, Defaults), (0, Aliases),
                               (0, Aliases), (0, Aliases), (0, Aliases),
                               (-5940405725068231575, Aliases), (0, Aliases),
                               (0, Aliases), (0, Aliases),
                               (8641844181895329213, Required), (0, Aliases),
                               (0, Aliases)], counter: 4)
Source   Edit  
hwylVersion {.strdefine, used.} = ""
Source   Edit  

Procs

proc `$`(c: Count): string {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc `$`(t: typedesc[KVString]): string
Source   Edit  
proc `$`[T](_: typedesc[SomeSet[T]]): string
Source   Edit  
proc `$`[X, Y](t: typedesc[KV[X, Y]]): string
Source   Edit  
proc `$`[X, Y](t: typedesc[seq[KV[X, Y]]]): string
Source   Edit  
func `+`[T](a: HashSet[T]; b: set[T]): HashSet[T]
Source   Edit  
func `-`[T](a: HashSet[T]; b: set[T]): HashSet[T]
Source   Edit  
proc bb(cli: HwylCliHelp): BbString {....raises: [KeyError], tags: [], forbids: [].}
Source   Edit  
proc checkVal(p: OptParser) {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc extractKey(p: var OptParser): string {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc fromBuiltinHelpStyles(kind: BuiltinStyleKind): HwylCliStyles {.
    ...raises: [ValueError], tags: [ReadEnvEffect], forbids: [].}
Source   Edit  
proc getGitDescribe(): string {....raises: [], tags: [], forbids: [].}
use git to obtain a version respresentation Source   Edit  
proc getGitHash(): string {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc hwylCliError[S: BbString | string](msg: S; showSuffix = true)

Shows the error message and quits the program

generated code will update hwylCliCurrentCmd to include in message whenever parsing args

Source   Edit  
func hwylDefaultUsage(name: string; hasSubcommands: bool;
                      args: seq[tuple[name: string, isSeq: bool]];
                      styles: HwylCliStyles): string {....raises: [], tags: [],
    forbids: [].}
generate a default BbMarkup usage string Source   Edit  
proc inferVersionFromNimble(): string {....raises: [OSError, IOError, ValueError,
    KeyError], tags: [ReadEnvEffect, ReadIOEffect, ReadDirEffect, WriteIOEffect,
                      RootEffect], forbids: [].}
Source   Edit  
func newHwylCliHelp(usage = ""; header = ""; footer = ""; description = "";
                    positionals: openArray[HwylPosArg] = @[];
                    subcmds: openArray[HwylSubCmdHelp] = @[];
                    flags: openArray[HwylFlagHelp] = @[];
                    styles = HwylCliStyles(); longHelp = false): HwylCliHelp {.
    ...raises: [], tags: [], forbids: [].}
Source   Edit  
proc newHwylCliStyles(name = "bold"; header = "bold cyan"; flagShort = "yellow";
                      flagLong = "magenta"; flagDesc = ""; default = "faint";
                      required = "red"; subcmd = "bold"; args = "bold italic";
                      typeRepr = "faint"; minCmdLen = 8; settings: HashSet[
    HwylCliStyleSetting] = defaultStyleSettings): HwylCliStyles {.
    ...raises: [ValueError], tags: [ReadEnvEffect], forbids: [].}
Source   Edit  
proc parse(p: OptParser; target: var bool) {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc parse(p: OptParser; target: var Count) {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc parse(p: OptParser; target: var float) {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc parse(p: OptParser; target: var int) {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc parse(p: OptParser; target: var string) {....raises: [], tags: [], forbids: [].}
Source   Edit  
proc parse(p: var OptParser; target: var object)
Source   Edit  
proc parse[E: enum](p: OptParser; target: var E)
Source   Edit  
proc parse[T](p: var OptParser; target: var Collection[T])
Source   Edit  
proc parse[T](p: var OptParser; target: var KV[string, T])
Source   Edit  
proc parseArgs(arg: string; target: var float) {....raises: [], tags: [],
    forbids: [].}
Source   Edit  
proc parseArgs(arg: string; target: var int) {....raises: [], tags: [], forbids: [].}
Source   Edit  
func parseArgs(arg: string; target: var string) {....raises: [], tags: [],
    forbids: [].}
Source   Edit  
proc parseArgs[E: enum](arg: string; target: var E)
Source   Edit  
proc parseArgs[T](arg: string; target: var seq[T])
Source   Edit  
proc parseArgs[T](args: seq[string]; target: var seq[T])
Source   Edit  
func render(cli: HwylCliHelp; f: HwylFlagHelp): string {....raises: [], tags: [],
    forbids: [].}
Source   Edit  
proc render(cli: HwylCliHelp; flags: seq[HwylFlagHelp]): string {.
    ...raises: [KeyError], tags: [], forbids: [].}
Source   Edit  
func render(cli: HwylCliHelp; pos: HwylPosArg): string {....raises: [], tags: [],
    forbids: [].}
Source   Edit  
proc render(cli: HwylCliHelp; positionals: seq[HwylPosArg]): string {.
    ...raises: [], tags: [], forbids: [].}
Source   Edit  
func render(cli: HwylCliHelp; subcmd: HwylSubCmdHelp): string {....raises: [],
    tags: [], forbids: [].}
Source   Edit  

Iterators

iterator items[X, Y](kvs: seq[KV[X, Y]]): (X, Y)
Source   Edit  

Macros

macro enumNames(a: typed): untyped
generates a sequence of strings representing enum names Source   Edit  
macro enumNamesWithValues(a: typed): untyped

generate seq with enum names and values

type Direction = enum
  North = "north"
  South = "south"
enumNamesWithValues(Direction) == @["North(north)", "South(south)"]

Source   Edit  
macro hwylCli(body: untyped)
generate a CLI styled by hwylterm and parsed by parseopt3 Source   Edit  

Templates

template render(cli: HwylCliHelp): string
Source   Edit