Alveus.Contour (0.2.1)
Installation
dotnet nuget add source --name public --username your_username --password your_token dotnet add package --source public --version 0.2.1 Alveus.ContourAbout this package
A modern, lightweight framework for building command-line applications in C#. Alveus.Contour provides an intuitive attribute-based API for defining commands, supports middleware pipelines, includes a built-in REPL, and is fully AOT-compatible.
Alveus Contour
A modern, lightweight framework for building command-line applications in C#. Alveus.Contour provides an intuitive attribute-based API for defining commands, supports middleware pipelines, includes a built-in REPL, and is fully AOT-compatible.
Features
- Attribute-based command definition - Use simple attributes to declare commands, subcommands, options, flags, and parameters
- Middleware pipeline - Wrap command execution with cross-cutting concerns (logging, validation, initialization)
- Global options - Define options available across all commands
- Built-in REPL - Interactive mode with command history, auto-completion, and customizable prompts
- Intelligent auto-completion - Tab completion for commands, subcommands, options, and flags
- Syntax highlighting - Real-time color highlighting of commands, subcommands, and options as you type
- Command history - Navigate previous commands with arrow keys and recall with
!<number>syntax - Configurable history - Option to save only successful commands
- Environment variable expansion - Support for Bash, CMD, and PowerShell variable syntax
- Dependency injection - Built on
Microsoft.Extensions.DependencyInjection - Theming system - Customize colors and styles throughout the application with built-in themes or create your own
- Extensible help system - Customize help output with custom providers
- Version handling - Display version information with custom providers or inline delegates
- AOT-ready - Full support for Native AOT compilation
- Spectre.Console integration - Rich terminal output with colors, tables, and panels
Requirements
- .NET 10.0 SDK or later
Quick Start
Installation
dotnet add package Alveus.Contour
Basic Usage
using Alveus.Contour;
var builder = new ContourBuilder();
// Register commands
builder.AddCommand<MyCommand>();
// Configure the app
var app = builder.Build();
return app.Run(args);
Defining a Command
using Alveus.Contour.Attributes;
[Command("greet", Description = "Greets a person")]
public class GreetCommand
{
[DefaultHandler]
public int Execute(
[Parameter(0, Description = "Name to greet")]
string name,
[Flag("formal", ShortName = "f", Description = "Use formal greeting")]
bool formal)
{
var greeting = formal ? $"Good day, {name}" : $"Hi, {name}!";
Console.WriteLine(greeting);
return 0;
}
}
$ myapp greet Alice
Hi, Alice!
$ myapp greet Alice --formal
Good day, Alice
Core Concepts
Commands
Commands are defined using the [Command] attribute on a class. Each command can have multiple handlers for subcommands.
[Command("git", Description = "Version control operations")]
public class GitCommand
{
[DefaultHandler]
public int Default()
{
Console.WriteLine("Usage: git <command>");
return 0;
}
[Subcommand("status", Description = "Show working tree status")]
public int Status()
{
// Implementation
return 0;
}
[Subcommand("commit", Description = "Record changes to the repository")]
public int Commit(
[Option("message", ShortName = "m", Description = "Commit message", Required = true)]
string message)
{
// Implementation
return 0;
}
}
Global Options
Global options are available across all commands:
public class AppGlobalOptions
{
[Flag("verbose", Description = "Enable verbose output")]
public bool Verbose { get; set; }
[Option("config", ShortName = "c", Description = "Configuration file path")]
public string? ConfigPath { get; set; }
}
// Register in builder
builder.UseGlobalOptions<AppGlobalOptions>();
// Access in commands via dependency injection
[Command("build")]
public class BuildCommand
{
private readonly AppGlobalOptions _options;
public BuildCommand(AppGlobalOptions options)
{
_options = options;
}
[DefaultHandler]
public int Execute()
{
if (_options.Verbose)
Console.WriteLine("Verbose mode enabled");
return 0;
}
}
Middleware
Middleware wraps command execution with cross-cutting concerns like logging, validation, or initialization:
// Class-based middleware
public class LoggingMiddleware : ICommandMiddleware
{
public int Execute(CommandContext context, Func<int> next)
{
Console.WriteLine($"Executing: {context.Arguments.Command}");
var result = next();
Console.WriteLine($"Completed with exit code: {result}");
return result;
}
}
builder.UseMiddleware<LoggingMiddleware>();
// Or use inline middleware
builder.UseMiddleware((context, next) =>
{
var startTime = DateTime.Now;
var result = next();
var elapsed = DateTime.Now - startTime;
Console.WriteLine($"Execution time: {elapsed.TotalMilliseconds}ms");
return result;
});
Skipping Middleware
You can skip middleware for specific commands or subcommands using the [SkipMiddleware] attribute. This is useful for informational commands (like help or version) or system commands where middleware would be inappropriate:
// Skip middleware for entire command
[Command("info")]
[SkipMiddleware]
public class InfoCommand
{
[DefaultHandler]
public int Execute()
{
Console.WriteLine("Application information...");
return 0;
}
}
// Skip middleware for specific subcommands only
[Command("database")]
public class DatabaseCommand
{
[Subcommand("migrate")]
public int Migrate()
{
// Middleware runs for this subcommand
Console.WriteLine("Running migrations...");
return 0;
}
[Subcommand("status")]
[SkipMiddleware] // Skip middleware for this subcommand
public int Status()
{
Console.WriteLine("Database status: OK");
return 0;
}
}
Note: Middleware is automatically skipped for:
- Help requests (
--help,-h, or thehelpcommand) - Version requests (
--version,-v) - Invalid commands or validation errors
REPL Mode
Enable an interactive Read-Eval-Print Loop with advanced features:
builder.ConfigureRepl(config =>
{
config.EnableRepl = true;
config.EnableHistory = true;
config.SaveHistoryOnSuccessOnly = false; // Save all commands, or only successful ones
config.DefaultToRepl = true; // Start in REPL when no args provided
config.EnableSyntaxHighlighting = true; // Enable real-time syntax highlighting (default: true)
config.PersistGlobalOptions = true; // Persist global options across REPL commands (default: false)
// Prompt receives ContourApp to access current theme
config.Prompt = (app) =>
{
var theme = app.Theme;
AnsiConsole.Markup($"[{theme.InfoStyle.ToMarkup()}]> [/]");
};
// Enable environment variable expansion with multiple syntaxes
config.EnableVariableExpansion = true;
config.VariableExpansionSyntax = VariableExpansionSyntax.OsDefault; // Platform defaults
config.ErrorOnUnknownVariable = false; // Return empty string for unknown vars (default: false)
// Or combine multiple syntaxes using flags:
// config.VariableExpansionSyntax = VariableExpansionSyntax.Bash | VariableExpansionSyntax.PowerShell;
});
REPL Features:
-
Syntax Highlighting - Commands, subcommands, and options are highlighted as you type
- Commands appear in the theme's command color
- Valid options and flags are highlighted in the option color
- Invalid or unknown tokens remain unhighlighted
- Colors update dynamically when you change themes
-
Auto-completion - Press Tab to complete commands, subcommands, options, and flags
> gre<Tab> # Completes to "greet" > greet --<Tab> # Shows all available options and flags > greet -<Tab> # Shows short-form options -
Command History - Navigate with arrow keys or recall by number
> greet Alice > greet Bob > history # Shows numbered command history > !1 # Recalls "greet Alice" for editing > <Up Arrow> # Navigate to previous commands -
Environment Variable Expansion - Use environment variables in commands with support for multiple syntaxes
# Bash/Linux style > greet $USER # Expands to current user > process ${HOME}/file.txt # Braced syntax # Windows CMD style > greet %USERNAME% # Windows user variable > process %TEMP%\file.txt # Temp directory # Enhanced Batch/DOS style with substring support > echo %PATH:~0,20% # First 20 characters of PATH > echo %USERNAME:~-5% # Last 5 characters of username > echo %TEMP:~3% # From character 3 to end # PowerShell style > greet $env:USERNAME # PowerShell syntaxAvailable syntaxes (can be combined with
|):VariableExpansionSyntax.None- Disable expansionVariableExpansionSyntax.Bash-$VARor${VAR}VariableExpansionSyntax.Cmd-%VAR%VariableExpansionSyntax.PowerShell-$env:VARVariableExpansionSyntax.BatchEnhanced-%VAR:~start,length%with substring supportVariableExpansionSyntax.OsDefault- Automatically use platform defaults- Windows:
Cmd | BatchEnhanced - Linux/macOS:
Bash
- Windows:
Examples of combining syntaxes:
// Support both Bash and PowerShell in same session config.VariableExpansionSyntax = VariableExpansionSyntax.Bash | VariableExpansionSyntax.PowerShell; // Support all Windows styles config.VariableExpansionSyntax = VariableExpansionSyntax.Cmd | VariableExpansionSyntax.BatchEnhanced | VariableExpansionSyntax.PowerShell; // Cross-platform support config.VariableExpansionSyntax = VariableExpansionSyntax.Bash | VariableExpansionSyntax.Cmd | VariableExpansionSyntax.PowerShell;Handling Unknown Variables:
By default, unknown environment variables expand to an empty string:
> echo $UNKNOWN_VAR something # Expands to "echo something"To receive an error when unknown variables are encountered:
config.ErrorOnUnknownVariable = true;With this enabled:
> echo $UNKNOWN_VAR ✗ Error: Unknown environment variable: UNKNOWN_VAR -
Persistent Global Options - Global options can persist across REPL commands
When
PersistGlobalOptionsis enabled, global options set when starting the app will remain active for all REPL commands:# Start app with global options $ myapp --verbose --log-level 3 # In REPL, these options remain active > info # Shows: verbose=true, log-level=3 > build # Runs with verbose=true, log-level=3 > test --log-level 1 # Override log-level to 1, verbose stays true > deploy --nogopt # Reset to defaults for this command onlyKey Features:
- Global options persist across all REPL commands
- Options can be overridden on a per-command basis
- Use
--nogoptflag to temporarily reset to defaults - When disabled (default), options reset to defaults before each command
Configuration:
builder.ConfigureRepl(config => { config.PersistGlobalOptions = true; // Enable persistence (default: false) }); -
Boolean Flag Values - Flags support explicit boolean values
Flags can now accept explicit true/false values in addition to the traditional presence/absence:
# Traditional syntax (presence = true) $ myapp --verbose # Explicit true values $ myapp --verbose true $ myapp --verbose yes $ myapp --verbose on $ myapp --verbose 1 # Explicit false values $ myapp --verbose false $ myapp --verbose no $ myapp --verbose off $ myapp --verbose 0This is particularly useful in REPL mode when you want to disable a flag that's already set:
> info --verbose # Enable verbose > info --verbose false # Disable verboseSupported values:
- True:
true,yes,on,1(case-insensitive) - False:
false,no,off,0(case-insensitive) - No value: Defaults to
true(traditional behavior)
- True:
-
Built-in Commands
exitorquit- Exit the REPLhelp- Display available commandshistory- Display command historyhistory clear- Clear command historyclear- Clear the screen
Example REPL Session:
$ myapp
Interactive REPL - Type 'exit' to quit
> greet Alice
Hi, Alice!
> greet Bob --formal
Good day, Bob
> history
# | Command
---+-------------------
1 | greet Alice
2 | greet Bob --formal
> !1
> greet Alice --formal # Edit recalled command before running
Good day, Alice
> exit
Theming
Customize the appearance of your CLI application with themes:
// Apply a built-in theme
var app = builder.Build();
app.ApplyTheme(ContourTheme.Dark()); // Dark theme
app.ApplyTheme(ContourTheme.Light()); // Light theme
app.ApplyTheme(ContourTheme.Ultraviolet()); // Vibrant purple/neon
app.ApplyTheme(ContourTheme.Rainbow()); // Colorful rainbow
// Create a custom theme
var customTheme = new ContourTheme
{
CommandStyle = new Style(Color.Magenta, decoration: Decoration.Bold),
SubcommandStyle = new Style(Color.Cyan),
OptionStyle = new Style(Color.Green),
ErrorStyle = new Style(Color.Red, decoration: Decoration.Bold),
WarningStyle = new Style(Color.Yellow),
SuccessStyle = new Style(Color.Green1),
InfoStyle = new Style(Color.Blue),
DimStyle = new Style(Color.Grey),
HighlightStyle = new Style(Color.White, decoration: Decoration.Bold)
};
app.ApplyTheme(customTheme);
// Themes affect:
// - Syntax highlighting in REPL
// - Help output (commands, options, descriptions)
// - Error and success messages
// - Tables, panels, and borders
// - Version display
// - Custom command output (when using theme)
Accessing Theme in Commands:
[Command("mycommand")]
public class MyCommand
{
private readonly ContourApp _app;
public MyCommand(ContourApp app)
{
_app = app;
}
[DefaultHandler]
public int Execute()
{
// Option 1: Use theme-aware markup methods (recommended)
_app.MarkupLine("[success]Operation completed successfully![/]");
_app.MarkupLine("[error]Something went wrong![/]");
_app.MarkupLine("[warning]Warning message[/]");
_app.MarkupLine("[info]Informational message[/]");
_app.MarkupLine("[dim]Dimmed text[/] [highlight]Highlighted text[/]");
_app.MarkupLine("[command]mycommand[/] [subcommand]subcommand[/] [option]--option[/]");
// Option 2: Access theme directly for advanced scenarios
var theme = _app.Theme;
AnsiConsole.MarkupLine($"[{theme.SuccessStyle.ToMarkup()}]Success![/]");
return 0;
}
}
Theme-Aware Markup Tags:
ContourApp provides convenient wrapper methods that automatically apply theme colors to your output. These methods support the following tags:
| Tag | Description | Use Case |
|---|---|---|
[error]...[/error] |
Error messages | Errors, failures, exceptions |
[warning]...[/warning] |
Warning messages | Warnings, cautions |
[success]...[/success] |
Success messages | Confirmations, completions |
[info]...[/info] |
Informational messages | General information, prompts |
[dim]...[/dim] |
Dimmed/muted text | Secondary information, hints |
[highlight]...[/highlight] |
Highlighted/emphasized text | Important values, keywords |
[command]...[/command] |
Command names | Commands in help text |
[subcommand]...[/subcommand] |
Subcommand names | Subcommands in help text |
[option]...[/option] |
Option/flag names | Options and flags in help text |
Available Methods:
// Write markup (no newline)
_app.Markup("[error]Error:[/] Something went wrong");
// Write markup with newline
_app.MarkupLine("[success]✓[/] Task completed");
// Write interpolated markup (no newline)
_app.MarkupInterpolated($"[info]Processing {filename}...[/]");
// Write interpolated markup with newline
_app.MarkupLineInterpolated($"[success]Processed {count} files[/]");
// Parse theme markup without writing to console
string spectreMarkup = _app.ParseThemeMarkup("[error]Error:[/] [warning]Warning[/]");
// Returns: "[red]Error:[/] [yellow]Warning[/]" (actual colors depend on theme)
// Use with Spectre.Console directly
var panel = new Panel(_app.ParseThemeMarkup("[info]Panel content[/]"));
AnsiConsole.Write(panel);
Benefits:
- Theme-independent: Your code doesn't need to know about specific colors
- Automatic updates: Changes to the theme automatically affect all output
- Cleaner code: Simpler syntax compared to accessing theme styles directly
- Consistent styling: Ensures consistent use of theme colors across commands
Version Handling
Customize version display with access to the current theme:
// Using a custom provider class
public class CustomVersionProvider : IVersionProvider
{
public int ShowVersion()
{
Console.WriteLine("MyApp v2.1.0");
return 0;
}
}
builder.UseVersionProvider<CustomVersionProvider>();
// Or use inline version provider with theme access
builder.UseVersionProvider((app) =>
{
var theme = app?.Theme ?? ContourTheme.Default();
AnsiConsole.MarkupLine($"[{theme.InfoStyle.ToMarkup()}]MyApp v2.1.0 - Build 12345[/]");
return 0;
});
Help System
Customize help output:
// Using a custom provider class
public class CustomHelpProvider : IHelpProvider
{
public int ShowHelp(Type commandType, CommandAvailability context)
{
// Custom help implementation
return 0;
}
public int ShowAllHelp(CommandAvailability context)
{
// Custom help implementation
return 0;
}
}
builder.UseHelpProvider<CustomHelpProvider>();
Attributes Reference
Command Attributes
-
[Command]- Marks a class as a commandName- Command name (required)Description- Command descriptionAliases- Alternative namesAvailability- Where command is available (Cli,Repl, orBoth)
-
[DefaultHandler]- Marks the default command handler method -
[Subcommand]- Marks a method as a subcommand handlerName- Subcommand name (required)Description- Subcommand descriptionAvailability- Where subcommand is available
Parameter Attributes
-
[Parameter]- Positional parameterPosition- Zero-based positionDescription- Parameter descriptionMinCount- Minimum required count (for collections)
-
[Option]- Named option that accepts a valueName- Option name (required)ShortName- Short form (e.g.,-o)Description- Option descriptionDefaultValue- Default value if not providedRequired- Whether option is required
-
[Flag]- Boolean flagName- Flag name (required)ShortName- Short form (e.g.,-v)Description- Flag descriptionDefaultValue- Default value (defaults tofalse)
Building and Running
Build the Solution
dotnet restore
dotnet build
Project Structure
Alveus.Contour/
├── Alveus.Contour/ # Core library
│ ├── Attributes/ # Command and parameter attributes
│ ├── Commands/ # Built-in commands (help, history, etc.)
│ ├── Help/ # Help system
│ ├── Middleware/ # Middleware infrastructure
│ ├── Parsing/ # Argument parsing
│ ├── REPL/ # REPL implementation
│ ├── Routing/ # Command routing
│ ├── Version/ # Version handling
│ ├── ContourApp.cs # Main application class
│ └── ContourBuilder.cs # Fluent builder API
└── Alveus.Contour.Demo/ # Demo application
├── Commands/ # Example commands
├── Middleware/ # Example middleware
└── Program.cs # Demo entry point
Advanced Examples
Command with Collection Parameters
[Command("process")]
public class ProcessCommand
{
[DefaultHandler]
public int Execute(
[Option("tag", ShortName = "t", Description = "Tags (can be specified multiple times)")]
IEnumerable<string> tags,
[Parameter(0, MinCount = 1, Description = "Files to process")]
IEnumerable<string> files)
{
foreach (var file in files)
{
Console.WriteLine($"Processing: {file}");
foreach (var tag in tags ?? Enumerable.Empty<string>())
{
Console.WriteLine($" Tag: {tag}");
}
}
return 0;
}
}
$ myapp process file1.txt file2.txt -t alpha -t beta
Processing: file1.txt
Tag: alpha
Tag: beta
Processing: file2.txt
Tag: alpha
Tag: beta
Async Command Handlers
public class AsyncMiddleware : ICommandMiddlewareAsync
{
public async Task<int> ExecuteAsync(CommandContext context, Func<Task<int>> next)
{
await Task.Delay(100); // Async work
return await next();
}
}
builder.UseMiddleware<AsyncMiddleware>();
Multiple Global Options Types
public class AppGlobalOptions
{
[Flag("verbose", ShortName = "v")]
public bool Verbose { get; set; }
}
public class DatabaseOptions
{
[Option("connection", ShortName = "db")]
public string? ConnectionString { get; set; }
}
builder.UseGlobalOptions<AppGlobalOptions>();
builder.UseGlobalOptions<DatabaseOptions>();
Dependencies
- Microsoft.Extensions.DependencyInjection (10.0.0) - Dependency injection
- Spectre.Console (0.54.0) - Rich terminal UI
What's New
Persistent Global Options in REPL
Global options can now persist across REPL commands, providing a more seamless interactive experience:
- Persistent State: Global options set when launching the app remain active for all REPL commands
- Per-command Override: Override global options on individual commands without changing the persisted values
- Reset to Defaults: Use the
--nogoptflag to temporarily reset global options to defaults for a single command - Configurable Behavior: Enable or disable persistence with
config.PersistGlobalOptions
This feature is particularly useful when you want to maintain a consistent set of options (like --verbose or --log-level) throughout a REPL session without having to specify them on every command.
Boolean Flag Values
Flags now support explicit boolean values, giving you more control over their state:
- Explicit True/False: Set flags to true or false explicitly using
--verbose trueor--verbose false - Multiple Formats: Supports multiple boolean formats:
true/false,yes/no,on/off,1/0 - Case Insensitive: All boolean values are parsed case-insensitively
- REPL Friendly: Especially useful in REPL mode for toggling flags on and off
This feature makes it easier to disable flags in REPL mode and provides a more intuitive syntax for flag manipulation.
Theming System
Alveus.Contour includes a comprehensive theming system that allows you to customize the appearance of your CLI application:
- Built-in themes: Default, Dark, Light, Monochrome, Ultraviolet, and Rainbow
- Custom themes: Create your own themes by defining styles for commands, options, errors, and more
- Dynamic theme switching: Change themes at runtime (even in REPL mode)
- Automatic propagation: Themes automatically apply to syntax highlighting, help output, error messages, and all UI elements
- Theme-aware commands: Commands can access the current theme via dependency injection
Syntax Highlighting in REPL
The REPL features real-time syntax highlighting:
- Commands, subcommands, and options are color-coded as you type
- Invalid or unknown elements remain unhighlighted for immediate visual feedback
- Highlighting uses the current theme and updates dynamically when themes change
- Provides a more intuitive and polished interactive experience
Custom Line Editor
Alveus.Contour includes a custom-built line editor, removing the dependency on the ReadLine library. This provides:
- Full control over keyboard input handling (arrow keys, Home, End, backspace, delete)
- Intelligent auto-completion for commands, subcommands, options, and flags (including global options)
- Command history with configurable success-only saving
- History recall with
!<number>syntax allowing pre-execution editing - Environment variable expansion with support for multiple shell syntaxes (Bash, CMD, PowerShell)
- Better integration with the REPL environment and syntax highlighting
License
Refer to the LICENSE file in the repository.
Release v0.2.0