CLAP is the framework of today. A venerable project with NuGet package and nice documentation.
Experience as a asset
CLAP presents itself as a parser for the command line and has been around for quite a few years. We could say it has reached stability (despite having some open issues).
It roots for simplicity and gets you going very, very fast. Despite some mistakes in their documentation (fixed in their code documentation), it is very good to find a project that offers nice, detailed documentation.
This attribute-based framework takes a slightly different approach as it is totally command-centered, thanks to its procedure of exposing methods as commands (verbs) and method arguments as command line options.
One can get going by simply having a static method decorated with the [Verb]
attribute and use a static method to dispatch that method :-O. That is a very low entry bar.
Implementing our example
Implementing our example was really simple, just a class with two methods and decorated arguments:
public class Doer | |
{ | |
private readonly TextWriter _dependencyForAVerb; | |
public Doer(TextWriter dependencyForAVerb) | |
{ | |
_dependencyForAVerb = dependencyForAVerb; | |
} | |
[Verb(Description = "Do something.")] | |
public void Something( | |
[Required, Description("place where the something was done")] | |
string location, | |
[Description("number of times something was done"), DefaultValue(1)] | |
int times, | |
[Description("include if the something was awesome")] | |
bool awesome) | |
{ | |
var cmd = new ADoerOfSomething(_dependencyForAVerb); | |
var options = new OptionsForSomething | |
{ | |
Location = location, | |
Times = times, | |
Awesome = awesome | |
}; | |
cmd.Do(options); | |
} | |
[Verb(Aliases = "something-else", Description = "Do something else.")] | |
public void SomethingElse( | |
[Required, Description("places where the something else was done")] | |
string[] locations, | |
[Description("include if the something else was awesome")] | |
bool awesome) | |
{ | |
var cmd = new ADoerOfSomethingElse(_dependencyForAVerb); | |
var options = new OptionsForSomethingElse | |
{ | |
Locations = locations, | |
NotSoAwesome = !awesome | |
}; | |
cmd.Do(options); | |
} | |
} |
Dispatching can be done statically (Parser.Run
) but I chose to use an instance of Parser
to be able to display help and not surface exceptions to the end user and pass a constructed instance of the class that contains all the methods to be exposed.
static void Main(string[] args) | |
{ | |
var parser = new Parser<Doer>(); | |
parser.Register.EmptyHelpHandler(Console.WriteLine); | |
parser.Register.HelpHandler("?,h,help", Console.WriteLine); | |
parser.Register.ErrorHandler(ex => | |
{ | |
ex.ReThrow = false; | |
Console.ForegroundColor = ConsoleColor.Red; | |
Console.WriteLine(ex.Exception.Message); | |
Console.ResetColor(); | |
}); | |
parser.Run(args, new Doer(Console.Out)); | |
} |
It does indeed work nicely and the framework offers a lot of interesting features (validation, default commands, multiple ways to plug in an IOC container, interceptions, complex type handling,...) that are not used in this sample, but could come handy in a real project.
The Challenges
Mandatory Arguments
Mandatory arguments are not the default, but decorating the argument property with [Required]
does the job:
*NotLastConsole_CLAP*>doer something | |
Missing argument for required parameter 'location' |
Extra, undefined arguments will make an error appear.
Non-Textual arguments
Declaring the right type for the argument seems to work just fine.
*NotLastConsole_CLAP*>doer something -l:here -t:asd | |
'asd' cannot be converted to System.Int32 |
Default values are provided as objects and booleans are flags.
Multi-arguments
Declaring the argument as a collection and passing comma-separated values works beautifully.
*NotLastConsole_CLAP*>doer something-else -l:here,there | |
I did something else not so awesome in here, there |
Showing Help
In order to provide auto-generated help, one needs to register a help handler (and an empty help handler for those that run tools recklessly). But that is a simple and well-documented pre-requisite to get a complete help message that shows verbs, options, shortcuts, required arguments, types and default values.
*NotLastConsole_CLAP*>doer -h | |
something: Do something. | |
/a /awesome : include if the something was awesome | |
/l /location : place where the something was done (String) (Required) | |
/t /times : number of times something was done (Int32) (Default = 1) | |
something-else|somethingelse: Do something else. | |
/a /awesome : include if the something else was awesome | |
/l /locations : places where the something else was done (String[]) (Required) | |
Global Parameters: | |
/?|h|help : Help |
There does not seem to be a way to display help for single commands, because everything is displayed for the general help.
That might be a problem for big applications with lots of commands, but those kind applications might have found other challenges with the framework before thinking about help.
Command Dispatching
Commands are methods decorated with [Verb]
and arguments are options. We can dispatch static methods, instance methods or we can let the framework instance objects, provide instances ourselves or delegate creation to a TargetResolver
.
On top of that there are multiple hooks to inject behaviors, so I believe we are good with this feature.
We also have the ability to have a default operation for those single-purpose applications.
Conclusion
I did not know about CLAP (or many of the frameworks I have looked into in this series) and the idea of decorating method arguments did not make me the happiest camper in the woods, but I have to admit that the framework has won me over. Their choices are sensible, it is good architected, feature rich and well documented.
Don't be fooled about its age, it is a very useful framework and one I could easily see myself using if I ever give up on Powershell.