CommandTree 0.3.5

dotnet add package CommandTree --version 0.3.5
                    
NuGet\Install-Package CommandTree -Version 0.3.5
                    
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.3.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CommandTree" Version="0.3.5" />
                    
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.3.5
                    
#r "nuget: CommandTree, 0.3.5"
                    
#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.3.5
                    
#: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.3.5
                    
Install as a Cake Addin
#tool nuget:?package=CommandTree&version=0.3.5
                    
Install as a Cake Tool

CommandTree

Define CLI commands as F# discriminated unions. Get type-safe parsing, help generation, and fish completions from 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.GlobalFlags spec.Tree "my-cli")
| Error(HelpRequested path) -> printfn "%s" (CommandTree.helpForPath spec.Tree path "my-cli")
| Error(UnknownCommand(input, _)) -> UI.fail $"Unknown command: %s{input}"
| _ -> ()

Installation

dotnet add package CommandTree

How It Works

Case names become kebab-case commands. Nested unions become subcommand groups. Fields become positional arguments.

// From examples/ExampleCli/Program.fs

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

// my-cli deploy push staging
// my-cli deploy status prod
// my-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

// my-cli files tag v1.0 src/App.fs src/Lib.fs   ← list field collects 1+ remaining args
// my-cli files 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

// my-cli job start build-assets 1024 true
// my-cli job status 550e8400-e29b-41d4-a716-446655440000
// my-cli job                              ← runs list (CmdDefault)
type JobCommand =
    | [<Cmd("Start a new job")>] Start of name: string * size: int64 * verbose: bool
    | [<Cmd("Check job status")>] Status of id: Guid
    | [<Cmd("List recent jobs"); CmdDefault>] List

// my-cli check --conf custom.json --strict --no-cache
type CheckFlag =
    | [<CmdFlag(Name = "conf", Short = "k")>] Config of string
    | [<Cmd("Enable strict checking")>] Strict
    | [<CmdEnvRaw("NO_CACHE")>] NoCache

type Command =
    | [<Cmd("Task management")>] Task of TaskCommand
    | [<Cmd("Database operations")>] Db of DbCommand
    | [<Cmd("Deployment")>] Deploy of DeployCommand
    | [<Cmd("File operations")>] Files of FilesCommand
    | [<Cmd("Job management")>] Job of JobCommand
    | [<Cmd("Run checks")>] Check of CheckFlag list
    | [<Cmd("Run the test suite")>] Test
    | [<Cmd("Show full help")>] Help

Mapping rules

F# definition CLI invocation Notes
Task of TaskCommand my-cli task ... Nested union becomes a subcommand group
Test my-cli test No-field case becomes a simple command
Add of title: string my-cli task add "Buy groceries" Fields become positional args
Start of name: string * size: int64 * verbose: bool my-cli job start build 1024 true Multiple fields in order
Status of env: string option my-cli deploy status prod or my-cli deploy status Option fields can be omitted
Push of env: Priority my-cli deploy push high or my-cli deploy push hig Union fields match by kebab-case prefix (min 3 chars)
Tag of label: string * files: string list my-cli files tag v1 a.fs b.fs List field (must be last) collects 1+ remaining args
Check of CheckFlag list my-cli check --conf x.json --strict DU flag list becomes named flags
[<CmdDefault>] List my-cli task Runs when group is invoked without a subcommand
[<Cmd("desc", Name = "fmt")>] Format my-cli fmt Name overrides the derived command name

Attributes

  • [<Cmd("desc")>] sets help text; optional Name = "custom" overrides the command name
  • [<CmdDefault>] marks the default subcommand when a group is invoked without arguments
  • [<CmdCompletion("a", "b")>] provides fish shell completion values
  • [<CmdFileCompletion>] enables file path completion in fish (multiple allowed per case with FieldIndex)
  • [<CmdFlag(Name = "conf", Short = "k")>] overrides flag name or short flag on DU flag cases (optional — names are auto-derived)
  • [<CmdEnv("SUFFIX")>] overrides the env var suffix for a flag (prefix still applied)
  • [<CmdEnvRaw("VAR_NAME")>] sets the exact env var name, ignoring the prefix

Basic Usage

Full example: examples/ExampleCli/Program.fs

Without global options

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

With global options and env vars

// From examples/ExampleCli/Program.fs
// GlobalFlag defined above in the intro
open CommandTree

let spec =
    CommandReflection.fromUnionWithGlobalsAndEnv<Command, GlobalFlag> "Example project management CLI" "EXAMPLE"

let cmdName = "example-cli"

[<EntryPoint>]
let main argv =
    match spec.Parse argv with
    | Ok(globals, cmd) ->
        if globals |> List.contains GlobalFlag.Verbose then
            UI.dimInfo "Verbose mode enabled"

        globals
        |> List.iter (function
            | GlobalFlag.LogLevel level -> UI.dimInfo $"Log level: %s{level}"
            | _ -> ())

        run spec.Tree cmdName cmd
        0
    | Error(HelpRequested path) ->
        if path.IsEmpty then
            printfn "%s" (CommandTree.helpWithGlobals spec.GlobalFlags spec.Tree cmdName)
        else
            printfn "%s" (CommandTree.helpForPath spec.Tree path cmdName)

        0
    | Error(UnknownCommand(input, path)) ->
        UI.fail $"Unknown command: %s{input}"
        printfn "%s" (CommandTree.helpForPath spec.Tree path cmdName)
        1
    | Error(InvalidArguments(cmd, msg)) ->
        UI.fail $"Invalid arguments for %s{cmd}: %s{msg}"
        1
    | Error(AmbiguousArgument(input, candidates)) ->
        let joined = String.concat ", " candidates
        UI.fail $"Ambiguous: '{input}' matches: {joined}"
        1
    | Error(UnknownFlag(flag, cmd, validFlags)) ->
        let joined = String.concat ", " validFlags
        UI.fail $"Unknown flag '%s{flag}' for command '%s{cmd}'. Valid flags: %s{joined}"
        1
    | Error(DuplicateFlag(flag, cmd)) ->
        UI.fail $"Flag '%s{flag}' provided more than once for command '%s{cmd}'"
        1

Global flags can appear anywhere in the arg list — before, after, or interleaved with command args. Duplicate flag names between global and command-level flags are rejected at tree construction time.

Reference

Parsing & Help

CommandTree.parse tree args              // Result<'Cmd, ParseError>
CommandTree.help tree path prefix        // Help text for one level
CommandTree.helpFull tree prefix         // Full recursive help
CommandTree.helpForPath tree path prefix // Help for a subcommand path
CommandTree.helpWithGlobals flags tree prefix // Help with global options section
CommandTree.format tree cmd path prefix  // Format command back to CLI string
CommandTree.findByPath tree path         // Navigate to a subtree
CommandTree.closestGroupPath tree args   // Deepest matching group path

Reflection

// Without global options
CommandReflection.fromUnion<'Cmd> "desc"                          // CommandTree<'Cmd>
CommandReflection.fromUnionWithEnv<'Cmd> "desc" "PREFIX"          // CommandTree<'Cmd> (with env vars)

// With global options (returns GlobalSpec with .Tree, .Parse, .GlobalFlags)
CommandReflection.fromUnionWithGlobals<'Cmd, 'G> "desc"           // GlobalSpec<'G, 'Cmd>
CommandReflection.fromUnionWithGlobalsAndEnv<'Cmd, 'G> "desc" "P" // GlobalSpec<'G, 'Cmd> (with env vars)

// Utilities
CommandReflection.formatCmd cmd              // Format command to CLI string
CommandReflection.caseName value             // Kebab-case name of union value
CommandReflection.toKebabCase "PascalCase"   // "pascal-case"
CommandReflection.parseFieldValue type str   // Result<obj option, string>
CommandReflection.formatFieldValue value     // Typed value to string

DU-Based Flags

Define flags as discriminated unions. No-field cases become boolean flags, single-field cases become value flags:

// From examples/ExampleCli/Program.fs

// example-cli check --conf custom.json --strict --no-cache
type CheckFlag =
    | [<CmdFlag(Name = "conf", Short = "k")>] Config of string
    | [<Cmd("Enable strict checking")>] Strict
    | [<CmdEnvRaw("NO_CACHE")>] NoCache

type Command =
    | [<Cmd("Run checks")>] Check of CheckFlag list

Short flags are auto-derived from the first letter (collision detection prevents duplicates). Use [<CmdFlag>] to override.

Env Var Binding

When an env prefix is configured, every DU flag case gets an auto-derived env var: PREFIX_SCREAMING_SNAKE_CASE.

// From examples/ExampleCli/Program.fs

type GlobalFlag =
    | [<Cmd("Enable verbose output")>] Verbose                     // env: EXAMPLE_VERBOSE
    | [<Cmd("Set log level"); CmdEnv("LVL")>] LogLevel of string   // env: EXAMPLE_LVL (suffix override)

type CheckFlag =
    | [<CmdEnvRaw("NO_CACHE")>] NoCache                            // env: NO_CACHE (exact name)

Resolution order: CLI flag > env var > absent. For boolean flags, env var values "true"/"1" mean present, "false"/"0"/unset mean absent.

Fish Completions

FishCompletions.generateContent tree "my-tool"  // Generate .fish content
FishCompletions.writeToFile tree "my-tool"      // Write to ~/.config/fish/completions/
FishCompletions.installHook "my-tool"           // Auto-update hook in conf.d

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

The library also includes Process (process execution helpers) and UI (colored terminal output) modules. See the API docs for details.

License

MIT

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.3.5 45 4/8/2026
0.3.4 33 4/8/2026
0.3.3 54 4/8/2026
0.3.2 90 4/7/2026
0.3.1 82 4/6/2026
0.3.0 152 4/5/2026
0.2.0 80 4/5/2026
0.1.0 90 3/12/2026
0.1.0-alpha.1 45 3/12/2026