RSCG – ArgumentParsing
name | ArgumentParsing |
nuget | https://www.nuget.org/packages/ArgumentParsing/ |
link | https://github.com/DoctorKrolic/ArgumentParsing |
author |
Transform command line arguments into strongly typed objects
This is how you can use ArgumentParsing .
The code that you start with is
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <!--<ItemGroup> <ProjectReference Include="..\src\ArgumentParsing\ArgumentParsing.csproj" /> <ProjectReference Include="..\src\ArgumentParsing.Generators\ArgumentParsing.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" /> </ItemGroup>--> <PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath> </PropertyGroup> <ItemGroup> <PackageReference Include="ArgumentParsing" Version="0.3.0" OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" /> </ItemGroup> </Project>
The code that you will use is
using ArgumentParsing; using ArgumentParsing.Results; namespace ArgPars; partial class Program { /// <summary> /// Execute in the folder with csproj file: /// /// dotnet run -- --help /// dotnet run -- --version /// dotnet run -- sample-input.txt /// dotnet run -- -v -f Xml sample-input.txt /// </summary> /// <param name="args"></param> private static void Main(string[] args) { // Parse the command line arguments with the generated parser var result = ParseArguments(args); // Handle the result based on its state switch (result.State) { case ParseResultState.ParsedOptions: ExecuteMainApp(result.Options!); break; case ParseResultState.ParsedWithErrors: Console.Error.WriteLine("Error parsing arguments:"); if (result.Errors != null) { foreach (var error in result.Errors) { Console.Error.WriteLine($" {error.GetMessage()}"); } } Environment.Exit(1); break; case ParseResultState.ParsedSpecialCommand: var exitCode = result.SpecialCommandHandler!.HandleCommand(); Environment.Exit(exitCode); break; } } [GeneratedArgumentParser] private static partial ParseResult<FileProcessorOptions> ParseArguments(string[] args); private static void ExecuteMainApp(FileProcessorOptions options) { // At this point all errors and special cases are handled, // so we get valid options object we can work with Console.WriteLine("=== File Processor Tool ==="); Console.WriteLine($"Verbose mode: {options.Verbose}"); if (options.Verbose) { Console.WriteLine($"Verbose mode: enabled"); Console.WriteLine($"Output format: {options.OutputFormat}"); Console.WriteLine($"Max file size: {options.MaxFileSizeBytes} bytes"); Console.WriteLine($"Input file: {options.InputFile}"); if (!string.IsNullOrEmpty(options.OutputFile)) Console.WriteLine($"Output file: {options.OutputFile}"); if (options.AdditionalFiles.Length > 0) { Console.WriteLine($"Additional files ({options.AdditionalFiles.Length}):"); foreach (var file in options.AdditionalFiles) { Console.WriteLine($" - {file}"); } } } //TODO: Simulate file processing } }
using ArgumentParsing; using ArgumentParsing.SpecialCommands.Help; using System.Collections.Immutable; namespace ArgPars; [OptionsType] class FileProcessorOptions { [Option('v', "verbose"), HelpInfo("Enable verbose logging and detailed output")] public bool Verbose { get; init; } [Option('f', "format"), HelpInfo("Output format for processed files (json, xml, csv)")] public OutputFormat OutputFormat { get; init; } = OutputFormat.Json; [Option('m', "max-size"), HelpInfo("Maximum file size in bytes (default: 10MB)")] public long MaxFileSizeBytes { get; init; } = 10 * 1024 * 1024; // 10MB default [Option('o', "output"), HelpInfo("Output file path (optional, defaults to input file with new extension)")] public string? OutputFile { get; init; } [Parameter(0, Name = "input-file"), HelpInfo("Path to the input file to process")] public required string InputFile { get; init; } [RemainingParameters, HelpInfo("Additional files to process")] public ImmutableArray<string> AdditionalFiles { get; init; } }
The code that is generated is
// <auto-generated/> #nullable disable #pragma warning disable namespace ArgumentParsing.Generated { internal static partial class ParseResultExtensions { /// <summary> /// Executes common default actions for the given <see cref="global::ArgumentParsing.Results.ParseResult{TOptions}"/> /// <list type="bullet"> /// <item>If <paramref name="result"/> is in <see cref="global::ArgumentParsing.Results.ParseResultState.ParsedOptions"/> state invokes provided <paramref name="action"/> with parsed options object</item> /// <item>If <paramref name="result"/> is in <see cref="global::ArgumentParsing.Results.ParseResultState.ParsedWithErrors"/> state writes help screen text with parse errors to <see cref="global::System.Console.Error"/> and exits application with code 1</item> /// <item>If <paramref name="result"/> is in <see cref="global::ArgumentParsing.Results.ParseResultState.ParsedSpecialCommand"/> state executes parsed handler and exits application with code, returned from the handler</item> /// </list> /// </summary> /// <param name="result">Parse result</param> /// <param name="action">Action, which will be invoked if options type is correctly parsed</param> [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ArgumentParsing.Generators.ArgumentParserGenerator", "0.3.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute] public static void ExecuteDefaults(this global::ArgumentParsing.Results.ParseResult<global::ExampleProject.FileProcessorOptions> result, global::System.Action<global::ExampleProject.FileProcessorOptions> action) { switch (result.State) { case global::ArgumentParsing.Results.ParseResultState.ParsedOptions: action(result.Options); break; case global::ArgumentParsing.Results.ParseResultState.ParsedWithErrors: string errorScreenText = global::ArgumentParsing.Generated.HelpCommandHandler_ExampleProject_FileProcessorOptions.GenerateHelpText(result.Errors); global::System.Console.Error.WriteLine(errorScreenText); global::System.Environment.Exit(1); break; case global::ArgumentParsing.Results.ParseResultState.ParsedSpecialCommand: int exitCode = result.SpecialCommandHandler.HandleCommand(); global::System.Environment.Exit(exitCode); break; } } } } namespace ExampleProject { partial class Program { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ArgumentParsing.Generators.ArgumentParserGenerator", "0.3.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute] private static partial global::ArgumentParsing.Results.ParseResult<global::ExampleProject.FileProcessorOptions> ParseArguments(string[] args) { bool Verbose_val = default(bool); global::ExampleProject.OutputFormat OutputFormat_val = default(global::ExampleProject.OutputFormat); long MaxFileSizeBytes_val = default(long); string OutputFile_val = default(string); string InputFile_val = default(string); global::System.Collections.Immutable.ImmutableArray<string>.Builder remainingParametersBuilder = global::System.Collections.Immutable.ImmutableArray.CreateBuilder<string>(); int state = -3; int seenOptions = 0; global::System.Collections.Generic.HashSet<global::ArgumentParsing.Results.Errors.ParseError> errors = null; global::System.Span<global::System.Range> longArgSplit = stackalloc global::System.Range[2]; global::System.ReadOnlySpan<char> latestOptionName = default(global::System.ReadOnlySpan<char>); string previousArgument = null; int parameterIndex = 0; foreach (string arg in args) { if (state == -3) { switch (arg) { case "--help": return new global::ArgumentParsing.Results.ParseResult<global::ExampleProject.FileProcessorOptions>(new global::ArgumentParsing.Generated.HelpCommandHandler_ExampleProject_FileProcessorOptions()); case "--version": return new global::ArgumentParsing.Results.ParseResult<global::ExampleProject.FileProcessorOptions>(new global::ArgumentParsing.Generated.VersionCommandHandler()); } state = 0; } global::System.ReadOnlySpan<char> val; bool hasLetters = global::System.Linq.Enumerable.Any(arg, char.IsLetter); bool startsOption = hasLetters && arg.Length > 1 && arg.StartsWith('-'); if (state > 0 && startsOption) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.OptionValueIsNotProvidedError(previousArgument)); state = 0; } if (state != -2) { if (arg.StartsWith("--") && (hasLetters || arg.Length == 2 || arg.Contains('='))) { global::System.ReadOnlySpan<char> slice = global::System.MemoryExtensions.AsSpan(arg, 2); int written = global::System.MemoryExtensions.Split(slice, longArgSplit, '='); latestOptionName = slice[longArgSplit[0]]; switch (latestOptionName) { case "": if (written == 1) { state = -2; } else { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError(arg)); } continue; case "verbose": if ((seenOptions & 0b0001) > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("verbose")); } Verbose_val = true; state = -10; seenOptions |= 0b0001; break; case "format": if ((seenOptions & 0b0010) > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("format")); } state = 2; seenOptions |= 0b0010; break; case "max-size": if ((seenOptions & 0b0100) > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("max-size")); } state = 3; seenOptions |= 0b0100; break; case "output": if ((seenOptions & 0b1000) > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("output")); } state = 4; seenOptions |= 0b1000; break; default: errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.UnknownOptionError(latestOptionName.ToString(), arg)); if (written == 1) { state = -1; } goto continueMainLoop; } if (written == 2) { val = slice[longArgSplit[1]]; goto decodeValue; } goto continueMainLoop; } if (startsOption) { global::System.ReadOnlySpan<char> slice = global::System.MemoryExtensions.AsSpan(arg, 1); for (int i = 0; i < slice.Length; i++) { if (state > 0) { val = slice.Slice(i); goto decodeValue; } char shortOptionName = slice[i]; latestOptionName = new global::System.ReadOnlySpan<char>(in slice[i]); switch (shortOptionName) { case 'v': if ((seenOptions & 0b0001) > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("v")); } Verbose_val = true; state = -10; seenOptions |= 0b0001; break; case 'f': if ((seenOptions & 0b0010) > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("f")); } state = 2; seenOptions |= 0b0010; break; case 'm': if ((seenOptions & 0b0100) > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("m")); } state = 3; seenOptions |= 0b0100; break; case 'o': if ((seenOptions & 0b1000) > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("o")); } state = 4; seenOptions |= 0b1000; break; default: if (state <= -10) { val = slice.Slice(i); latestOptionName = new global::System.ReadOnlySpan<char>(in slice[i - 1]); goto decodeValue; } errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.UnknownOptionError(shortOptionName.ToString(), arg)); state = -1; goto continueMainLoop; } } goto continueMainLoop; } } val = global::System.MemoryExtensions.AsSpan(arg); decodeValue: switch (state) { case -1: break; case 2: if (!global::System.Enum.TryParse<global::ExampleProject.OutputFormat>(val, out OutputFormat_val)) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError(val.ToString(), latestOptionName.ToString())); } break; case 3: if (!long.TryParse(val, global::System.Globalization.NumberStyles.Integer, global::System.Globalization.CultureInfo.InvariantCulture, out MaxFileSizeBytes_val)) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError(val.ToString(), latestOptionName.ToString())); } break; case 4: OutputFile_val = val.ToString(); break; default: switch (parameterIndex++) { case 0: InputFile_val = arg; break; default: remainingParametersBuilder.Add(arg); break; } break; } state = 0; continueMainLoop: previousArgument = arg; } if (state > 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.OptionValueIsNotProvidedError(previousArgument)); } if (parameterIndex <= 0) { errors ??= new(); errors.Add(new global::ArgumentParsing.Results.Errors.MissingRequiredParameterError( "input-file", 0)); } if (errors != null) { return new global::ArgumentParsing.Results.ParseResult<global::ExampleProject.FileProcessorOptions>(global::ArgumentParsing.Results.Errors.ParseErrorCollection.AsErrorCollection(errors)); } global::ExampleProject.FileProcessorOptions options = new global::ExampleProject.FileProcessorOptions { Verbose = Verbose_val, OutputFormat = OutputFormat_val, MaxFileSizeBytes = MaxFileSizeBytes_val, OutputFile = OutputFile_val, InputFile = InputFile_val, AdditionalFiles = remainingParametersBuilder.ToImmutable(), }; return new global::ArgumentParsing.Results.ParseResult<global::ExampleProject.FileProcessorOptions>(options); } } }
// <auto-generated/> #nullable disable #pragma warning disable namespace ArgumentParsing.Generated { /// <summary> /// Default implementation of <c>--help</c> command for <see cref="global::ExampleProject.FileProcessorOptions"/> type /// </summary> [global::ArgumentParsing.SpecialCommands.SpecialCommandAliasesAttribute("--help")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ArgumentParsing.Generators.ArgumentParserGenerator", "0.3.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute] internal sealed class HelpCommandHandler_ExampleProject_FileProcessorOptions : global::ArgumentParsing.SpecialCommands.ISpecialCommandHandler { /// <summary> /// Generates help text for <see cref="global::ExampleProject.FileProcessorOptions"/> type. /// If <paramref name="errors"/> parameter is supplied, generated text will contain an error section /// </summary> /// <param name="errors">Parse errors to include into help text</param> /// <returns>Generated help text</returns> public static string GenerateHelpText(global::ArgumentParsing.Results.Errors.ParseErrorCollection? errors = null) { global::System.Text.StringBuilder helpBuilder = new(); helpBuilder.AppendLine("ArgPars 1.0.0"); helpBuilder.AppendLine("Copyright (C) " + global::System.DateTime.UtcNow.Year.ToString()); if ((object)errors != null) { helpBuilder.AppendLine(); helpBuilder.AppendLine("ERROR(S):"); foreach (global::ArgumentParsing.Results.Errors.ParseError error in errors) { helpBuilder.AppendLine(" " + error.GetMessage()); } } helpBuilder.AppendLine(); helpBuilder.AppendLine("OPTIONS:"); helpBuilder.AppendLine(); helpBuilder.AppendLine(" -v, --verbose\tEnable verbose logging and detailed output"); helpBuilder.AppendLine(); helpBuilder.AppendLine(" -f, --format\tOutput format for processed files (json, xml, csv)"); helpBuilder.AppendLine(); helpBuilder.AppendLine(" -m, --max-size\tMaximum file size in bytes (default: 10MB)"); helpBuilder.AppendLine(); helpBuilder.AppendLine(" -o, --output\tOutput file path (optional, defaults to input file with new extension)"); helpBuilder.AppendLine(); helpBuilder.AppendLine("PARAMETERS:"); helpBuilder.AppendLine(); helpBuilder.AppendLine(" input-file (at index 0)\tRequired. Path to the input file to process"); helpBuilder.AppendLine(); helpBuilder.AppendLine(" Remaining parameters\tAdditional files to process"); helpBuilder.AppendLine(); helpBuilder.AppendLine("COMMANDS:"); helpBuilder.AppendLine(); helpBuilder.AppendLine(" --help\tShow help screen"); helpBuilder.AppendLine(); helpBuilder.AppendLine(" --version\tShow version information"); return helpBuilder.ToString(); } /// <inheritdoc/> public int HandleCommand() { global::System.Console.Out.WriteLine(GenerateHelpText()); return 0; } } }
// <auto-generated/> #nullable disable #pragma warning disable namespace ArgumentParsing.Generated { /// <summary> /// Default implementation of <c>--version</c> command for <c>ArgPars</c> assembly /// </summary> [global::ArgumentParsing.SpecialCommands.SpecialCommandAliasesAttribute("--version")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ArgumentParsing.Generators.ArgumentParserGenerator", "0.3.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute] internal sealed class VersionCommandHandler : global::ArgumentParsing.SpecialCommands.ISpecialCommandHandler { /// <inheritdoc/> public int HandleCommand() { global::System.Console.WriteLine("ArgPars 1.0.0"); return 0; } } }
Code and pdf at
https://ignatandrei.github.io/RSCG_Examples/v2/docs/ArgumentParsing