McMaster.Extensions.CommandLineUtils
A command-line parsing and utilities library for .NET Core/Standard.
Framework
McMaster.Extensions.CommandLineUtils
Overview
McMaster.Extensions.CommandLineUtils is a command-line parsing and utilities library for .NET Core/Standard. Developed by ASP.NET Core team members with excellent integration in the .NET Core ecosystem. It's a library that balances ease of use in modern .NET applications with rich functionality.
Details
This library is designed with emphasis on integration with .NET Core's dependency injection, configuration system, and logging features. It supports both attribute-based and fluent API approaches, allowing developers to choose according to their preferences. It's an optimal choice especially when you want to fully utilize .NET Core features.
Key Features
- .NET Core Integration: Deep integration with dependency injection, configuration, and logging
- Flexible API: Supports both attribute-based and fluent APIs
- Subcommands: Supports hierarchical command structures
- Validation: Rich validation functionality
- Prompts: Interactive user input prompts
- Automatic Help Generation: Automatic generation of beautiful help text
- Plugin Architecture: Extensible design
- Async Support: Full support for async/await patterns
Pros and Cons
Pros
- .NET Core Affinity: Excellent integration with .NET Core features
- Flexible Design: Supports various development styles
- Modern API: Utilizes latest .NET features
- Comprehensive Documentation: Detailed official documentation
- Active Development: Continuous improvements and updates
- Dependency Injection Support: Easy integration with DI containers
- Testable: Design that's easy to unit test
Cons
- .NET Core Dependency: Limited in .NET Framework environments
- Learning Curve: Takes time to master due to rich features
- Relatively New: Less mature compared to other libraries
- Scattered Documentation: Information is scattered due to diverse features
Key Links
Usage Examples
Basic Attribute-Based Example
using McMaster.Extensions.CommandLineUtils;
using System;
using System.ComponentModel.DataAnnotations;
[Command(Name = "myapp", Description = "Sample application")]
public class Program
{
[Option("-n|--name", Description = "Your name")]
[Required]
public string Name { get; set; }
[Option("-a|--age", Description = "Age")]
[Range(0, 150)]
public int Age { get; set; } = 0;
[Option("-v|--verbose", Description = "Verbose output")]
public bool Verbose { get; set; }
[Argument(0, Description = "Files to process")]
public string[] Files { get; set; }
static int Main(string[] args)
=> CommandLineApplication.Execute<Program>(args);
private int OnExecute()
{
Console.WriteLine($"Hello, {Name}!");
if (Age > 0)
{
Console.WriteLine($"Age: {Age}");
}
if (Verbose)
{
Console.WriteLine("Verbose mode enabled");
}
if (Files?.Length > 0)
{
Console.WriteLine("Files to process:");
foreach (var file in Files)
{
Console.WriteLine($" - {file}");
}
}
return 0;
}
}
Fluent API Example
using McMaster.Extensions.CommandLineUtils;
using System;
class Program
{
static int Main(string[] args)
{
var app = new CommandLineApplication
{
Name = "filemanager",
Description = "File management tool"
};
app.HelpOption();
var nameOption = app.Option("-n|--name <NAME>", "Username", CommandOptionType.SingleValue)
.IsRequired();
var verboseOption = app.Option("-v|--verbose", "Verbose output", CommandOptionType.NoValue);
var filesArgument = app.Argument("files", "Files to process", multipleValues: true);
app.OnExecute(() =>
{
Console.WriteLine($"User: {nameOption.Value()}");
if (verboseOption.HasValue())
{
Console.WriteLine("Verbose mode enabled");
}
if (filesArgument.Values.Count > 0)
{
Console.WriteLine("File list:");
foreach (var file in filesArgument.Values)
{
Console.WriteLine($" - {file}");
}
}
return 0;
});
return app.Execute(args);
}
}
Subcommands Example
using McMaster.Extensions.CommandLineUtils;
using System;
using System.ComponentModel.DataAnnotations;
[Command(Name = "git-tool", Description = "Git operations tool")]
[Subcommand(typeof(CloneCommand))]
[Subcommand(typeof(PushCommand))]
[Subcommand(typeof(StatusCommand))]
public class GitToolProgram
{
static int Main(string[] args)
=> CommandLineApplication.Execute<GitToolProgram>(args);
private int OnExecute(CommandLineApplication app)
{
app.ShowHelp();
return 1;
}
}
[Command(Name = "clone", Description = "Clone a repository")]
public class CloneCommand
{
[Argument(0, Description = "Repository URL")]
[Required]
public string Repository { get; set; }
[Option("--depth <DEPTH>", Description = "History depth")]
public int? Depth { get; set; }
[Option("-b|--branch <BRANCH>", Description = "Branch name")]
public string Branch { get; set; }
[Option("-v|--verbose", Description = "Verbose output")]
public bool Verbose { get; set; }
private int OnExecute()
{
Console.WriteLine($"Cloning repository: {Repository}");
if (!string.IsNullOrEmpty(Branch))
{
Console.WriteLine($"Branch: {Branch}");
}
if (Depth.HasValue)
{
Console.WriteLine($"History depth: {Depth.Value}");
}
if (Verbose)
{
Console.WriteLine("Running in verbose mode...");
}
// Implement actual clone logic here
return 0;
}
}
[Command(Name = "push", Description = "Push changes")]
public class PushCommand
{
[Argument(0, Description = "Remote name")]
public string Remote { get; set; } = "origin";
[Argument(1, Description = "Branch name")]
public string Branch { get; set; } = "main";
[Option("-f|--force", Description = "Force push")]
public bool Force { get; set; }
[Option("-u|--set-upstream", Description = "Set upstream")]
public bool SetUpstream { get; set; }
private int OnExecute()
{
Console.WriteLine($"Pushing: {Remote}/{Branch}");
if (Force)
{
Console.WriteLine("Force push enabled");
}
if (SetUpstream)
{
Console.WriteLine("Setting upstream");
}
// Implement actual push logic here
return 0;
}
}
[Command(Name = "status", Description = "Show repository status")]
public class StatusCommand
{
[Option("-s|--short", Description = "Short format")]
public bool Short { get; set; }
[Option("--porcelain", Description = "Script-friendly output")]
public bool Porcelain { get; set; }
private int OnExecute()
{
Console.WriteLine("Repository status:");
if (Short)
{
Console.WriteLine("M modified_file.txt");
Console.WriteLine("A new_file.txt");
Console.WriteLine("?? untracked_file.txt");
}
else if (Porcelain)
{
Console.WriteLine("M modified_file.txt");
Console.WriteLine("A new_file.txt");
Console.WriteLine("?? untracked_file.txt");
}
else
{
Console.WriteLine("On branch main");
Console.WriteLine("Changes to be committed:");
Console.WriteLine(" (use \"git reset HEAD <file>...\" to unstage)");
Console.WriteLine();
Console.WriteLine("\tnew file: new_file.txt");
Console.WriteLine();
Console.WriteLine("Changes not staged for commit:");
Console.WriteLine(" (use \"git add <file>...\" to update what will be committed)");
Console.WriteLine();
Console.WriteLine("\tmodified: modified_file.txt");
Console.WriteLine();
Console.WriteLine("Untracked files:");
Console.WriteLine(" (use \"git add <file>...\" to include in what will be committed)");
Console.WriteLine();
Console.WriteLine("\tuntracked_file.txt");
}
return 0;
}
}
Dependency Injection Integration
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
// Service interface
public interface IDataService
{
Task ProcessDataAsync(string[] files);
}
public class DataService : IDataService
{
private readonly ILogger<DataService> _logger;
public DataService(ILogger<DataService> logger)
{
_logger = logger;
}
public async Task ProcessDataAsync(string[] files)
{
_logger.LogInformation("Starting data processing");
foreach (var file in files)
{
_logger.LogInformation($"Processing: {file}");
await Task.Delay(500); // Simulate processing
}
_logger.LogInformation("Data processing completed");
}
}
[Command(Name = "processor", Description = "Data processing application")]
public class ProcessorCommand
{
private readonly IDataService _dataService;
private readonly ILogger<ProcessorCommand> _logger;
public ProcessorCommand(IDataService dataService, ILogger<ProcessorCommand> logger)
{
_dataService = dataService;
_logger = logger;
}
[Argument(0, Description = "Files to process")]
public string[] Files { get; set; }
[Option("-p|--parallel", Description = "Parallel processing")]
public bool Parallel { get; set; }
[Option("-v|--verbose", Description = "Verbose logging")]
public bool Verbose { get; set; }
private async Task<int> OnExecuteAsync()
{
if (Files?.Length == 0)
{
_logger.LogError("No files specified for processing");
return 1;
}
_logger.LogInformation($"Processing mode: {(Parallel ? "parallel" : "sequential")}");
try
{
await _dataService.ProcessDataAsync(Files);
return 0;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred during processing");
return 1;
}
}
}
class Program
{
static async Task<int> Main(string[] args)
{
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddScoped<IDataService, DataService>();
})
.ConfigureLogging((context, logging) =>
{
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Information);
})
.Build();
return await host.Services
.GetRequiredService<CommandLineApplication<ProcessorCommand>>()
.ExecuteAsync(args);
}
}
Prompts and Interactive Input
using McMaster.Extensions.CommandLineUtils;
using System;
using System.ComponentModel.DataAnnotations;
[Command(Name = "setup", Description = "Application setup tool")]
public class SetupCommand
{
[Option("-i|--interactive", Description = "Interactive mode")]
public bool Interactive { get; set; }
[Option("--name <NAME>", Description = "Application name")]
public string AppName { get; set; }
[Option("--port <PORT>", Description = "Port number")]
public int? Port { get; set; }
[Option("--database <URL>", Description = "Database URL")]
public string DatabaseUrl { get; set; }
static int Main(string[] args)
=> CommandLineApplication.Execute<SetupCommand>(args);
private int OnExecute(IConsole console)
{
if (Interactive)
{
return RunInteractiveSetup(console);
}
else
{
return RunNonInteractiveSetup(console);
}
}
private int RunInteractiveSetup(IConsole console)
{
console.WriteLine("Starting interactive setup...");
console.WriteLine();
// Application name input
if (string.IsNullOrEmpty(AppName))
{
AppName = Prompt.GetString("Enter application name:", defaultValue: "MyApp");
}
// Port number input
if (!Port.HasValue)
{
Port = Prompt.GetInt("Enter port number:", defaultValue: 8080);
}
// Database URL input
if (string.IsNullOrEmpty(DatabaseUrl))
{
DatabaseUrl = Prompt.GetString("Enter database URL:", defaultValue: "sqlite:///app.db");
}
// Password input (masked)
var adminPassword = Prompt.GetPassword("Set administrator password:");
// Confirmation
var confirm = Prompt.GetYesNo("Save configuration?", defaultAnswer: true);
if (confirm)
{
SaveConfiguration(console);
return 0;
}
else
{
console.WriteLine("Configuration cancelled.");
return 1;
}
}
private int RunNonInteractiveSetup(IConsole console)
{
if (string.IsNullOrEmpty(AppName) || !Port.HasValue || string.IsNullOrEmpty(DatabaseUrl))
{
console.Error.WriteLine("Non-interactive mode requires all options.");
console.Error.WriteLine("Please specify --name, --port, --database.");
return 1;
}
SaveConfiguration(console);
return 0;
}
private void SaveConfiguration(IConsole console)
{
console.WriteLine("Saving configuration...");
console.WriteLine($"Application name: {AppName}");
console.WriteLine($"Port: {Port}");
console.WriteLine($"Database: {DatabaseUrl}");
// Implement actual save logic here
console.WriteLine("Configuration saved successfully.");
}
}
Custom Validation Example
using McMaster.Extensions.CommandLineUtils;
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Text.RegularExpressions;
// Custom validation attributes
public class FileExistsAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (value is string filePath && !string.IsNullOrEmpty(filePath))
{
return File.Exists(filePath);
}
return false;
}
public override string FormatErrorMessage(string name)
{
return $"File '{name}' does not exist.";
}
}
public class PortRangeAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (value is int port)
{
return port >= 1024 && port <= 65535;
}
return false;
}
public override string FormatErrorMessage(string name)
{
return $"Port number must be in range 1024-65535.";
}
}
public class EmailAttribute : ValidationAttribute
{
private static readonly Regex EmailRegex = new Regex(
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
RegexOptions.Compiled);
public override bool IsValid(object value)
{
if (value is string email && !string.IsNullOrEmpty(email))
{
return EmailRegex.IsMatch(email);
}
return true; // Empty is considered valid
}
public override string FormatErrorMessage(string name)
{
return "Please enter a valid email address.";
}
}
[Command(Name = "validate-example", Description = "Validation example")]
public class ValidateExampleCommand
{
[Option("-f|--file", Description = "Input file")]
[Required]
[FileExists]
public string InputFile { get; set; }
[Option("-p|--port", Description = "Port number")]
[Required]
[PortRange]
public int Port { get; set; }
[Option("-e|--email", Description = "Email address")]
[Email]
public string Email { get; set; }
[Option("-n|--name", Description = "Name")]
[Required]
[StringLength(50, MinimumLength = 2)]
public string Name { get; set; }
[Option("-a|--age", Description = "Age")]
[Range(0, 150)]
public int Age { get; set; }
static int Main(string[] args)
=> CommandLineApplication.Execute<ValidateExampleCommand>(args);
private int OnExecute()
{
Console.WriteLine("Validation successful!");
Console.WriteLine($"File: {InputFile}");
Console.WriteLine($"Port: {Port}");
Console.WriteLine($"Name: {Name}");
Console.WriteLine($"Age: {Age}");
if (!string.IsNullOrEmpty(Email))
{
Console.WriteLine($"Email: {Email}");
}
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="McMaster.Extensions.CommandLineUtils" Version="4.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
</ItemGroup>
</Project>