Sub-Commands in a .NET Core CLI
In my last post, I hinted as some more advanced usage of the CommandLineUtils library. In this post I want to show you how to set up sub-commands, which can make a big CLI much easier to manage and a lot easier to use.
I’m going to use my previous
sample-cli project from last time and expand it a bit. The previous version was basically
echo with an option to upper-case the text. In this one, we’ll keep the same concept, but break the casing into sub-commands. Here’s what we’re going for:
> sample-cli camel "My Text" # -> myText
Nothing changes in the project file. We’re still using our settings from last time.
I recommend creating a folder called
Commands and putting your commands in there. Each will be it’s own class and will have a similar structure to our previous
MainCommand.cs. Here, in order, are
Note that each has a
Command attribute and an
OnExecute method. The
IConsole parameter is injected by the library and is a basic abstraction of the .NET
Console class. Also note that each has an argument of it’s own. They can have separate arguments and options.
You can also use DI with constructor arguments!
Now, our updated
MainCommand.cs, which I have also moved in to the
Commands folder (and updated the namespace):
Note that the
OnExecute method now does nothing but show the help text. That will only be called if we do not call into one of the sub-commands. Also note the
Subcommand attribute which takes any number of arguments. Also, the
Program.cs file will need a new using to the
Commands namespace, but is otherwise unchanged.
I wish there was a less-static way of defining sub-commands. I’d like to have a convention-based approach where I can either name all my sub-commands in a similar way or inherit from some class or interface and have the library do the rest.
There is much more that you can do by combining sub-commands with C# inheritance. You can nest types to keep your sub-commands isolated from each other. You can set up a base class with common options and have your commands inherit from it. The possibilities are wide and it’s easy to get lost. Remember to keep it simple.
> sample-cli camel --help
> sample-cli camel "My Text"
> sample-cli pascal "My Text"
> sample-cli snake "My Text"
Sub-commands are a great way to separate a large command into smaller, more manageable parts. I use this style for a tool that I made to rename various video files for use in Plex. I have separate sub-commands for movie and show files. The code for each shares some stuff, shared via service classes which are injected into each command, but can have their own separate logic. It’s a great way to make a flexible tool.