How to Create a Command-Line Utility Using C# / .NET

How to Create a Command-Line Utility Using C# / .NET

.NET
C#
CLI
Tools
2021-11-19

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.

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.

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.

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

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.

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.

  • 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.
  • 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 rundotnet 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.

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