Web-style routing for CLI applications - bringing familiar web development patterns to the command line.
TimeWarp.Nuru supports intuitive route patterns:
| Pattern | Example | Matches |
|---|---|---|
| Literal | status |
./cli status |
| Parameter | greet {name} |
./cli greet Alice |
| Typed Parameter | delay {ms:int} |
./cli delay 1000 |
| Optional | deploy {env} {tag?} |
./cli deploy prod or ./cli deploy prod v1.2 |
| Options | build --config {mode} |
./cli build --config Release |
| Catch-all | docker {*args} |
./cli docker run -it ubuntu |
Parameters are automatically converted to the correct types:
NuruApp app = NuruApp.CreateBuilder(args)
// Supports common types out of the box
.Map("wait {seconds:int}")
.WithHandler((int seconds) => Thread.Sleep(seconds * 1000))
.AsCommand()
.Done()
.Map("download {url:uri}")
.WithHandler((Uri url) => Download(url))
.AsCommand()
.Done()
.Map("verbose {enabled:bool}")
.WithHandler((bool enabled) => SetVerbose(enabled))
.AsCommand()
.Done()
.Map("process {date:datetime}")
.WithHandler((DateTime date) => Process(date))
.AsCommand()
.Done()
.Map("scale {factor:double}")
.WithHandler((double factor) => Scale(factor))
.AsCommand()
.Done()
.Build();TimeWarp.Nuru includes built-in type converters for:
| Type Syntax | C# Type | Example |
|---|---|---|
string |
string |
{name:string} or {name} (default) |
int |
int (Int32) |
{count:int} |
double |
double |
{factor:double} |
bool |
bool |
{enabled:bool} |
DateTime |
DateTime |
{date:DateTime} |
Guid |
Guid |
{id:Guid} |
long |
long (Int64) |
{value:long} |
decimal |
decimal |
{price:decimal} |
TimeSpan |
TimeSpan |
{duration:TimeSpan} |
uri |
Uri |
{url:uri} |
See Supported Types Reference for complete list and custom type converters.
The MapDefault method registers a handler that executes when no arguments are provided:
NuruApp app = NuruApp.CreateBuilder(args)
.MapDefault()
.WithHandler(() => Console.WriteLine("Usage: myapp <command>"))
.AsCommand()
.Done()
.Map("greet {name}")
.WithHandler((string name) => Console.WriteLine($"Hello, {name}!"))
.AsCommand()
.Done()
.Build();./myapp # Prints: Usage: myapp <command>
./myapp greet Alice # Prints: Hello, Alice!A typical pattern is to display help information when users run your CLI without arguments:
NuruApp app = NuruApp.CreateBuilder(args)
.MapDefault()
.WithHandler(() =>
{
Console.WriteLine("myapp - A sample CLI application");
Console.WriteLine();
Console.WriteLine("Commands:");
Console.WriteLine(" greet {name} Greet someone by name");
Console.WriteLine(" version Show version info");
Console.WriteLine(" help Show detailed help");
})
.AsCommand()
.Done()
.Map("greet {name}")
.WithHandler((string name) => Console.WriteLine($"Hello, {name}!"))
.AsCommand()
.Done()
.Map("version")
.WithHandler(() => Console.WriteLine("v1.0.0"))
.AsCommand()
.Done()
.Build();While both can handle "fallback" scenarios, they serve different purposes:
| Feature | MapDefault |
Catch-all {*args} |
|---|---|---|
| Matches | Empty input only (no arguments) | Any unmatched input |
| Use case | Show usage/help when CLI invoked alone | Forward unknown commands elsewhere |
| Handler receives | Nothing | All arguments as string[] |
| Specificity | Most specific (exact empty match) | Least specific (matches anything) |
NuruApp app = NuruApp.CreateBuilder(args)
.MapDefault()
.WithHandler(() => Console.WriteLine("No command provided. Try 'help'."))
.AsCommand()
.Done()
.Map("help")
.WithHandler(() => Console.WriteLine("Available: greet, version"))
.AsCommand()
.Done()
.Map("{*args}")
.WithHandler((string[] args) => Console.WriteLine($"Unknown: {string.Join(" ", args)}"))
.AsCommand()
.Done()
.Build();./myapp # "No command provided. Try 'help'."
./myapp help # "Available: greet, version"
./myapp unknown command # "Unknown: unknown command"Literal segments must match exactly:
builder.Map("status")
.WithHandler(() => ShowStatus())
.AsCommand()
.Done();
builder.Map("version")
.WithHandler(() => ShowVersion())
.AsCommand()
.Done();
builder.Map("git status") // Multi-word literal
.WithHandler(() => GitStatus())
.AsCommand()
.Done();./cli status # Matches
./cli version # Matches
./cli git status # Matches
./cli stat # No matchParameters capture values from command-line arguments:
builder.Map("greet {name}")
.WithHandler((string name) => Console.WriteLine($"Hello, {name}!"))
.AsCommand()
.Done();
builder.Map("add {x:double} {y:double}")
.WithHandler((double x, double y) => Console.WriteLine($"{x} + {y} = {x + y}"))
.AsCommand()
.Done();./cli greet Alice # name = "Alice"
./cli add 10 20 # x = 10.0, y = 20.0
./cli add 3.14 2.86 # x = 3.14, y = 2.86- Must be valid C# identifiers
- Automatically match handler delegate parameters by name
- Case-sensitive in the route pattern
- Must be unique within a route
Parameters marked with ? are optional:
builder.Map("deploy {env} {tag?}")
.WithHandler((string env, string? tag) =>
{
Console.WriteLine($"Deploying to {env}");
if (tag != null)
Console.WriteLine($"Version: {tag}");
})
.AsCommand()
.Done();./cli deploy prod # env = "prod", tag = null
./cli deploy prod v1.2.3 # env = "prod", tag = "v1.2.3"Rules:
- Optional parameters must appear after all required parameters
- Use nullable types in handler (
string?,int?, etc.) - Multiple consecutive optional parameters create ambiguity (analyzer error NURU_S002)
Options provide named arguments with -- or - prefixes:
// Long form
builder.Map("build --verbose")
.WithHandler(() => BuildVerbose())
.AsCommand()
.Done();
// Short form
builder.Map("list -l")
.WithHandler(() => ListDetailed())
.AsCommand()
.Done();
// With values
builder.Map("serve --port {port:int}")
.WithHandler((int port) => StartServer(port))
.AsCommand()
.Done();
// Optional options
builder.Map("build --config? {mode?}")
.WithHandler((string? mode) => Build(mode ?? "Release"))
.AsCommand()
.Done();./cli build --verbose
./cli list -l
./cli serve --port 8080
./cli build --config Debug
./cli build # mode = null, defaults to "Release"Options can have both long and short forms:
builder.Map("backup {source} --compress,-c")
.WithHandler((string source, bool compress) => Backup(source, compress))
.AsCommand()
.Done();./cli backup ./data --compress # compress = true
./cli backup ./data -c # compress = true (same)
./cli backup ./data # compress = falseCatch-all parameters capture all remaining arguments:
builder.Map("echo {*words}")
.WithHandler((string[] words) => Console.WriteLine(string.Join(" ", words)))
.AsCommand()
.Done();
builder.Map("git add {*files}")
.WithHandler((string[] files) => StageFiles(files))
.AsCommand()
.Done();./cli echo Hello World from Nuru
# words = ["Hello", "World", "from", "Nuru"]
./cli git add file1.cs file2.cs file3.cs
# files = ["file1.cs", "file2.cs", "file3.cs"]Rules:
- Must be the last positional parameter
- Cannot be combined with optional parameters (analyzer error NURU_S004)
- Handler parameter must be
string[]
Build hierarchical command structures:
// Repository commands
builder.Map("git init")
.WithHandler(() => GitInit())
.AsCommand()
.Done();
builder.Map("git clone {url}")
.WithHandler((string url) => GitClone(url))
.AsCommand()
.Done();
builder.Map("git status")
.WithHandler(() => GitStatus())
.AsCommand()
.Done();
// Branch commands
builder.Map("git branch")
.WithHandler(() => ListBranches())
.AsCommand()
.Done();
builder.Map("git branch {name}")
.WithHandler((string name) => CreateBranch(name))
.AsCommand()
.Done();
builder.Map("git checkout {branch}")
.WithHandler((string branch) => Checkout(branch))
.AsCommand()
.Done();
// Commit commands
builder.Map("git add {*files}")
.WithHandler((string[] files) => GitAdd(files))
.AsCommand()
.Done();
builder.Map("git commit -m {message}")
.WithHandler((string message) => GitCommit(message))
.AsCommand()
.Done();
builder.Map("git push")
.WithHandler(() => GitPush())
.AsCommand()
.Done();
builder.Map("git push --force")
.WithHandler(() => GitPushForce())
.AsCommand()
.Done();Complex option combinations:
builder.Map("run {image}")
.WithHandler((string image) => Docker.Run(image))
.AsCommand()
.Done();
builder.Map("run {image} --port {port:int}")
.WithHandler((string image, int port) => Docker.Run(image, port: port))
.AsCommand()
.Done();
builder.Map("run {image} --port {port:int} --detach")
.WithHandler((string image, int port) => Docker.Run(image, port: port, detached: true))
.AsCommand()
.Done();
builder.Map("run {image} --env {*vars}")
.WithHandler((string image, string[] vars) => Docker.Run(image, envVars: vars))
.AsCommand()
.Done();Different handlers for different option combinations:
// Dry run
builder.Map("deploy {app} --env {environment} --dry-run")
.WithHandler((string app, string environment) => DeployDryRun(app, environment))
.AsCommand()
.Done();
// Actual deployment
builder.Map("deploy {app} --env {environment}")
.WithHandler((string app, string environment) => DeployReal(app, environment))
.AsCommand()
.Done();
// Force deployment
builder.Map("deploy {app} --env {environment} --force")
.WithHandler((string app, string environment) => DeployForce(app, environment))
.AsCommand()
.Done();When multiple routes could match, Nuru uses specificity rules:
- Most specific wins: Routes with more literals are more specific
- Parameters vs catch-all: Regular parameters are more specific than catch-all
- Options matter: Routes with specific options are more specific
builder.Map("deploy prod") // Most specific
.WithHandler(() => DeployProduction())
.AsCommand()
.Done();
builder.Map("deploy {env}") // Less specific
.WithHandler((string env) => DeployEnv(env))
.AsCommand()
.Done();
builder.Map("{*args}") // Least specific
.WithHandler((string[] args) => HandleGeneric(args))
.AsCommand()
.Done();./cli deploy prod # Matches first route (most specific)
./cli deploy dev # Matches second route
./cli anything else # Matches third routeDesign routes to minimize the number of route definitions:
// ❌ Factorial explosion with optional parameters
builder.Map("deploy {env}").WithHandler(handler).AsCommand().Done();
builder.Map("deploy {env} {version}").WithHandler(handler).AsCommand().Done();
builder.Map("deploy {env} {version} {region}").WithHandler(handler).AsCommand().Done();
// Creates 3 routes for one concept
// ✅ Use optional parameters
builder.Map("deploy {env} {version?} {region?}").WithHandler(handler).AsCommand().Done();
// One route, same flexibilityUse descriptive names that indicate purpose:
// ❌ Unclear
builder.Map("copy {arg1} {arg2}").WithHandler(handler).AsCommand().Done();
// ✅ Clear
builder.Map("copy {source} {destination}").WithHandler(handler).AsCommand().Done();Follow CLI conventions:
// ✅ Standard conventions
builder.Map("build --verbose").WithHandler(handler).AsCommand().Done(); // Long form
builder.Map("build -v").WithHandler(handler).AsCommand().Done(); // Short form
builder.Map("build --verbose,-v").WithHandler(handler).AsCommand().Done(); // Both (preferred)- Roslyn Analyzer - Compile-time route validation
- Supported Types - Complete type reference
- Auto-Help - Generating help from routes
- Developer Guide: Route Pattern Syntax - Implementation details