Calq CLI

Calq CLI is a compiler-like engine that transforms source code into backend tools. Designed for instantly building production-ready backend (CLI) tools from existing source code, without any additional development.

Comparison

Feature Calq CLI Imperative Builder CLI Frameworks Attribute-Based CLI Frameworks Convention-Based CLI Frameworks Source Generator CLI Frameworks
Instant development ❌ full surface duplication ❌ command classes + wiring ❌ command classes + registration ❌ command classes + wiring
Zero CLI-specific code / source reuse ✅ plain classes, reusable anywhere ❌ coupled to builder API ❌ attributes leak into library ⚠️ base class leaks ❌ attributes leak into library
Unlimited submodule nesting ✅ property/field → class ⚠️ manual wiring per level ⚠️ manual nesting ⚠️ manual registration ⚠️ manual nesting
Automatic member discovery ✅ plain members ❌ explicit registration ❌ attributed members only ✅ convention-based ❌ attributed members only
AI-operability ✅ plain C# classes ❌ complex builder API ⚠️ attribute DSL ⚠️ base class + config ⚠️ attribute DSL
Parameter shadowing ✅ built-in
Object population (standalone) ✅ OptionDeserializer
Replaceable naming scheme (global) ✅ ClassMemberStringifier ❌ per-option only ❌ per-attribute only ⚠️ framework conventions ❌ per-attribute only
Help from XML documentation <summary> and <param> ❌ description strings ❌ attribute strings ⚠️ varies ❌ attribute strings
Shell completion ✅ bash/zsh/pwsh/fish + install
Extensibility (DI, middleware, converters, validators)

Both examples implement a tool for the backend project from:

Example.NestedSubmodules.CloudProvider

Calq CLI

The following template is a complete implementation.

using CalqFramework.Cli;
using CalqFramework.Cli.DataAccess;
using System;
using System.Text.Json;
using Example.NestedSubmodules.CloudProvider;

try {
    var result = new CommandLineInterface() {
        CliComponentStoreFactory = new CliComponentStoreFactory() {
            EnableShadowing = true
        }
    }.Execute(new CloudManager());

    switch (result) {
        case ValueTuple:
            break;
        case string str:
            Console.WriteLine(str);
            break;
        case object obj:
            Console.WriteLine(JsonSerializer.Serialize(obj));
            break;
    }
}
catch (CliException ex) {
    Console.Error.WriteLine(ex.Message);
    Environment.Exit(1);
}

System.CommandLine

The following code was generated with AI using Gemini 2.5 Pro.

The build failed with 170 errors, compiled with Visual Studio 2022 using .NET 9.

using System.CommandLine;
using CloudProviderTool;

// Helper Methods
static T CreateModule<T>(string? apiKey) where T : SubmoduleBase, new()
{
    var module = new T();
    if (!string.IsNullOrEmpty(apiKey))
// ... another 132 lines of code

var rootCommand = new RootCommand("A CLI for managing cloud provider resources.");

// Global Options and Root Commands
var apiKeyOption = new Option<string>("--api-key", "API Key used for authentication. Overrides saved key.");
rootCommand.AddGlobalOption(apiKeyOption);
var addCommand = new Command("add", "Permanently saves an API key for the CLI.")
// ... another 19 lines of code

// Compute Module
var computeCommand = new Command("compute", "Manage compute resources.");
AddComputeOptions(computeCommand);
var computeRunCommand = new Command("run", "Runs a generic compute action.")
// ... another 50 lines of code

// Storage Module
var storageCommand = new Command("storage", "Manage storage resources.");
AddStorageOptions(storageCommand);
var storageRunCommand = new Command("run", "Runs a generic storage action.") { new Argument<string>("action", () => "default", "The action to perform.") };
// ... another 61 lines of code

// Network Module
var networkCommand = new Command("network", "Manage network resources.");
AddNetworkOptions(networkCommand);
var networkRunCommand = new Command("run", "Runs a generic network action.") { new Argument<string>("action", () => "default", "The action to perform.") };
// ... another 77 lines of code

return await rootCommand.InvokeAsync(args);

Table of Contents

Usage - Calq CLI

1. Foundations

1.1 Command definition

using CalqFramework.Cli;
using System;
using System.Text.Json;

try {
    var result = new CommandLineInterface().Execute(new MyApplication());

    switch (result) {
        case ValueTuple:
            break;
        case string str:
            Console.WriteLine(str);
            break;
        case object obj:
            Console.WriteLine(JsonSerializer.Serialize(obj));
            break;
    }
}
catch (CliException ex) {
    Console.Error.WriteLine(ex.Message);
    Environment.Exit(1);
}

Custom configuration:

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        EnableShadowing = true,
        AccessFields = true,
        AccessProperties = true
    },
    SkipUnknown = false,
    InterfaceOut = Console.Out
};

var result = cli.Execute(new MyApplication());

Using custom arguments:

var args = new[] { "submodule", "subcommand", "--option", "value" };
var result = cli.Execute(new MyApplication(), args);

Key points:

  • CommandLineInterface parses arguments from Environment.GetCommandLineArgs() by default
  • Discovers submodules (properties/fields returning class instances), subcommands (methods), and options (properties/fields with convertible types) automatically
  • Routes to the appropriate method based on parsed arguments
  • Always wrap Execute() in a try-catch for CliException
  • Built-in subcommands (help/version/completion) and void methods return ValueTuple; non-void methods return the invocation result

1.2 Subcommands

Subcommands are public methods on your class.

class MyApplication {
    public void Build() {
        Console.WriteLine("Building project...");
    }

    public void Deploy(string environment, string configuration = "Release") {
        Console.WriteLine($"Deploying to {environment} in {configuration} mode");
    }

    public string GetVersion() {
        return "1.0.0";
    }
}
myapp build
myapp deploy production Debug
myapp get-version  # Returns: 1.0.0

Key points:

  • CommandLineInterface automatically exposes public methods as CLI commands
  • Method names are converted to kebab-case (see 2.1 Kebab-case conversion)
  • Method arguments become parameters (positional or named)
  • Non-void methods return their result through Execute()

1.3 Submodules

Submodules are properties or fields that return class instances, creating multi-level command hierarchies.

class MyApplication {
    public DatabaseCommands Database { get; } = new DatabaseCommands();
}

class DatabaseCommands {
    public void Migrate() {
        Console.WriteLine("Running migrations...");
    }
}
myapp database migrate
cloudmanager compute instance start i-12345

Key points:

  • Submodules are properties or fields that return class instances (not primitives or collections)
  • The class can be defined anywhere (same file, different file, separate assembly)
  • Nesting depth is unlimited
  • Each level can have its own options and subcommands

1.4 Options and flags

Options are class-level properties or fields that become named arguments.

class MyApplication {
    // Becomes: --verbose or -v
    public bool Verbose { get; set; } = false;

    // Becomes: --output or -o
    public string Output { get; set; } = "output.txt";

    public void Run() {
        if (Verbose) {
            Console.WriteLine($"Writing to {Output}");
        }
    }
}
myapp run --verbose --output result.txt
myapp run -v -o result.txt

Collection options:

class MyApplication {
    public List<string> Tags { get; set; } = new();

    public void Tag() {
        Console.WriteLine($"Tags: {string.Join(", ", Tags)}");
    }
}
myapp tag --tags feature bugfix hotfix
myapp tag --tags feature --tags bugfix --tags hotfix

Key points:

  • Options are properties or fields with convertible types
  • Apply to the current submodule and all subcommands within it
  • Collection options MUST use named syntax (e.g., --files), not positional arguments

1.5 Parameters (positional arguments)

Parameters are method arguments that can be used either positionally or as named options.

class MyApplication {
    public void Copy(string source, string destination) {
        Console.WriteLine($"Copying {source} to {destination}");
    }
}
myapp copy file1.txt file2.txt
myapp copy --source file1.txt --destination file2.txt
myapp copy file1.txt --destination file2.txt

Key points:

  • Parameters can be used positionally or as named options (e.g., --source)
  • Options are class properties/fields; parameters are method arguments

See also: 1.4 Options and flags

1.6 Naming conventions

The default ClassMemberStringifier converts C# names to kebab-case automatically and generates single-character aliases.

class MyApplication {
    public void RunTests() { }  // Becomes: run-tests, r
}

Key points:

  • Kebab-case is applied automatically to all member names
  • Automatic single-character aliases are created unless [CliName] is present

1.7 Attributes and metadata

Use [CliName] to override names or add aliases.

class MyApplication {
    [CliName("run")]
    [CliName("execute")]
    public void ExecuteTests() { }  // Becomes: run, execute (no automatic alias)
}

Key points:

  • [CliName] can be applied to methods, properties, fields, and parameters
  • Multiple [CliName] attributes create multiple aliases
  • Automatic single-character aliases and kebab-case naming are suppressed when [CliName] is present

See also: 1.6 Naming conventions


2. Formatting & Naming

2.1 Kebab-case conversion

The default ClassMemberStringifier converts PascalCase C# identifiers to kebab-case.

Key points:

  • RunTestsrun-tests
  • EnableSslenable-ssl
  • GetVersionget-version
  • Applied to subcommands, submodules, options, and parameters

See also: 1.6 Naming conventions

2.2 Member stringification

Implement ClassMemberStringifierBase to replace the default naming scheme.

class SnakeCaseStringifier : ClassMemberStringifierBase {
    protected override IEnumerable<string> GetRequiredNames(string name, IEnumerable<CliNameAttribute> cliNameAttributes) {
        if (cliNameAttributes.Any()) {
            return cliNameAttributes.Select(a => a.Name);
        }
        return new[] { ToSnakeCase(name) };
    }

    protected override IEnumerable<string> GetAlternativeNames(string name, IEnumerable<CliNameAttribute> cliNameAttributes) {
        return Enumerable.Empty<string>();
    }

    private string ToSnakeCase(string value) {
        return Regex.Replace(value, "([a-z0-9])([A-Z])", "$1_$2").ToLower();
    }
}

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        ClassMemberStringifier = new SnakeCaseStringifier()
    }
};

class MyApplication {
    public void RunTests() { }  // Becomes: run_tests
}

Key points:

  • GetRequiredNames returns the primary names (used for matching and display)
  • GetAlternativeNames returns additional aliases (used for matching only)
  • When [CliName] attributes are present, they override automatic name generation
  • Assign via CliComponentStoreFactory.ClassMemberStringifier

See also: 1.6 Naming conventions, 1.7 Attributes and metadata, 2.1 Kebab-case conversion

2.3 Alias support

The default ClassMemberStringifier generates single-character aliases from the first character of the member name.

Key points:

  • RunTests → alias r
  • Verbose → alias v
  • Aliases are suppressed when [CliName] is used

See also: 1.7 Attributes and metadata, 2.2 Member stringification

2.4 CLI name attribute

[CliName] overrides automatic name generation.

[CliName("run")]
[CliName("execute")]
public void ExecuteTests() { }  // Becomes: run, execute

Key points:

  • Can be applied to methods, properties, fields, and parameters
  • Multiple attributes create multiple aliases
  • Suppresses automatic kebab-case naming and single-character aliases

See also: 1.7 Attributes and metadata, 2.2 Member stringification, 2.1 Kebab-case conversion, 2.3 Alias support


3. Parsing & Data Binding

3.1 Value conversion

The default ValueConverter automatically converts string arguments to .NET types.

Supported types:

  • Primitives (bool, byte, sbyte, char, decimal, double, float, int, uint, long, ulong, short, ushort), string, DateTime
  • Enums (case-insensitive matching)
  • Any IParsable<T> type (e.g., Guid, FileInfo, DirectoryInfo, FileSystemInfo)
  • Nullable versions of all the above
  • List-like collection types from System.Collections and System.Collections.Generic namespaces, with automatic concrete type mapping for collection interfaces (e.g., IList<T>List<T>, ISet<T>HashSet<T>). Dictionary types are NOT supported.

Custom format provider:

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        FormatProvider = CultureInfo.CurrentCulture
    }
};

Key points:

  • Conversion is automatic for all supported types
  • The default ValueConverter converts type parsing errors to CliException

See also: 1.4 Options and flags

3.2 Composite value conversion

CompositeValueConverter chains multiple converters together, enabling support for collections and custom types simultaneously.

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        CompositeValueConverter = new CompositeValueConverter(new CustomValueConverter(), new CollectionElementStoreFactory())
    }
};

Key points:

  • Converters are evaluated in order — the first converter that can handle the target type wins
  • CollectionElementStoreFactory provides collection element binding support

See also: 3.1 Value conversion

3.3 Collection binding

Collection properties and parameters accept multiple values.

myapp tag --tags feature bugfix hotfix
myapp tag --tags feature --tags bugfix --tags hotfix

Key points:

  • Supported collection interfaces are automatically mapped to concrete types (e.g., IList<T>List<T>, ISet<T>HashSet<T>)
  • Dictionary types are NOT supported
  • Collection options MUST use named syntax, not positional arguments

See also: 1.4 Options and flags, 3.1 Value conversion, 3.2 Composite value conversion

3.4 Boolean / flag handling

Boolean options act as flags.

myapp run --verbose           # Verbose = true
myapp run --verbose false     # Verbose = false

Key points:

  • Providing the option name without a value sets it to true
  • Explicitly passing true or false is also supported

See also: 1.4 Options and flags, 3.1 Value conversion

3.5 Enum binding

Enums are matched case-insensitively.

public enum LogLevel { Debug, Info, Warning, Error }

class MyApplication {
    public LogLevel Level { get; set; } = LogLevel.Info;
}
myapp --level debug    # Matches LogLevel.Debug
myapp --level DEBUG    # Also matches LogLevel.Debug

Key points:

  • Case-insensitive matching by default
  • Invalid enum values throw CliException

See also: 3.1 Value conversion

3.6 Nullable and optional values

Key points:

  • Nullable types (int?, string?, etc.) and parameters with default values are optional
  • Parameters without default values are required

See also: 1.4 Options and flags, 3.1 Value conversion

3.7 Custom type converters

Implement IValueConverter<string?> to add support for types not covered by the default converter.

class CustomValueConverter : IValueConverter<string?> {
    public bool CanConvert(Type targetType) {
        return targetType == typeof(CustomType);
    }

    public string? ConvertFrom(object? value, Type targetType) {
        return value?.ToString();
    }

    public object? ConvertToOrUpdate(string? value, Type targetType, object? currentValue) {
        if (targetType == typeof(CustomType)) {
            return CustomType.Parse(value);
        }
        throw new NotSupportedException();
    }
}

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        CompositeValueConverter = new CompositeValueConverter(new CustomValueConverter(), new CollectionElementStoreFactory())
    }
};

Key points:

  • CanConvert determines whether this converter handles a given type
  • ConvertToOrUpdate performs the string-to-target conversion
  • ConvertFrom performs the reverse conversion (target-to-string)
  • Register via CompositeValueConverter to chain with built-in converters

See also: 3.1 Value conversion, 3.2 Composite value conversion

3.8 Argument tokenization

CommandLineInterface accepts arguments as a string array and interprets them left-to-right.

myapp database migrate --verbose        # submodule → subcommand → option
myapp copy file1.txt file2.txt          # subcommand → positional args
myapp deploy --environment production   # subcommand → named parameter

Key points:

  • Tokens matching a submodule name navigate into that submodule
  • Tokens matching a subcommand name select that method for invocation
  • Tokens prefixed with -- or - are treated as named options
  • Remaining tokens are bound positionally to method parameters

See also: 1.2 Subcommands, 1.3 Submodules

3.9 Strict vs lenient parsing

Control how CommandLineInterface handles unknown or unexpected options.

// Strict mode (default) — throws on unknown options
var cli = new CommandLineInterface() {
    SkipUnknown = false
};

// Lenient mode — ignores unknown options
var cli = new CommandLineInterface() {
    SkipUnknown = true
};

Key points:

  • Default is strict: unknown options throw CliException with message "Unknown option: --invalid"
  • Lenient mode silently ignores unrecognized tokens

3.10 Object population (without execution)

OptionDeserializer populates an existing object with CLI arguments without executing a full command. This is a standalone utility, independent from CommandLineInterface.

using CalqFramework.Cli;

class AppSettings {
    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 8080;
    public bool EnableSsl { get; set; } = false;
}

var settings = new AppSettings();

// From environment args
OptionDeserializer.Deserialize(settings);

// From custom args
OptionDeserializer.Deserialize(settings, new[] { "--host", "example.com", "--port", "443", "--enable-ssl", "true" });

// With configuration
OptionDeserializer.Deserialize(settings, args, new OptionDeserializerConfiguration { SkipUnknown = true});

Key points:

  • Only populates properties and fields — does not execute methods
  • Supports all the same type conversions as full CLI execution
  • The default ValueConverter automatically converts type parsing errors to CliException

See also: 1.1 Command definition, 1.4 Options and flags, 3.1 Value conversion, 3.9 Strict vs lenient parsing

3.11 Parameter shadowing

Parameter shadowing allows method parameters to override class-level properties with the same name.

Without shadowing (default):

class MyApplication {
    public string Environment { get; set; } = "development";

    public void Deploy(string environment) {
        Console.WriteLine($"Property: {Environment}, Parameter: {environment}");
    }
}
myapp deploy production --environment staging
# Output: Property: staging, Parameter: production

With shadowing enabled:

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        EnableShadowing = true
    }
};
myapp deploy production --environment staging
# With shadowing: Parameter wins, environment = "staging"
# Without shadowing: Both are separate, environment = "production", Environment = "staging"

Key points:

  • Shadowing only affects members with the same name
  • The parameter value takes precedence when shadowing is enabled
  • Disabled by default to avoid confusion
  • Works with both properties and fields

See also: 1.1 Command definition, 1.4 Options and flags, 1.5 Parameters (positional arguments)


4. Validation & Access Control

4.1 Required vs optional

Key points:

  • Parameters without default values are required
  • Properties and fields always have a default value (assigned in code), making them optional
  • Missing required parameters result in positional binding or CliException if insufficient arguments are provided

See also: 1.4 Options and flags, 1.5 Parameters (positional arguments), 3.6 Nullable and optional values

4.2 Error handling

CommandLineInterface throws CliException for all CLI-related errors.

try {
    var result = new CommandLineInterface().Execute(new MyApplication());

    switch (result) {
        case ValueTuple:
            break;
        case string str:
            Console.WriteLine(str);
            break;
        case object obj:
            Console.WriteLine(JsonSerializer.Serialize(obj));
            break;
    }
}
catch (CliException ex) {
    Console.Error.WriteLine($"Error: {ex.Message}");
    Environment.Exit(1);
}

Common error messages:

# Unknown option
# "Unknown option: --invalid"
myapp command --invalid value

# Option requires value
# "Option '--output' requires a value"
myapp command --output

# Ambiguous value (starts with - or +)
# "Ambiguous value '-123' for '--number', use option=value format for values starting with '-' or '+'"
myapp command --number -123
# Fix: myapp command --number=-123

# Invalid format
# "option '--port=abc': Invalid format (expected Int32)"
myapp command --port abc

# Out of range
# "option '--count=9999999999': Out of range (-2147483648-2147483647)"
myapp command --count 9999999999

# Invalid enum value
# "option '--level=invalid': Invalid format (expected LogLevel)"
myapp command --level invalid

Key points:

  • CliException is the only exception type thrown by CommandLineInterface
  • The default ValueConverter automatically converts type parsing errors to CliException
  • Application exceptions thrown inside your methods propagate normally and are not wrapped

See also: 1.1 Command definition, 3.1 Value conversion, 3.9 Strict vs lenient parsing

4.3 Option access validation

The default CliComponentStoreFactory exposes both fields and properties as CLI options.

Access only properties (not fields):

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        AccessFields = false,
        AccessProperties = true
    }
};

class MyApplication {
    public string Option1 { get; set; } = "value";  // Exposed
    public string _option2 = "value";               // Hidden
}

Control case sensitivity:

// Case-insensitive (default)
var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        BindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase
    }
};

// Case-sensitive
var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        BindingFlags = BindingFlags.Instance | BindingFlags.Public
    }
};

Separate binding flags for methods:

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        BindingFlags = BindingFlags.Instance | BindingFlags.Public,
        MethodBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase
    }
};

Fine-grained control with access validators:

class CustomOptionValidator : IAccessValidator {
    public bool IsValid(MemberInfo member) {
        return member.GetCustomAttribute<ExposeToCliAttribute>() != null;
    }
}

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        OptionAccessValidator = new CustomOptionValidator()
    }
};

Key points:

  • AccessFields and AccessProperties control broad member categories
  • BindingFlags control case sensitivity and visibility scope
  • MethodBindingFlags can differ from BindingFlags for independent method/option sensitivity
  • IAccessValidator provides attribute-based or logic-based fine-grained control

See also: 1.4 Options and flags, 2.2 Member stringification

4.4 Subcommand access validation

Use IAccessValidator to control which methods become CLI subcommands.

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        SubcommandAccessValidator = new CustomSubcommandValidator()
    }
};

Key points:

  • Same IAccessValidator interface as option validation
  • Applied to MethodInfo instances

See also: 1.2 Subcommands, 4.3 Option access validation

4.5 Submodule access validation

Use IAccessValidator to control which properties/fields become CLI submodules.

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        SubmoduleAccessValidator = new CustomSubmoduleValidator()
    }
};

Attribute-based validation example:

class ValidatedOptionAccessValidator : IAccessValidator {
    private readonly ICompositeValueConverter<string?> _converter;

    public ValidatedOptionAccessValidator(ICompositeValueConverter<string?> converter) {
        _converter = converter;
    }

    public bool IsValid(MemberInfo member) {
        if (member.GetCustomAttribute<RequiredAttribute>() != null ||
            member.GetCustomAttribute<RangeAttribute>() != null) {
            Type memberType = member switch {
                PropertyInfo prop => prop.PropertyType,
                FieldInfo field => field.FieldType,
                _ => throw new NotSupportedException()
            };
            return _converter.CanConvert(memberType);
        }
        return false;
    }
}

Key points:

  • Same IAccessValidator interface as option and subcommand validation
  • Applied to PropertyInfo or FieldInfo instances that return class types

See also: 1.3 Submodules, 4.3 Option access validation, 4.4 Subcommand access validation


5. Execution

5.1 Command resolution

CommandLineInterface resolves commands by traversing arguments left-to-right.

Key points:

  • Matches tokens against submodule names to navigate the hierarchy
  • Matches the next token against subcommand (method) names
  • Remaining tokens are parsed as options and parameters
  • Help is automatically shown when the user provides --help/-h, provides no arguments, or provides only a submodule name without a subcommand

See also: 1.2 Subcommands, 1.3 Submodules, 3.8 Argument tokenization

5.2 Command invocation

Once a subcommand is resolved, CommandLineInterface binds all parsed arguments to the method's parameters and the parent class's properties/fields, then invokes the method.

Key points:

  • Options are set on the class instance before method invocation
  • Parameters are bound to method arguments (positional or named)
  • The method is invoked via reflection

See also: 1.4 Options and flags, 1.5 Parameters (positional arguments), 3.11 Parameter shadowing, 5.1 Command resolution

5.3 Return values

switch (result) {
    case ValueTuple:
        // Void methods, help, version, completion
        break;
    case string str:
        Console.WriteLine(str);
        break;
    case object obj:
        Console.WriteLine(JsonSerializer.Serialize(obj));
        break;
}

Key points:

  • Void methods return ValueTuple
  • Built-in subcommands (help/version/completion) return ValueTuple
  • Non-void methods return their result as object?
  • The caller is responsible for handling and displaying the result

See also: 1.1 Command definition, 1.2 Subcommands, 5.2 Command invocation


6. User Feedback

6.1 Help generation

The default HelpPrinter automatically generates help menus from your code structure and XML documentation comments.

myapp --help              # Root help
myapp database --help     # Submodule help
myapp build --help        # Subcommand help

Adding descriptions with XML documentation:

/// <summary>
/// Main application for managing cloud resources.
/// </summary>
class CloudManager {
    /// <summary>
    /// Compute resource management commands.
    /// </summary>
    public ComputeModule Compute { get; } = new ComputeModule();

    /// <summary>
    /// API key for authentication.
    /// </summary>
    public string ApiKey { get; set; } = "";
}

class ComputeModule {
    /// <summary>
    /// Number of instances to create.
    /// </summary>
    public int Count { get; set; } = 1;

    /// <summary>
    /// Starts a new compute instance.
    /// </summary>
    /// <param name="instanceType">The type of instance to start (e.g., t2.micro, t2.small).</param>
    /// <param name="region">The region where the instance will be created.</param>
    public void Start(string instanceType, string region = "us-east-1") {
        Console.WriteLine($"Starting {Count} {instanceType} instance(s) in {region}");
    }
}
myapp --help
Main application for managing cloud resources.

Submodules:
  compute    Compute resource management commands

Options:
  --api-key    API key for authentication
myapp compute start --help
Starts a new compute instance.

Parameters:
  instance-type    The type of instance to start (e.g., t2.micro, t2.small)
  region          The region where the instance will be created (default: us-east-1)

Options:
  --count    Number of instances to create (default: 1)

Key points:

  • The default HelpPrinter reads <summary> and <param> tags only
  • <GenerateDocumentationFile> must be enabled in your .csproj for the default HelpPrinter to find the XML file
  • Help is automatically shown when the user provides --help/-h, provides no arguments, or provides only a submodule name without a subcommand

See also: 1.6 Naming conventions, 2.1 Kebab-case conversion, 5.1 Command resolution

6.2 Version display

Version information is automatically available with --version or -v. The default VersionPrinter reads from the entry assembly's version (set via <Version> in your .csproj).

Include revision number (4 digits):

var cli = new CommandLineInterface() {
    VersionPrinter = new VersionPrinter() {
        UseRevisionVersion = true
    }
};
// Prints: "1.0.0.0" instead of "1.0.0"

Custom version printer:

using CalqFramework.Cli.Formatting;

class CustomVersionPrinter : IVersionPrinter {
    public void PrintVersion(ICliContext context, Type rootSubmoduleType) {
        var version = Assembly.GetEntryAssembly()?.GetName().Version;
        context.InterfaceOut.WriteLine($"MyApp v{version} (build {DateTime.Now:yyyyMMdd})");
    }
}

var cli = new CommandLineInterface() {
    VersionPrinter = new CustomVersionPrinter()
};

Key points:

  • Available via --version or -v
  • Default reads from entry assembly version
  • UseRevisionVersion = true includes the fourth version segment

See also: 5.3 Return values, 6.1 Help generation

6.3 Error formatting

Error messages follow a consistent format indicating the failing option and the nature of the error.

option '--port=abc': Invalid format (expected Int32)
option '--count=9999999999': Out of range (-2147483648-2147483647)
Unknown option: --invalid
Option '--output' requires a value
Ambiguous value '-123' for '--number', use option=value format for values starting with '-' or '+'

Key points:

  • Application exceptions thrown inside your methods propagate normally and are not wrapped by CommandLineInterface
  • All framework errors are surfaced as CliException with descriptive messages

See also: 3.1 Value conversion, 3.9 Strict vs lenient parsing, 4.2 Error handling


7. Input/Output

7.1 Output redirection

By default, CommandLineInterface writes help, version, and completion information to Console.Out. Redirect this by setting InterfaceOut to any TextWriter.

var cli = new CommandLineInterface() {
    InterfaceOut = new StringWriter()  // or any TextWriter
};

Key points:

  • InterfaceOut redirects help text, version info, completion output, and framework messages
  • Your application's Console.WriteLine() calls, CliException error messages, and subcommand return values are not redirected

See also: 1.1 Command definition, 6.1 Help generation, 6.2 Version display


8. Completions

8.1 Automatic completion

The default CompletionHandler provides automatic completion for many types without any configuration.

What gets completed automatically:

  • Submodules and subcommands at all levels
  • All option and parameter names
  • Enum values (case-insensitive)
  • Boolean values (true, false)
  • File paths (via FileInfo), directory paths (via DirectoryInfo), file system paths (via FileSystemInfo)
  • Collection element types (if they are enums or bools)

Key points:

  • No configuration required for built-in type completions
  • Completion is context-aware and can access application state

See also: 3.5 Enum binding, 3.4 Boolean / flag handling, 3.1 Value conversion

8.2 Completion script generation

The default CompletionHandler generates completion scripts for multiple shells and can install them automatically.

Generate completion script:

myapp completion bash
myapp completion zsh
myapp completion powershell
myapp completion pwsh
myapp completion fish

Install completion:

myapp completion bash install
myapp completion zsh install
myapp completion powershell install
myapp completion pwsh install
myapp completion fish install
myapp completion all install

Uninstall completion:

myapp completion bash uninstall
myapp completion all uninstall

Installation locations:

  • Bash (Linux/Mac): /etc/bash_completion.d/myapp
  • Bash (Windows): ~/.bash_completion.d/myapp.bash
  • Zsh (Linux/Mac): /usr/local/share/zsh/site-functions/_myapp
  • Zsh (Windows): ~/.zsh/completion/_myapp
  • PowerShell 5: ~/Documents/WindowsPowerShell/Completions/myapp.ps1
  • PowerShell 7 (Linux/Mac): ~/.config/powershell/Completions/myapp.ps1
  • PowerShell 7 (Windows): ~/Documents/PowerShell/Completions/myapp.ps1
  • Fish (Linux/Mac): ~/.config/fish/completions/myapp.fish
  • Fish (Windows): %APPDATA%/fish/completions/myapp.fish

After installation:

source ~/.bashrc          # Bash
source ~/.zshrc           # Zsh
. $PROFILE                # PowerShell
# Fish automatically loads completions

Key points:

  • Install scripts with completion <shell> install
  • Uninstall with completion <shell> uninstall
  • completion all install installs for all supported shells

See also: 7.1 Output redirection, 8.1 Automatic completion

8.3 Completion protocols

The default CompletionHandler automatically detects and handles both Cobra-style and dotnet-suggest protocols.

# Cobra-style (used by generated shell scripts)
myapp __complete deploy --region

# dotnet-suggest protocol
dotnet tool install -g dotnet-suggest
myapp [suggest:1] deploy --region

Key points:

  • Both protocols are handled automatically — no configuration needed
  • Cobra-style is used by the generated shell scripts
  • dotnet-suggest is supported for interoperability with the .NET ecosystem

See also: 8.1 Automatic completion, 8.2 Completion script generation

8.4 Custom completion providers

Use class-based completion providers for reusable completion logic.

using CalqFramework.Cli.Completion.Providers;

class RegionCompletionProvider : ICompletionProvider {
    public IEnumerable<string> GetCompletions(ICompletionProviderContext context) {
        var regions = new[] { "us-east-1", "us-west-2", "eu-west-1", "eu-central-1", "ap-southeast-1" };
        return regions.Where(r => r.StartsWith(context.PartialInput, StringComparison.OrdinalIgnoreCase));
    }
}

class MyApplication {
    [CliCompletion(typeof(RegionCompletionProvider))]
    public string Region { get; set; } = "us-east-1";
}

Built-in file completion providers:

using CalqFramework.Cli.Completion.Providers;

class MyApplication {
    [CliCompletion(typeof(FileCompletionProvider), "*.json;*.yaml;*.yml")]
    public string ConfigFile { get; set; } = "config.json";

    [CliCompletion(typeof(DirectoryCompletionProvider))]
    public string OutputDir { get; set; } = "output";

    [CliCompletion(typeof(FileSystemCompletionProvider))]
    public string Path { get; set; } = "";
}

Key points:

  • Implement ICompletionProvider for reusable provider classes
  • ICompletionProviderContext.PartialInput provides the current user input for filtering
  • Built-in providers: FileCompletionProvider, DirectoryCompletionProvider, FileSystemCompletionProvider
  • FileCompletionProvider accepts a semicolon-separated glob pattern for filtering

See also: 8.1 Automatic completion

8.5 Completion attributes

The [CliCompletion] attribute provides custom completion via method reference or provider type.

Method-based custom completion:

class MyApplication {
    [CliCompletion("GetRegions")]
    public string Region { get; set; } = "us-east-1";

    // Signature: IEnumerable<string> MethodName(string partialInput)
    // Can be static or instance, public or private
    private IEnumerable<string> GetRegions(string partialInput) {
        var regions = new[] { "us-east-1", "us-west-2", "eu-west-1", "eu-central-1", "ap-southeast-1" };
        return regions.Where(r => r.StartsWith(partialInput, StringComparison.OrdinalIgnoreCase));
    }
}

Dynamic completion based on state:

class MyApplication {
    public string Environment { get; set; } = "development";

    [CliCompletion("GetAvailableRegions")]
    public string Region { get; set; } = "us-east-1";

    private IEnumerable<string> GetAvailableRegions(string partialInput) {
        var regions = Environment switch {
            "development" => new[] { "us-east-1", "us-west-2" },
            "staging" => new[] { "us-east-1", "eu-west-1" },
            "production" => new[] { "us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1" },
            _ => Array.Empty<string>()
        };
        return regions.Where(r => r.StartsWith(partialInput, StringComparison.OrdinalIgnoreCase));
    }
}

Completion for method parameters:

class MyApplication {
    public void Deploy(
        [CliCompletion("GetEnvironments")] string environment,
        [CliCompletion("GetRegions")] string region) {
        Console.WriteLine($"Deploying to {environment} in {region}");
    }

    private IEnumerable<string> GetEnvironments(string partialInput) {
        return new[] { "development", "staging", "production" }
            .Where(e => e.StartsWith(partialInput, StringComparison.OrdinalIgnoreCase));
    }

    private IEnumerable<string> GetRegions(string partialInput) {
        return new[] { "us-east-1", "us-west-2", "eu-west-1" }
            .Where(r => r.StartsWith(partialInput, StringComparison.OrdinalIgnoreCase));
    }
}

Key points:

  • Method-based: [CliCompletion("MethodName")] — method must return IEnumerable<string> and accept string partialInput
  • Provider-based: [CliCompletion(typeof(ProviderClass))] or [CliCompletion(typeof(ProviderClass), "args")]
  • Can be applied to properties, fields, and method parameters
  • Completion methods can access instance state for dynamic suggestions

See also: 1.4 Options and flags, 1.5 Parameters (positional arguments), 1.7 Attributes and metadata, 8.1 Automatic completion, 8.4 Custom completion providers


9. Extensibility

9.1 Custom value converters

Implement IValueConverter<string?> to add support for types not covered by the default converter.

Key points:

  • Register via CompositeValueConverter on CliComponentStoreFactory
  • Converters are evaluated in order — first match wins

See also: 3.1 Value conversion, 3.2 Composite value conversion, 3.7 Custom type converters

9.2 Custom access validators

Implement IAccessValidator to control which members become CLI options, subcommands, or submodules.

class CustomOptionValidator : IAccessValidator {
    public bool IsValid(MemberInfo member) {
        return member.GetCustomAttribute<ExposeToCliAttribute>() != null;
    }
}

var cli = new CommandLineInterface() {
    CliComponentStoreFactory = new CliComponentStoreFactory() {
        OptionAccessValidator = new CustomOptionValidator(),
        SubcommandAccessValidator = new CustomSubcommandValidator(),
        SubmoduleAccessValidator = new CustomSubmoduleValidator()
    }
};

Key points:

  • Three independent validators: OptionAccessValidator, SubcommandAccessValidator, SubmoduleAccessValidator
  • Each receives the relevant MemberInfo or MethodInfo
  • Return true to expose, false to hide

See also: 1.1 Command definition, 4.3 Option access validation, 4.4 Subcommand access validation, 4.5 Submodule access validation

9.3 Custom help printers

Implement IHelpPrinter to fully customize the help menu layout.

using CalqFramework.Cli.Formatting;
using CalqFramework.Cli.InterfaceComponents;

class CustomHelpPrinter : IHelpPrinter {
    public void PrintHelp(ICliContext context, Type rootSubmoduleType, IEnumerable<Submodule> submodules, IEnumerable<Subcommand> subcommands, IEnumerable<Option> options) {
        context.InterfaceOut.WriteLine("=== MY CUSTOM CLI ===");
        // Custom layout...
    }

    public void PrintHelp(ICliContext context, Type rootSubmoduleType, Submodule submodule,
        IEnumerable<Submodule> submodules, IEnumerable<Subcommand> subcommands, IEnumerable<Option> options) {
        context.InterfaceOut.WriteLine($"=== {string.Join(" ", submodule.Keys)} ===");
        // Custom layout...
    }

    public void PrintSubcommandHelp(ICliContext context, Type rootSubmoduleType, Subcommand subcommand, IEnumerable<Option> options) {
        context.InterfaceOut.WriteLine($"Command: {string.Join(", ", subcommand.Keys)}");
        // Custom layout...
    }
}

var cli = new CommandLineInterface() {
    HelpPrinter = new CustomHelpPrinter()
};

Accessing component metadata:

// Submodule metadata
submodule.Keys            // ["database", "db"]
submodule.MemberInfo      // PropertyInfo or FieldInfo

// Subcommand metadata
subcommand.Keys           // ["migrate", "m"]
subcommand.MethodInfo     // MethodInfo
subcommand.Parameters     // Parameter collection
subcommand.ReturnType     // Type

// Option metadata
option.Keys               // ["verbose", "v"]
option.MemberInfo         // PropertyInfo or FieldInfo
option.ValueType          // Type (string, int, bool, etc.)
option.IsMultiValue       // bool (true for collections)
option.Value              // Current value (string)

// Parameter metadata
parameter.Keys            // ["filename", "f"]
parameter.ParameterInfo   // ParameterInfo
parameter.ValueType       // Type
parameter.HasDefaultValue // bool
parameter.IsMultiValue    // bool (true for collections)

Key points:

  • Three overloads: root help, submodule help, subcommand help
  • Rich metadata available for all CLI components
  • ICliContext.InterfaceOut provides the output stream

See also: 1.2 Subcommands, 1.3 Submodules, 1.4 Options and flags, 1.5 Parameters (positional arguments), 6.1 Help generation, 7.1 Output redirection

9.4 Custom completion handlers

Implement ICompletionHandler to fully replace the built-in completion logic.

class CustomCompletionHandler : ICompletionHandler {
    public void HandleComplete(ICliContext context, IEnumerable<string> args, object target) {
        var suggestions = GetCustomSuggestions(args);
        foreach (var suggestion in suggestions) {
            context.InterfaceOut.WriteLine(suggestion);
        }
    }

    public void HandleCompletion(ICliContext context, IEnumerable<string> args, object target) {
        // Handle "completion" command (install/uninstall/generate scripts)
    }
}

var cli = new CommandLineInterface() {
    CompletionHandler = new CustomCompletionHandler()
};

Key points:

  • HandleComplete provides suggestions for the current input state
  • HandleCompletion handles the completion subcommand (script generation, install/uninstall)
  • target is the resolved object at the current navigation depth

See also: 7.1 Output redirection, 8.1 Automatic completion, 8.2 Completion script generation, 8.3 Completion protocols, 8.5 Completion attributes


Demo Examples

Interface Collections Example

Autocomplete Example

Nested Submodules Example

SubmoduleHelpExample

SubcommandHelpExample

Quick Start

QuickStart Example

git clone --branch latest https://github.com/calq-framework/cli docs/cli
dotnet new console -n QuickStart
cd QuickStart
cp ../docs/cli/Examples/Example.Basics.QuickStart/Program.cs ./Program.cs
dotnet add package CalqFramework.Cli
dotnet run -- --help

License

Calq CLI is dual-licensed under PolyForm Noncommercial (with Evaluation Grant) and the Calq Commercial License.

An unhandled error has occurred. Reload 🗙