CliFx

repository·prime·Indexed 23 days ago

https://github.com/tyrrrz/clifx

An opinionated framework for building command-line applications in .NET. CliFx uses a class-based model with attributes to handle argument parsing, routing, error handling, and help generation. It targets .NET Standard 2.0+ and is compatible with Native AOT and trimming. Key features include support for command hierarchies, custom input conversion via IInputConverter, validation via IInputValidator, and a testable console interaction layer using IConsole.

Tokens
2.9K
Snippets
7
Records
13
Agent score
32%

What's inside CliFx

  1. Overview of CliFx features

    prime

    CliFx is an opinionated framework for building command-line applications. Instead of manually parsing arguments, you express command interactions through classes and properties.

    Key features include:

    • Class-first configuration: Use attributes to define commands and properties.
    • Automatic Infrastructure: Handles argument parsing, routing, error handling, and help generation automatically.
    • Command Hierarchies: Supports deeply nested command structures.
    • Lifecycle Management: Provides graceful cancellation via interrupt signals.
    • Advanced Capabilities: Support for reading/writing binary data and a testable console interaction layer.
    • Deployment Friendly: Compatible with Native AOT and trimming; targets .NET Standard 2.0+.
  2. Use CommandOptions for named inputs

    prime

    Options are bound by name (e.g., --foo) or short name (e.g., -f). Use the [CommandOption] attribute.

    Rules and Best Practices:

    • Naming: Specify a long name and an optional short name: [CommandOption("name", 'n')]. Names are case-insensitive; short names are case-sensitive.
    • Requirement: Unlike parameters, options can be marked required or non-required arbitrarily.
    • Sequences: Options can be sequence-based (e.g., IReadOnlyList<T>) to consume multiple values.
    • Environment Variables: You can configure an option to fall back to an environment variable if the command-line argument is missing using the EnvironmentVariable property.
    • Usage: Prefer options for inputs that have sensible defaults, enable alternative execution paths, or are not easily expressed as positional parameters.
    // Named option with short name
    [CommandOption("opt", 'o')]
    public double Option { get; set; }
    
    // Required option with environment variable fallback
    [CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
    public required string Foo { get; set; }
    
    // Sequence-based option
    [CommandOption("bar")]
    public required IReadOnlyList<string> Bar { get; set; }
  3. Configure command routing and hierarchy

    prime

    Commands can be named or unnamed (root commands). Named commands allow for complex, hierarchical CLI structures.

    • Root Command: A command with no name in [Command] is treated as the default command executed when no command name is provided.
    • Named Commands: Specify a name in [Command("name")] to create a specific entry point.
    • Hierarchical Commands: Commands with common name segments are treated as nested. For example, [Command("cmd1")] and [Command("cmd1 sub")] create a parent-child relationship where sub is a subcommand of cmd1.
  4. Use CommandParameters for positional inputs

    prime

    Parameters are bound from command-line arguments based on their position. Use the [CommandParameter] attribute.

    Rules and Best Practices:

    • Order: Specify the position using the first argument of the attribute (e.g., [CommandParameter(0)]). Order values must be unique within a command.
    • Mandatory vs Optional: Parameters are best suited for mandatory inputs. Use the required modifier for mandatory parameters. Only the last parameter in a command can be non-required (omitting required).
    • Sequences: Only the last parameter in a command can be sequence-based (e.g., IReadOnlyList<T>), allowing it to consume multiple trailing arguments.
    • Metadata: You can provide a custom Name and Description for use in auto-generated help text.
    // Mandatory positional parameter
    [CommandParameter(0)]
    public required double Param { get; set; }
    
    // Sequence-based parameter (must be last)
    [CommandParameter(2)]
    public required IReadOnlyList<string> Third { get; set; }
  5. Test commands using FakeConsole

    prime

    CliFx commands can be tested in isolation using the IConsole abstraction. Use FakeConsole or FakeInMemoryConsole to simulate console interaction.

    Command-level testing: Instantiate the command, set its properties, and call ExecuteAsync(console) directly.

    Application-level (End-to-End) testing: Use CommandLineApplicationBuilder to build the full application, inject a FakeInMemoryConsole via .UseConsole(console), and call .RunAsync(args).

  6. Install CliFx via NuGet

    prime

    You can install CliFx as a NuGet package using the .NET CLI. It targets .NET Standard 2.0+ and has no external dependencies, making it compatible with Native AOT and trimming.

    dotnet add package CliFx
  7. Define a command in CliFx

    prime

    In CliFx, application functionality is encapsulated in commands. To define a command:

    1. Declare a partial class.
    2. Annotate it with the [Command] attribute.
    3. Implement the ICommand interface.

    Important: The class must be partial so CliFx can extend it with metadata. If the command is nested, all parent types must also be partial.

    The ICommand interface requires an ExecuteAsync(IConsole console) method. The IConsole parameter is a decoupled abstraction for interacting with the console (writing text, reading data, etc.).

    using CliFx;
    using CliFx.Binding;
    using CliFx.Infrastructure;
    
    [Command(Description = "Calculates the logarithm of a value.")]
    public partial class LogCommand : ICommand
    {
        [CommandParameter(0, Description = "Value whose logarithm is to be found.")]
        public required double Value { get; set; }
    
        [CommandOption("base", 'b', Description = "Logarithm base.")]
        public double Base { get; set; } = 10;
    
        public ValueTask ExecuteAsync(IConsole console)
        {
            var result = Math.Log(Value, Base);
            console.WriteLine(result);
            return default;
        }
    }
  8. Integrate Dependency Injection

    prime

    To use an external dependency container (like Microsoft.Extensions.DependencyInjection), use UseTypeInstantiator(...) on the CommandLineApplicationBuilder. You can pass a factory delegate or an IServiceProvider.

    public static async Task<int> Main() =>
        await new CommandLineApplicationBuilder()
            .AddCommandsFromThisAssembly()
            .UseTypeInstantiator(commands =>
            {
                var services = new ServiceCollection();
                services.AddSingleton<MyService>();
                foreach (var command in commands)
                    services.AddTransient(command.Type);
    
                return services.BuildServiceProvider();
            })
            .Build()
            .RunAsync();
  9. Implement graceful cancellation

    prime
    To handle interrupt signals (like Ctrl+C) gracefully, use console.RegisterCancellationHandler(). This method returns a CancellationToken that triggers when the signal is received. This prevents the application from terminating immediately, allowing you to perform cleanup or controlled exits.
  10. Handle errors using CommandException

    prime

    Commands communicate errors and exit codes by throwing a CommandException. This exception allows you to:

    1. Print a specific error message.
    2. Return a specific exit code (recommended range: 1-255 to avoid Unix overflow).
    3. Optionally show help text for the current command.

    Warning: On Unix systems, using exit codes outside the 8-bit unsigned range (1-255) can cause overflows.

  11. Implement custom input conversion with IInputConverter

    prime

    CliFx automatically converts strings to common types (bool, enum, int, etc.). For custom types, implement IInputConverter<T>.

    • Scalar types: Derive from ScalarInputConverter<T>.
    • Sequence types: Derive from SequenceInputConverter<T>.
    • Requirement: Converters must have a public parameter-less constructor.

    To use a converter, assign it to the Converter property of the [CommandParameter] or [CommandOption] attribute.

  12. Implement custom validation with IInputValidator

    prime

    Validators verify converted values before the command executes. Implement IInputValidator<T> by deriving from InputValidator<T>.

    • Execution: Validators run sequentially. All errors are merged and reported together.
    • Requirement: Validators must have a public parameter-less constructor.

    To use a validator, assign it to the Validators property of the [CommandParameter] or [CommandOption] attribute.

    using CliFx.Activation;
    
    public class PositiveNumberValidator : InputValidator<double>
    {
        public override IEnumerable<InputValidationError> Validate(double value)
        {
            if (value <= 0)
                yield return Error("Value must be positive.");
        }
    }
    
    // Usage in command
    [CommandParameter(0, Validators = [typeof(PositiveNumberValidator)])]
    public required double Value { get; set; }