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."); } } }