CommandLineParser

A mature command-line argument parser for C#. Features an attribute-based declarative approach.

csharpclicommand-linedotnet

Framework

CommandLineParser

Overview

CommandLineParser is a mature command-line argument parser for C#. It features an attribute-based declarative approach and maintains stable popularity over the years, being trusted in many .NET projects. With rich functionality and an intuitive API, it can handle everything from simple CLI tools to complex applications.

Details

CommandLineParser is an open-source library available for .NET Framework, .NET Core, and .NET 5+. It defines option classes using attributes and automatically parses command-line arguments to map them to objects. It provides rich customization options and comprehensive error handling to support professional CLI application development.

Key Features

  • Attribute-Based Design: Declarative option definition using attributes
  • Automatic Mapping: Automatically maps command-line arguments to object properties
  • Rich Type Support: Supports primitive types, enums, collections, and custom types
  • Validation: Built-in and custom validation functionality
  • Automatic Help Generation: Automatically generates beautifully organized help text
  • Internationalization: Supports localization features
  • Mutually Exclusive Options: Mutual exclusion constraints between options
  • Dynamic Options: Ability to add options dynamically at runtime

Pros and Cons

Pros

  • Declarative Approach: Intuitive option definition using attributes
  • Type Safety: Safe argument processing through strong typing
  • Rich Features: Validation, mutual exclusion, customization, etc.
  • Mature Library: Long track record and stability
  • Excellent Documentation: Comprehensive official documentation and samples
  • Active Community: Continuous development and support
  • NuGet Support: Easy package management

Cons

  • Learning Curve: Understanding the attribute system is required
  • Reflection Usage: Performance overhead from runtime reflection
  • Complex Configuration: Complexity of configuration when using advanced features
  • Dependencies: Dependency on external libraries

Key Links

Usage Examples

Basic Usage

using CommandLine;
using System;

public class Options
{
    [Option('v', "verbose", Required = false, HelpText = "Enable verbose output")]
    public bool Verbose { get; set; }

    [Option('f', "file", Required = true, HelpText = "File name to process")]
    public string FileName { get; set; }

    [Option('c', "count", Required = false, Default = 1, HelpText = "Number of times to process")]
    public int Count { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        Parser.Default.ParseArguments<Options>(args)
            .WithParsed<Options>(opts => RunOptions(opts))
            .WithNotParsed<Options>((errs) => HandleParseError(errs));
    }

    static void RunOptions(Options opts)
    {
        Console.WriteLine($"File name: {opts.FileName}");
        Console.WriteLine($"Process count: {opts.Count}");
        
        if (opts.Verbose)
        {
            Console.WriteLine("Verbose mode enabled");
        }

        // Implement main processing here
        for (int i = 0; i < opts.Count; i++)
        {
            Console.WriteLine($"Processing ({i + 1}/{opts.Count}): {opts.FileName}");
        }
    }

    static void HandleParseError(IEnumerable<Error> errs)
    {
        foreach (var error in errs)
        {
            Console.WriteLine($"Error: {error}");
        }
    }
}

Multiple Subcommands

using CommandLine;
using System;
using System.Collections.Generic;
using System.Linq;

[Verb("add", HelpText = "Add files")]
public class AddOptions
{
    [Option('f', "files", Required = true, HelpText = "Files to add")]
    public IEnumerable<string> Files { get; set; }

    [Option('r', "recursive", Required = false, HelpText = "Add recursively")]
    public bool Recursive { get; set; }

    [Option('v', "verbose", Required = false, HelpText = "Verbose output")]
    public bool Verbose { get; set; }
}

[Verb("remove", HelpText = "Remove files")]
public class RemoveOptions
{
    [Option('f', "files", Required = true, HelpText = "Files to remove")]
    public IEnumerable<string> Files { get; set; }

    [Option("force", Required = false, HelpText = "Force removal")]
    public bool Force { get; set; }

    [Option('v', "verbose", Required = false, HelpText = "Verbose output")]
    public bool Verbose { get; set; }
}

[Verb("list", HelpText = "List files")]
public class ListOptions
{
    [Option('p', "path", Required = false, Default = ".", HelpText = "Search path")]
    public string Path { get; set; }

    [Option('a', "all", Required = false, HelpText = "Show hidden files")]
    public bool ShowHidden { get; set; }

    [Option('l', "long", Required = false, HelpText = "Long format display")]
    public bool LongFormat { get; set; }
}

class Program
{
    static int Main(string[] args)
    {
        return Parser.Default.ParseArguments<AddOptions, RemoveOptions, ListOptions>(args)
            .MapResult(
                (AddOptions opts) => RunAddCommand(opts),
                (RemoveOptions opts) => RunRemoveCommand(opts),
                (ListOptions opts) => RunListCommand(opts),
                errs => 1);
    }

    static int RunAddCommand(AddOptions options)
    {
        Console.WriteLine("Running add command...");
        
        foreach (var file in options.Files)
        {
            if (options.Verbose)
            {
                Console.WriteLine($"Adding: {file}");
            }
        }

        if (options.Recursive)
        {
            Console.WriteLine("Recursive processing enabled");
        }

        return 0;
    }

    static int RunRemoveCommand(RemoveOptions options)
    {
        Console.WriteLine("Running remove command...");
        
        foreach (var file in options.Files)
        {
            if (options.Verbose)
            {
                Console.WriteLine($"Removing: {file}");
            }
        }

        if (options.Force)
        {
            Console.WriteLine("Force removal mode");
        }

        return 0;
    }

    static int RunListCommand(ListOptions options)
    {
        Console.WriteLine($"Listing files: {options.Path}");
        
        if (options.ShowHidden)
        {
            Console.WriteLine("Showing hidden files");
        }

        if (options.LongFormat)
        {
            Console.WriteLine("Long format display");
        }

        return 0;
    }
}

Advanced Attributes and Validation

using CommandLine;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

public class AdvancedOptions
{
    [Option('i', "input", Required = true, HelpText = "Input file path")]
    [FileExists]
    public string InputFile { get; set; }

    [Option('o', "output", Required = false, HelpText = "Output file path")]
    public string OutputFile { get; set; }

    [Option('p', "port", Required = false, Default = 8080, HelpText = "Port number (1024-65535)")]
    [Range(1024, 65535)]
    public int Port { get; set; }

    [Option('e', "email", Required = false, HelpText = "Email address")]
    [EmailAddress]
    public string Email { get; set; }

    [Option('t', "tags", Required = false, HelpText = "Tag list (comma-separated)")]
    public IEnumerable<string> Tags { get; set; }

    [Option("format", Required = false, Default = OutputFormat.Json, HelpText = "Output format")]
    public OutputFormat Format { get; set; }

    [Option("config", Required = false, HelpText = "Configuration file path")]
    public string ConfigFile { get; set; }

    // Mutually exclusive options
    [Option('q', "quiet", Required = false, HelpText = "Quiet mode", Group = "verbosity")]
    public bool Quiet { get; set; }

    [Option('v', "verbose", Required = false, HelpText = "Verbose mode", Group = "verbosity")]
    public bool Verbose { get; set; }
}

public enum OutputFormat
{
    Json,
    Xml,
    Csv,
    Text
}

// Custom validation attributes
public class FileExistsAttribute : Attribute
{
    // Validation logic implemented separately
}

public class RangeAttribute : Attribute
{
    public int Min { get; }
    public int Max { get; }

    public RangeAttribute(int min, int max)
    {
        Min = min;
        Max = max;
    }
}

public class EmailAddressAttribute : Attribute
{
    // Validation logic implemented separately
}

class Program
{
    static int Main(string[] args)
    {
        var parser = new Parser(with => with.HelpWriter = null);
        var parserResult = parser.ParseArguments<AdvancedOptions>(args);

        return parserResult
            .MapResult(
                (AdvancedOptions opts) => RunApplication(opts),
                errs => DisplayHelp(parserResult, errs));
    }

    static int RunApplication(AdvancedOptions options)
    {
        try
        {
            // Custom validation
            ValidateOptions(options);

            Console.WriteLine("Application configuration:");
            Console.WriteLine($"  Input file: {options.InputFile}");
            Console.WriteLine($"  Output file: {options.OutputFile ?? "standard output"}");
            Console.WriteLine($"  Port: {options.Port}");
            Console.WriteLine($"  Output format: {options.Format}");

            if (!string.IsNullOrEmpty(options.Email))
            {
                Console.WriteLine($"  Email: {options.Email}");
            }

            if (options.Tags?.Any() == true)
            {
                Console.WriteLine($"  Tags: {string.Join(", ", options.Tags)}");
            }

            if (options.Verbose)
            {
                Console.WriteLine("Verbose mode enabled");
            }
            else if (options.Quiet)
            {
                Console.WriteLine("Quiet mode enabled");
            }

            // Main processing
            ProcessFiles(options);

            return 0;
        }
        catch (ValidationException ex)
        {
            Console.WriteLine($"Validation error: {ex.Message}");
            return 1;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
            return 1;
        }
    }

    static void ValidateOptions(AdvancedOptions options)
    {
        // File existence check
        if (!File.Exists(options.InputFile))
        {
            throw new ValidationException($"Input file does not exist: {options.InputFile}");
        }

        // Email address validation
        if (!string.IsNullOrEmpty(options.Email))
        {
            var emailPattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
            if (!Regex.IsMatch(options.Email, emailPattern))
            {
                throw new ValidationException("Please enter a valid email address");
            }
        }

        // Port number validation
        if (options.Port < 1024 || options.Port > 65535)
        {
            throw new ValidationException("Port number must be in range 1024-65535");
        }

        // Mutual exclusion check
        if (options.Quiet && options.Verbose)
        {
            throw new ValidationException("--quiet and --verbose cannot be specified simultaneously");
        }
    }

    static void ProcessFiles(AdvancedOptions options)
    {
        Console.WriteLine("Starting file processing...");
        
        // Implement actual processing logic here
        var inputContent = File.ReadAllText(options.InputFile);
        
        if (!options.Quiet)
        {
            Console.WriteLine($"Input data size: {inputContent.Length} characters");
        }

        // Output processing
        string output = ProcessContent(inputContent, options.Format);
        
        if (!string.IsNullOrEmpty(options.OutputFile))
        {
            File.WriteAllText(options.OutputFile, output);
            if (!options.Quiet)
            {
                Console.WriteLine($"Results saved to {options.OutputFile}");
            }
        }
        else
        {
            Console.WriteLine(output);
        }
    }

    static string ProcessContent(string content, OutputFormat format)
    {
        // Processing according to format
        return format switch
        {
            OutputFormat.Json => $"{{\"content\": \"{content}\"}}",
            OutputFormat.Xml => $"<content>{content}</content>",
            OutputFormat.Csv => $"content\n\"{content}\"",
            OutputFormat.Text => content,
            _ => content
        };
    }

    static int DisplayHelp<T>(ParserResult<T> result, IEnumerable<Error> errs)
    {
        var helpText = CommandLine.Text.HelpText.AutoBuild(result, h =>
        {
            h.AdditionalNewLineAfterOption = false;
            h.Heading = "Advanced CLI Application v1.0.0";
            h.Copyright = "Copyright (C) 2024 Example Corp.";
            return CommandLine.Text.HelpText.DefaultParsingErrorsHandler(result, h);
        }, e => e);

        Console.WriteLine(helpText);
        return 1;
    }
}

public class ValidationException : Exception
{
    public ValidationException(string message) : base(message) { }
}

Configuration File Integration

using CommandLine;
using System;
using System.IO;
using System.Text.Json;

public class ConfigurableOptions
{
    [Option('c', "config", Required = false, HelpText = "Configuration file path")]
    public string ConfigFile { get; set; }

    [Option('h', "host", Required = false, HelpText = "Host name")]
    public string Host { get; set; }

    [Option('p', "port", Required = false, HelpText = "Port number")]
    public int? Port { get; set; }

    [Option('d', "database", Required = false, HelpText = "Database connection string")]
    public string DatabaseConnection { get; set; }

    [Option('v', "verbose", Required = false, HelpText = "Verbose logging")]
    public bool Verbose { get; set; }
}

public class ConfigFile
{
    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 8080;
    public string DatabaseConnection { get; set; }
    public bool Verbose { get; set; }
}

class Program
{
    static int Main(string[] args)
    {
        return Parser.Default.ParseArguments<ConfigurableOptions>(args)
            .MapResult(
                (ConfigurableOptions opts) => RunWithConfig(opts),
                errs => 1);
    }

    static int RunWithConfig(ConfigurableOptions options)
    {
        try
        {
            // Load configuration file
            var config = LoadConfig(options.ConfigFile);

            // Override config file values with command-line arguments
            var finalConfig = MergeConfig(config, options);

            Console.WriteLine("Final configuration:");
            Console.WriteLine($"  Host: {finalConfig.Host}");
            Console.WriteLine($"  Port: {finalConfig.Port}");
            Console.WriteLine($"  Database: {finalConfig.DatabaseConnection ?? "not configured"}");
            Console.WriteLine($"  Verbose logging: {(finalConfig.Verbose ? "enabled" : "disabled")}");

            // Start application
            return StartApplication(finalConfig);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
            return 1;
        }
    }

    static ConfigFile LoadConfig(string configPath)
    {
        if (string.IsNullOrEmpty(configPath))
        {
            configPath = "appsettings.json";
        }

        if (File.Exists(configPath))
        {
            Console.WriteLine($"Loading configuration file: {configPath}");
            var json = File.ReadAllText(configPath);
            return JsonSerializer.Deserialize<ConfigFile>(json);
        }
        else
        {
            Console.WriteLine("Configuration file not found. Using default settings.");
            return new ConfigFile();
        }
    }

    static ConfigFile MergeConfig(ConfigFile fileConfig, ConfigurableOptions cliOptions)
    {
        return new ConfigFile
        {
            Host = cliOptions.Host ?? fileConfig.Host,
            Port = cliOptions.Port ?? fileConfig.Port,
            DatabaseConnection = cliOptions.DatabaseConnection ?? fileConfig.DatabaseConnection,
            Verbose = cliOptions.Verbose || fileConfig.Verbose
        };
    }

    static int StartApplication(ConfigFile config)
    {
        Console.WriteLine("Starting application...");
        
        // Actual application logic
        if (config.Verbose)
        {
            Console.WriteLine("Running in verbose log mode");
        }

        return 0;
    }
}

Project File Example

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="CommandLineParser" Version="2.9.1" />
    <PackageReference Include="System.Text.Json" Version="6.0.0" />
  </ItemGroup>

</Project>