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
- 2. Formatting & Naming
- 3. Parsing & Data Binding
- 3.1 Value conversion
- 3.2 Composite value conversion
- 3.3 Collection binding
- 3.4 Boolean / flag handling
- 3.5 Enum binding
- 3.6 Nullable and optional values
- 3.7 Custom type converters
- 3.8 Argument tokenization
- 3.9 Strict vs lenient parsing
- 3.10 Object population (without execution)
- 3.11 Parameter shadowing
- 4. Validation & Access Control
- 5. Execution
- 6. User Feedback
- 7. Input/Output
- 8. Completions
- 9. Extensibility
- Demo Examples
- Quick Start
- License
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:
CommandLineInterfaceparses arguments fromEnvironment.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 forCliException - 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:
CommandLineInterfaceautomatically 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:
RunTests→run-testsEnableSsl→enable-sslGetVersion→get-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:
GetRequiredNamesreturns the primary names (used for matching and display)GetAlternativeNamesreturns 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→ aliasrVerbose→ aliasv- 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.CollectionsandSystem.Collections.Genericnamespaces, 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
ValueConverterconverts type parsing errors toCliException
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
CollectionElementStoreFactoryprovides 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
trueorfalseis 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:
CanConvertdetermines whether this converter handles a given typeConvertToOrUpdateperforms the string-to-target conversionConvertFromperforms the reverse conversion (target-to-string)- Register via
CompositeValueConverterto 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
CliExceptionwith 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
ValueConverterautomatically converts type parsing errors toCliException
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
CliExceptionif 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:
CliExceptionis the only exception type thrown byCommandLineInterface- The default
ValueConverterautomatically converts type parsing errors toCliException - 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:
AccessFieldsandAccessPropertiescontrol broad member categoriesBindingFlagscontrol case sensitivity and visibility scopeMethodBindingFlagscan differ fromBindingFlagsfor independent method/option sensitivityIAccessValidatorprovides 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
IAccessValidatorinterface as option validation - Applied to
MethodInfoinstances
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
IAccessValidatorinterface as option and subcommand validation - Applied to
PropertyInfoorFieldInfoinstances 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
HelpPrinterreads<summary>and<param>tags only <GenerateDocumentationFile>must be enabled in your.csprojfor the defaultHelpPrinterto 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
--versionor-v - Default reads from entry assembly version
UseRevisionVersion = trueincludes 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
CliExceptionwith 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:
InterfaceOutredirects help text, version info, completion output, and framework messages- Your application's
Console.WriteLine()calls,CliExceptionerror 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 (viaDirectoryInfo), file system paths (viaFileSystemInfo) - 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 installinstalls 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
ICompletionProviderfor reusable provider classes ICompletionProviderContext.PartialInputprovides the current user input for filtering- Built-in providers:
FileCompletionProvider,DirectoryCompletionProvider,FileSystemCompletionProvider FileCompletionProvideraccepts 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 returnIEnumerable<string>and acceptstring 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
CompositeValueConverteronCliComponentStoreFactory - 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
MemberInfoorMethodInfo - Return
trueto expose,falseto 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.InterfaceOutprovides 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:
HandleCompleteprovides suggestions for the current input stateHandleCompletionhandles thecompletionsubcommand (script generation, install/uninstall)targetis 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


Quick Start
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.