McMaster.Extensions.CommandLineUtils
.NET Core/Standard向けのコマンドライン解析およびユーティリティライブラリ。
フレームワーク
McMaster.Extensions.CommandLineUtils
概要
McMaster.Extensions.CommandLineUtilsは、.NET Core/Standard向けのコマンドライン解析およびユーティリティライブラリです。ASP.NET Coreチームのメンバーによって開発され、.NET Coreエコシステムでの統合が優れています。モダンな.NETアプリケーションでの使いやすさと、豊富な機能を両立したライブラリです。
詳細
このライブラリは、.NET Coreの依存性注入、設定システム、ログ機能との統合を重視して設計されています。属性ベースとフルーエントAPIの両方をサポートし、開発者が好みに応じてアプローチを選択できます。特に.NET Coreの機能をフル活用したい場合に最適な選択肢です。
主な特徴
- .NET Core統合: 依存性注入、設定、ログとの深い統合
- 柔軟なAPI: 属性ベースとフルーエントAPIの両方をサポート
- サブコマンド: 階層的なコマンド構造をサポート
- バリデーション: 豊富なバリデーション機能
- プロンプト: 対話的なユーザー入力プロンプト
- 自動ヘルプ生成: 美しいヘルプテキストの自動生成
- プラグインアーキテクチャ: 拡張可能な設計
- 非同期サポート: async/awaitパターンの完全サポート
メリット・デメリット
メリット
- .NET Core親和性: .NET Coreの機能との優れた統合
- 柔軟な設計: 様々な開発スタイルに対応
- モダンなAPI: 最新の.NET機能を活用
- 充実したドキュメント: 詳細な公式ドキュメント
- 活発な開発: 継続的な改善とアップデート
- 依存性注入対応: DIコンテナとの統合が容易
- テスト可能: 単体テストしやすい設計
デメリット
- .NET Core依存: .NET Framework環境では制限がある
- 学習コスト: 豊富な機能ゆえに習得に時間が必要
- 相対的に新しい: 他のライブラリに比べて歴史が浅い
- ドキュメント分散: 機能が多岐にわたるため情報が分散
主要リンク
書き方の例
基本的な属性ベースの例
using McMaster.Extensions.CommandLineUtils;
using System;
using System.ComponentModel.DataAnnotations;
[Command(Name = "myapp", Description = "サンプルアプリケーション")]
public class Program
{
[Option("-n|--name", Description = "お名前")]
[Required]
public string Name { get; set; }
[Option("-a|--age", Description = "年齢")]
[Range(0, 150)]
public int Age { get; set; } = 0;
[Option("-v|--verbose", Description = "詳細出力")]
public bool Verbose { get; set; }
[Argument(0, Description = "処理するファイル")]
public string[] Files { get; set; }
static int Main(string[] args)
=> CommandLineApplication.Execute<Program>(args);
private int OnExecute()
{
Console.WriteLine($"こんにちは、{Name}さん!");
if (Age > 0)
{
Console.WriteLine($"年齢: {Age}歳");
}
if (Verbose)
{
Console.WriteLine("詳細モードが有効です");
}
if (Files?.Length > 0)
{
Console.WriteLine("処理ファイル:");
foreach (var file in Files)
{
Console.WriteLine($" - {file}");
}
}
return 0;
}
}
フルーエントAPIの例
using McMaster.Extensions.CommandLineUtils;
using System;
class Program
{
static int Main(string[] args)
{
var app = new CommandLineApplication
{
Name = "filemanager",
Description = "ファイル管理ツール"
};
app.HelpOption();
var nameOption = app.Option("-n|--name <NAME>", "ユーザー名", CommandOptionType.SingleValue)
.IsRequired();
var verboseOption = app.Option("-v|--verbose", "詳細出力", CommandOptionType.NoValue);
var filesArgument = app.Argument("files", "処理するファイル", multipleValues: true);
app.OnExecute(() =>
{
Console.WriteLine($"ユーザー: {nameOption.Value()}");
if (verboseOption.HasValue())
{
Console.WriteLine("詳細モードが有効です");
}
if (filesArgument.Values.Count > 0)
{
Console.WriteLine("ファイル一覧:");
foreach (var file in filesArgument.Values)
{
Console.WriteLine($" - {file}");
}
}
return 0;
});
return app.Execute(args);
}
}
サブコマンドの例
using McMaster.Extensions.CommandLineUtils;
using System;
using System.ComponentModel.DataAnnotations;
[Command(Name = "git-tool", Description = "Git操作ツール")]
[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 = "リポジトリをクローンします")]
public class CloneCommand
{
[Argument(0, Description = "リポジトリURL")]
[Required]
public string Repository { get; set; }
[Option("--depth <DEPTH>", Description = "履歴の深さ")]
public int? Depth { get; set; }
[Option("-b|--branch <BRANCH>", Description = "ブランチ名")]
public string Branch { get; set; }
[Option("-v|--verbose", Description = "詳細出力")]
public bool Verbose { get; set; }
private int OnExecute()
{
Console.WriteLine($"リポジトリをクローン中: {Repository}");
if (!string.IsNullOrEmpty(Branch))
{
Console.WriteLine($"ブランチ: {Branch}");
}
if (Depth.HasValue)
{
Console.WriteLine($"履歴の深さ: {Depth.Value}");
}
if (Verbose)
{
Console.WriteLine("詳細モードで実行中...");
}
// 実際のクローン処理をここに実装
return 0;
}
}
[Command(Name = "push", Description = "変更をプッシュします")]
public class PushCommand
{
[Argument(0, Description = "リモート名")]
public string Remote { get; set; } = "origin";
[Argument(1, Description = "ブランチ名")]
public string Branch { get; set; } = "main";
[Option("-f|--force", Description = "強制プッシュ")]
public bool Force { get; set; }
[Option("-u|--set-upstream", Description = "アップストリームを設定")]
public bool SetUpstream { get; set; }
private int OnExecute()
{
Console.WriteLine($"プッシュ中: {Remote}/{Branch}");
if (Force)
{
Console.WriteLine("強制プッシュが有効です");
}
if (SetUpstream)
{
Console.WriteLine("アップストリームを設定します");
}
// 実際のプッシュ処理をここに実装
return 0;
}
}
[Command(Name = "status", Description = "リポジトリの状態を表示します")]
public class StatusCommand
{
[Option("-s|--short", Description = "短縮形式で表示")]
public bool Short { get; set; }
[Option("--porcelain", Description = "スクリプト用出力")]
public bool Porcelain { get; set; }
private int OnExecute()
{
Console.WriteLine("リポジトリ状態:");
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;
}
}
依存性注入との統合例
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
// サービスインターフェース
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("データ処理を開始します");
foreach (var file in files)
{
_logger.LogInformation($"処理中: {file}");
await Task.Delay(500); // 処理をシミュレート
}
_logger.LogInformation("データ処理が完了しました");
}
}
[Command(Name = "processor", Description = "データ処理アプリケーション")]
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 = "処理するファイル")]
public string[] Files { get; set; }
[Option("-p|--parallel", Description = "並列処理")]
public bool Parallel { get; set; }
[Option("-v|--verbose", Description = "詳細ログ")]
public bool Verbose { get; set; }
private async Task<int> OnExecuteAsync()
{
if (Files?.Length == 0)
{
_logger.LogError("処理するファイルが指定されていません");
return 1;
}
_logger.LogInformation($"処理モード: {(Parallel ? "並列" : "順次")}");
try
{
await _dataService.ProcessDataAsync(Files);
return 0;
}
catch (Exception ex)
{
_logger.LogError(ex, "処理中にエラーが発生しました");
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);
}
}
プロンプトと対話的入力の例
using McMaster.Extensions.CommandLineUtils;
using System;
using System.ComponentModel.DataAnnotations;
[Command(Name = "setup", Description = "アプリケーション設定ツール")]
public class SetupCommand
{
[Option("-i|--interactive", Description = "対話モード")]
public bool Interactive { get; set; }
[Option("--name <NAME>", Description = "アプリケーション名")]
public string AppName { get; set; }
[Option("--port <PORT>", Description = "ポート番号")]
public int? Port { get; set; }
[Option("--database <URL>", Description = "データベース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("対話式セットアップを開始します...");
console.WriteLine();
// アプリケーション名の入力
if (string.IsNullOrEmpty(AppName))
{
AppName = Prompt.GetString("アプリケーション名を入力してください:", defaultValue: "MyApp");
}
// ポート番号の入力
if (!Port.HasValue)
{
Port = Prompt.GetInt("ポート番号を入力してください:", defaultValue: 8080);
}
// データベースURLの入力
if (string.IsNullOrEmpty(DatabaseUrl))
{
DatabaseUrl = Prompt.GetString("データベースURLを入力してください:", defaultValue: "sqlite:///app.db");
}
// パスワードの入力(マスク)
var adminPassword = Prompt.GetPassword("管理者パスワードを設定してください:");
// 確認
var confirm = Prompt.GetYesNo("設定を保存しますか?", defaultAnswer: true);
if (confirm)
{
SaveConfiguration(console);
return 0;
}
else
{
console.WriteLine("設定はキャンセルされました。");
return 1;
}
}
private int RunNonInteractiveSetup(IConsole console)
{
if (string.IsNullOrEmpty(AppName) || !Port.HasValue || string.IsNullOrEmpty(DatabaseUrl))
{
console.Error.WriteLine("非対話モードでは、すべてのオプションが必要です。");
console.Error.WriteLine("--name, --port, --database を指定してください。");
return 1;
}
SaveConfiguration(console);
return 0;
}
private void SaveConfiguration(IConsole console)
{
console.WriteLine("設定を保存中...");
console.WriteLine($"アプリケーション名: {AppName}");
console.WriteLine($"ポート: {Port}");
console.WriteLine($"データベース: {DatabaseUrl}");
// 実際の保存処理をここに実装
console.WriteLine("設定が正常に保存されました。");
}
}
カスタムバリデーションの例
using McMaster.Extensions.CommandLineUtils;
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Text.RegularExpressions;
// カスタムバリデーション属性
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 $"ファイル '{name}' が存在しません。";
}
}
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 $"ポート番号は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; // 空の場合は有効とする
}
public override string FormatErrorMessage(string name)
{
return "有効なメールアドレスを入力してください。";
}
}
[Command(Name = "validate-example", Description = "バリデーション例")]
public class ValidateExampleCommand
{
[Option("-f|--file", Description = "入力ファイル")]
[Required]
[FileExists]
public string InputFile { get; set; }
[Option("-p|--port", Description = "ポート番号")]
[Required]
[PortRange]
public int Port { get; set; }
[Option("-e|--email", Description = "メールアドレス")]
[Email]
public string Email { get; set; }
[Option("-n|--name", Description = "名前")]
[Required]
[StringLength(50, MinimumLength = 2)]
public string Name { get; set; }
[Option("-a|--age", Description = "年齢")]
[Range(0, 150)]
public int Age { get; set; }
static int Main(string[] args)
=> CommandLineApplication.Execute<ValidateExampleCommand>(args);
private int OnExecute()
{
Console.WriteLine("バリデーション成功!");
Console.WriteLine($"ファイル: {InputFile}");
Console.WriteLine($"ポート: {Port}");
Console.WriteLine($"名前: {Name}");
Console.WriteLine($"年齢: {Age}");
if (!string.IsNullOrEmpty(Email))
{
Console.WriteLine($"メール: {Email}");
}
return 0;
}
}
プロジェクトファイル例
<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>