How to Create Command Line Console Applications in .NET

antonmartyniuk

Anton Martyniuk

Posted on November 27, 2024

How to Create Command Line Console Applications in .NET

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}");
    }
}
Enter fullscreen mode Exit fullscreen mode

In Program.cs with a top-level statements args is just magically available:

var name = args[0];
Console.WriteLine($"Hello, {name}");
Enter fullscreen mode Exit fullscreen mode

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}");
})
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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) => { });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) => { });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) => { });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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}");
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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();
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let's see how to use our HtmlTool application.

Displaying Help Information

Cocona automatically generates help messages. Run:

HtmlTool --help
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Minifying an HTML File

HtmlTool minify -i sample.html -o minified.html
Enter fullscreen mode Exit fullscreen mode

This will create a minified.html file with the minified content.

Beautifying an HTML File

HtmlTool beautify -i minified.html -o beautified.html
Enter fullscreen mode Exit fullscreen mode

This will create a beautified.html file with beautified content using 1 tab for indentation.

Validating an HTML File

HtmlTool validate -i sample.html
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

Subcommands

Organize commands using subcommands.

var htmlCommands = new CommandCollection();
htmlCommands.AddCommand("minify", ...);
htmlCommands.AddCommand("beautify", ...);

app.AddSubCommand("html", htmlCommands, "Commands for HTML processing");
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
antonmartyniuk
Anton Martyniuk

Posted on November 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related