CommandTree 0.3.2
See the version list below for details.
dotnet add package CommandTree --version 0.3.2
NuGet\Install-Package CommandTree -Version 0.3.2
<PackageReference Include="CommandTree" Version="0.3.2" />
<PackageVersion Include="CommandTree" Version="0.3.2" />
<PackageReference Include="CommandTree" />
paket add CommandTree --version 0.3.2
#r "nuget: CommandTree, 0.3.2"
#:package CommandTree@0.3.2
#addin nuget:?package=CommandTree&version=0.3.2
#tool nuget:?package=CommandTree&version=0.3.2
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; optionalName = "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 withFieldIndex)[<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 | Versions 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. |
-
net10.0
- FSharp.Core (>= 10.1.201)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.