Initial commit with code already here.

This commit is contained in:
That-One-Nerd 2025-02-04 18:43:36 -05:00
commit 9fdd499d7a
12 changed files with 543 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Visual Studio things.
.vs/
# Build Stuff
*/bin
*/obj

25
ArgumentBase.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34309.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArgumentBase", "ArgumentBase\ArgumentBase.csproj", "{E6AFC35D-D69A-4956-9ED4-AF0276192EE8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E6AFC35D-D69A-4956-9ED4-AF0276192EE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E6AFC35D-D69A-4956-9ED4-AF0276192EE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E6AFC35D-D69A-4956-9ED4-AF0276192EE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E6AFC35D-D69A-4956-9ED4-AF0276192EE8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DF77961F-683C-444B-9866-FB4C081B3E24}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,12 @@
using System.Reflection;
namespace ArgumentBase;
public class ArgFlagInfo
{
public required string Name { get; init; }
public required string Description { get; init; }
public required PropertyInfo Property { get; init; }
internal ArgFlagInfo() { }
}

View File

@ -0,0 +1,13 @@
using System.Reflection;
namespace ArgumentBase;
public class ArgParameterInfo
{
public required int Order { get; init; }
public required string Name { get; init; }
public required string Description { get; init; }
public required PropertyInfo Property { get; init; }
internal ArgParameterInfo() { }
}

View File

@ -0,0 +1,12 @@
using System.Reflection;
namespace ArgumentBase;
public class ArgVariableInfo
{
public required string Name { get; init; }
public required string Description { get; init; }
public required PropertyInfo Property { get; init; }
internal ArgVariableInfo() { }
}

View File

@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Text;
namespace ArgumentBase;
#pragma warning disable IL2070, IL2090 // Shut up.
public abstract class ArgumentBase<TSelf> where TSelf : ArgumentBase<TSelf>, new()
{
public static ReadOnlyCollection<ArgParameterInfo> Parameters { get; private set; }
public static ReadOnlyCollection<ArgVariableInfo> Variables { get; private set; }
public static ReadOnlyCollection<ArgFlagInfo> Flags { get; private set; }
public static ReadOnlyDictionary<string, string> ParameterDescriptions { get; private set; }
public static ReadOnlyDictionary<string, string> VariableDescriptions { get; private set; }
public static ReadOnlyDictionary<string, string> FlagDescriptions { get; private set; }
public static ReadOnlyDictionary<string, PropertyInfo> VariableTable { get; private set; }
public static ReadOnlyDictionary<string, PropertyInfo> FlagTable { get; private set; }
static ArgumentBase()
{
IEnumerable<PropertyInfo> allProps = typeof(TSelf).GetProperties().Where(x => x.SetMethod is not null);
List<ArgParameterInfo> parameters = [];
List<ArgVariableInfo> variables = [];
List<ArgFlagInfo> flags = [];
Dictionary<string, string> paramDesc = [], varDesc = [], flagDesc = [];
Dictionary<string, PropertyInfo> varTable = [], flagTable = [];
foreach (PropertyInfo prop in allProps)
{
IsParameterAttribute? paramAtt = prop.GetCustomAttribute<IsParameterAttribute>();
IsVariableAttribute? varAtt = prop.GetCustomAttribute<IsVariableAttribute>();
IsFlagAttribute? flagAtt = prop.GetCustomAttribute<IsFlagAttribute>();
if (paramAtt is not null)
{
ArgParameterInfo info = new()
{
Order = paramAtt.order,
Name = paramAtt.name ?? prop.Name,
Description = paramAtt.description,
Property = prop
};
parameters.Add(info);
string trueName = info.Name.Trim();
paramDesc.Add(trueName, info.Description);
}
if (varAtt is not null)
{
ArgVariableInfo info = new()
{
Name = varAtt.name ?? prop.Name,
Description = varAtt.description,
Property = prop
};
variables.Add(info);
string trueName = $"-{info.Name.Trim()}";
varDesc.Add(trueName, info.Description);
varTable.Add(trueName, info.Property);
}
if (flagAtt is not null)
{
ArgFlagInfo info = new()
{
Name = flagAtt.name ?? prop.Name,
Description = flagAtt.description,
Property = prop
};
flags.Add(info);
string trueName = $"--{info.Name.Trim()}";
flagDesc.Add(trueName, info.Description);
flagTable.Add(trueName, info.Property);
}
}
parameters.Sort((a, b) => a.Order.CompareTo(b.Order));
Parameters = parameters.AsReadOnly();
Variables = variables.AsReadOnly();
Flags = flags.AsReadOnly();
// I would sort these, but I would need yet another for loop to do that.
ParameterDescriptions = paramDesc.AsReadOnly();
VariableDescriptions = varDesc.AsReadOnly();
FlagDescriptions = flagDesc.AsReadOnly();
VariableTable = varTable.AsReadOnly();
FlagTable = flagTable.AsReadOnly();
}
public static TSelf Parse(string[] args)
{
TSelf result = new();
if (args.Length == 0) return result;
result.anyArguments = true;
int parameterIndex = 0;
List<string> unknownParams = [], unknownVars = [], unknownFlags = [];
List<string> badValParams = [], badValVars = [];
foreach (string arg in args)
{
if (arg.StartsWith("--")) // Flag
{
string name = arg[2..];
ArgFlagInfo? flag = Flags.SingleOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (flag is null)
{
// Unknown flag.
unknownFlags.Add(name);
continue;
}
// Flip flag.
bool original = (bool)flag.Property.GetValue(result)!;
flag.Property.SetValue(result, !original);
result.anyFlags = true;
}
else if (arg.StartsWith('-')) // Variable
{
int splitter = arg.IndexOf(':');
string name = arg[1..splitter], valueStr = arg[(splitter + 1)..];
ArgVariableInfo? var = Variables.SingleOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (var is null)
{
// Unknown variable.
unknownVars.Add(name);
continue;
}
object? value = parseObject(valueStr, var.Property.PropertyType);
if (value is null)
{
// Issue parsing the object.
badValVars.Add(name);
continue;
}
var.Property.SetValue(result, value);
result.anyVars = true;
}
else // Parameter
{
if (parameterIndex >= Parameters.Count)
{
// Too many parameters.
unknownParams.Add(arg);
continue;
}
ArgParameterInfo param = Parameters[parameterIndex];
parameterIndex++;
object? value = parseObject(arg, param.Property.PropertyType);
if (value is null)
{
// Issue parsing the object.
badValParams.Add(arg);
continue;
}
param.Property.SetValue(result, value);
result.anyParams = true;
}
}
int unknownArgs = unknownParams.Count + unknownVars.Count + unknownFlags.Count;
if (unknownArgs > 0)
{
// Unknown arguments.
StringBuilder warnMsg = new();
warnMsg.Append($" \x1b[3;33m{unknownArgs} {(unknownArgs == 1 ? "argument was" : "arguments were")} not recognized:\n ");
int lineLen = 2, maxLineLen = (int)(0.65 * Console.WindowWidth - 1);
foreach (string badParam in unknownParams)
{
if (lineLen + badParam.Length + 3 > maxLineLen)
{
warnMsg.Append("\n ");
lineLen = 2;
}
warnMsg.Append($"\"\x1b[37m{badParam}\x1b[33m\" ");
lineLen += badParam.Length + 3;
}
foreach (string badVar in unknownVars)
{
if (lineLen + badVar.Length + 4 > maxLineLen)
{
warnMsg.Append("\n ");
lineLen = 2;
}
warnMsg.Append($"\"\x1b[36m-{badVar}\x1b[33m\" ");
lineLen += badVar.Length + 4;
}
foreach (string badFlag in unknownFlags)
{
if (lineLen + badFlag.Length + 5 > maxLineLen)
{
warnMsg.Append("\n ");
lineLen = 2;
}
warnMsg.Append($"\"\x1b[90m--{badFlag}\x1b[33m\" ");
lineLen += badFlag.Length + 5;
}
warnMsg.AppendLine("\x1b[0m");
Console.WriteLine(warnMsg);
}
int badValArgs = badValParams.Count + badValVars.Count;
if (badValArgs > 0)
{
// Issue parsing arguments.
StringBuilder warnMsg = new();
warnMsg.Append($" \x1b[3;33m{badValArgs} {(badValArgs == 1 ? "value" : "values")} couldn't be parsed:\n ");
int lineLen = 2, maxLineLen = (int)(0.65 * Console.WindowWidth - 1);
foreach (string badParam in badValParams)
{
if (lineLen + badParam.Length + 3 > maxLineLen)
{
warnMsg.Append("\n ");
lineLen = 2;
}
warnMsg.Append($"\"\x1b[37m{badParam}\x1b[33m\" ");
lineLen += badParam.Length + 3;
}
foreach (string badVar in badValVars)
{
if (lineLen + badVar.Length + 4 > maxLineLen)
{
warnMsg.Append("\n ");
lineLen = 2;
}
warnMsg.Append($"\"\x1b[36m-{badVar}\x1b[33m\" ");
lineLen += badVar.Length + 4;
}
warnMsg.AppendLine("\x1b[0m");
Console.WriteLine(warnMsg);
}
return result;
static object? parseObject(string value, Type desired)
{
if (desired == typeof(string)) return value;
else if (desired.IsEnum) return Enum.TryParse(desired, value, true, out object? enumResult) ? enumResult : null;
else if (desired.GetInterface("IParsable`1") is null) throw new("Type must derive from IParsable. Sorry.");
else
{
// A bit large. Couldn't condense into a single lambda because I wanted to cache the parameters.
// I have to do all this weird stuff as a whole to be able to parse any object that derives
// from IParsable, making extra stuff easier to implement.
MethodInfo tryParseMethod = (from x in desired.GetMethods()
let parameters = x.GetParameters()
let goodName = x.Name == "TryParse"
let goodAttributes = x.IsPublic && x.IsStatic
let goodParams = parameters.Length == 2
&& parameters[0].ParameterType == typeof(string)
&& parameters[1].ParameterType.GetElementType() == desired
where goodName && goodAttributes && goodParams
select x).Single(); // Must exist according to IParsable.
// Output parameters are placed in the array.
object?[] methodParams = [value, null];
bool success = (bool)tryParseMethod.Invoke(null, methodParams)!;
return success ? methodParams[1] : null;
}
}
}
public static void PrintParameters(string? keyFormat = null)
{
if (Parameters.Count == 0) return;
else PrintHelper.PrintKeyValues("Parameters", 2, ParameterDescriptions.ToDictionary(), keyFormat: keyFormat ?? "\x1b[37m");
}
public static void PrintVariables()
{
if (Variables.Count == 0) return;
else PrintHelper.PrintKeyValues("Variables", 2, VariableDescriptions.ToDictionary(), keyFormat: "\x1b[36m");
}
public static void PrintFlags()
{
if (Flags.Count == 0) return;
else PrintHelper.PrintKeyValues("Flags", 2, FlagDescriptions.ToDictionary());
}
public bool anyArguments;
public bool anyParams, anyVars, anyFlags;
}

View File

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageId>ArgumentBase</PackageId>
<Title>ArgumentBase</Title>
<Version>1.0.0</Version>
<Authors>That_One_Nerd</Authors>
<Description>A small library that handles parsing console arguments.</Description>
<Copyright>MIT</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,21 @@
using System;
namespace ArgumentBase;
[AttributeUsage(AttributeTargets.Property)]
public class IsFlagAttribute : Attribute
{
public readonly string description;
public readonly string? name;
public IsFlagAttribute(string description)
{
this.description = description;
name = null;
}
public IsFlagAttribute(string name, string description)
{
this.description = description;
this.name = name;
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace ArgumentBase;
[AttributeUsage(AttributeTargets.Property)]
public class IsParameterAttribute : Attribute
{
public readonly int order;
public readonly string description;
public readonly string? name;
public IsParameterAttribute(int order, string description)
{
this.order = order;
this.description = description;
name = null;
}
public IsParameterAttribute(int order, string name, string description)
{
this.order = order;
this.description = description;
this.name = name;
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace ArgumentBase;
[AttributeUsage(AttributeTargets.Property)]
public class IsVariableAttribute : Attribute
{
public readonly string description;
public readonly string? name;
public IsVariableAttribute(string description)
{
this.description = description;
name = null;
}
public IsVariableAttribute(string name, string description)
{
this.description = description;
this.name = name;
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
namespace ArgumentBase;
internal static class PrintHelper
{
public static void PrintKeyValues(string title, int indent, Dictionary<string, string> values,
string? keyFormat = null, string? separatorFormat = null, string? valueFormat = null)
{
StringBuilder result = new();
result.Append($"{new string(' ', indent)}\x1b[1;97m{title}:\x1b[22m\n");
int maxLength = 0;
StringBuilder[] lines = new StringBuilder[values.Count];
IEnumerator<KeyValuePair<string, string>> iterator = values.GetEnumerator();
for (int i = 0; i < values.Count; i++)
{
iterator.MoveNext();
KeyValuePair<string, string> kv = iterator.Current;
lines[i] = new StringBuilder().Append($"{new string(' ', indent + 2)}{keyFormat ?? "\x1b[90m"}{kv.Key}");
int rawKeyLength = visibleStringLength(kv.Key);
if (rawKeyLength > maxLength) maxLength = kv.Key.Length;
}
int desired = maxLength + 2;
iterator.Reset();
for (int i = 0; i < values.Count; i++)
{
iterator.MoveNext();
KeyValuePair<string, string> kv = iterator.Current;
int rawKeyLength = visibleStringLength(kv.Key);
int remaining = desired - rawKeyLength;
lines[i].Append($"{new string(' ', remaining)}{separatorFormat ?? "\x1b[91m- "}{valueFormat ?? "\x1b[37m"}{kv.Value}\x1b[0m");
result.Append(lines[i]);
result.AppendLine();
}
Console.WriteLine(result);
static int visibleStringLength(string str) => str.ToCharArray().Where(x => !char.IsControl(x)).Count();
}
}

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# ArgumentBase
This is a small library that will parse your command-line arguments for you in a pretty easy-to-use fashion.
This library only exists because I wrote this code for a different project and then found myself copy-pasting
it constantly. This is to ease my own suffering.
This is largely purpose-built for me, but feel free to use it. That's why it's here.
## How to use
Import the package using your preferred method. Then, once it's installed, create a class like this:
```csharp
public class YourArguments : ArgumentBase<YourArguments>
{
// Positional arguments are called "parameters." No prefix is necessary.
[IsParameter(1, "An example description for the parameter.")]
public string ExampleParameter { get; set; } // It must be a property with a set method or it won't be identified.
// Variables are formatted like this: "-var:3.14"
[IsVariable("var", "An example description for the variable.")]
public double ExampleVariable { get; set; } // Parameters and variables can be any object that derives from IParsable.
// Flags are formatted like this: "--flag"
[IsFlag("flag", "An example description for the flag.")]
public bool ExampleFlag { get; set; } // Flags, however, can only be booleans.
// Whenever a flag is included as an argument, its value is flipped.
}
```
Then, in your Main method, paste the following:
```csharp
static void Main(string[] args)
{
YourArguments args = YourArguments.Parse(args);
}
```
That's it. Not too bad, right?