RSCG – ConsoleAppFramework
| name | ConsoleAppFramework |
| nuget | https://www.nuget.org/packages/ConsoleAppFramework/ |
| link | https://github.com/Cysharp/ConsoleAppFramework |
| author | Cysharp, Inc. |
Generating console parser for functions
This is how you can use ConsoleAppFramework .
The code that you start with is
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ConsoleAppFramework" Version="5.6.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
The code that you will use is
var app = ConsoleAppFramework.ConsoleApp.Create();
app.Add("", (string msg) => Console.WriteLine(msg));
app.Add("echo", (string msg) => Console.WriteLine(msg));
app.Add("sum", (int x, int y) => Console.WriteLine(x + y));
// --help
// --msg Andrei
// echo --msg Andrei
// sum --x 55 --y 0
app.Run(args);
The code that is generated is
// <auto-generated/>
#nullable enable
#pragma warning disable
namespace ConsoleAppFramework;
using System;
using System.Text;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel.DataAnnotations;
internal static partial class ConsoleApp
{
partial class ConsoleAppBuilder
{
Action<string> command0 = default!;
Action<string> command1 = default!;
Action<int, int> command2 = default!;
partial void AddCore(string commandName, Delegate command)
{
switch (commandName)
{
case "":
this.command0 = Unsafe.As<Action<string>>(command);
break;
case "echo":
this.command1 = Unsafe.As<Action<string>>(command);
break;
case "sum":
this.command2 = Unsafe.As<Action<int, int>>(command);
break;
default:
break;
}
}
partial void RunCore(string[] args, CancellationToken cancellationToken)
{
if (args.Length == 1 && args[0] is "--help" or "-h")
{
ShowHelp(-1);
return;
}
if (args.Length == 0)
{
RunCommand0(args, 0, args.AsSpan().IndexOf("--"), command0, cancellationToken);
return;
}
switch (args[0])
{
case "echo":
RunCommand1(args, 1, args.AsSpan().IndexOf("--"), command1, cancellationToken);
break;
case "sum":
RunCommand2(args, 1, args.AsSpan().IndexOf("--"), command2, cancellationToken);
break;
default:
RunCommand0(args, 0, args.AsSpan().IndexOf("--"), command0, cancellationToken);
break;
}
}
private static void RunCommand0(string[] args, int commandDepth, int escapeIndex, Action<string> command, CancellationToken __ExternalCancellationToken__)
{
var commandArgs = (escapeIndex == -1) ? args.AsSpan(commandDepth) : args.AsSpan(commandDepth, escapeIndex - commandDepth);
if (TryShowHelpOrVersion(commandArgs, 1, 0)) return;
var arg0 = default(string);
var arg0Parsed = false;
try
{
for (int i = 0; i < commandArgs.Length; i++)
{
var name = commandArgs[i];
switch (name)
{
case "--msg":
{
if (!TryIncrementIndex(ref i, commandArgs.Length)) { ThrowArgumentParseFailed("msg", commandArgs[i]); } else { arg0 = commandArgs[i]; }
arg0Parsed = true;
break;
}
default:
if (string.Equals(name, "--msg", StringComparison.OrdinalIgnoreCase))
{
if (!TryIncrementIndex(ref i, commandArgs.Length)) { ThrowArgumentParseFailed("msg", commandArgs[i]); } else { arg0 = commandArgs[i]; }
arg0Parsed = true;
break;
}
ThrowArgumentNameNotFound(name);
break;
}
}
if (!arg0Parsed) ThrowRequiredArgumentNotParsed("msg");
command(arg0!);
}
catch (Exception ex)
{
Environment.ExitCode = 1;
if (ex is ValidationException or ArgumentParseFailedException)
{
LogError(ex.Message);
}
else
{
LogError(ex.ToString());
}
}
}
private static void RunCommand1(string[] args, int commandDepth, int escapeIndex, Action<string> command, CancellationToken __ExternalCancellationToken__)
{
var commandArgs = (escapeIndex == -1) ? args.AsSpan(commandDepth) : args.AsSpan(commandDepth, escapeIndex - commandDepth);
if (TryShowHelpOrVersion(commandArgs, 1, 1)) return;
var arg0 = default(string);
var arg0Parsed = false;
try
{
for (int i = 0; i < commandArgs.Length; i++)
{
var name = commandArgs[i];
switch (name)
{
case "--msg":
{
if (!TryIncrementIndex(ref i, commandArgs.Length)) { ThrowArgumentParseFailed("msg", commandArgs[i]); } else { arg0 = commandArgs[i]; }
arg0Parsed = true;
break;
}
default:
if (string.Equals(name, "--msg", StringComparison.OrdinalIgnoreCase))
{
if (!TryIncrementIndex(ref i, commandArgs.Length)) { ThrowArgumentParseFailed("msg", commandArgs[i]); } else { arg0 = commandArgs[i]; }
arg0Parsed = true;
break;
}
ThrowArgumentNameNotFound(name);
break;
}
}
if (!arg0Parsed) ThrowRequiredArgumentNotParsed("msg");
command(arg0!);
}
catch (Exception ex)
{
Environment.ExitCode = 1;
if (ex is ValidationException or ArgumentParseFailedException)
{
LogError(ex.Message);
}
else
{
LogError(ex.ToString());
}
}
}
private static void RunCommand2(string[] args, int commandDepth, int escapeIndex, Action<int, int> command, CancellationToken __ExternalCancellationToken__)
{
var commandArgs = (escapeIndex == -1) ? args.AsSpan(commandDepth) : args.AsSpan(commandDepth, escapeIndex - commandDepth);
if (TryShowHelpOrVersion(commandArgs, 2, 2)) return;
var arg0 = default(int);
var arg0Parsed = false;
var arg1 = default(int);
var arg1Parsed = false;
try
{
for (int i = 0; i < commandArgs.Length; i++)
{
var name = commandArgs[i];
switch (name)
{
case "--x":
{
if (!TryIncrementIndex(ref i, commandArgs.Length) || !int.TryParse(commandArgs[i], out arg0)) { ThrowArgumentParseFailed("x", commandArgs[i]); }
arg0Parsed = true;
break;
}
case "--y":
{
if (!TryIncrementIndex(ref i, commandArgs.Length) || !int.TryParse(commandArgs[i], out arg1)) { ThrowArgumentParseFailed("y", commandArgs[i]); }
arg1Parsed = true;
break;
}
default:
if (string.Equals(name, "--x", StringComparison.OrdinalIgnoreCase))
{
if (!TryIncrementIndex(ref i, commandArgs.Length) || !int.TryParse(commandArgs[i], out arg0)) { ThrowArgumentParseFailed("x", commandArgs[i]); }
arg0Parsed = true;
break;
}
if (string.Equals(name, "--y", StringComparison.OrdinalIgnoreCase))
{
if (!TryIncrementIndex(ref i, commandArgs.Length) || !int.TryParse(commandArgs[i], out arg1)) { ThrowArgumentParseFailed("y", commandArgs[i]); }
arg1Parsed = true;
break;
}
ThrowArgumentNameNotFound(name);
break;
}
}
if (!arg0Parsed) ThrowRequiredArgumentNotParsed("x");
if (!arg1Parsed) ThrowRequiredArgumentNotParsed("y");
command(arg0!, arg1!);
}
catch (Exception ex)
{
Environment.ExitCode = 1;
if (ex is ValidationException or ArgumentParseFailedException)
{
LogError(ex.Message);
}
else
{
LogError(ex.ToString());
}
}
}
}
}
[/code]
[code lang="csharp"]
// <auto-generated/>
#nullable enable
#pragma warning disable
namespace ConsoleAppFramework;
using System;
using System.Text;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel.DataAnnotations;
internal static partial class ConsoleApp
{
internal partial class ConsoleAppBuilder
{
static partial void ShowHelp(int helpId)
{
switch (helpId)
{
case 0:
Log("""
Usage: [options...] [-h|--help] [--version]
Options:
--msg <string> (Required)
""");
break;
case 1:
Log("""
Usage: echo [options...] [-h|--help] [--version]
Options:
--msg <string> (Required)
""");
break;
case 2:
Log("""
Usage: sum [options...] [-h|--help] [--version]
Options:
--x <int> (Required)
--y <int> (Required)
""");
break;
default:
Log("""
Usage: [command] [options...] [-h|--help] [--version]
Options:
--msg <string> (Required)
Commands:
echo
sum
""");
break;
}
}
}
}
// <auto-generated/>
#nullable enable
#pragma warning disable
namespace ConsoleAppFramework;
using System;
using System.Text;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel.DataAnnotations;
internal static partial class ConsoleApp
{
internal partial class ConsoleAppBuilder
{
public void Run(string[] args) => Run(args, true);
public void Run(string[] args, CancellationToken cancellationToken) => Run(args, true, cancellationToken);
public void Run(string[] args, bool disposeServiceProvider, CancellationToken cancellationToken = default)
{
BuildAndSetServiceProvider();
try
{
RunCore(args, cancellationToken);
}
finally
{
if (disposeServiceProvider)
{
if (ServiceProvider is IDisposable d)
{
d.Dispose();
}
}
}
}
public Task RunAsync(string[] args) => RunAsync(args, true);
public Task RunAsync(string[] args, CancellationToken cancellationToken) => RunAsync(args, true, cancellationToken);
public async Task RunAsync(string[] args, bool disposeServiceProvider, CancellationToken cancellationToken = default)
{
BuildAndSetServiceProvider();
try
{
Task? task = null;
RunAsyncCore(args, cancellationToken, ref task!);
if (task != null)
{
await task;
}
}
finally
{
if (disposeServiceProvider)
{
if (ServiceProvider is IAsyncDisposable ad)
{
await ad.DisposeAsync();
}
else if (ServiceProvider is IDisposable d)
{
d.Dispose();
}
}
}
}
}
}
// <auto-generated/>
#nullable enable
#pragma warning disable
namespace ConsoleAppFramework;
using System;
using System.Text;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel.DataAnnotations;
#if !USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS
internal interface IArgumentParser<T>
{
static abstract bool TryParse(ReadOnlySpan<char> s, out T result);
}
internal record ConsoleAppContext
{
public string CommandName { get; init; }
public string[] Arguments { get; init; }
public object? State { get; init; }
internal int CommandDepth { get; }
internal int EscapeIndex { get; }
public ReadOnlySpan<string> CommandArguments
{
get => (EscapeIndex == -1)
? Arguments.AsSpan(CommandDepth)
: Arguments.AsSpan(CommandDepth, EscapeIndex - CommandDepth);
}
public ReadOnlySpan<string> EscapedArguments
{
get => (EscapeIndex == -1)
? Array.Empty<string>()
: Arguments.AsSpan(EscapeIndex + 1);
}
public ConsoleAppContext(string commandName, string[] arguments, object? state, int commandDepth, int escapeIndex)
{
this.CommandName = commandName;
this.Arguments = arguments;
this.State = state;
this.CommandDepth = commandDepth;
this.EscapeIndex = escapeIndex;
}
public override string ToString()
{
return string.Join(" ", Arguments);
}
}
internal abstract class ConsoleAppFilter(ConsoleAppFilter next)
{
protected readonly ConsoleAppFilter Next = next;
public abstract Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken);
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
internal sealed class ConsoleAppFilterAttribute<T> : Attribute
where T : ConsoleAppFilter
{
}
internal sealed class ArgumentParseFailedException(string message) : Exception(message)
{
}
#endif
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class FromServicesAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ArgumentAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class CommandAttribute : Attribute
{
public string Command { get; }
public CommandAttribute(string command)
{
this.Command = command;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class HiddenAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal sealed class RegisterCommandsAttribute : Attribute
{
public string CommandPath { get; }
public RegisterCommandsAttribute()
{
this.CommandPath = "";
}
public RegisterCommandsAttribute(string commandPath)
{
this.CommandPath = commandPath;
}
}
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
public class ConsoleAppFrameworkGeneratorOptionsAttribute : Attribute
{
public bool DisableNamingConversion { get; set; }
}
[UnconditionalSuppressMessage("Trimming", "IL2026")]
[UnconditionalSuppressMessage("AOT", "IL3050")]
internal static partial class ConsoleApp
{
public static IServiceProvider? ServiceProvider { get; set; }
public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5);
public static System.Text.Json.JsonSerializerOptions? JsonSerializerOptions { get; set; }
public static string? Version { get; set; }
static Action<string>? logAction;
public static Action<string> Log
{
get => logAction ??= Console.WriteLine;
set => logAction = value;
}
static Action<string>? logErrorAction;
public static Action<string> LogError
{
get => logErrorAction ??= (static msg => Log(msg));
set => logErrorAction = value;
}
/// <summary>
/// <para>You can pass second argument that generates new Run overload.</para>
/// ConsoleApp.Run(args, (int x, int y) => { });<br/>
/// ConsoleApp.Run(args, Foo);<br/>
/// ConsoleApp.Run(args, &Foo);<br/>
/// </summary>
public static void Run(string[] args)
{
}
/// <summary>
/// <para>You can pass second argument that generates new RunAsync overload.</para>
/// ConsoleApp.RunAsync(args, (int x, int y) => { });<br/>
/// ConsoleApp.RunAsync(args, Foo);<br/>
/// ConsoleApp.RunAsync(args, &Foo);<br/>
/// </summary>
public static Task RunAsync(string[] args)
{
return Task.CompletedTask;
}
public static ConsoleAppBuilder Create() => new ConsoleAppBuilder();
static void ThrowArgumentParseFailed(string argumentName, string value)
{
throw new ArgumentParseFailedException($"Argument '{argumentName}' failed to parse, provided value: {value}");
}
static void ThrowRequiredArgumentNotParsed(string name)
{
throw new ArgumentParseFailedException($"Required argument '{name}' was not specified.");
}
static void ThrowArgumentNameNotFound(string argumentName)
{
throw new ArgumentParseFailedException($"Argument '{argumentName}' is not recognized.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool TryIncrementIndex(ref int index, int length)
{
if ((index + 1) < length)
{
index += 1;
return true;
}
return false;
}
static bool TryParseParamsArray<T>(ReadOnlySpan<string> args, ref T[] result, ref int i)
where T : IParsable<T>
{
result = new T[args.Length - i];
var resultIndex = 0;
for (; i < args.Length; i++)
{
if (!T.TryParse(args[i], null, out result[resultIndex++]!)) return false;
}
return true;
}
static bool TrySplitParse<T>(ReadOnlySpan<char> s, out T[] result)
where T : ISpanParsable<T>
{
if (s.StartsWith("["))
{
try
{
result = System.Text.Json.JsonSerializer.Deserialize<T[]>(s, JsonSerializerOptions)!;
return true;
}
catch
{
result = default!;
return false;
}
}
var count = s.Count(',') + 1;
result = new T[count];
var source = s;
var destination = result.AsSpan();
Span<Range> ranges = stackalloc Range[Math.Min(count, 128)];
while (true)
{
var splitCount = source.Split(ranges, ',');
var parseTo = splitCount;
if (splitCount == 128 && source[ranges[^1]].Contains(','))
{
parseTo = splitCount - 1;
}
for (int i = 0; i < parseTo; i++)
{
if (!T.TryParse(source[ranges[i]], null, out destination[i]!))
{
return false;
}
}
destination = destination.Slice(parseTo);
if (destination.Length != 0)
{
source = source[ranges[^1]];
continue;
}
else
{
break;
}
}
return true;
}
static void ValidateParameter(object? value, ParameterInfo parameter, ValidationContext validationContext, ref StringBuilder? errorMessages)
{
validationContext.DisplayName = parameter.Name ?? "";
validationContext.Items.Clear();
foreach (var validator in parameter.GetCustomAttributes<ValidationAttribute>(false))
{
var result = validator.GetValidationResult(value, validationContext);
if (result != null)
{
if (errorMessages == null)
{
errorMessages = new StringBuilder();
}
errorMessages.AppendLine(result.ErrorMessage);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool TryShowHelpOrVersion(ReadOnlySpan<string> args, int requiredParameterCount, int helpId)
{
if (args.Length == 0)
{
if (requiredParameterCount == 0) return false;
ShowHelp(helpId);
return true;
}
if (args.Length == 1)
{
switch (args[0])
{
case "--version":
ShowVersion();
return true;
case "-h":
case "--help":
ShowHelp(helpId);
return true;
default:
break;
}
}
return false;
}
static void ShowVersion()
{
if (Version != null)
{
Log(Version);
return;
}
var asm = Assembly.GetEntryAssembly();
var version = "1.0.0";
var infoVersion = asm!.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
if (infoVersion != null)
{
version = infoVersion.InformationalVersion;
var i = version.IndexOf('+');
if (i != -1)
{
version = version.Substring(0, i);
}
}
else
{
var asmVersion = asm!.GetCustomAttribute<AssemblyVersionAttribute>();
if (asmVersion != null)
{
version = asmVersion.Version;
}
}
Log(version);
}
static partial void ShowHelp(int helpId);
static async Task RunWithFilterAsync(string commandName, string[] args, int commandDepth, int escapeIndex, ConsoleAppFilter invoker, CancellationToken cancellationToken)
{
using var posixSignalHandler = PosixSignalHandler.Register(Timeout, cancellationToken);
try
{
await Task.Run(() => invoker.InvokeAsync(new ConsoleAppContext(commandName, args, null, commandDepth, escapeIndex), posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken);
}
catch (Exception ex)
{
if (ex is OperationCanceledException)
{
Environment.ExitCode = 130;
return;
}
Environment.ExitCode = 1;
if (ex is ValidationException or ArgumentParseFailedException)
{
LogError(ex.Message);
}
else
{
LogError(ex.ToString());
}
}
}
sealed class PosixSignalHandler : IDisposable
{
public CancellationToken Token => cancellationTokenSource.Token;
public CancellationToken TimeoutToken => timeoutCancellationTokenSource.Token;
CancellationTokenSource cancellationTokenSource;
CancellationTokenSource timeoutCancellationTokenSource;
TimeSpan timeout;
PosixSignalRegistration? sigInt;
PosixSignalRegistration? sigQuit;
PosixSignalRegistration? sigTerm;
PosixSignalHandler(TimeSpan timeout, CancellationToken cancellationToken)
{
this.cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
this.timeoutCancellationTokenSource = new CancellationTokenSource();
this.timeout = timeout;
}
public static PosixSignalHandler Register(TimeSpan timeout, CancellationToken cancellationToken)
{
var handler = new PosixSignalHandler(timeout, cancellationToken);
Action<PosixSignalContext> handleSignal = handler.HandlePosixSignal;
handler.sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, handleSignal);
handler.sigQuit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handleSignal);
handler.sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handleSignal);
return handler;
}
void HandlePosixSignal(PosixSignalContext context)
{
context.Cancel = true;
cancellationTokenSource.Cancel();
timeoutCancellationTokenSource.CancelAfter(timeout);
}
public void Dispose()
{
sigInt?.Dispose();
sigQuit?.Dispose();
sigTerm?.Dispose();
cancellationTokenSource.Dispose();
timeoutCancellationTokenSource.Dispose();
}
}
struct SyncAsyncDisposeWrapper<T>(T value) : IDisposable
where T : IAsyncDisposable
{
public readonly T Value => value;
public void Dispose()
{
value.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
}
internal partial class ConsoleAppBuilder
{
public ConsoleAppBuilder()
{
}
public void Add(string commandName, Delegate command)
{
AddCore(commandName, command);
}
[System.Diagnostics.Conditional("DEBUG")]
public void Add<T>() { }
[System.Diagnostics.Conditional("DEBUG")]
public void Add<T>(string commandPath) { }
[System.Diagnostics.Conditional("DEBUG")]
public void UseFilter<T>() where T : ConsoleAppFilter { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
partial void AddCore(string commandName, Delegate command);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
partial void RunCore(string[] args, CancellationToken cancellationToken);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
partial void RunAsyncCore(string[] args, CancellationToken cancellationToken, ref Task result);
partial void BuildAndSetServiceProvider();
static partial void ShowHelp(int helpId);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool TryShowHelpOrVersion(ReadOnlySpan<string> args, int requiredParameterCount, int helpId)
{
if (args.Length == 0)
{
if (requiredParameterCount == 0) return false;
ShowHelp(helpId);
return true;
}
if (args.Length == 1)
{
switch (args[0])
{
case "--version":
ShowVersion();
return true;
case "-h":
case "--help":
ShowHelp(helpId);
return true;
default:
break;
}
}
return false;
}
}
}
Code and pdf at
https://ignatandrei.github.io/RSCG_Examples/v2/docs/ConsoleAppFramework