CommandDotNet Documentation

repository·master·Indexed 20 days ago

https://github.com/bilal-fazlani/commanddotnet

A modern .NET framework for building CLI applications following POSIX conventions. It provides automatic command discovery, argument validation, help generation, and support for dependency injection, middleware, and piping. The framework distinguishes between Options (named arguments), Operands (positional arguments), and Flags (boolean options), offering flexible argument arity control and testing patterns via RunInMem and BDD Verify.

Tokens
78K
Snippets
231
Records
339
Agent score
70%

What's inside CommandDotNet

  1. What is CommandDotNet?

    master

    CommandDotNet is a modern framework designed for building CLI applications that follow POSIX conventions. It provides built-in support for several advanced CLI features:

    • Command Structure: Commands and sub-commands.
    • Data Handling: Enums, custom types, and support for piping and streaming.
    • User Experience: Typo suggestions, interactive prompting, and password handling.
    • Validation & Logic: Argument validation, dependency injection, and response files.
    • Extensibility: Ability to modify and extend functionality via configuration and middleware.

    The framework also includes a suite of test tools used internally to ensure feature reliability.

  2. Summary of New Features in v3

    master

    CommandDotNet v3 introduces several advanced capabilities:

    • Argument Handling:
      • Response files: Support for providing arguments via files.
      • Password type: A specialized type to prevent accidental logging and hide values during prompting.
      • Piped input: Mapping piped input to operand lists.
      • Arity: Calculated for arguments and updatable via middleware.
      • Lists: Parameters can be arrays or enumerables streamed from files or piped input.
    • User Interaction:
      • Prompting: A tool for interactive input that honors Password types and Ctrl+C cancellation. Note: List values are now delimited by line instead of comma; enter an empty line to submit.
    • Extensibility & Testing:
      • Middleware pipeline: The core architecture for all framework operations.
      • Parameter resolvers: For custom argument resolution.
      • Test tools: For end-to-end testing and middleware testing.
      • HelpTextProvider: Can be overridden for targeted help section changes.
    • Integration:
      • SimpleInjector support.
      • IoC runInScope: Enables isolated instances per run.
      • IArgumentModel: Can be resolved from containers with support for AppSettings defaults.
  3. CommandDotNet Overview and Features

    master

    CommandDotNet is a modern framework for building CLI applications in .NET that favors POSIX conventions.

    Key features include:

    • Commands and sub-commands
    • Automatic argument validation and type conversion
    • Dependency Injection support
    • Piping and streaming
    • Enums and custom type support
    • Typo suggestions
    • Interactive prompting and password handling
    • Response files
    • Middleware and configuration for extensibility
  4. Implement the Middleware Configuration pattern

    master

    When creating middleware that requires additional parameters (like connection strings or flags), use a private Config class to store these parameters in the service container. This allows the middleware delegate to access the configuration via the CommandContext services.

    Important: Call UseMiddleware before adding the configuration to the services. UseMiddleware provides more informative error messages if the middleware is registered multiple times compared to a standard service duplicate key exception.

    public static class FluentValidationMiddleware
    {
        public static AppRunner UseFluentValidation(this AppRunner appRunner, bool showHelpOnError = false)
        {
            return appRunner.Configure(c =>
            {
                // 1. Register the middleware first
                c.UseMiddleware(ValidateModels, MiddlewareSteps.FluentValidation);
                // 2. Add the config to services second
                c.Services.Add(new Config(showHelpOnError));
            });
        }
        
        private class Config
        {
            public bool ShowHelpOnError { get; }
            public Config(bool showHelpOnError) => ShowHelpOnError = showHelpOnError;
        }
    
        private static Task<int> ValidateModels(CommandContext ctx, ExecutionDelegate next)
        {
            // Access config via services
            var showHelpOnError = ctx.AppConfig.Services.GetOrThrow<Config>().ShowHelpOnError;
            ...
            return next(ctx);
        }
    }
  5. Compose commands using multiple Response Files

    master

    Response files can be used to 'pin' or compose complex command strings. Instead of long shell scripts, you can create fine-grained response files for specific environments or customer contexts and mix them together.

    Example Composition:

    • @test.rsp contains: --env test --username lala
    • @migrate-acme.rsp contains: migrate-orders --customer acme

    Combined Call: Migrations @test.rsp @migrate-acme-orders.rsp ...

    # Mixing and matching pre-defined configurations
    Migrations @test.rsp @migrate-acme-orders.rsp
  6. Understand argument arity and default values by definition type

    master

    CommandDotNet handles argument optionality and default values differently depending on whether the argument is defined as a method parameter or a class property.

    Parameters

    When defining arguments as method parameters, behavior is consistent for both struct and class types:

    • Defaults: Uses the optional parameter value provided in the method signature.
    • Optionality: An argument is considered optional if the parameter is marked as Nullable or is an optional parameter in the C# signature.

    Properties

    When defining arguments as properties, behavior depends on the type:

    • Defaults: Uses the property value immediately after initialization.
    • Optionality (Structs): A struct property is considered optional if it is Nullable or if its current value is not equal to default(T).
    • Optionality (Classes): A class property is considered optional if its current value is not null.

    Summary Table

    Defined usingDefaultsOptional
    parameteroptional parameterswhen Nullable or parameter is optional
    propertyproperty value immediately after initialization
    -- structwhen Nullable or default != default(T)
    -- classwhen default != null
  7. Implement a custom Dependency Resolver

    master

    To use a custom DI container that does not have a dedicated CommandDotNet package, implement the IDependencyResolver interface.

    Your implementation should handle:

    • Resolve(Type type): Used for command classes.
    • TryResolve(Type type, out object instance): Used for IArgumentModel classes.

    Once implemented, register it using appRunner.UseDependencyResolver(yourResolverInstance).

  8. How cancellation works in interactive sessions

    master
    In an interactive session where one command uses an AppRunner to execute another command, the framework provides scoped cancellation. When Console.CancelKeyPress occurs, the token for the newest CommandContext (the sub-command being run) is cancelled. This allows you to cancel a long-running sub-command without terminating the entire interactive session hosting it.
  9. How BDD Verify orchestration works

    master

    The Verify method follows a specific orchestration flow:

    1. It calls RunInMem to execute the command.
    2. It captures any exceptions, returning an exit code of 1 and outputting the exception message to the console (mimicking shell behavior).
    3. It performs assertions on the ExitCode, Console Output, and TestCaptures.
    4. It returns an AppRunnerResult, meaning all configuration options available to RunInMem are also available within the Verify call.
  10. Define arguments using parameters and properties

    master

    In CommandDotNet, you define the arguments for your commands using two primary methods:

    1. Method Parameters: Arguments defined as parameters within a command method.
    2. Properties: Arguments defined as properties on an IArgumentModel object.

    Argument Types and Hierarchy

    Arguments are categorized into two concrete types of IArgument:

    • Option: Named arguments.
    • Operand: Positional arguments.

    These types, along with Command, implement the IArgumentNode interface. A Command acts as a container that holds collections of Option, Operand, and other Command objects (subcommands).

  11. Handle optional arguments as options to avoid ambiguity

    master

    When designing commands with optional arguments, it is recommended to define them as Options rather than Operands (positional arguments), especially if there are multiple optional values.

    Why? CommandDotNet assigns positional arguments based on the order they appear in the shell. If a user skips the first optional operand but provides a value for the second, the framework will incorrectly assign that value to the first operand because it cannot infer the user's intent.

    By using Options, users can specify exactly which argument they are providing by name, similar to how optional parameters work in C#.

  12. Use the Password type for confidential arguments

    master

    The Password type is used to define arguments that contain sensitive data. It provides two security features to prevent accidental leakage:

    1. Serialization Protection: The actual value is only accessible via the GetPassword() method. Because it is not exposed via a standard property, serializers will not automatically capture the sensitive value.
    2. Masked Output: The ToString() method is overridden to output ***** if a value is present (or an empty string if no value is provided). This allows logs to show that a password was provided without revealing the actual content or its length.

    To access the actual sensitive string, you must call .GetPassword() on the Password instance.

    public void Login(IConsole console, string username, Password password)
    {
        // password.ToString() will output '*****'
        console.WriteLine($"u:{username} p:{password}");
    
        // Use GetPassword() to retrieve the actual value
        string actualPassword = password.GetPassword();
    }