Recently, I was working with a developer who was putting the final touches on a command-line tool. I usually opt for a good Web-App and API, but it got me thinking about a pair of tools I've used to package up Windows console applications for use. ManyConsole makes command-line parameter creation and documentation easy and Fody abstracts a lot of MSBuild configuration to pack required dll files into one *.exe. Both tickle my fancy in the "get stuff done quickly and well" buttons. I'll cover Fody in the next post.
ManyConsole
Install-Package ManyConsole
Extending NDesk.Options, ManyConsole aims to speed up argument parsing in a manner that keeps the main method uncluttered.
ManyConsole provides a console interface for the user to list available commands, call and get help for each. - ManyConsole Quickstart Guide
Let's assume we want a command to run a ThingyService which will ostensibly print some results to some csv file.
- We want to give the caller the option to override the default file name
- We want to give the caller the option to add a timestamp to the filename
The first thing we'll do is build the command:
public class DoTheThingCommand : ConsoleCommand
{
// using dependency injection to resolve
private readonly IThingyService _thingy;
// set a default filename for some output
private string Filename { get; set; } = "ThingyReport.csv";
// build the command
public DoTheThingCommand(IThingyService thingy)
{
_thingy = thingy;
// define command line switch and description
IsCommand("csv", "output csv report");
HasLongDescription("outputs a CSV report for the thingy to the filesystem");
// build options
HasOption("f|filename:", "specify filename",
f => Filename = f ?? Filename);
HasOption("t|timestamp:", "add timestamp to filename",
t => Filename = $"{Filename}-{DateTimeOffset.Now:yy-MM-dd}");
}
// define the behavior
public override int Run(string[] remainingArguments)
{
_thingy.DoIt(Filename);
return 1;
}
}
We'll add a little Dependency Injection:
public static class Container
{
public static IContainer Build()
{
var builder = new ContainerBuilder();
// register the service implementation
builder.RegisterType<ThingyService>().As<IThingyService>();
// register all ManyConsole.ConsoleCommands
builder.RegisterAssemblyTypes(typeof(Program).Assembly)
.Where(x => x.IsSubclassOf(typeof(ConsoleCommand)))
.As<ConsoleCommand>();
return builder.Build();
}
}
Sample Main, resolving the implemented console commands
public class Program
{
public static int Main(string[] args)
{
using (var scope = Container.Build().BeginLifetimeScope())
{
return ConsoleCommandDispatcher
.DispatchCommand(
scope.Resolve<IEnumerable<ConsoleCommand>>(),
args,
Console.Out);
}
}
}
All the options for the command-line argument have been placed in the same area, while still allowing the dependency injector to handle the details of the service itself.
Now here's the payoff: when I run the console command someprogram.exe /?
, I'll get the following output:
Extra parameters specified: /?
'csv' - output csv report
outputs a CSV report for the thingy to the filesystem
Expected usage: ManyConsoleReference.exe csv <options>
<options> available:
-f, --filename[=VALUE] specify filename
-t, --timestamp[=VALUE] add timestamp to filename
This shows me I can run the command someprogram.exe csv -f "Spiffy.csv"-t
which will change the filename and add a timestamp.
I can also add many other commands all segmented from each other with the documentation built into the command definitions.
Software Development Nerd