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.
$ dotnet add package 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.
!<number> syntaxMicrosoft.Extensions.DependencyInjectiondotnet add package Alveus.Contour
using Alveus.Contour;
var builder = new ContourBuilder();
// Register commands
builder.AddCommand<MyCommand>();
// Configure the app
var app = builder.Build();
return app.Run(args);
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
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 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 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;
});
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, -h, or the help command)--version, -v)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
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 syntax
Available syntaxes (can be combined with |):
VariableExpansionSyntax.None - Disable expansionVariableExpansionSyntax.Bash - $VAR or ${VAR}VariableExpansionSyntax.Cmd - %VAR%VariableExpansionSyntax.PowerShell - $env:VARVariableExpansionSyntax.BatchEnhanced - %VAR:~start,length% with substring supportVariableExpansionSyntax.OsDefault - Automatically use platform defaults
Cmd | BatchEnhancedBashExamples 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 PersistGlobalOptions is 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 only
Key Features:
--nogopt flag to temporarily reset to defaultsConfiguration:
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 0
This 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 verbose
Supported values:
true, yes, on, 1 (case-insensitive)false, no, off, 0 (case-insensitive)true (traditional behavior)Built-in Commands
exit or quit - Exit the REPLhelp - Display available commandshistory - Display command historyhistory clear - Clear command historyclear - Clear the screenExample 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
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]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:
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;
});
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>();
[Command] - Marks a class as a command
Name - Command name (required)Description - Command descriptionAliases - Alternative namesAvailability - Where command is available (Cli, Repl, or Both)[DefaultHandler] - Marks the default command handler method
[Subcommand] - Marks a method as a subcommand handler
Name - Subcommand name (required)Description - Subcommand descriptionAvailability - Where subcommand is available[Parameter] - Positional parameter
Position - Zero-based positionDescription - Parameter descriptionMinCount - Minimum required count (for collections)[Option] - Named option that accepts a value
Name - Option name (required)ShortName - Short form (e.g., -o)Description - Option descriptionDefaultValue - Default value if not providedRequired - Whether option is required[Flag] - Boolean flag
Name - Flag name (required)ShortName - Short form (e.g., -v)Description - Flag descriptionDefaultValue - Default value (defaults to false)dotnet restore
dotnet build
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
[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
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>();
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>();
Global options can now persist across REPL commands, providing a more seamless interactive experience:
--nogopt flag to temporarily reset global options to defaults for a single commandconfig.PersistGlobalOptionsThis 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.
Flags now support explicit boolean values, giving you more control over their state:
--verbose true or --verbose falsetrue/false, yes/no, on/off, 1/0This feature makes it easier to disable flags in REPL mode and provides a more intuitive syntax for flag manipulation.
Alveus.Contour includes a comprehensive theming system that allows you to customize the appearance of your CLI application:
The REPL features real-time syntax highlighting:
Alveus.Contour includes a custom-built line editor, removing the dependency on the ReadLine library. This provides:
!<number> syntax allowing pre-execution editingRefer to the LICENSE file in the repository.