SergeiM.Cli 0.3.0

dotnet add package SergeiM.Cli --version 0.3.0
                    
NuGet\Install-Package SergeiM.Cli -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SergeiM.Cli" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SergeiM.Cli" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="SergeiM.Cli" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SergeiM.Cli --version 0.3.0
                    
#r "nuget: SergeiM.Cli, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SergeiM.Cli@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SergeiM.Cli&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=SergeiM.Cli&version=0.3.0
                    
Install as a Cake Tool

SergeiM.Cli

An object-oriented .NET 8 library for building command-line applications. No static classes. No magic strings. Interfaces all the way down.

Features

  • Tree-based command model — commands and branches form a typed tree
  • Strongly-typed options and argumentsOption<T> / Argument<T> with built-in type conversion
  • Inherited options — options declared on a branch are available to all its subcommands
  • Auto help--help / -h is injected automatically by Application
  • Decorators over inheritance — extend behaviour by wrapping ICommand / IBranch
  • Exit codes0 success · 1 unhandled exception · 2 parse error

Installation

dotnet add package SergeiM.Cli

Quick start

using SergeiM.Cli;
using SergeiM.Cli.Abstractions;
using SergeiM.Cli.Arguments;
using SergeiM.Cli.Options;

var nameOpt  = new Option<string>("--name", "-n", "Your name", isRequired: true);
var countArg = new Argument<int>("<count>", "Number of greetings", defaultValue: 1);

var greetCmd = new GreetCommand(nameOpt, countArg);
return await new Application(greetCmd).RunAsync(args);

// ---

sealed class GreetCommand(Option<string> nameOpt, Argument<int> countArg) : ICommand
{
    public string Name => "greet";
    public string Description => "Print a greeting.";
    public IReadOnlyList<IOption> Options => [nameOpt];
    public IReadOnlyList<IArgument> Arguments => [countArg];

    public Task<int> ExecuteAsync(ICommandContext ctx, CancellationToken ct = default)
    {
        var name  = ctx.GetOption(nameOpt)!;
        var count = ctx.GetArgument(countArg);
        for (var i = 0; i < count; i++)
            Console.WriteLine($"Hello, {name}!");
        return Task.FromResult(0);
    }
}
$ myapp greet --name Alice 3
Hello, Alice!
Hello, Alice!
Hello, Alice!

$ myapp greet --help
Print a greeting.

USAGE:
  greet [options] <count>

ARGUMENTS:
  <count>  Number of greetings

OPTIONS:
  --name, -n  Your name
  --help, -h  Show help and exit.

Core concepts

ICommand

A leaf node that performs work. Implement ExecuteAsync and return an integer exit code. For synchronous logic, extend SyncCommand instead — it only requires an Execute method.

sealed class DeployCommand : ICommand
{
    private static readonly Option<string> _env =
        new("--env", "-e", "Target environment", isRequired: true);

    public string Name => "deploy";
    public string Description => "Deploy the application.";
    public IReadOnlyList<IOption> Options => [_env];
    public IReadOnlyList<IArgument> Arguments => [];

    public async Task<int> ExecuteAsync(ICommandContext ctx, CancellationToken ct = default)
    {
        var env = ctx.GetOption(_env)!;
        Console.WriteLine($"Deploying to {env}…");
        return 0;
    }
}

SyncCommand

Base class for commands without async I/O. Override Execute and return an exit code — no Task.FromResult boilerplate.

sealed class VersionCommand : SyncCommand
{
    public override string Name => "version";
    public override string Description => "Print version.";
    public override IReadOnlyList<IOption> Options => [];

    public override int Execute(ICommandContext ctx)
    {
        Console.WriteLine("1.0.0");
        return 0;
    }
}

Arguments defaults to [] and can be overridden when needed.

Branch

Groups related subcommands. Use the built-in Branch class for declarative trees, or implement IBranch for custom behaviour.

var root = new Branch("myapp", "My CLI tool", [
    new Branch("remote", "Manage remotes", [
        new AddRemoteCommand(),
        new RemoveRemoteCommand(),
    ]),
    new DeployCommand(),
]);

return await new Application(root).RunAsync(args);

Options

Option<T> supports string, int, double, bool, and any enum out of the box.

// Long name only, optional
var verbose = new Option<bool>("--verbose", "Enable verbose output");

// Long + short alias, required
var output = new Option<string>("--output", "-o", "Output path", isRequired: true);

// Optional with explicit default
var retries = new Option<int>("--retries", "Retry count", isRequired: false, defaultValue: 3);

// Required, but falls back to an environment variable via factory callback
var url = new Option<string>("--url", "API URL",
    defaultFactory: () => Environment.GetEnvironmentVariable("API_URL"),
    isRequired: true);

When the option is not supplied on the command line, values are resolved with this cascade: explicit arg → static default → factory() → required check. If the factory returns null, the value is treated as missing. Bool options are flags — supply the name alone to set them to true:

myapp build --verbose

Options are matched by name, not by reference. Two Option<string> instances with the same Name resolve to the same parsed value, so you can declare a descriptor once and reuse it across commands:

static readonly Option<string> Url = new("--url", "API URL");

class ExportCommand : ICommand
{
    public IReadOnlyList<IOption> Options => [Url];
    // ctx.GetOption(Url) works regardless of where Url was declared
}

Arguments

Argument<T> captures positional values in declaration order.

// Required positional argument
var source = new Argument<string>("<source>", "Source path");

// Optional positional argument with default
var dest = new Argument<string>("<dest>", "Destination path", defaultValue: ".");

ICommandContext

Inside ExecuteAsync, use ICommandContext for type-safe access to parsed values.

var name  = ctx.GetOption(nameOpt);       // T? — null when not supplied and no default
var file  = ctx.GetArgument(fileArg);     // T?
var extra = ctx.RemainingArgs;            // string[] — tokens after --
var token = ctx.CancellationToken;

Inherited options

Options declared on a branch are available to all subcommands:

var verbose = new Option<bool>("--verbose", "Enable verbose output");

var root = new Branch("myapp", "My CLI", [verbose], [
    new BuildCommand(),   // can read --verbose
    new DeployCommand(),  // can read --verbose
]);

Application

Application wraps the root node, injects --help, and handles exit codes.

// Minimal — uses ConsoleHelpRenderer, writes errors to Console.Error
new Application(root)

// Custom renderer
new Application(root, new MyHelpRenderer())

// Custom renderer + redirect errors (useful in tests)
new Application(root, new ConsoleHelpRenderer(), myTextWriter)

RunAsync returns:

Code Meaning
0 Success
1 Unhandled exception thrown by a command
2 Parse error (unknown option, missing required value, …)

Project structure

src/
  SergeiM.Cli/
    Abstractions/       INode, ICommand, IBranch, IOption<T>, IArgument<T>,
                        ICommandContext, IHelpRenderer, IParser, IApplication,
                        ParseResult, ParseError
    Options/            Option<T>
    Arguments/          Argument<T>
    Parsing/            Token, Tokenizer, Parser
    Branch.cs
    CommandContext.cs
    ConsoleHelpRenderer.cs
    WithHelp.cs
    Application.cs
  SergeiM.Cli.Tests/

Conventional Commits

This project follows Conventional Commits to automate versioning and changelog generation via release-please.

Type Purpose Bump
feat New feature minor
fix Bug fix patch
docs Documentation only changes
style Code style (formatting, whitespace)
refactor Code refactoring
test Adding or updating tests
chore Maintenance (CI, deps, etc.)

Breaking changes are signaled with ! after the type (feat!:) or a BREAKING CHANGE: footer — triggers a major bump.

feat(#4): support factory callback as default value for options
fix(#3): don't show <subcommand> in usage for branches with no subcommands
chore: update CI dependencies

License

See LICENSE.txt.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 83 6/23/2026
0.2.0 92 6/23/2026
0.1.1 113 4/22/2026
0.1.0 118 3/28/2026