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