CommandTree 0.8.0

dotnet add package CommandTree --version 0.8.0
                    
NuGet\Install-Package CommandTree -Version 0.8.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CommandTree" Version="0.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CommandTree" Version="0.8.0" />
                    
Directory.Packages.props
<PackageReference Include="CommandTree" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CommandTree --version 0.8.0
                    
#r "nuget: CommandTree, 0.8.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CommandTree@0.8.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CommandTree&version=0.8.0
                    
Install as a Cake Addin
#tool nuget:?package=CommandTree&version=0.8.0
                    
Install as a Cake Tool

CommandTree

Define CLI commands as F# discriminated unions, and let type-safe parsing, help generation, and fish completions fall out of your types.

// From examples/ExampleCli/Program.fs

// my-cli task add "Buy groceries"
// my-cli task add "Buy groceries" high
// my-cli task                             ← runs list (CmdDefault)
// my-cli task complete 5
type TaskCommand =
    | [<Cmd("Add a new task")>] Add of title: string * priority: Priority option
    | [<Cmd("List all tasks"); CmdDefault>] List
    | [<Cmd("Complete a task")>] Complete of id: int
    | [<Cmd("Remove a task")>] Remove of id: int

// my-cli --verbose task add "Buy groceries"   ← global flag before command
// my-cli task add "Buy groceries" --verbose   ← global flag after command
type GlobalFlag =
    | [<Cmd("Enable verbose output")>] Verbose
    | [<Cmd("Set log level"); CmdEnv("LVL")>] LogLevel of string

type Command =
    | [<Cmd("Task management")>] Task of TaskCommand
    | [<Cmd("Run the test suite")>] Test
    | [<Cmd("Show full help")>] Help

let spec =
    CommandReflection.fromUnionWithGlobalsAndEnv<Command, GlobalFlag> "My CLI" "MYAPP"

match spec.Parse argv with
| Ok(globals, Task(Add(title, _))) -> printfn "Adding %s" title
| Ok(_, Help) -> printfn "%s" (CommandTree.helpWithGlobals spec.Tree spec.GlobalFlags "my-cli")
| Error(HelpRequested path) -> printfn "%s" (CommandTree.helpForPath spec.Tree path "my-cli")
| Error(UnknownCommand(input, _rest, _)) -> UI.fail $"Unknown command: %s{input}"
| _ -> ()

Status: early alpha, and substantially AI-written. It runs the author's own F# CLIs, but behavior and APIs shift between versions and rough edges are expected — your mileage may vary. Issues and PRs very welcome.

The problem

Hand-written CLI parsing tends to drift: strings get parsed into the wrong types, a --help branch goes missing, and the help text stops matching the real flags. The idea here is to describe the command surface once as a discriminated union and derive the parser, help text, and shell completions from that single definition by reflection — so they aim to stay in sync because there's nothing to keep in sync by hand.

Install

dotnet add package CommandTree

How it works

Case names become kebab-case commands. Nested unions become subcommand groups. Fields become positional arguments. Every snippet below is sourced verbatim from the compiled examples/ExampleCli/Program.fs, so it can't drift from code that builds.

A simple subcommand group with a default subcommand:

// example-cli db migrate
// example-cli db                               ← runs status (CmdDefault)
// example-cli db reset
type DbCommand =
    | [<Cmd("Run database migrations")>] Migrate
    | [<Cmd("Reset the database")>] Reset
    | [<Cmd("Show connection status"); CmdDefault>] Status

Value completions and an optional positional field:

// example-cli deploy push staging
// example-cli deploy status prod
// example-cli deploy                           ← runs status with no env (CmdDefault)
type DeployCommand =
    | [<Cmd("Deploy to environment"); CmdCompletion("dev", "staging", "prod")>] Push of env: string
    | [<Cmd("Show deploy status"); CmdCompletion("dev", "staging", "prod"); CmdDefault>] Status of env: string option

A trailing list field collects remaining args; file completions per field:

// example-cli tag v1.0 src/App.fs src/Lib.fs   ← list field collects 1+ remaining args
// example-cli diff old.dll new.dll             ← multiple CmdFileCompletion with FieldIndex
type FilesCommand =
    | [<Cmd("Tag files with a label"); CmdFileCompletion>] Tag of label: string * files: string list
    | [<Cmd("Compare two DLLs"); CmdFileCompletion(FieldIndex = 0); CmdFileCompletion(FieldIndex = 1)>] Diff of
        oldDll: string *
        newDll: string

A flag DU becomes named --flags:

// example-cli check --conf custom.json --strict --no-cache --wait=30
// example-cli check --wait          ← bare optional-value flag binds `Wait None` (never swallows a value)
type CheckFlag =
    | [<CmdFlag(Name = "conf", Short = "k")>] Config of string
    | [<Cmd("Enable strict checking")>] Strict
    | [<CmdEnvRaw("NO_CACHE")>] NoCache
    | [<Cmd("Wait N seconds; bare for the default")>] Wait of int option
F# definition CLI invocation Notes
Task of TaskCommand example-cli task ... Nested union becomes a subcommand group
Test example-cli test No-field case becomes a simple command
Add of title: string example-cli task add "Buy groceries" Fields become positional args
Start of name: string * size: int64 example-cli job start build 1024 Multiple fields, in order
Status of env: string option example-cli deploy status prod or example-cli deploy status Option fields can be omitted
Push of env: Priority example-cli deploy push high or example-cli deploy push hig Union fields match by kebab-case prefix (min 3 chars)
Tag of label: string * files: string list example-cli files tag v1 a.fs b.fs List field (must be last) collects 1+ remaining args
Check of CheckFlag list example-cli check --conf x.json --strict DU flag list becomes named flags
Wait of int option (flag case) example-cli check --wait or --wait=5 Optional-value flag: bare binds None, inline = binds Some; never swallows the next token
Remove of name: string * flags: RemoveFlag list example-cli remove old-ws --force Positionals + trailing DU flag list; flags may appear anywhere
[<CmdDefault>] List example-cli task Runs when a group is invoked without a subcommand
[<Cmd(Name = "fmt")>] Format example-cli fmt Name overrides the derived command name

Basic usage

Build a tree from your command DU, then parse argv against it. The full runnable example lives in examples/ExampleCli/Program.fs.

open CommandTree

let tree = CommandReflection.fromUnion<Command> "My CLI"

[<EntryPoint>]
let main argv =
    match CommandTree.parse tree argv with
    | Ok cmd ->
        run cmd
        0
    | Error(HelpRequested path) ->
        printfn "%s" (CommandTree.helpForPath tree path "my-cli")
        0
    | Error e ->
        // UnknownCommand, InvalidArguments, AmbiguousArgument, UnknownFlag, DuplicateFlag
        UI.fail $"%A{e}"
        1

For global flags and env-var binding, use fromUnionWithGlobalsAndEnv, which returns a GlobalSpec whose Parse yields (globals, command). Global flags can appear anywhere in the arg list — before, after, or interleaved with command args. See the example for a full match over every ParseError case.

Attributes

Decorate union cases to customize parsing, help, and completions:

Attribute Effect
[<Cmd("desc")>] Sets help text. Name = "custom" overrides the derived command name. Description is optional — omit it to derive from the case name.
[<CmdDefault>] Marks the subcommand to run when a group is invoked with no arguments.
[<CmdArg("desc")>] Documents a positional argument (shows in an Arguments: help section). FieldIndex = N targets one field; Default = "value" adds a default hint. For record-typed args, apply directly to record fields.
[<CmdExample("ex1", "ex2")>] Adds an Examples: help section. The command path is prepended automatically.
[<CmdCompletion("a", "b")>] Provides fish completion values; FieldIndex selects which argument.
[<CmdFileCompletion>] Enables file-path completion in fish (repeatable per case with FieldIndex).
[<CmdFlag(Name, Short, Description)>] Overrides the derived name/short/description of a DU flag case.
[<CmdEnv("SUFFIX")>] Overrides the env-var suffix for a flag (the prefix still applies).
[<CmdEnvRaw("VAR_NAME")>] Sets the exact env-var name, ignoring the prefix.

Flags and env vars

Define flags as their own DU and attach them as a list field (e.g. Check of CheckFlag list). A no-field case is a boolean flag; a single 'T option field is an optional-value flag; any other single field is a required-value flag. Short flags are auto-derived from the first letter (collisions are dropped); override with [<CmdFlag>].

Required-value flags take their value space-separated (--conf custom.json) or inline (--conf=custom.json). Optional-value flags are inline-only: bare --wait binds None, --wait=5 binds Some 5, and they never consume the following token — so --wait --detach is Wait None plus Detach, and --wait 5 leaves 5 as a positional. A bare-but-= form with no value (--wait=) or a value that does not parse (--wait=abc) is a parse error.

A case can mix positional fields with a trailing flag DU list (e.g. Remove of name: string * flags: RemoveFlag list). Flags may appear anywhere relative to the positionals, and everything after a standalone -- binds as positional values — so a value that looks like a flag (remove -- --force) stays a value.

// example-cli check --conf custom.json --strict --no-cache --wait=30
// example-cli check --wait          ← bare optional-value flag binds `Wait None` (never swallows a value)
type CheckFlag =
    | [<CmdFlag(Name = "conf", Short = "k")>] Config of string
    | [<Cmd("Enable strict checking")>] Strict
    | [<CmdEnvRaw("NO_CACHE")>] NoCache
    | [<Cmd("Wait N seconds; bare for the default")>] Wait of int option

When an env prefix is configured (fromUnionWithEnv / fromUnionWithGlobalsAndEnv), each flag case also reads PREFIX_SCREAMING_SNAKE_CASE. Resolution order is CLI flag > env var > absent. For booleans, "true"/"1" mean present and "false"/"0"/unset mean absent.

Supported field types

Type Example Notes
string of name: string Any string value
int of count: int Int32
int64 of id: int64 Int64
float of rate: float Double
decimal of price: decimal Decimal
bool of force: bool Boolean
Guid of id: Guid Guid
'T option of env: string option None when omitted
Union of env: Priority Kebab-case name, prefix matching (min 3 chars)
'T list of files: string list Collects remaining args (1+, must be last field)
'Flag list of CheckFlag list DU flag list becomes named --flags

A field whose type isn't supported is a spec error — a malformed command shape, distinct from a runtime parse error. See Advanced usage for the full API reference (parsing, help rendering, version stamping, spec errors, fish completions) and the optional build-time analyzer.

More

  • Advanced usage — full CommandTree/CommandReflection reference, structured errors, version stamping, fish completions, and the build-time analyzer.
  • API docs — generated reference for every public member, including the Process and UI helper modules.

License

MIT

</content> </invoke>

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.8.0 40 7/22/2026
0.7.1 38 7/22/2026
0.7.0 145 6/15/2026
0.6.3 112 6/12/2026
0.6.2 120 6/3/2026
0.6.1 107 6/2/2026
0.6.0 107 6/2/2026
0.5.1 109 5/26/2026
0.5.0 150 4/27/2026
0.4.0 171 4/11/2026
0.3.5 125 4/8/2026
0.3.4 111 4/8/2026
0.3.3 130 4/8/2026
0.3.2 125 4/7/2026
0.3.1 112 4/6/2026
0.3.0 180 4/5/2026
0.2.0 110 4/5/2026
0.1.0 122 3/12/2026
0.1.0-alpha.1 72 3/12/2026