Added CommandLineParser to AMWD.Common collection
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configurations>Debug;Release;DebugLocal</Configurations>
|
<Configurations>Debug;Release;DebugLocal</Configurations>
|
||||||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>10.0</LangVersion>
|
||||||
|
|
||||||
<AssemblyName>AMWD.Common.AspNetCore</AssemblyName>
|
<AssemblyName>AMWD.Common.AspNetCore</AssemblyName>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configurations>Debug;Release;DebugLocal</Configurations>
|
<Configurations>Debug;Release;DebugLocal</Configurations>
|
||||||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>10.0</LangVersion>
|
||||||
|
|
||||||
<AssemblyName>AMWD.Common.EntityFrameworkCore</AssemblyName>
|
<AssemblyName>AMWD.Common.EntityFrameworkCore</AssemblyName>
|
||||||
@@ -28,18 +28,11 @@
|
|||||||
<None Include="../icon.png" Pack="true" PackagePath="/" />
|
<None Include="../icon.png" Pack="true" PackagePath="/" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.20" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.20" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.20" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.20" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.9" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.9" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
36
AMWD.Common/Cli/Argument.cs
Normal file
36
AMWD.Common/Cli/Argument.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
namespace AMWD.Common.Cli
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a logical argument in the command line. Options with their additional
|
||||||
|
/// parameters are combined in one argument.
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
public class Argument
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initialises a new instance of the <see cref="Argument"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="option">The <see cref="Option"/> that is set in this argument; or null.</param>
|
||||||
|
/// <param name="values">The additional parameter values for the option; or the argument value.</param>
|
||||||
|
internal Argument(Option option, string[] values)
|
||||||
|
{
|
||||||
|
Option = option;
|
||||||
|
Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Option"/> that is set in this argument; or null.
|
||||||
|
/// </summary>
|
||||||
|
public Option Option { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the additional parameter values for the option; or the argument value.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Values { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the first item of <see cref="Values"/>; or null.
|
||||||
|
/// </summary>
|
||||||
|
public string Value => Values.Length > 0 ? Values[0] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
366
AMWD.Common/Cli/CommandLineParser.cs
Normal file
366
AMWD.Common/Cli/CommandLineParser.cs
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AMWD.Common.Cli
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides options and arguments parsing from command line arguments or a single string.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandLineParser
|
||||||
|
{
|
||||||
|
#region Private data
|
||||||
|
|
||||||
|
private string[] args;
|
||||||
|
private List<Argument> parsedArguments;
|
||||||
|
private readonly List<Option> options = new();
|
||||||
|
|
||||||
|
#endregion Private data
|
||||||
|
|
||||||
|
#region Configuration properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the option names are case-sensitive.
|
||||||
|
/// (Default: false)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCaseSensitive { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether incomplete options can be automatically
|
||||||
|
/// completed if there is only a single matching option.
|
||||||
|
/// (Default: true)
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoCompleteOptions { get; set; } = true;
|
||||||
|
|
||||||
|
#endregion Configuration properties
|
||||||
|
|
||||||
|
#region Custom arguments line parsing
|
||||||
|
|
||||||
|
// Source: http://stackoverflow.com/a/23961658/143684
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a single string into an arguments array.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="argsString">The string that contains the entire command line.</param>
|
||||||
|
public static string[] ParseArgsString(string argsString)
|
||||||
|
{
|
||||||
|
// Collects the split argument strings
|
||||||
|
var args = new List<string>();
|
||||||
|
|
||||||
|
// Builds the current argument
|
||||||
|
var currentArg = new StringBuilder();
|
||||||
|
|
||||||
|
// Indicates whether the last character was a backslash escape character
|
||||||
|
bool escape = false;
|
||||||
|
|
||||||
|
// Indicates whether we're in a quoted range
|
||||||
|
bool inQuote = false;
|
||||||
|
|
||||||
|
// Indicates whether there were quotes in the current arguments
|
||||||
|
bool hadQuote = false;
|
||||||
|
|
||||||
|
// Remembers the previous character
|
||||||
|
char prevCh = '\0';
|
||||||
|
|
||||||
|
// Iterate all characters from the input string
|
||||||
|
for (int i = 0; i < argsString.Length; i++)
|
||||||
|
{
|
||||||
|
char ch = argsString[i];
|
||||||
|
if (ch == '\\' && !escape)
|
||||||
|
{
|
||||||
|
// Beginning of a backslash-escape sequence
|
||||||
|
escape = true;
|
||||||
|
}
|
||||||
|
else if (ch == '\\' && escape)
|
||||||
|
{
|
||||||
|
// Double backslash, keep one
|
||||||
|
currentArg.Append(ch);
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
else if (ch == '"' && !escape)
|
||||||
|
{
|
||||||
|
// Toggle quoted range
|
||||||
|
inQuote = !inQuote;
|
||||||
|
hadQuote = true;
|
||||||
|
if (inQuote && prevCh == '"')
|
||||||
|
{
|
||||||
|
// Doubled quote within a quoted range is like escaping
|
||||||
|
currentArg.Append(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ch == '"' && escape)
|
||||||
|
{
|
||||||
|
// Backslash-escaped quote, keep it
|
||||||
|
currentArg.Append(ch);
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
else if (char.IsWhiteSpace(ch) && !inQuote)
|
||||||
|
{
|
||||||
|
if (escape)
|
||||||
|
{
|
||||||
|
// Add pending escape char
|
||||||
|
currentArg.Append('\\');
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
// Accept empty arguments only if they are quoted
|
||||||
|
if (currentArg.Length > 0 || hadQuote)
|
||||||
|
{
|
||||||
|
args.Add(currentArg.ToString());
|
||||||
|
}
|
||||||
|
// Reset for next argument
|
||||||
|
currentArg.Clear();
|
||||||
|
hadQuote = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (escape)
|
||||||
|
{
|
||||||
|
// Add pending escape char
|
||||||
|
currentArg.Append('\\');
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
// Copy character from input, no special meaning
|
||||||
|
currentArg.Append(ch);
|
||||||
|
}
|
||||||
|
prevCh = ch;
|
||||||
|
}
|
||||||
|
// Save last argument
|
||||||
|
if (currentArg.Length > 0 || hadQuote)
|
||||||
|
{
|
||||||
|
args.Add(currentArg.ToString());
|
||||||
|
}
|
||||||
|
return args.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the command line arguments from a single string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="argsString">The string that contains the entire command line.</param>
|
||||||
|
public void ReadArgs(string argsString)
|
||||||
|
{
|
||||||
|
args = ParseArgsString(argsString);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Custom arguments line parsing
|
||||||
|
|
||||||
|
#region Options management
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a named option without additional parameters.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The option name.</param>
|
||||||
|
/// <returns>The option instance.</returns>
|
||||||
|
public Option RegisterOption(string name)
|
||||||
|
{
|
||||||
|
return RegisterOption(name, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a named option.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The option name.</param>
|
||||||
|
/// <param name="parameterCount">The number of additional parameters for this option.</param>
|
||||||
|
/// <returns>The option instance.</returns>
|
||||||
|
public Option RegisterOption(string name, int parameterCount)
|
||||||
|
{
|
||||||
|
var option = new Option(name, parameterCount);
|
||||||
|
options.Add(option);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Options management
|
||||||
|
|
||||||
|
#region Parsing method
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses all command line arguments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">The command line arguments.</param>
|
||||||
|
public void Parse(string[] args)
|
||||||
|
{
|
||||||
|
this.args = args ?? throw new ArgumentNullException(nameof(args));
|
||||||
|
Parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses all command line arguments.
|
||||||
|
/// </summary>
|
||||||
|
public void Parse()
|
||||||
|
{
|
||||||
|
// Use args of the current process if no other source was given
|
||||||
|
if (args == null)
|
||||||
|
{
|
||||||
|
args = Environment.GetCommandLineArgs();
|
||||||
|
if (args.Length > 0)
|
||||||
|
{
|
||||||
|
// Skip myself (args[0])
|
||||||
|
args = args.Skip(1).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear/reset data
|
||||||
|
parsedArguments = new();
|
||||||
|
foreach (var option in options)
|
||||||
|
{
|
||||||
|
option.IsSet = false;
|
||||||
|
option.SetCount = 0;
|
||||||
|
option.Argument = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comparison = IsCaseSensitive
|
||||||
|
? StringComparison.Ordinal
|
||||||
|
: StringComparison.OrdinalIgnoreCase;
|
||||||
|
var argumentWalker = new EnumerableWalker<string>(args);
|
||||||
|
bool optMode = true;
|
||||||
|
foreach (string arg in argumentWalker.Cast<string>())
|
||||||
|
{
|
||||||
|
if (arg == "--")
|
||||||
|
{
|
||||||
|
optMode = false;
|
||||||
|
}
|
||||||
|
else if (optMode && (arg.StartsWith("/") || arg.StartsWith("-")))
|
||||||
|
{
|
||||||
|
string optName = arg.Substring(arg.StartsWith("--") ? 2 : 1);
|
||||||
|
|
||||||
|
// Split option value if separated with : or = instead of whitespace
|
||||||
|
int separatorIndex = optName.IndexOfAny(new[] { ':', '=' });
|
||||||
|
string optValue = null;
|
||||||
|
if (separatorIndex != -1)
|
||||||
|
{
|
||||||
|
optValue = optName.Substring(separatorIndex + 1);
|
||||||
|
optName = optName.Substring(0, separatorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the option with complete name match
|
||||||
|
var option = options.FirstOrDefault(o => o.Names.Any(n => n.Equals(optName, comparison)));
|
||||||
|
if (option == null)
|
||||||
|
{
|
||||||
|
// Try to complete the name to a unique registered option
|
||||||
|
var matchingOptions = options.Where(o => o.Names.Any(n => n.StartsWith(optName, comparison))).ToList();
|
||||||
|
if (AutoCompleteOptions && matchingOptions.Count > 1)
|
||||||
|
throw new Exception("Invalid option, completion is not unique: " + arg);
|
||||||
|
|
||||||
|
if (!AutoCompleteOptions || matchingOptions.Count == 0)
|
||||||
|
throw new Exception("Unknown option: " + arg);
|
||||||
|
|
||||||
|
// Accept the single auto-completed option
|
||||||
|
option = matchingOptions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for single usage
|
||||||
|
if (option.IsSingle && option.IsSet)
|
||||||
|
throw new Exception("Option cannot be set multiple times: " + arg);
|
||||||
|
|
||||||
|
// Collect option values from next argument strings
|
||||||
|
string[] values = new string[option.ParameterCount];
|
||||||
|
for (int i = 0; i < option.ParameterCount; i++)
|
||||||
|
{
|
||||||
|
if (optValue != null)
|
||||||
|
{
|
||||||
|
// The first value was included in this argument string
|
||||||
|
values[i] = optValue;
|
||||||
|
optValue = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fetch another argument string
|
||||||
|
values[i] = argumentWalker.GetNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values[i] == null)
|
||||||
|
throw new Exception("Missing argument " + (i + 1) + " for option: " + arg);
|
||||||
|
}
|
||||||
|
var argument = new Argument(option, values);
|
||||||
|
|
||||||
|
// Set usage data on the option instance for quick access
|
||||||
|
option.IsSet = true;
|
||||||
|
option.SetCount++;
|
||||||
|
option.Argument = argument;
|
||||||
|
|
||||||
|
if (option.Action != null)
|
||||||
|
{
|
||||||
|
option.Action(argument);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parsedArguments.Add(argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parsedArguments.Add(new Argument(null, new[] { arg }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingOption = options.FirstOrDefault(o => o.IsRequired && !o.IsSet);
|
||||||
|
if (missingOption != null)
|
||||||
|
throw new Exception("Missing required option: /" + missingOption.Names[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Parsing method
|
||||||
|
|
||||||
|
#region Parsed data properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parsed arguments.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||||
|
/// exception handling.
|
||||||
|
/// </remarks>
|
||||||
|
public Argument[] Arguments
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (parsedArguments == null)
|
||||||
|
Parse();
|
||||||
|
|
||||||
|
return parsedArguments.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the options that are set in the command line, including their value.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||||
|
/// exception handling.
|
||||||
|
/// </remarks>
|
||||||
|
public Option[] SetOptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (parsedArguments == null)
|
||||||
|
Parse();
|
||||||
|
|
||||||
|
return parsedArguments
|
||||||
|
.Where(a => a.Option != null)
|
||||||
|
.Select(a => a.Option)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the free arguments that are set in the command line and don't belong to an option.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// To avoid exceptions thrown, call the <see cref="Parse()"/> method in advance for
|
||||||
|
/// exception handling.
|
||||||
|
/// </remarks>
|
||||||
|
public string[] FreeArguments
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (parsedArguments == null)
|
||||||
|
Parse();
|
||||||
|
|
||||||
|
return parsedArguments
|
||||||
|
.Where(a => a.Option == null)
|
||||||
|
.Select(a => a.Value)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Parsed data properties
|
||||||
|
}
|
||||||
|
}
|
||||||
58
AMWD.Common/Cli/EnumerableWalker.cs
Normal file
58
AMWD.Common/Cli/EnumerableWalker.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AMWD.Common.Cli
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Walks through an <see cref="IEnumerable{T}"/> and allows retrieving additional items.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
internal class EnumerableWalker<T> : IEnumerable<T>
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<T> array;
|
||||||
|
private IEnumerator<T> enumerator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialises a new instance of the <see cref="EnumerableWalker{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="array">The array to walk though.</param>
|
||||||
|
public EnumerableWalker(IEnumerable<T> array)
|
||||||
|
{
|
||||||
|
this.array = array ?? throw new ArgumentNullException(nameof(array));
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator<T> IEnumerable<T>.GetEnumerator()
|
||||||
|
{
|
||||||
|
enumerator = array.GetEnumerator();
|
||||||
|
return enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the enumerator.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The enumerator.</returns>
|
||||||
|
public IEnumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
enumerator = array.GetEnumerator();
|
||||||
|
return enumerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the next item.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The next item.</returns>
|
||||||
|
public T GetNext()
|
||||||
|
{
|
||||||
|
if (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
return enumerator.Current;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
AMWD.Common/Cli/Option.cs
Normal file
113
AMWD.Common/Cli/Option.cs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AMWD.Common.Cli
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a named option.
|
||||||
|
/// </summary>
|
||||||
|
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||||
|
public class Option
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initialises a new instance of the <see cref="Option"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The primary name of the option.</param>
|
||||||
|
/// <param name="parameterCount">The number of additional parameters for this option.</param>
|
||||||
|
internal Option(string name, int parameterCount)
|
||||||
|
{
|
||||||
|
Names = new List<string>() { name };
|
||||||
|
ParameterCount = parameterCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the names of this option.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Names { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of additional parameters for this option.
|
||||||
|
/// </summary>
|
||||||
|
public int ParameterCount { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this option is required.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRequired { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this option can only be specified once.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSingle { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the action to invoke when the option is set.
|
||||||
|
/// </summary>
|
||||||
|
public Action<Argument> Action { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this option is set in the command line.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSet { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of times that this option is set in the command line.
|
||||||
|
/// </summary>
|
||||||
|
public int SetCount { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="Argument"/> instance that contains additional parameters set
|
||||||
|
/// for this option.
|
||||||
|
/// </summary>
|
||||||
|
public Argument Argument { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of the <see cref="Argument"/> instance for this option.
|
||||||
|
/// </summary>
|
||||||
|
public string Value => Argument?.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets alias names for this option.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="names">The alias names for this option.</param>
|
||||||
|
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||||
|
public Option Alias(params string[] names)
|
||||||
|
{
|
||||||
|
Names.AddRange(names);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks this option as required. If a required option is not set in the command line,
|
||||||
|
/// an exception is thrown on parsing.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||||
|
public Option Required()
|
||||||
|
{
|
||||||
|
IsRequired = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks this option as single. If a single option is set multiple times in the
|
||||||
|
/// command line, an exception is thrown on parsing.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||||
|
public Option Single()
|
||||||
|
{
|
||||||
|
IsSingle = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the action to invoke when the option is set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">The action to invoke when the option is set.</param>
|
||||||
|
/// <returns>The current <see cref="Option"/> instance.</returns>
|
||||||
|
public Option Do(Action<Argument> action)
|
||||||
|
{
|
||||||
|
Action = action;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.11.0...main) - 0000-00-00
|
## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.11.0...main) - 0000-00-00
|
||||||
|
|
||||||
_no changes yet_
|
### Added
|
||||||
|
|
||||||
|
- `CommandLineParser` as alternative to the `ConfigurationBuilder.AddCommandLine` from Microsoft
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Support for .NET Core 3.1
|
||||||
|
|
||||||
|
|
||||||
## [v1.11.0](https://git.am-wd.de/AM.WD/common/compare/v1.10.0...v1.11.0) - 2023-03-29
|
## [v1.11.0](https://git.am-wd.de/AM.WD/common/compare/v1.10.0...v1.11.0) - 2023-03-29
|
||||||
|
|
||||||
|
|||||||
306
UnitTests/Common/Cli/CommandLineParserTests.cs
Normal file
306
UnitTests/Common/Cli/CommandLineParserTests.cs
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using AMWD.Common.Cli;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace UnitTests.Common.Cli
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class CommandLineParserTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldParseStringToArgs()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
|
||||||
|
// act
|
||||||
|
string[] result = CommandLineParser.ParseArgsString("Option1 \"Option 2\" \"Some \"\" Option\" Foo=Bar \\ /help \\\\backslash \\\"escapedquote \\test");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsNotNull(result);
|
||||||
|
Assert.AreEqual(9, result.Length);
|
||||||
|
|
||||||
|
Assert.AreEqual("Option1", result[0]);
|
||||||
|
Assert.AreEqual("Option 2", result[1]);
|
||||||
|
Assert.AreEqual("Some \" Option", result[2]);
|
||||||
|
Assert.AreEqual("Foo=Bar", result[3]);
|
||||||
|
Assert.AreEqual("\\", result[4]);
|
||||||
|
Assert.AreEqual("/help", result[5]);
|
||||||
|
Assert.AreEqual("\\backslash", result[6]);
|
||||||
|
Assert.AreEqual("\"escapedquote", result[7]);
|
||||||
|
Assert.AreEqual("\\test", result[8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldReadArgs()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.ReadArgs("Option1 \"Option 2\"");
|
||||||
|
string[] args = parser.GetType().GetField("args", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(parser) as string[];
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsNotNull(args);
|
||||||
|
Assert.AreEqual(2, args.Length);
|
||||||
|
|
||||||
|
Assert.AreEqual("Option1", args[0]);
|
||||||
|
Assert.AreEqual("Option 2", args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldRegisterOptions()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.RegisterOption("opt1");
|
||||||
|
parser.RegisterOption("opt2", 1);
|
||||||
|
parser.RegisterOption("opt3", 2).Required().Single();
|
||||||
|
parser.RegisterOption("opt4").Alias("option4");
|
||||||
|
|
||||||
|
var options = parser.GetType().GetField("options", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(parser) as List<Option>;
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsNotNull(options);
|
||||||
|
Assert.AreEqual(4, options.Count);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, options.ElementAt(0).Names.Count);
|
||||||
|
Assert.AreEqual("opt1", options.ElementAt(0).Names.First());
|
||||||
|
Assert.AreEqual(0, options.ElementAt(0).ParameterCount);
|
||||||
|
Assert.IsFalse(options.ElementAt(0).IsSingle);
|
||||||
|
Assert.IsFalse(options.ElementAt(0).IsRequired);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, options.ElementAt(1).Names.Count);
|
||||||
|
Assert.AreEqual("opt2", options.ElementAt(1).Names.First());
|
||||||
|
Assert.AreEqual(1, options.ElementAt(1).ParameterCount);
|
||||||
|
Assert.IsFalse(options.ElementAt(1).IsSingle);
|
||||||
|
Assert.IsFalse(options.ElementAt(1).IsRequired);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, options.ElementAt(2).Names.Count);
|
||||||
|
Assert.AreEqual("opt3", options.ElementAt(2).Names.First());
|
||||||
|
Assert.AreEqual(2, options.ElementAt(2).ParameterCount);
|
||||||
|
Assert.IsTrue(options.ElementAt(2).IsSingle);
|
||||||
|
Assert.IsTrue(options.ElementAt(2).IsRequired);
|
||||||
|
|
||||||
|
Assert.AreEqual(2, options.ElementAt(3).Names.Count);
|
||||||
|
Assert.AreEqual("opt4", options.ElementAt(3).Names.First());
|
||||||
|
Assert.AreEqual("option4", options.ElementAt(3).Names.Last());
|
||||||
|
Assert.AreEqual(0, options.ElementAt(3).ParameterCount);
|
||||||
|
Assert.IsFalse(options.ElementAt(3).IsSingle);
|
||||||
|
Assert.IsFalse(options.ElementAt(3).IsRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldParse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
string argString = "/opt1 /opt2:two -opt3=three1 three2 --opt4:four /test:done -- foo bar";
|
||||||
|
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
parser.RegisterOption("opt1");
|
||||||
|
parser.RegisterOption("opt2", 1);
|
||||||
|
parser.RegisterOption("opt3", 2);
|
||||||
|
parser.RegisterOption("opt4", 1);
|
||||||
|
var opt = parser.RegisterOption("notUsed");
|
||||||
|
parser.RegisterOption("testing", 1);
|
||||||
|
|
||||||
|
parser.ReadArgs(argString);
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.Parse();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsFalse(opt.IsSet);
|
||||||
|
Assert.AreEqual(7, parser.Arguments.Length);
|
||||||
|
Assert.AreEqual(2, parser.FreeArguments.Length);
|
||||||
|
|
||||||
|
Assert.AreEqual("foo", parser.FreeArguments.First());
|
||||||
|
Assert.AreEqual("bar", parser.FreeArguments.Last());
|
||||||
|
|
||||||
|
Assert.IsTrue(parser.Arguments.ElementAt(0).Option.IsSet);
|
||||||
|
Assert.IsNull(parser.Arguments.ElementAt(0).Option.Value);
|
||||||
|
Assert.AreEqual(0, parser.Arguments.ElementAt(0).Values.Length);
|
||||||
|
|
||||||
|
Assert.IsTrue(parser.Arguments.ElementAt(1).Option.IsSet);
|
||||||
|
Assert.AreEqual("two", parser.Arguments.ElementAt(1).Option.Value);
|
||||||
|
Assert.AreEqual(1, parser.Arguments.ElementAt(1).Values.Length);
|
||||||
|
Assert.AreEqual("two", parser.Arguments.ElementAt(1).Values.First());
|
||||||
|
|
||||||
|
Assert.IsTrue(parser.Arguments.ElementAt(2).Option.IsSet);
|
||||||
|
Assert.AreEqual("three1", parser.Arguments.ElementAt(2).Option.Value);
|
||||||
|
Assert.AreEqual(2, parser.Arguments.ElementAt(2).Values.Length);
|
||||||
|
Assert.AreEqual("three1", parser.Arguments.ElementAt(2).Values.First());
|
||||||
|
Assert.AreEqual("three2", parser.Arguments.ElementAt(2).Values.Last());
|
||||||
|
|
||||||
|
Assert.IsTrue(parser.Arguments.ElementAt(3).Option.IsSet);
|
||||||
|
Assert.AreEqual("four", parser.Arguments.ElementAt(3).Option.Value);
|
||||||
|
Assert.AreEqual(1, parser.Arguments.ElementAt(3).Values.Length);
|
||||||
|
Assert.AreEqual("four", parser.Arguments.ElementAt(3).Values.First());
|
||||||
|
|
||||||
|
Assert.IsTrue(parser.Arguments.ElementAt(4).Option.IsSet);
|
||||||
|
Assert.AreEqual("testing", parser.Arguments.ElementAt(4).Option.Names.First());
|
||||||
|
Assert.AreEqual("done", parser.Arguments.ElementAt(4).Option.Value);
|
||||||
|
Assert.AreEqual(1, parser.Arguments.ElementAt(4).Values.Length);
|
||||||
|
Assert.AreEqual("done", parser.Arguments.ElementAt(4).Values.First());
|
||||||
|
|
||||||
|
Assert.IsNull(parser.Arguments.ElementAt(5).Option);
|
||||||
|
Assert.AreEqual("foo", parser.Arguments.ElementAt(5).Value);
|
||||||
|
Assert.AreEqual(1, parser.Arguments.ElementAt(5).Values.Length);
|
||||||
|
|
||||||
|
Assert.IsNull(parser.Arguments.ElementAt(6).Option);
|
||||||
|
Assert.AreEqual("bar", parser.Arguments.ElementAt(6).Value);
|
||||||
|
Assert.AreEqual(1, parser.Arguments.ElementAt(6).Values.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldExecuteOptionActionOnParse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
Argument actionArgument = null;
|
||||||
|
|
||||||
|
string[] args = new[] { "/run", "--opt" };
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
parser.RegisterOption("opt").Required();
|
||||||
|
parser.RegisterOption("run").Do(arg => actionArgument = arg);
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.Parse(args);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsNotNull(actionArgument);
|
||||||
|
Assert.IsNotNull(actionArgument.Option);
|
||||||
|
Assert.AreEqual("run", actionArgument.Option.Names.First());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldReturnSetOptions()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
string argString = "/Opt1 --opt3";
|
||||||
|
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
parser.ReadArgs(argString);
|
||||||
|
|
||||||
|
parser.RegisterOption("opt1");
|
||||||
|
parser.RegisterOption("opt2");
|
||||||
|
parser.RegisterOption("opt3");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var opts = parser.SetOptions;
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.AreEqual(2, opts.Length);
|
||||||
|
|
||||||
|
Assert.AreEqual("opt1", opts.First().Names.First());
|
||||||
|
Assert.AreEqual("opt3", opts.Last().Names.First());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(ArgumentNullException))]
|
||||||
|
public void ShouldThrowExceptionOnNullArgs()
|
||||||
|
{
|
||||||
|
string[] args = null;
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.Parse(args);
|
||||||
|
|
||||||
|
// assert - ArgumentNullException
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(Exception))]
|
||||||
|
public void ShouldThrowExceptionOnMultipleAutocomplete()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
string[] args = new[] { "/Opt:on" };
|
||||||
|
var parser = new CommandLineParser
|
||||||
|
{
|
||||||
|
IsCaseSensitive = true
|
||||||
|
};
|
||||||
|
parser.RegisterOption("Option1", 1);
|
||||||
|
parser.RegisterOption("Option2", 1);
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.Parse(args);
|
||||||
|
|
||||||
|
// assert - Exception
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(Exception))]
|
||||||
|
public void ShouldThrowExceptionOnMissingOption()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
string[] args = new[] { "/Option:on" };
|
||||||
|
var parser = new CommandLineParser
|
||||||
|
{
|
||||||
|
AutoCompleteOptions = false
|
||||||
|
};
|
||||||
|
parser.RegisterOption("Opt1", 1);
|
||||||
|
parser.RegisterOption("Opt2", 1);
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.Parse(args);
|
||||||
|
|
||||||
|
// assert - Exception
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(Exception))]
|
||||||
|
public void ShouldTrhowExceptionOnDuplicateOption()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
string[] args = new[] { "/Opt:on", "--opt=off" };
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
parser.RegisterOption("opt", 1).Single();
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.Parse(args);
|
||||||
|
|
||||||
|
// assert - Exception
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(Exception))]
|
||||||
|
public void ShouldThrowExceptionOnMissingArgument()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
string[] args = new[] { "/Option" };
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
parser.RegisterOption("option", 1);
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.Parse(args);
|
||||||
|
|
||||||
|
// assert - Exception
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(Exception))]
|
||||||
|
public void ShouldThrowExceptionForMissingRequiredOption()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
string[] args = new[] { "/opt" };
|
||||||
|
var parser = new CommandLineParser();
|
||||||
|
parser.RegisterOption("opt").Required();
|
||||||
|
parser.RegisterOption("foo").Required();
|
||||||
|
|
||||||
|
// act
|
||||||
|
parser.Parse(args);
|
||||||
|
|
||||||
|
// assert - Exception
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
UnitTests/Common/Cli/EnumerableWalkerTests.cs
Normal file
96
UnitTests/Common/Cli/EnumerableWalkerTests.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using AMWD.Common.Cli;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace UnitTests.Common.Cli
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class EnumerableWalkerTests
|
||||||
|
{
|
||||||
|
private List<string> list;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
list = new List<string>
|
||||||
|
{
|
||||||
|
"one",
|
||||||
|
"two",
|
||||||
|
"three",
|
||||||
|
"four",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[ExpectedException(typeof(ArgumentNullException))]
|
||||||
|
public void ShouldThrowExceptionOnNullReference()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
|
||||||
|
// act
|
||||||
|
_ = new EnumerableWalker<object>(null);
|
||||||
|
|
||||||
|
// assert - ArgumentNullException
|
||||||
|
Assert.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldReturnEnumerator()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var walker = new EnumerableWalker<string>(list);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var enumerator = walker.GetEnumerator();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsNotNull(enumerator);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldReturnGenericEnumerator()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var walker = new EnumerableWalker<string>(list);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var enumerator = ((IEnumerable<string>)walker).GetEnumerator();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.IsNotNull(enumerator);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldReturnItems()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var walker = new EnumerableWalker<string>(list);
|
||||||
|
_ = walker.GetEnumerator();
|
||||||
|
|
||||||
|
string[] items = new string[list.Count];
|
||||||
|
|
||||||
|
// act
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
items[i] = walker.GetNext();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
Assert.AreEqual(list[i], items[i], $"Position {i} failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldReturnDefaultWhenNothingLeft()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var walker = new EnumerableWalker<string>(Array.Empty<string>());
|
||||||
|
_ = walker.GetEnumerator();
|
||||||
|
|
||||||
|
// act
|
||||||
|
string item = walker.GetNext();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.AreEqual(default(string), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user