How to Create Command Line Console Applications in .NET
Anton Martyniuk
Posted on November 27, 2024
Throughout my career, I have built a ton of console command-line applications.
From simple to really complex ones with a lot of commands.
In this blog post, I want to introduce you to a Cocona Nuget package that helped me create a new command-line tool this year.
On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.
What is Cocona?
Creating console applications in .NET often involves parsing command-line arguments, handling options, and providing help messages.
All these options require a lot of boilerplate-heavy and error-prone code.
Cocona is a micro-framework that streamlines this process, allowing you to focus on your application's core functionality.
This library uses ASP.NET Core-like Minimal API style for handling commands.
Features supported by Cocona:
- Command-line option semantics like UNIX tools standard (can handle both
-rf /
and-r -f /
) - Support single command and multiple commands style:
-
myapp --foo --bar -n arg0 "arg1"
(e.g. dir, cp, ls ...) -
myapp server -m "Hello world!"
(e.g. dotnet, git, kubectl ...)
-
- Built-in help documentation support (see a help message by typing
-h
or--help
) - Built-in similar commands suggestion
- Highly modulable/customizable CLI framework (Cocona built on top of
Microsoft.Extensions.*
framework. Cocona natively supports Logging, DI, Configuration and ConsoleLifetime)
In this blog post, we'll use Cocona to build a command-line tool that performs the following operations on HTML files:
- Minify: Compresses the HTML by removing unnecessary whitespace and comments.
- Beautify: Formats the HTML to make it more readable.
- Validate: Checks the HTML for syntax errors.
Brief Overview of Command-Line Arguments
In traditional .NET console applications, handling command-line arguments requires parsing the string[] args
array in the Main method.
Here is how you can access args
in the Program.cs:
class Program
{
static void Main(string[] args)
{
var name = args[0];
Console.WriteLine($"Hello, {name}");
}
}
In Program.cs with a top-level statements args
is just magically available:
var name = args[0];
Console.WriteLine($"Hello, {name}");
Handling command-line arguments involves the following:
- Manually splitting arguments.
- Validating input.
- Providing help messages.
Cocona abstracts away this complexity, allowing you to define commands and options using attributes and method parameters.
Here is how this code will look like in Cocona:
CoconaApp.Run((string name) =>
{
Console.WriteLine($"Hello {name}");
})
It looks like a Minimal API endpoint with one argument that is coming from a command-line argument.
Understanding Commands, Arguments, and Options
When building command-line applications, it's essential to understand the components that make up the command-line interface (CLI).
These components allow users to interact with your application.
Commands
A command is a specific action or operation that your application can perform.
In the context of CLI applications, commands are typically the first word(s) after the application's name in the command line.
git commit -m "Commit message"
Here, commit is a command that tells git to create a new commit.
In Cocona, commands are represented as methods provided into AddCommand
:
var app = CoconaApp.Create();
app.AddCommand("commit", (string message) => { });
Arguments
An argument (or positional argument) is a value that a command requires to perform its operation.
Arguments are typically specified after the command and are not prefixed by any indicator (like - or --):
cp source.txt destination.txt
Here, source.txt
and destination.txt
are arguments to the cp (copy) command.
In Cocona, arguments are represented as method parameters decorated with the [Argument]
attribute or inferred from the parameter position:
app.AddCommand("copy", (string sourceFile, string destinationFile) => { });
app.AddCommand("copy", ([Argument] string sourceFile, [Argument] string destinationFile) => { });
Options
An option (or flag) modifies the behavior of a command.
Options are typically prefixed with one or two dashes (e.g., -o or --output) and can be followed by a value or act as a boolean flag.
ls -l --color
Here, -l
and --color
are options that change how the ls
command behaves.
In Cocona, options are represented as method parameters decorated with the [Option]
attribute:
app.AddCommand("ls", ([Option('l')] bool longFormat, [Option("color")] bool useColor) => { });
Putting It All Together
When a user runs your application from the command line, they might provide a combination of commands, arguments, and options:
appname command argument1 argument2 --option1 value1 -o value2
- appname: The name of your application.
- command: The action to perform.
- argument1, argument2: Positional arguments required by the command.
- --option1 value1, -o value2: Options that modify the command's behavior.
Building the HTML Tool with Cocona
We'll build a command-line application named HtmlTool that accepts commands to minify, beautify, and validate HTML files.
We'll define the following commands:
- Minify: app.AddCommand("minify", ...)
- Beautify: app.AddCommand("beautify", ...)
- Validate: app.AddCommand("validate", ...)
Here is an initial template for our application:
var app = CoconaApp.Create();
app.AddCommand("minify", (string inputFile, string outputFile) => { })
.WithDescription("Minify an HTML file by removing unnecessary whitespace and comments");
app.AddCommand("beautify", (string inputFile, string outputFile) => { })
.WithDescription("Beautify an HTML file for better readability");
app.AddCommand("validate", (string inputFile) => { })
.WithDescription("Minify an HTML file by removing unnecessary whitespace and comments");
app.Run();
First, we create a Cocona application by calling CoconaApp.Create
method.
Next, we can register commands with an AddCommand
method.
Implementing Minify Command
app.AddCommand("minify", (
[Option('i', Description = "The input file path")] string inputFile,
[Option('o', Description = "The output file path")] string? outputFile = null
) =>
{
var htmlContent = File.ReadAllText(inputFile);
var minifiedHtml = MinifyHtml(htmlContent);
var outPath = outputFile ?? GetOutputFilePath(inputFile, "min");
File.WriteAllText(outPath, minifiedHtml);
Console.WriteLine($"Minified HTML saved to {outPath}");
}).WithDescription("Minify an HTML file by removing unnecessary whitespace and comments");
Command parameters:
-
inputFile
: Positional argument for the input HTML file. -
outputFile
: Optional parameter (-o
or--output
) for specifying the output file path.
Here is the code for the helper methods:
string MinifyHtml(string html)
{
// Remove comments
var noComments = Regex.Replace(html, @"<!--(.*?)-->", "", RegexOptions.Singleline);
// Remove unnecessary whitespace including tabs
var noWhitespace = Regex.Replace(noComments, @"\s+", " ");
// Remove spaces between tags
var minified = Regex.Replace(noWhitespace, @">\s+<", "><");
return minified.Trim();
}
string GetOutputFilePath(string inputFile, string suffix)
{
var directory = Path.GetDirectoryName(inputFile);
var filename = Path.GetFileNameWithoutExtension(inputFile);
var extension = Path.GetExtension(inputFile);
return Path.Combine(directory, $"{filename}_{suffix}{extension}");
}
Implementing Beautify Command
app.AddCommand("beautify", (
[Option('i', Description = "The input file path")] string inputFile,
[Option('o', Description = "The output file path")] string? outputFile = null
) =>
{
var htmlContent = File.ReadAllText(inputFile);
var beautifiedHtml = BeautifyHtml(htmlContent);
var outPath = outputFile ?? GetOutputFilePath(inputFile, "beautify");
File.WriteAllText(outPath, beautifiedHtml);
Console.WriteLine($"Beautified HTML saved to {outPath}");
}).WithDescription("Beautify an HTML file for better readability");
Command Parameters:
-
inputFile
: Positional argument for the input HTML file. -
outputFile
: Optional parameter for the output file path.
You can download the full source code for this application at the end of the blog post
Implementing Validate Command
To implement a Validate command, we need to download the following Nuget packages that allow parsing and formatting the HTML:
dotnet add package HtmlAgilityPack
dotnet add package AngleSharp
app.AddCommand("validate", (
[Option('i', Description = "The input file path")] string inputFile
) =>
{
var htmlContent = File.ReadAllText(inputFile);
var isValid = ValidateHtml(htmlContent);
if (!isValid)
{
Console.WriteLine("The HTML is invalid.");
return;
}
Console.WriteLine("The HTML is valid.");
}).WithDescription("Validate an HTML file for syntax errors");
bool ValidateHtml(string html)
{
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
return !htmlDoc.ParseErrors.Any();
}
Command Parameters:
-
inputFile
: Positional argument for the input HTML file.
Using the Application
I recommend publishing the HtmlTool app as a SingleFile, for example, for Windows:
dotnet publish -r win-x64 -p:PublishSingleFile=true --self-contained false
Let's see how to use our HtmlTool application.
Displaying Help Information
Cocona automatically generates help messages. Run:
HtmlTool --help
Usage: HtmlTool [command] [arguments] [options]
Commands:
minify Minify an HTML file by removing unnecessary whitespace and comments
beautify Beautify an HTML file for better readability
validate Validate an HTML file for syntax errors
Options:
-h, --help Show help message
--version Show version
Minifying an HTML File
HtmlTool minify -i sample.html -o minified.html
This will create a minified.html
file with the minified content.
Beautifying an HTML File
HtmlTool beautify -i minified.html -o beautified.html
This will create a beautified.html
file with beautified content using 1 tab for indentation.
Validating an HTML File
HtmlTool validate -i sample.html
This will output whether the HTML is valid or not.
Additional Features of Cocona
Asynchronous Commands
Define async commands by returning Task.
app.AddCommand("minify", async (string inputFile, string outputFile) =>
{
// Async code here
});
Subcommands
Organize commands using subcommands.
var htmlCommands = new CommandCollection();
htmlCommands.AddCommand("minify", ...);
htmlCommands.AddCommand("beautify", ...);
app.AddSubCommand("html", htmlCommands, "Commands for HTML processing");
Dependency Injection
If you need dependency injection, you can configure services when creating the app.
var app = CoconaApp.CreateBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<IHtmlProcessor, HtmlProcessor>();
})
.Build();
// Use the service in your commands
app.AddCommand("minify", ([FromService] IHtmlProcessor htmlProcessor,
string inputFile, string outputFile) =>
{
// Use htmlProcessor
});
For more information, you can read an official Cocona documentation on their GitHub page.
Summary
Cocona simplifies the development of .NET console applications by reducing boilerplate code and providing powerful features out of the box.
Cocona has the following advantages:
- Simplicity: Define commands, arguments, and options directly when adding commands without the need to write boilerplate code.
- Automatic Help Generation: Cocona provides help messages without additional code.
- Flexible Command and SubCommand Registration: Use either method-based commands or inline command definitions.
- Asynchronous and DI Support: You can define async commands easily and use services from the DI container.
With Cocona you can quickly build feature-rich tools with minimal effort.
On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.
Posted on November 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.