C#命令行参数解析:从基础到高级实践指南


命令行参数是应用程序与用户交互的重要方式之一,它允许用户在启动程序时配置其行为。本文将全面介绍C#中解析命令行参数的各种方法,从基础API到现代高级库的使用。

一、命令行参数基础

1.1 命令行参数格式

常见的命令行参数风格:

风格类型示例特点
POSIX风格-v --input file.txtUnix/Linux系统常用
Windows风格/v /input:file.txt传统Windows程序常用
简单风格file.txt /s 100无前缀,顺序敏感

1.2 Main方法接收参数

static void Main(string[] args)
{
    // args数组包含所有命令行参数
    foreach (var arg in args)
    {
        Console.WriteLine(arg);
    }
}

示例调用:

dotnet run -- input.txt --output=result.txt -v

二、基础解析方法

2.1 手动解析简单参数

static void Main(string[] args)
{
    string inputFile = null;
    string outputFile = null;
    bool verbose = false;

    for (int i = 0; i < args.Length; i++)
    {
        switch (args[i])
        {
            case "-i":
            case "--input":
                inputFile = args[++i];
                break;
            case "-o":
            case "--output":
                outputFile = args[++i];
                break;
            case "-v":
            case "--verbose":
                verbose = true;
                break;
            default:
                Console.WriteLine($"未知参数: {args[i]}");
                break;
        }
    }

    Console.WriteLine($"输入文件: {inputFile}");
    Console.WriteLine($"输出文件: {outputFile}");
    Console.WriteLine($"详细模式: {verbose}");
}

2.2 环境命令行获取

// 获取完整命令行(包括程序路径)
string fullCommandLine = Environment.CommandLine;

// 在Windows服务等环境中获取参数
string[] args = Environment.GetCommandLineArgs();

三、使用System.CommandLine(.NET 7+官方库)

3.1 基本配置

using System.CommandLine;

var rootCommand = new RootCommand("示例应用程序");

var inputOption = new Option<FileInfo>(
    name: "--input",
    description: "输入文件路径")
{
    IsRequired = true
};
inputOption.AddAlias("-i");

var outputOption = new Option<FileInfo>(
    name: "--output",
    description: "输出文件路径");
outputOption.AddAlias("-o");

var verboseOption = new Option<bool>(
    name: "--verbose",
    description: "启用详细输出");
verboseOption.AddAlias("-v");

rootCommand.AddOption(inputOption);
rootCommand.AddOption(outputOption);
rootCommand.AddOption(verboseOption);

rootCommand.SetHandler((input, output, verbose) =>
{
    Console.WriteLine($"处理文件: {input?.FullName}");
    if (output != null)
        Console.WriteLine($"输出到: {output.FullName}");
    if (verbose)
        Console.WriteLine("详细模式已启用");
}, inputOption, outputOption, verboseOption);

return rootCommand.Invoke(args);

3.2 子命令支持

var cloneCommand = new Command("clone", "克隆仓库")
{
    new Argument<string>("url", "仓库URL"),
    new Option<string>("--branch", "指定分支")
};
cloneCommand.SetHandler((url, branch) => 
{
    Console.WriteLine($"克隆 {url} {(branch != null ? $"分支 {branch}" : "")}");
}, 
cloneCommand.Arguments[0], cloneCommand.Options[0]);

var rootCommand = new RootCommand();
rootCommand.AddCommand(cloneCommand);

return rootCommand.Invoke(args);

四、流行第三方库解析

4.1 CommandLineParser

dotnet add package CommandLineParser
using CommandLine;

class Options
{
    [Option('i', "input", Required = true, HelpText = "输入文件路径")]
    public string InputFile { get; set; }

    [Option('o', "output", HelpText = "输出文件路径")]
    public string OutputFile { get; set; }

    [Option('v', "verbose", HelpText = "详细输出")]
    public bool Verbose { get; set; }
}

static void Main(string[] args)
{
    Parser.Default.ParseArguments<Options>(args)
        .WithParsed(options => 
        {
            Console.WriteLine($"输入: {options.InputFile}");
            if (!string.IsNullOrEmpty(options.OutputFile))
                Console.WriteLine($"输出: {options.OutputFile}");
            if (options.Verbose)
                Console.WriteLine("详细模式");
        })
        .WithNotParsed(errors => 
        {
            Console.WriteLine("参数解析错误");
        });
}

4.2 McMaster.Extensions.CommandLineUtils

dotnet add package McMaster.Extensions.CommandLineUtils
using McMaster.Extensions.CommandLineUtils;

[Command(Name = "myapp", Description = "文件处理工具")]
[HelpOption("--help")]
class Program
{
    [Argument(0, Description = "输入文件")]
    [FileExists]
    public string InputFile { get; }

    [Option("-o|--output", Description = "输出文件")]
    public string OutputFile { get; }

    [Option("-v|--verbose", Description = "详细输出")]
    public bool Verbose { get; }

    public static int Main(string[] args)
        => CommandLineApplication.Execute<Program>(args);

    private void OnExecute()
    {
        if (Verbose)
            Console.WriteLine($"处理 {InputFile}...");

        // 处理逻辑

        if (!string.IsNullOrEmpty(OutputFile))
        {
            if (Verbose)
                Console.WriteLine($"写入 {OutputFile}");
            // 写入输出
        }
    }
}

五、高级解析场景

5.1 数组参数

// System.CommandLine方式
var filesOption = new Option<FileInfo[]>(
    name: "--files",
    description: "要处理的文件列表")
{
    AllowMultipleArgumentsPerToken = true
};

// CommandLineParser方式
[Option('f', "files", HelpText = "文件列表")]
public IEnumerable<string> Files { get; set; }

5.2 枚举参数

// 定义枚举
enum LogLevel { Quiet, Normal, Verbose }

// System.CommandLine
var logLevelOption = new Option<LogLevel>(
    name: "--log-level",
    description: "日志级别");

// CommandLineParser
[Option('l', "log-level", Default = LogLevel.Normal)]
public LogLevel Level { get; set; }

5.3 交互式提示

// McMaster.Extensions.CommandLineUtils
[Option]
[Prompt("请输入用户名")]
public string Username { get; set; }

[Option]
[Prompt("请输入密码", PromptSecret = true)]
public string Password { get; set; }

六、验证与错误处理

6.1 参数验证

// System.CommandLine验证
var portOption = new Option<int>("--port")
{
    IsRequired = true
};
portOption.AddValidator(result =>
{
    var port = result.GetValueForOption(portOption);
    if (port < 1 || port > 65535)
        result.ErrorMessage = "端口必须在1-65535之间";
});

// CommandLineParser验证
[Option('p', "port", Required = true)]
[Range(1, 65535, ErrorMessage = "端口无效")]
public int Port { get; set; }

6.2 自定义错误处理

Parser.Default.ParseArguments<Options>(args)
    .WithNotParsed(errors => 
    {
        if (errors.IsVersion())
        {
            Console.WriteLine("版本信息");
            return;
        }

        if (errors.IsHelp())
        {
            DisplayHelp();
            return;
        }

        foreach (var error in errors)
        {
            switch (error)
            {
                case MissingRequiredOptionError missing:
                    Console.WriteLine($"缺少必要选项: {missing.NameInfo.NameText}");
                    break;
                case UnknownOptionError unknown:
                    Console.WriteLine($"未知选项: {unknown.Token}");
                    break;
                default:
                    Console.WriteLine($"参数错误: {error.Tag}");
                    break;
            }
        }
    });

七、帮助系统与文档生成

7.1 自动帮助生成

// System.CommandLine自动支持
// 用户输入 --help 或 -? 时显示帮助

// McMaster.Extensions.CommandLineUtils
[HelpOption("--help")]
class Program
{
    // ...
}

7.2 自定义帮助文本

var rootCommand = new RootCommand
{
    Description = "文件处理工具 v1.0\nCopyright (c) 2023"
};

// 添加自定义帮助选项
var helpOption = new Option<bool>("--help", "显示帮助信息");
rootCommand.AddOption(helpOption);

rootCommand.SetHandler((help) => 
{
    if (help)
        Console.WriteLine("自定义帮助文本...");
}, helpOption);

八、性能优化

8.1 延迟解析

// 只在需要时解析
private static Lazy<Options> _options = new Lazy<Options>(() => 
    Parser.Default.ParseArguments<Options>(args).Value);

public static Options Config => _options.Value;

8.2 缓存解析结果

private static Options _cachedOptions;
public static Options GetOptions(string[] args)
{
    if (_cachedOptions == null)
    {
        _cachedOptions = Parser.Default.ParseArguments<Options>(args).Value;
    }
    return _cachedOptions;
}

九、跨平台注意事项

9.1 路径参数处理

// 统一路径格式
string inputPath = args[0].Replace('\\', Path.DirectorySeparatorChar);

// 使用Path组合
string fullPath = Path.Combine(
    Environment.CurrentDirectory, 
    args[0].TrimStart('.', '/', '\\'));

9.2 大小写敏感

// Linux/macOS下区分大小写
bool verbose = args.Any(a => 
    a.Equals("-v", StringComparison.OrdinalIgnoreCase) || 
    a.Equals("--verbose", StringComparison.OrdinalIgnoreCase));

十、测试策略

10.1 单元测试参数解析

[TestClass]
public class ArgumentTests
{
    [TestMethod]
    public void TestBasicArguments()
    {
        var args = new[] { "--input", "test.txt", "--verbose" };
        var result = Parser.Default.ParseArguments<Options>(args);
        Assert.IsFalse(result.Errors.Any());
        Assert.AreEqual("test.txt", result.Value.InputFile);
        Assert.IsTrue(result.Value.Verbose);
    }

    [TestMethod]
    public void TestMissingRequiredArgument()
    {
        var args = new[] { "--verbose" };
        var result = Parser.Default.ParseArguments<Options>(args);
        Assert.IsTrue(result.Errors.Any(e => e is MissingRequiredOptionError));
    }
}

10.2 模拟命令行

[TestMethod]
public void TestCommandLineApp()
{
    using var output = new StringWriter();
    Console.SetOut(output);

    var args = new[] { "clone", "https://github.com/user/repo", "--branch", "main" };
    Program.Main(args);

    StringAssert.Contains(output.ToString(), "克隆 https://github.com/user/repo 分支 main");
}

十一、最佳实践总结

  1. 选择合适库
  • 简单应用:System.CommandLine或内置解析
  • 复杂CLI:CommandLineParser或McMaster.Extensions
  1. 良好文档
  • 为所有参数提供清晰的帮助文本
  • 包含使用示例
  1. 输入验证
  • 尽早验证参数有效性
  • 提供有意义的错误信息
  1. 一致性
  • 保持参数命名风格一致
  • 遵循平台惯例(POSIX/Windows)
  1. 用户友好
  • 提供合理的默认值
  • 支持–help和–version等标准选项

十二、进阶资源

  1. 官方文档
  1. 设计指南
  1. 示例项目

通过本文的介绍,您应该能够根据项目需求选择合适的命令行参数解析方法,并实现健壮、用户友好的命令行界面。良好的命令行设计可以显著提升应用程序的易用性和专业性。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注