A modern, lightweight framework for building command-line applications in C#. Contour provides an intuitive attribute-based API for defining commands, supports middleware pipelines, includes a built-in REPL, and is fully AOT-compatible.
Find a file
mike df60849a92
All checks were successful
Build & Publish Nuget Package / Build, Pack & Publish (push) Successful in 1m9s
Update Alveus.Contour.Demo/Commands/ThemeCommand.cs
2025-12-21 17:17:01 +01:00
.forgejo/workflows Update pipeline 2025-11-20 17:14:17 +00:00
Alveus.Contour Bump version 2025-12-21 14:46:03 +00:00
Alveus.Contour.Demo Update Alveus.Contour.Demo/Commands/ThemeCommand.cs 2025-12-21 17:17:01 +01:00
Alveus.Contour.Tests 2025-12-08-guid-support (#11) 2025-12-08 17:13:28 +01:00
.gitignore Initial commit for beta version 2025-11-20 17:11:04 +00:00
Alveus.Contour.sln Introduce tests (#1) 2025-11-21 12:26:37 +01:00
Alveus.ruleset Initial commit for beta version 2025-11-20 17:11:04 +00:00
LICENSE.md Initial commit for beta version 2025-11-20 17:11:04 +00:00
README.md Standardise messages 2025-12-21 14:43:15 +00:00

Alveus Contour

Release Build status

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.

Buy me a Coffee

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 the help command)
  • 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 syntax
    

    Available syntaxes (can be combined with |):

    • VariableExpansionSyntax.None - Disable expansion
    • VariableExpansionSyntax.Bash - $VAR or ${VAR}
    • VariableExpansionSyntax.Cmd - %VAR%
    • VariableExpansionSyntax.PowerShell - $env:VAR
    • VariableExpansionSyntax.BatchEnhanced - %VAR:~start,length% with substring support
    • VariableExpansionSyntax.OsDefault - Automatically use platform defaults
      • Windows: Cmd | BatchEnhanced
      • Linux/macOS: Bash

    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 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:

    • Global options persist across all REPL commands
    • Options can be overridden on a per-command basis
    • Use --nogopt flag 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 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: true, yes, on, 1 (case-insensitive)
    • False: false, no, off, 0 (case-insensitive)
    • No value: Defaults to true (traditional behavior)
  • Built-in Commands

    • exit or quit - Exit the REPL
    • help - Display available commands
    • history - Display command history
    • history clear - Clear command history
    • clear - 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]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 command

    • Name - Command name (required)
    • Description - Command description
    • Aliases - Alternative names
    • Availability - 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 description
    • Availability - Where subcommand is available

Parameter Attributes

  • [Parameter] - Positional parameter

    • Position - Zero-based position
    • Description - Parameter description
    • MinCount - Minimum required count (for collections)
  • [Option] - Named option that accepts a value

    • Name - Option name (required)
    • ShortName - Short form (e.g., -o)
    • Description - Option description
    • DefaultValue - Default value if not provided
    • Required - Whether option is required
  • [Flag] - Boolean flag

    • Name - Flag name (required)
    • ShortName - Short form (e.g., -v)
    • Description - Flag description
    • DefaultValue - Default value (defaults to false)

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 --nogopt flag 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 true or --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.