Introduction
Command-line utilities (CLIs) allow you to automate tasks, process data, and provide users with fast, scriptable interfaces. In the .NET ecosystem, creating a CLI tool in C# is very straightforward. This guide will walk you through creating a basic CLI app—from setting up the .NET console project to parsing arguments and publishing your finished utility for distribution.
Step 1: Setting Up a .NET Console Project
To begin, you’ll need the .NET SDK. If you don’t have it yet, download it from the official .NET download page. Next, open your terminal and create a new console application:
dotnet new console -n MyCliUtility
cd MyCliUtility
This scaffolds a project named MyCliUtility
, with a default Program.cs
containing a “Hello World” program.
Step 2: Parsing Command-Line Arguments
Let’s replace the default Program.cs
logic with something that reads in the user’s name. We can access arguments through the args
parameter in Main
. Here’s the simplest possible approach:
using System;
namespace MyCliUtility
{
class Program
{
static void Main(string[] args)
{
// If no arguments, show usage
if (args.Length == 0)
{
Console.WriteLine("Usage: MyCliUtility <name>");
// Return a special exit code to indicate improper usage
Environment.Exit(1);
}
var name = args[0];
Console.WriteLine($"Hello, {name}!");
}
}
}
Running dotnet run -- Alice
would print:
Hello, Alice!
This is fine for basic scenarios, but for more advanced parsing (e.g., multiple arguments, subcommands, or custom help text), you may want to leverage the official System.CommandLine
library.
Step 3: Using System.CommandLine (Advanced Parsing)
System.CommandLine
provides first-party support for creating flexible CLI tools with built-in help text, argument validation, and subcommands. It’s available on NuGet. Install it by running:
dotnet add package System.CommandLine --prerelease
Then, replace the contents of Program.cs
with:
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
namespace MyCliUtility
{
class Program
{
static async Task Main(string[] args)
{
var rootCommand = new RootCommand("Greets a user by name")
{
new Argument<string>(
"name",
description: "The name to greet")
};
var greetOption = new Option<bool>(
"--greet",
description: "Add a friendly greeting"
);
rootCommand.AddOption(greetOption);
rootCommand.SetHandler((string name, bool greet) =>
{
if (greet)
{
Console.WriteLine($"Hello, {name}!");
}
else
{
Console.WriteLine($"Name provided: {name}");
}
},
rootCommand.Arguments[0],
greetOption);
await rootCommand.InvokeAsync(args);
}
}
}
Now, dotnet run -- Bob
simply shows “Name provided: Bob”, while dotnet run -- Bob --greet
prints “Hello, Bob!” You’ll also get automatic help text:
dotnet run -- --help
Step 4: Packaging and Publishing the CLI Tool
Once you’ve built and tested your CLI locally, you may want to distribute it. You can create a self-contained binary (which includes the .NET runtime) or a framework-dependent one (requires .NET installed). Here’s how to publish a self-contained app:
dotnet publish -c Release -r <RID> --self-contained true
Common RIDs (Runtime Identifiers) include win10-x64
, linux-x64
, or osx.11.0-x64
. This command produces binaries in /bin/Release/net7.0/[RID]/publish
. You can zip this folder and share it with others. If you’re building for all three platforms, you’d repeat with the relevant RIDs.
Step 5: Installing the Tool as a Global Tool
Another option is to distribute your CLI as a .NET global tool. You can define your project as a local or NuGet-based package. First, add these properties in your MyCliUtility.csproj
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<PackAsTool>true</PackAsTool>
<PackageOutputPath>./nupkg</PackageOutputPath>
<ToolCommandName>mycli</ToolCommandName>
</PropertyGroup>
</Project>
Then, run:
dotnet pack -c Release
This places a .nupkg
file in nupkg
. Install it as a global tool:
dotnet tool install --global --add-source ./nupkg MyCliUtility
Now you can run mycli
from any terminal. For official distribution, push the .nupkg
file to a NuGet feed.
Step 6: Best Practices
- Clear and Consistent Help Text: Provide meaningful help descriptions, usage examples, and argument details.
- Meaningful Exit Codes: Use zero for success and nonzero codes for different error conditions. This greatly aids scripting or CI/CD pipelines.
- Verbose/Debug Modes: Offer a
--verbose
or--debug
flag for additional logs, especially helpful for troubleshooting. - Subcommands for Complexity: If your tool grows, consider subcommands (e.g.
git commit
,git push
) to group related actions. - Versioning & Changelog: Keep track of changes between releases so users know about new or deprecated functionality.
- Testing & Continuous Integration: Automate tests (including integration tests) against your CLI to ensure reliability and consistency.
Common Gotchas & Debugging Tips
- Forgetting to Set the Project as Executable: If you’re on Linux or macOS, ensure your published file has the execute bit set. You can do this with
chmod +x
. - Multiple RIDs: If you need to support Windows, macOS, and Linux, remember that you’ll have to run
dotnet publish
for each platform specifically, or provide multiple-r
arguments if you’re scripting it. - Inconsistent .NET Versions: Double-check you and your collaborators share the same .NET version to avoid unexpected behaviors or missing features.
- Complex Dependencies in Global Tools:Remember that global tools are generally self-contained applications. If you rely on certain OS-specific features, your tool may not behave identically across platforms.
- Poor Error Handling: Provide clear error messages so users know what went wrong and how to fix it.
- Ignoring ANSI Support: Windows consoles have historically had spotty ANSI escape sequence support. If you plan to use advanced text coloring or cursor moves, test carefully on multiple environments.
Conclusion
Building a CLI tool in C# is simpler than you might anticipate. With .NET’s straightforward tooling and optional advanced libraries, you can deliver a polished and user-friendly command-line experience. Whether you need a quick script for personal productivity or a globally distributed package, .NET’s flexibility can handle it all.
By following best practices like meaningful exit codes, versioning, and thorough testing, you’ll create a reliable and maintainable CLI. Keep the “gotchas” in mind—especially runtime identifiers and cross-platform quirks—to ensure users on all operating systems can benefit.
Good luck building your next great CLI tool!
– Nate