using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
{
/// Provides methods for parsing command-line arguments.
internal class ArgumentParser : IReadOnlyList
{
/*********
** Fields
*********/
/// The command name for errors.
private readonly string CommandName;
/// The arguments to parse.
private readonly string[] Args;
/// Writes messages to the console and log file.
private readonly IMonitor Monitor;
/*********
** Accessors
*********/
/// Get the number of arguments.
public int Count => this.Args.Length;
/// Get the argument at the specified index in the list.
/// The zero-based index of the element to get.
public string this[int index] => this.Args[index];
/*********
** Public methods
*********/
/// Construct an instance.
/// The command name for errors.
/// The arguments to parse.
/// Writes messages to the console and log file.
public ArgumentParser(string commandName, string[] args, IMonitor monitor)
{
this.CommandName = commandName;
this.Args = args;
this.Monitor = monitor;
}
/// Try to read a string argument.
/// The argument index.
/// The argument name for error messages.
/// The parsed value.
/// Whether to show an error if the argument is missing.
/// Require that the argument match one of the given values (case-insensitive).
public bool TryGet(int index, string name, out string value, bool required = true, string[] oneOf = null)
{
value = null;
// validate
if (this.Args.Length < index + 1)
{
if (required)
this.LogError($"Argument {index} ({name}) is required.");
return false;
}
if (oneOf?.Any() == true && !oneOf.Contains(this.Args[index], StringComparer.InvariantCultureIgnoreCase))
{
this.LogError($"Argument {index} ({name}) must be one of {string.Join(", ", oneOf)}.");
return false;
}
// get value
value = this.Args[index];
return true;
}
/// Try to read an integer argument.
/// The argument index.
/// The argument name for error messages.
/// The parsed value.
/// Whether to show an error if the argument is missing.
/// The minimum value allowed.
/// The maximum value allowed.
public bool TryGetInt(int index, string name, out int value, bool required = true, int? min = null, int? max = null)
{
value = 0;
// get argument
if (!this.TryGet(index, name, out string raw, required))
return false;
// parse
if (!int.TryParse(raw, out value))
{
this.LogIntFormatError(index, name, min, max);
return false;
}
// validate
if ((min.HasValue && value < min) || (max.HasValue && value > max))
{
this.LogIntFormatError(index, name, min, max);
return false;
}
return true;
}
/// Try to read a decimal argument.
/// The argument index.
/// The argument name for error messages.
/// The parsed value.
/// Whether to show an error if the argument is missing.
/// The minimum value allowed.
/// The maximum value allowed.
public bool TryGetDecimal(int index, string name, out decimal value, bool required = true, decimal? min = null, decimal? max = null)
{
value = 0;
// get argument
if (!this.TryGet(index, name, out string raw, required))
return false;
// parse
if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out value))
{
this.LogDecimalFormatError(index, name, min, max);
return false;
}
// validate
if ((min.HasValue && value < min) || (max.HasValue && value > max))
{
this.LogDecimalFormatError(index, name, min, max);
return false;
}
return true;
}
/// Returns an enumerator that iterates through the collection.
/// An enumerator that can be used to iterate through the collection.
public IEnumerator GetEnumerator()
{
return ((IEnumerable)this.Args).GetEnumerator();
}
/// Returns an enumerator that iterates through a collection.
/// An object that can be used to iterate through the collection.
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
/*********
** Private methods
*********/
/// Log a usage error.
/// The message describing the error.
private void LogError(string message)
{
this.Monitor.Log($"{message} Type 'help {this.CommandName}' for usage.", LogLevel.Error);
}
/// Print an error for an invalid int argument.
/// The argument index.
/// The argument name for error messages.
/// The minimum value allowed.
/// The maximum value allowed.
private void LogIntFormatError(int index, string name, int? min, int? max)
{
if (min.HasValue && max.HasValue)
this.LogError($"Argument {index} ({name}) must be an integer between {min} and {max}.");
else if (min.HasValue)
this.LogError($"Argument {index} ({name}) must be an integer and at least {min}.");
else if (max.HasValue)
this.LogError($"Argument {index} ({name}) must be an integer and at most {max}.");
else
this.LogError($"Argument {index} ({name}) must be an integer.");
}
/// Print an error for an invalid decimal argument.
/// The argument index.
/// The argument name for error messages.
/// The minimum value allowed.
/// The maximum value allowed.
private void LogDecimalFormatError(int index, string name, decimal? min, decimal? max)
{
if (min.HasValue && max.HasValue)
this.LogError($"Argument {index} ({name}) must be a decimal between {min} and {max}.");
else if (min.HasValue)
this.LogError($"Argument {index} ({name}) must be a decimal and at least {min}.");
else if (max.HasValue)
this.LogError($"Argument {index} ({name}) must be a decimal and at most {max}.");
else
this.LogError($"Argument {index} ({name}) must be a decimal.");
}
}
}