CommandLineParser
C#のための成熟したコマンドライン引数パーサー。属性ベースの宣言的なアプローチが特徴です。
フレームワーク
CommandLineParser
概要
CommandLineParserは、C#のための成熟したコマンドライン引数パーサーです。属性ベースの宣言的なアプローチが特徴で、長年にわたって安定した人気を保ち、多くの.NETプロジェクトで信頼されて使用されています。豊富な機能と直感的なAPIにより、簡単なCLIツールから複雑なアプリケーションまで幅広く対応できます。
詳細
CommandLineParserは、.NET Framework、.NET Core、.NET 5+で利用可能なオープンソースライブラリです。属性を使用してオプションクラスを定義し、自動的にコマンドライン引数を解析してオブジェクトにマッピングします。豊富なカスタマイズオプションと包括的なエラーハンドリングを提供し、プロフェッショナルなCLIアプリケーションの開発を支援します。
主な特徴
- 属性ベース設計: 属性を使用した宣言的なオプション定義
- 自動マッピング: コマンドライン引数を自動的にオブジェクトプロパティにマッピング
- 豊富な型サポート: プリミティブ型、列挙型、コレクション、カスタム型に対応
- バリデーション: 組み込みおよびカスタムバリデーション機能
- 自動ヘルプ生成: 美しく整理されたヘルプテキストを自動生成
- 多言語対応: ローカライゼーション機能をサポート
- 相互排他オプション: オプション間の相互排他制約
- 動的オプション: 実行時にオプションを動的に追加可能
メリット・デメリット
メリット
- 宣言的アプローチ: 属性を使用した直感的なオプション定義
- 型安全: 強力な型付けによる安全な引数処理
- 豊富な機能: バリデーション、相互排他、カスタマイズなど
- 成熟したライブラリ: 長年の実績と安定性
- 優れたドキュメント: 充実した公式ドキュメントとサンプル
- 活発なコミュニティ: 継続的な開発とサポート
- NuGetサポート: 簡単なパッケージ管理
デメリット
- 学習曲線: 属性システムの理解が必要
- リフレクション使用: 実行時リフレクションによるパフォーマンスオーバーヘッド
- 複雑な設定: 高度な機能を使用する際の設定の複雑さ
- 依存関係: 外部ライブラリへの依存
主要リンク
書き方の例
基本的な使用例
using CommandLine;
using System;
public class Options
{
[Option('v', "verbose", Required = false, HelpText = "詳細な出力を有効にします")]
public bool Verbose { get; set; }
[Option('f', "file", Required = true, HelpText = "処理するファイル名")]
public string FileName { get; set; }
[Option('c', "count", Required = false, Default = 1, HelpText = "処理回数")]
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($"ファイル名: {opts.FileName}");
Console.WriteLine($"処理回数: {opts.Count}");
if (opts.Verbose)
{
Console.WriteLine("詳細モードが有効です");
}
// メイン処理をここに実装
for (int i = 0; i < opts.Count; i++)
{
Console.WriteLine($"処理中 ({i + 1}/{opts.Count}): {opts.FileName}");
}
}
static void HandleParseError(IEnumerable<Error> errs)
{
foreach (var error in errs)
{
Console.WriteLine($"エラー: {error}");
}
}
}
複数のサブコマンドの例
using CommandLine;
using System;
using System.Collections.Generic;
using System.Linq;
[Verb("add", HelpText = "ファイルを追加します")]
public class AddOptions
{
[Option('f', "files", Required = true, HelpText = "追加するファイル一覧")]
public IEnumerable<string> Files { get; set; }
[Option('r', "recursive", Required = false, HelpText = "再帰的に追加")]
public bool Recursive { get; set; }
[Option('v', "verbose", Required = false, HelpText = "詳細出力")]
public bool Verbose { get; set; }
}
[Verb("remove", HelpText = "ファイルを削除します")]
public class RemoveOptions
{
[Option('f', "files", Required = true, HelpText = "削除するファイル一覧")]
public IEnumerable<string> Files { get; set; }
[Option("force", Required = false, HelpText = "強制削除")]
public bool Force { get; set; }
[Option('v', "verbose", Required = false, HelpText = "詳細出力")]
public bool Verbose { get; set; }
}
[Verb("list", HelpText = "ファイル一覧を表示します")]
public class ListOptions
{
[Option('p', "path", Required = false, Default = ".", HelpText = "検索パス")]
public string Path { get; set; }
[Option('a', "all", Required = false, HelpText = "隠しファイルも表示")]
public bool ShowHidden { get; set; }
[Option('l', "long", Required = false, HelpText = "詳細形式で表示")]
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("ファイル追加コマンド実行中...");
foreach (var file in options.Files)
{
if (options.Verbose)
{
Console.WriteLine($"追加中: {file}");
}
}
if (options.Recursive)
{
Console.WriteLine("再帰的処理が有効です");
}
return 0;
}
static int RunRemoveCommand(RemoveOptions options)
{
Console.WriteLine("ファイル削除コマンド実行中...");
foreach (var file in options.Files)
{
if (options.Verbose)
{
Console.WriteLine($"削除中: {file}");
}
}
if (options.Force)
{
Console.WriteLine("強制削除モードです");
}
return 0;
}
static int RunListCommand(ListOptions options)
{
Console.WriteLine($"ファイル一覧表示: {options.Path}");
if (options.ShowHidden)
{
Console.WriteLine("隠しファイルも表示します");
}
if (options.LongFormat)
{
Console.WriteLine("詳細形式で表示します");
}
return 0;
}
}
高度な属性とバリデーションの例
using CommandLine;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
public class AdvancedOptions
{
[Option('i', "input", Required = true, HelpText = "入力ファイルパス")]
[FileExists]
public string InputFile { get; set; }
[Option('o', "output", Required = false, HelpText = "出力ファイルパス")]
public string OutputFile { get; set; }
[Option('p', "port", Required = false, Default = 8080, HelpText = "ポート番号 (1024-65535)")]
[Range(1024, 65535)]
public int Port { get; set; }
[Option('e', "email", Required = false, HelpText = "メールアドレス")]
[EmailAddress]
public string Email { get; set; }
[Option('t', "tags", Required = false, HelpText = "タグ一覧(カンマ区切り)")]
public IEnumerable<string> Tags { get; set; }
[Option("format", Required = false, Default = OutputFormat.Json, HelpText = "出力形式")]
public OutputFormat Format { get; set; }
[Option("config", Required = false, HelpText = "設定ファイルパス")]
public string ConfigFile { get; set; }
// 相互排他オプション
[Option('q', "quiet", Required = false, HelpText = "静寂モード", Group = "verbosity")]
public bool Quiet { get; set; }
[Option('v', "verbose", Required = false, HelpText = "詳細モード", Group = "verbosity")]
public bool Verbose { get; set; }
}
public enum OutputFormat
{
Json,
Xml,
Csv,
Text
}
// カスタムバリデーション属性
public class FileExistsAttribute : Attribute
{
// バリデーションロジックは別途実装
}
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
{
// バリデーションロジックは別途実装
}
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
{
// カスタムバリデーション
ValidateOptions(options);
Console.WriteLine("アプリケーション設定:");
Console.WriteLine($" 入力ファイル: {options.InputFile}");
Console.WriteLine($" 出力ファイル: {options.OutputFile ?? "標準出力"}");
Console.WriteLine($" ポート: {options.Port}");
Console.WriteLine($" 出力形式: {options.Format}");
if (!string.IsNullOrEmpty(options.Email))
{
Console.WriteLine($" メール: {options.Email}");
}
if (options.Tags?.Any() == true)
{
Console.WriteLine($" タグ: {string.Join(", ", options.Tags)}");
}
if (options.Verbose)
{
Console.WriteLine("詳細モードが有効です");
}
else if (options.Quiet)
{
Console.WriteLine("静寂モードが有効です");
}
// メイン処理
ProcessFiles(options);
return 0;
}
catch (ValidationException ex)
{
Console.WriteLine($"バリデーションエラー: {ex.Message}");
return 1;
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
return 1;
}
}
static void ValidateOptions(AdvancedOptions options)
{
// ファイル存在チェック
if (!File.Exists(options.InputFile))
{
throw new ValidationException($"入力ファイルが存在しません: {options.InputFile}");
}
// メールアドレス検証
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("有効なメールアドレスを入力してください");
}
}
// ポート番号検証
if (options.Port < 1024 || options.Port > 65535)
{
throw new ValidationException("ポート番号は1024-65535の範囲で指定してください");
}
// 相互排他チェック
if (options.Quiet && options.Verbose)
{
throw new ValidationException("--quiet と --verbose は同時に指定できません");
}
}
static void ProcessFiles(AdvancedOptions options)
{
Console.WriteLine("ファイル処理開始...");
// 実際の処理ロジックをここに実装
var inputContent = File.ReadAllText(options.InputFile);
if (!options.Quiet)
{
Console.WriteLine($"入力データサイズ: {inputContent.Length} 文字");
}
// 出力処理
string output = ProcessContent(inputContent, options.Format);
if (!string.IsNullOrEmpty(options.OutputFile))
{
File.WriteAllText(options.OutputFile, output);
if (!options.Quiet)
{
Console.WriteLine($"結果を {options.OutputFile} に保存しました");
}
}
else
{
Console.WriteLine(output);
}
}
static string ProcessContent(string content, OutputFormat 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 = "高度なCLIアプリケーション 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) { }
}
設定ファイルとの統合例
using CommandLine;
using System;
using System.IO;
using System.Text.Json;
public class ConfigurableOptions
{
[Option('c', "config", Required = false, HelpText = "設定ファイルパス")]
public string ConfigFile { get; set; }
[Option('h', "host", Required = false, HelpText = "ホスト名")]
public string Host { get; set; }
[Option('p', "port", Required = false, HelpText = "ポート番号")]
public int? Port { get; set; }
[Option('d', "database", Required = false, HelpText = "データベース接続文字列")]
public string DatabaseConnection { get; set; }
[Option('v', "verbose", Required = false, HelpText = "詳細ログ")]
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
{
// 設定ファイルの読み込み
var config = LoadConfig(options.ConfigFile);
// コマンドライン引数で設定ファイルの値を上書き
var finalConfig = MergeConfig(config, options);
Console.WriteLine("最終設定:");
Console.WriteLine($" ホスト: {finalConfig.Host}");
Console.WriteLine($" ポート: {finalConfig.Port}");
Console.WriteLine($" データベース: {finalConfig.DatabaseConnection ?? "未設定"}");
Console.WriteLine($" 詳細ログ: {(finalConfig.Verbose ? "有効" : "無効")}");
// アプリケーション実行
return StartApplication(finalConfig);
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
return 1;
}
}
static ConfigFile LoadConfig(string configPath)
{
if (string.IsNullOrEmpty(configPath))
{
configPath = "appsettings.json";
}
if (File.Exists(configPath))
{
Console.WriteLine($"設定ファイルを読み込み中: {configPath}");
var json = File.ReadAllText(configPath);
return JsonSerializer.Deserialize<ConfigFile>(json);
}
else
{
Console.WriteLine("設定ファイルが見つかりません。デフォルト設定を使用します。");
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("アプリケーションを開始しています...");
// 実際のアプリケーションロジック
if (config.Verbose)
{
Console.WriteLine("詳細ログモードで実行中");
}
return 0;
}
}
プロジェクトファイル例
<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>