AgentSkillsCN

commandline-cheatsheet

在.NET中使用System.CommandLine构建命令行应用时使用。涵盖命令/选项/参数定义、中间件、Tab补全、解析与主机集成,用于CLI工具。 适用范围:使用类型化参数解析构建CLI应用、创建带有子命令的dotnet工具、配置与设置System.CommandLine、为CLI应用添加Tab补全、解析并验证命令行参数。 不适用范围:执行外部CLI进程(使用cliwrap)、使用spectre-console渲染丰富的终端UI与表格和进度条、构建Web API(使用ASP.NET Core)、使用Console.ReadLine进行简单输入。

SKILL.md
--- frontmatter
name: commandline-cheatsheet
description: |
  Use when building command-line applications in .NET with System.CommandLine. Covers command/option/argument definitions, middleware, tab completion, parsing, and hosting integration for CLI tools.
  USE FOR: building CLI applications with typed argument parsing, creating dotnet tools with subcommands, System.CommandLine setup and configuration, adding tab completion to CLI apps, parsing and validating command-line arguments
  DO NOT USE FOR: executing external CLI processes (use cliwrap), rendering rich terminal UI with tables and progress bars (use spectre-console), building web APIs (use ASP.NET Core), simple Console.ReadLine input
license: MIT
metadata:
  displayName: "Command-Line Cheatsheet"
  author: "Tyler-R-Kendrick"
  version: "1.0.0"
compatibility: claude, copilot, cursor

Command-Line Cheatsheet

Overview

System.CommandLine is the official .NET library for building command-line applications with typed argument parsing, automatic help generation, tab completion, and middleware support. It provides a structured way to define commands, options, and arguments that map directly to handler methods, with built-in validation and error reporting.

NuGet Packages

bash
dotnet add package System.CommandLine
dotnet add package System.CommandLine.Hosting   # Generic Host integration

Minimal CLI Application

csharp
using System.CommandLine;

var nameOption = new Option<string>(
    name: "--name",
    description: "Your name")
{ IsRequired = true };

var greetingOption = new Option<string>(
    name: "--greeting",
    description: "The greeting to use",
    getDefaultValue: () => "Hello");

var rootCommand = new RootCommand("A simple greeting CLI tool");
rootCommand.AddOption(nameOption);
rootCommand.AddOption(greetingOption);

rootCommand.SetHandler((string name, string greeting) =>
{
    Console.WriteLine($"{greeting}, {name}!");
}, nameOption, greetingOption);

return await rootCommand.InvokeAsync(args);
// Usage: myapp --name Alice --greeting "Good morning"

Commands with Subcommands

csharp
using System.CommandLine;

var rootCommand = new RootCommand("Project management CLI");

// 'new' command
var newCommand = new Command("new", "Create a new project");
var templateArg = new Argument<string>("template", "Project template name");
var outputOption = new Option<DirectoryInfo>(
    "--output", "Output directory") { IsRequired = false };

newCommand.AddArgument(templateArg);
newCommand.AddOption(outputOption);

newCommand.SetHandler((string template, DirectoryInfo? output) =>
{
    var dir = output?.FullName ?? Directory.GetCurrentDirectory();
    Console.WriteLine($"Creating '{template}' project in {dir}");
}, templateArg, outputOption);

// 'build' command
var buildCommand = new Command("build", "Build the project");
var configOption = new Option<string>(
    "--configuration", () => "Debug", "Build configuration");
configOption.AddAlias("-c");

var verbosityOption = new Option<Verbosity>(
    "--verbosity", () => Verbosity.Normal, "Output verbosity");
verbosityOption.AddAlias("-v");

buildCommand.AddOption(configOption);
buildCommand.AddOption(verbosityOption);

buildCommand.SetHandler((string config, Verbosity verbosity) =>
{
    Console.WriteLine($"Building with configuration={config}, verbosity={verbosity}");
}, configOption, verbosityOption);

// 'test' command with nested subcommands
var testCommand = new Command("test", "Run tests");
var unitCommand = new Command("unit", "Run unit tests");
var integrationCommand = new Command("integration", "Run integration tests");

var filterOption = new Option<string?>("--filter", "Test filter expression");

unitCommand.AddOption(filterOption);
unitCommand.SetHandler((string? filter) =>
{
    Console.WriteLine($"Running unit tests{(filter != null ? $" with filter: {filter}" : "")}");
}, filterOption);

integrationCommand.SetHandler(() =>
{
    Console.WriteLine("Running integration tests");
});

testCommand.AddCommand(unitCommand);
testCommand.AddCommand(integrationCommand);

rootCommand.AddCommand(newCommand);
rootCommand.AddCommand(buildCommand);
rootCommand.AddCommand(testCommand);

return await rootCommand.InvokeAsync(args);

enum Verbosity { Quiet, Normal, Detailed, Diagnostic }

// Usage:
// myapp new webapp --output ./MyApp
// myapp build -c Release -v Detailed
// myapp test unit --filter "Category=Smoke"
// myapp test integration

Options, Arguments, and Aliases

ConceptDescriptionExample
Option<T>Named parameter with -- prefix--output, -o
Argument<T>Positional parameter (no prefix)myapp <file>
AddAliasShort alias for an option-c for --configuration
IsRequiredMakes an option mandatory{ IsRequired = true }
getDefaultValueFactory for default values() => "Debug"
FromAmongRestricts to allowed valuesFromAmong("Debug", "Release")

Argument Validation

csharp
using System.CommandLine;

var portOption = new Option<int>("--port", "Server port");
portOption.AddValidator(result =>
{
    var port = result.GetValueForOption(portOption);
    if (port < 1024 || port > 65535)
    {
        result.ErrorMessage = "Port must be between 1024 and 65535";
    }
});

var fileArgument = new Argument<FileInfo>("file", "Input file");
fileArgument.AddValidator(result =>
{
    var file = result.GetValueForArgument(fileArgument);
    if (file is not null && !file.Exists)
    {
        result.ErrorMessage = $"File not found: {file.FullName}";
    }
});

// Restrict to specific values
var envOption = new Option<string>("--environment");
envOption.FromAmong("Development", "Staging", "Production");

Global Options

csharp
var verboseOption = new Option<bool>("--verbose", "Enable verbose logging");
verboseOption.AddAlias("-V");

var rootCommand = new RootCommand("My CLI");
rootCommand.AddGlobalOption(verboseOption); // Available to all subcommands

var serveCommand = new Command("serve", "Start the server");
serveCommand.SetHandler((bool verbose) =>
{
    if (verbose) Console.WriteLine("Verbose mode enabled");
    Console.WriteLine("Starting server...");
}, verboseOption);

rootCommand.AddCommand(serveCommand);

// Usage: myapp serve --verbose
// Usage: myapp --verbose serve   (global options work before or after subcommand)

Hosting Integration

Integrate System.CommandLine with the .NET Generic Host for DI, configuration, and logging.

csharp
using System.CommandLine;
using System.CommandLine.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var rootCommand = new RootCommand("CLI with hosting");

var nameOption = new Option<string>("--name", "User name") { IsRequired = true };
rootCommand.AddOption(nameOption);

rootCommand.SetHandler(async (IHost host) =>
{
    var greeter = host.Services.GetRequiredService<GreeterService>();
    var parseResult = host.Services.GetRequiredService<ParseResult>();
    var name = parseResult.GetValueForOption(nameOption)!;
    await greeter.GreetAsync(name);
});

var builder = new CommandLineBuilder(rootCommand)
    .UseHost(hostBuilder =>
    {
        hostBuilder.ConfigureServices(services =>
        {
            services.AddTransient<GreeterService>();
        });
        hostBuilder.ConfigureLogging(logging =>
        {
            logging.AddConsole();
            logging.SetMinimumLevel(LogLevel.Information);
        });
    })
    .UseDefaults();

return await builder.Build().InvokeAsync(args);

public class GreeterService(ILogger<GreeterService> logger)
{
    public Task GreetAsync(string name)
    {
        logger.LogInformation("Greeting user: {Name}", name);
        Console.WriteLine($"Hello, {name}!");
        return Task.CompletedTask;
    }
}

Middleware

csharp
var builder = new CommandLineBuilder(rootCommand)
    .UseDefaults()
    .AddMiddleware(async (context, next) =>
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        Console.WriteLine($"Executing: {context.ParseResult.CommandResult.Command.Name}");

        await next(context);

        sw.Stop();
        Console.WriteLine($"Completed in {sw.ElapsedMilliseconds}ms");
    });

var parser = builder.Build();
return await parser.InvokeAsync(args);

File and Directory Arguments

csharp
var inputFile = new Argument<FileInfo>("input", "Input file path");
var outputDir = new Option<DirectoryInfo>(
    "--output-dir", () => new DirectoryInfo("."), "Output directory");

var convertCommand = new Command("convert", "Convert a file");
convertCommand.AddArgument(inputFile);
convertCommand.AddOption(outputDir);

convertCommand.SetHandler((FileInfo input, DirectoryInfo outDir) =>
{
    if (!input.Exists)
    {
        Console.Error.WriteLine($"File not found: {input.FullName}");
        return;
    }
    Console.WriteLine($"Converting {input.Name} to {outDir.FullName}");
}, inputFile, outputDir);

Creating a dotnet Tool

xml
<!-- .csproj file -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>mytool</ToolCommandName>
    <PackageId>MyOrg.MyTool</PackageId>
    <Version>1.0.0</Version>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.*" />
  </ItemGroup>
</Project>
bash
# Pack and install locally
dotnet pack
dotnet tool install --global --add-source ./nupkg MyOrg.MyTool

# Usage
mytool build -c Release
mytool test unit --filter "Priority=1"

Best Practices

  • Use RootCommand with descriptive text as the entry point and organize functionality into Command subcommands rather than overloading the root with many options.
  • Add both long (--configuration) and short (-c) aliases for frequently used options to reduce typing in interactive use while keeping discoverability for scripts.
  • Use Option<T> with strongly-typed generics (Option<int>, Option<FileInfo>, Option<Verbosity>) instead of Option<string> with manual parsing to get automatic validation and help text.
  • Set IsRequired = true on mandatory options and provide getDefaultValue factories on optional ones so the help text accurately reflects what is required vs. optional.
  • Use AddValidator for custom validation rules (port ranges, file existence, format checks) to provide clear error messages before the handler runs.
  • Use FromAmong to restrict string options to a fixed set of allowed values (e.g., environments, log levels) with automatic validation and tab completion.
  • Integrate with System.CommandLine.Hosting for applications that need dependency injection, IConfiguration, or ILogger rather than constructing services manually in handlers.
  • Add AddGlobalOption for cross-cutting options like --verbose, --output-format, or --no-color that apply to all subcommands without repeating them.
  • Set <PackAsTool>true</PackAsTool> in the .csproj to distribute the CLI as a dotnet tool that users install with dotnet tool install.
  • Use CommandLineBuilder.UseDefaults() to enable automatic help (--help), version (--version), parse error reporting, and suggest-typo features without manual configuration.