Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removes custom args validation and uses library one #7359

Merged
merged 3 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Nethermind/Nethermind.Config/ExitCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ public static class ExitCodes
public const int TooLongExtraData = 102;
public const int ConflictingConfigurations = 103;
public const int LowDiskSpace = 104;
public const int DuplicatedArguments = 105;
public const int UnrecognizedArgument = 106;
public const int DuplicatedOption = 105;
public const int UnrecognizedOption = 106;
public const int ForbiddenOptionValue = 107;

// Posix exit code
// https://tldp.org/LDP/abs/html/exitcodes.html
Expand Down
114 changes: 39 additions & 75 deletions src/Nethermind/Nethermind.Runner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

namespace Nethermind.Runner;

public static class Program
public static partial class Program
{
private const string FailureString = "Failure";
private const string DefaultConfigsDirectory = "configs";
Expand Down Expand Up @@ -152,8 +152,6 @@ private static void Run(string[] args)

BuildOptionsFromConfigFiles(app);

if (!ValidateArguments(args, app)) return;

app.OnExecute(async () =>
{
IConfigProvider configProvider = BuildConfigProvider(app, loggerConfigSource, logLevelOverride, configsDirectory, configFile);
Expand Down Expand Up @@ -232,6 +230,41 @@ private static void Run(string[] args)
{
Environment.ExitCode = app.Execute(args);
}
catch (UnrecognizedCommandParsingException e)
{
string[] matches = e.NearestMatches.Take(3).ToArray();
string suggestion = matches.Length switch
{
0 => "",
1 => $" Did you mean {matches[0]}",
_ => $" Did you mean one of: {string.Join(", ", matches)}"
};
_logger.Error($"{e.Message}.{suggestion}");
Environment.ExitCode = ExitCodes.UnrecognizedOption;
}
catch (CommandParsingException e)
{
Regex regex = GetUnexpectedConfigValueRegex();
Match match = regex.Match(e.Message);
if (match.Success)
{
string option = match.Groups["Name"].Value;
CommandOption? optionInfo = app.GetOptions().FirstOrDefault(o => o.ShortName == option || o.LongName == option);
switch (optionInfo?.OptionType)
{
case CommandOptionType.SingleValue or CommandOptionType.SingleOrNoValue:
_logger.Error($"Duplicated option '{option}'");
Environment.ExitCode = ExitCodes.DuplicatedOption;
return;
case CommandOptionType.NoValue:
_logger.Error($"Value {match.Groups["Value"].Value} passed for value-less option '{option}'");
Environment.ExitCode = ExitCodes.ForbiddenOptionValue;
return;
}
}

_logger.Error($"{e.Message}");
}
catch (Exception e)
{
if (e is IExceptionWithExitCode withExit)
Expand All @@ -250,6 +283,9 @@ private static void Run(string[] args)
}
}

[GeneratedRegex("^Unexpected value '(?<Value>.+)' for option '(?<Name>.+)'", RegexOptions.Singleline)]
private static partial Regex GetUnexpectedConfigValueRegex();

private static IntPtr OnResolvingUnmanagedDll(Assembly _, string nativeLibraryName)
{
var alternativePath = nativeLibraryName switch
Expand Down Expand Up @@ -560,76 +596,4 @@ private static string GetProductInfo()

return info.ToString();
}

private static bool ValidateArguments(string[] args, CommandLineApplication app)
{
static HashSet<ReadOnlyMemory<char>> GetValidArguments(CommandLineApplication app)
{
HashSet<ReadOnlyMemory<char>> validArguments = new(new MemoryContentsComparer<char>());
foreach (var option in app.GetOptions())
{
if (!string.IsNullOrEmpty(option.LongName))
{
validArguments.Add(option.LongName.AsMemory());
}
if (!string.IsNullOrEmpty(option.ShortName))
{
validArguments.Add(option.ShortName.AsMemory());
}
}

return validArguments;
}

static ReadOnlyMemory<char> GetArgumentName(string arg) => arg.StartsWith("--") ? arg.AsMemory(2) : arg.StartsWith('-') ? arg.AsMemory(1) : ReadOnlyMemory<char>.Empty;
static IEnumerable<ReadOnlyMemory<char>> GetArgumentNames(IEnumerable<string> args)
{
bool lastWasArgument = false;
foreach (ReadOnlyMemory<char> potentialArgument in args.Select(GetArgumentName))
{
if (!lastWasArgument)
{
bool isCurrentArgument = lastWasArgument = !potentialArgument.IsEmpty;
if (isCurrentArgument)
{
yield return potentialArgument;
}
}
else
{
lastWasArgument = false;
}
}
}

static IEnumerable<ReadOnlyMemory<char>> GetDuplicateArguments(IEnumerable<ReadOnlyMemory<char>> argumentsNames) =>
argumentsNames.GroupBy(n => n, new MemoryContentsComparer<char>())
.Where(g => g.Count() > 1)
.Select(g => g.Key);

// Get all valid options from the configuration files
HashSet<ReadOnlyMemory<char>> validArguments = GetValidArguments(app);

IEnumerable<ReadOnlyMemory<char>> argumentsNamesProvided = GetArgumentNames(args);
foreach (ReadOnlyMemory<char> argumentName in argumentsNamesProvided)
{
// Check if the argument provided is a valid option/argument
if (!validArguments.Contains(argumentName))
{
_logger.Error($"Failed due to unrecognized argument - [{argumentName}].\nRun --help for a list of available options and commands.");
Environment.ExitCode = ExitCodes.UnrecognizedArgument;
return false;
}
}

string duplicateArgumentsList = string.Join(", ", GetDuplicateArguments(argumentsNamesProvided));
if (!string.IsNullOrEmpty(duplicateArgumentsList))
{
_logger.Error($"Failed due to duplicated arguments - [{duplicateArgumentsList}] passed while execution");
Environment.ExitCode = ExitCodes.DuplicatedArguments;
return false;
}

return true;
}
}