dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.41k stars 382 forks source link

Class and Property Attributes #2480

Open Bru456 opened 2 months ago

Bru456 commented 2 months ago

I am relatively new to this library but I really like it.

It is somewhat complicated and difficult to get started on and requires a lot of code to get something up and running.

I would love to see the ability to add commands and options via attributes.

For example:

[Command(Description = "This is a sub command")]
public class SubCommand1
{
    // Options are set via properties and the Names are the name of the property with the type taken from the property.
    // Any default value is the properties default value, if set otherwise its the type default value or null. 
    // Aliases are set via attributes, a default alias would be the properties name.
    [Option (Alias = ["--alais1", "-a"], Description = "Description for Option1")]
    public string Option1 { get; set; } = "DefaultValue";

    [Option (Alias = ["--alais2", "-b"], Description = "Description for Option2")]
    public bool Option2 { get; set; } = true;

    // Any properties without the option attribute is ignored / not mapped.
    public string IgnoredProperty { get; set; }

    // Sub commands of the command is added via a SubCommand Attribute
    [SubCommand]
    public SubCommand2 NestedCommand { get; set; }

    [CommandInvocation]
    public int Execute()
    {
        // Called when command is invoked
        // Take the result of this as the commands exit code.
        // This would not be called if the NestedCommand is invoked.
        // If a sub command does not have this then it is a holder for other nested commands and should fail.
    }
}

[Command(Description = "This is a sub command of SubCommand1")]
public class SubCommand2
{
    [Option (Alias = ["--alais1", "-a"], Description = "Description for Option1")]
    public string Option1 { get; set; } = "DefaultValue";

    [CommandInvocation]
    public int Execute()
    {
        // Called when command is invoked
        // This would only be called if the sub command is invoked
        // Take the result of this as the commands exit code.
    }
}

// Any class without the command attribute can not be a command but could be used for inheritance of common properties into other commands.
public abstract class CommonOptions
{
    [Option (Alias = ["--verbose", "-v"], Description = "Verbose messaging")]
    public bool Verbose { get; set; } 
}

This could then be used in Program.cs to configure the CLI

   RootCommand rootCommand = new RootCommand("This is the root command");
   rootCommand.AddCommand<SubCommand1>();
   // Sub commands added as needed.
   rootCommand.InvokeAsync(args);

This to me would allow for easy config, separation and testing of commands. Not to mention a cleaner looking Program.cs

To summarise:

  1. Allow the config of commands and sub commands via class, property and method level attributes. a. Command - Class level to indicate it is a command i. I would allow the name to be overridden via a parameter in the attribute. b. Option - Property level to indicate it is a CLI option i. The Name is the property name ii. The type is the property type. I would keep this simple for starts and only accept basic types. iii. Default values are provided via what the property is set to. iv. Things like alias and others are set and configured via attribute properties. c. SubCommand - Property level to indicate it is a nested command d. CommandInvocation - Method level to indicate which method should be used on command invocation. i. The expected output should be void or int. Treat it as the exit code of the application. ii. If errors are thrown treat it as a none 0 exit code.
  2. Allow the commands to be added to the root command via a Type Parameter and look at the attributes to configure the CLI arguments.

Additional future improvements if this is taken forward:

  1. A method attribute to override help text for a specific command. This should expect a string to be returned.
  2. An method attribute to allow examples of the command to be injected into existing help text of the command.
  3. A CommandValidate method attribute that is used to perform validation of the options passed. This should expect a bool to be returned.
  4. Custom parsers for complicated object types.

This would allow for cool things to be done such as common options (quiet, verbose, outpath, etc.) to be declared in a BaseOptions class and inherited into command classes.

mikepal2 commented 1 month ago

I agree that the current form of the System.CommandLine library requires a significant amount of code to get started.

Many CLI frameworks, including the one suggested, require a separate class implementation for each command. In my opinion, creating per-command classes adds unnecessary bloat to the code with little to no benefit. Attributes are still needed for additional information, such as descriptions and aliases.

My attempt to solve this problem resulted in the creation of the SnapCLI library, which is built on top of System.CommandLine and offers a simplified CLI API. This solution eliminates the need for separate classes by using only attributes (just 4 of them) and static methods as command handlers, aiming to streamline usage as much as possible. For more information, see the documentation and samples. I hope you find it useful.