// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text; using Microsoft.Extensions.Logging; #pragma warning disable CA1710 // Identifiers should have correct suffix namespace EpicGames.Core { /// /// Helper class to visualize an argument list /// class CommandLineArgumentListView { /// /// The list of arguments /// [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public string[] Arguments { get; } /// /// Constructor /// /// The argument list to proxy public CommandLineArgumentListView(CommandLineArguments argumentList) { Arguments = argumentList.GetRawArray(); } } /// /// Exception thrown for invalid command line arguments /// public class CommandLineArgumentException : Exception { /// /// Constructor /// /// Message to display for this exception public CommandLineArgumentException(string message) : base(message) { } /// /// Constructor /// /// Message to display for this exception /// The inner exception public CommandLineArgumentException(string message, Exception innerException) : base(message, innerException) { } /// /// Converts this exception to a string /// /// Exception message public override string ToString() { return Message; } } /// /// Stores a list of command line arguments, allowing efficient ad-hoc queries of particular options (eg. "-Flag") and retreival of typed values (eg. "-Foo=Bar"), as /// well as attribute-driven application to fields with the [CommandLine] attribute applied. /// /// Also tracks which arguments have been retrieved, allowing the display of diagnostic messages for invalid arguments. /// [DebuggerDisplay("Count = {Count}")] [DebuggerTypeProxy(typeof(CommandLineArgumentListView))] public class CommandLineArguments : IReadOnlyList, IReadOnlyCollection, IEnumerable, IEnumerable { /// /// Information about a property or field that can receive an argument /// class ArgumentTarget { public MemberInfo Member { get; } public Type ValueType { get; } public Action SetValue { get; } public Func GetValue { get; } public CommandLineAttribute[] Attributes { get; } public ArgumentTarget(MemberInfo member, Type valueType, Action setValue, Func getValue, CommandLineAttribute[] attributes) { Member = member; ValueType = valueType; SetValue = setValue; GetValue = getValue; Attributes = attributes; } } /// /// The raw array of arguments /// readonly string[] _arguments; /// /// Bitmask indicating which arguments are flags rather than values /// readonly BitArray _flagArguments; /// /// Bitmask indicating which arguments have been used, via calls to GetOption(), GetValues() etc... /// readonly BitArray _usedArguments; /// /// Dictionary of argument names (or prefixes, in the case of "-Foo=" style arguments) to their index into the arguments array. /// readonly Dictionary _argumentToFirstIndex; /// /// For each argument which is seen more than once, keeps a list of indices for the second and subsequent arguments. /// readonly int[] _nextArgumentIndex; /// /// List of positional arguments /// readonly List _positionalArgumentIndices = []; /// /// Array of characters that separate argument names from values /// static readonly char[] s_valueSeparators = ['=', ':']; /// /// Constructor /// /// The raw list of arguments public CommandLineArguments(string[] arguments) { _arguments = arguments; _flagArguments = new BitArray(arguments.Length); _usedArguments = new BitArray(arguments.Length); // Clear the linked list of identical arguments _nextArgumentIndex = new int[arguments.Length]; for(int idx = 0; idx < arguments.Length; idx++) { _nextArgumentIndex[idx] = -1; } // Temporarily store the index of the last matching argument int[] lastArgumentIndex = new int[arguments.Length]; // Parse the argument array and build a lookup _argumentToFirstIndex = new Dictionary(arguments.Length, StringComparer.OrdinalIgnoreCase); for(int idx = 0; idx < arguments.Length; idx++) { if (arguments[idx].Equals("--", StringComparison.Ordinal)) { // End of option arguments MarkAsUsed(idx++); for (; idx < arguments.Length; idx++) { _positionalArgumentIndices.Add(idx); } break; } else if (arguments[idx].StartsWith('-')) { // Option argument int separatorIdx = arguments[idx].IndexOfAny(s_valueSeparators); if (separatorIdx == -1) { // Ignore duplicate -Option flags; they are harmless. if (_argumentToFirstIndex.ContainsKey(arguments[idx])) { _usedArguments.Set(idx, true); } else { _argumentToFirstIndex.Add(arguments[idx], idx); } // Mark this argument as a flag _flagArguments.Set(idx, true); } else { // Just take the part up to and including the separator character string prefix = arguments[idx].Substring(0, separatorIdx + 1); // Add the prefix to the argument lookup, or update the appropriate matching argument list if it's been seen before int existingArgumentIndex; if (_argumentToFirstIndex.TryGetValue(prefix, out existingArgumentIndex)) { _nextArgumentIndex[lastArgumentIndex[existingArgumentIndex]] = idx; lastArgumentIndex[existingArgumentIndex] = idx; } else { _argumentToFirstIndex.Add(prefix, idx); lastArgumentIndex[idx] = idx; } } } else { // Positional argument _positionalArgumentIndices.Add(idx); } } } /// /// The number of arguments in this list /// public int Count => _arguments.Length; /// /// Access an argument by index /// /// Index of the argument /// The argument at the given index public string this[int index] => _arguments[index]; /// /// Determines if an argument has been used /// /// Index of the argument /// True if the argument has been used, false otherwise public bool HasBeenUsed(int index) { return _usedArguments.Get(index); } /// /// Marks an argument as having been used /// /// Index of the argument to mark as used public void MarkAsUsed(int index) { _usedArguments.Set(index, true); } /// /// Marks an argument as not having been used /// /// Index of the argument to mark as being unused public void MarkAsUnused(int index) { _usedArguments.Set(index, false); } /// /// Checks if the given option (eg. "-Foo") was specified on the command line. /// /// The option to look for /// True if the option was found, false otherwise. public bool HasOption(string option) { int index; if(_argumentToFirstIndex.TryGetValue(option, out index)) { _usedArguments.Set(index, true); return true; } return false; } /// /// Checks for an argument prefixed with the given string is present. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// True if an argument with the given prefix was specified public bool HasValue(string prefix) { CheckValidPrefix(prefix); return _argumentToFirstIndex.ContainsKey(prefix); } /// /// Gets the positional argument at the given index /// /// Number of positional arguments public int GetPositionalArgumentCount() { return _positionalArgumentIndices.Count; } /// /// Gets the index of the numbered positional argument /// /// Number of the positional argument /// Index of the positional argument public int GetPositionalArgumentIndex(int num) { return _positionalArgumentIndices[num]; } /// /// Attempts to read the next unused positional argument /// /// Receives the argument that was read, on success /// True if an argument was read public bool TryGetPositionalArgument([NotNullWhen(true)] out string? argument) { for (int idx = 0; idx < _positionalArgumentIndices.Count; idx++) { int index = _positionalArgumentIndices[idx]; if (!HasBeenUsed(index)) { MarkAsUsed(index); argument = _arguments[index]; return true; } } argument = null; return false; } /// /// Returns all the positional arguments, and marks them as used /// /// Array of positional arguments public string[] GetPositionalArguments() { string[] positionalArguments = new string[_positionalArgumentIndices.Count]; for (int idx = 0; idx < positionalArguments.Length; idx++) { int index = _positionalArgumentIndices[idx]; MarkAsUsed(index); positionalArguments[idx] = _arguments[index]; } return positionalArguments; } /// /// Consume positional arguments from command line /// /// Number of arguments to remove public void RemovePositionalArguments(int count) { _positionalArgumentIndices.RemoveRange(0, count); } /// /// Gets the value specified by an argument with the given prefix. Throws an exception if the argument was not specified. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument public string GetString(string prefix) { string? value; if(!TryGetValue(prefix, out value)) { throw new CommandLineArgumentException(String.Format("Missing '{0}...' argument", prefix)); } return value; } /// /// Gets the value specified by an argument with the given prefix. Throws an exception if the argument was not specified. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument public int GetInteger(string prefix) { int value; if(!TryGetValue(prefix, out value)) { throw new CommandLineArgumentException(String.Format("Missing '{0}...' argument", prefix)); } return value; } /// /// Gets the value specified by an argument with the given prefix. Throws an exception if the argument was not specified. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument public FileReference GetFileReference(string prefix) { FileReference? value; if(!TryGetValue(prefix, out value)) { throw new CommandLineArgumentException(String.Format("Missing '{0}...' argument", prefix)); } return value; } /// /// Gets the value specified by an argument with the given prefix. Throws an exception if the argument was not specified. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument public DirectoryReference GetDirectoryReference(string prefix) { DirectoryReference? value; if(!TryGetValue(prefix, out value)) { throw new CommandLineArgumentException(String.Format("Missing '{0}...' argument", prefix)); } return value; } /// /// Gets the value specified by an argument with the given prefix. Throws an exception if the argument was not specified. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument public T GetEnum(string prefix) where T : struct { T value; if(!TryGetValue(prefix, out value)) { throw new CommandLineArgumentException(String.Format("Missing '{0}...' argument", prefix)); } return value; } /// /// Gets the value specified by an argument with the given prefix, or a default value. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Default value for the argument /// Value of the argument [return: NotNullIfNotNull(nameof(defaultValue))] public string? GetStringOrDefault(string prefix, string? defaultValue) { string? value; if(!TryGetValue(prefix, out value)) { value = defaultValue; } return value; } /// /// Gets the value specified by an argument with the given prefix, or a default value. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Default value for the argument /// Value of the argument public int GetIntegerOrDefault(string prefix, int defaultValue) { int value; if(!TryGetValue(prefix, out value)) { value = defaultValue; } return value; } /// /// Gets the value specified by an argument with the given prefix, or a default value. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Default value for the argument /// Value of the argument [return: NotNullIfNotNull(nameof(defaultValue))] public FileReference? GetFileReferenceOrDefault(string prefix, FileReference? defaultValue) { FileReference? value; if(!TryGetValue(prefix, out value)) { value = defaultValue; } return value; } /// /// Gets the value specified by an argument with the given prefix, or a default value. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Default value for the argument /// Value of the argument [return: NotNullIfNotNull(nameof(defaultValue))] public DirectoryReference? GetDirectoryReferenceOrDefault(string prefix, DirectoryReference? defaultValue) { DirectoryReference? value; if(!TryGetValue(prefix, out value)) { value = defaultValue; } return value; } /// /// Gets the value specified by an argument with the given prefix, or a default value. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Default value for the argument /// Value of the argument public T GetEnumOrDefault(string prefix, T defaultValue) where T : struct { T value; if(!TryGetValue(prefix, out value)) { value = defaultValue; } return value; } /// /// Tries to gets the value specified by an argument with the given prefix. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument, if found /// True if the argument was found (and Value was set), false otherwise. public bool TryGetValue(string prefix, [NotNullWhen(true)] out string? value) { CheckValidPrefix(prefix); int index; if(!_argumentToFirstIndex.TryGetValue(prefix, out index)) { value = null; return false; } if(_nextArgumentIndex[index] != -1) { throw new CommandLineArgumentException(String.Format("Multiple {0}... arguments are specified", prefix)); } _usedArguments.Set(index, true); value = _arguments[index].Substring(prefix.Length); return true; } /// /// Tries to gets the value specified by an argument with the given prefix. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument, if found /// True if the argument was found (and Value was set), false otherwise. public bool TryGetValue(string prefix, out int value) { // Try to get the string value of this argument string? stringValue; if(!TryGetValue(prefix, out stringValue)) { value = 0; return false; } // Try to parse it. If it fails, throw an exception. try { value = Int32.Parse(stringValue); return true; } catch(Exception ex) { throw new CommandLineArgumentException(String.Format("The argument '{0}{1}' does not specify a valid integer", prefix, stringValue), ex); } } /// /// Tries to gets the value specified by an argument with the given prefix. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument, if found /// True if the argument was found (and Value was set), false otherwise. public bool TryGetValue(string prefix, [NotNullWhen(true)] out FileReference? value) { // Try to get the string value of this argument string? stringValue; if(!TryGetValue(prefix, out stringValue)) { value = null; return false; } // Try to parse it. If it fails, throw an exception. try { value = new FileReference(stringValue); return true; } catch(Exception ex) { throw new CommandLineArgumentException(String.Format("The argument '{0}{1}' does not specify a valid file name", prefix, stringValue), ex); } } /// /// Tries to gets the value specified by an argument with the given prefix. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument, if found /// True if the argument was found (and Value was set), false otherwise. public bool TryGetValue(string prefix, [NotNullWhen(true)] out DirectoryReference? value) { // Try to get the string value of this argument string? stringValue; if(!TryGetValue(prefix, out stringValue)) { value = null; return false; } // Try to parse it. If it fails, throw an exception. try { value = new DirectoryReference(stringValue); return true; } catch(Exception ex) { throw new CommandLineArgumentException(String.Format("The argument '{0}{1}' does not specify a valid directory name", prefix, stringValue), ex); } } /// /// Tries to gets the value specified by an argument with the given prefix. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Value of the argument, if found /// True if the argument was found (and Value was set), false otherwise. public bool TryGetValue(string prefix, out T value) where T : struct { // Try to get the string value of this argument string? stringValue; if(!TryGetValue(prefix, out stringValue)) { value = new T(); return false; } // Try to parse it. If it fails, throw an exception. try { value = (T)Enum.Parse(typeof(T), stringValue, true); return true; } catch(Exception ex) { throw new CommandLineArgumentException(String.Format("The argument '{0}{1}' does not specify a valid {2}", prefix, stringValue, typeof(T).Name), ex); } } /// /// Returns all arguments with the given prefix. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// Sequence of values for the given prefix. public IEnumerable GetValues(string prefix) { CheckValidPrefix(prefix); int index; if(_argumentToFirstIndex.TryGetValue(prefix, out index)) { for(; index != -1; index = _nextArgumentIndex[index]) { _usedArguments.Set(index, true); yield return _arguments[index].Substring(prefix.Length); } } } /// /// Returns all arguments with the given prefix, allowing multiple arguments to be specified in a single argument with a separator character. /// /// The argument prefix (eg. "-Foo="). Must end with an '=' character. /// The separator character (eg. '+') /// Sequence of values for the given prefix. public IEnumerable GetValues(string prefix, char separator) { foreach(string value in GetValues(prefix)) { foreach(string splitValue in value.Split(separator)) { yield return splitValue; } } } /// /// Gets the prefix for a particular argument /// /// The target hosting the attribute /// The attribute instance /// Prefix for this argument private static string GetArgumentPrefix(ArgumentTarget target, CommandLineAttribute attribute) { // Get the inner field type, unwrapping nullable types Type valueType = target.ValueType; if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>)) { valueType = valueType.GetGenericArguments()[0]; } string? prefix = attribute.Prefix; if (prefix == null) { if (valueType == typeof(bool)) { prefix = String.Format("-{0}", target.Member.Name); } else { prefix = String.Format("-{0}=", target.Member.Name); } } else { if (valueType != typeof(bool) && attribute.Value == null && !prefix.EndsWith('=') && !prefix.EndsWith(':')) { prefix += "="; } } return prefix; } /// /// Applies these arguments to fields with the [CommandLine] attribute in the given object. /// /// The object to configure public void ApplyTo(object targetObject) { ApplyTo(targetObject, Log.Logger); } /// /// Applies these arguments to fields with the [CommandLine] attribute in the given object. /// /// The object to configure /// Sink for error/warning messages public void ApplyTo(object targetObject, ILogger logger) { List missingArguments = []; // Build a mapping from name to field and attribute for this object List targets = GetArgumentTargetsForType(targetObject.GetType()); foreach (ArgumentTarget target in targets) { // If any attribute is required, keep track of it so we can include an error for it string? requiredPrefix = null; // Keep track of whether a value has already been assigned to this field string? assignedArgument = null; // Loop through all the attributes for different command line options that can modify it foreach(CommandLineAttribute attribute in target.Attributes) { // Add in any positional arguments if (attribute.Positional) { foreach (int argIdx in _positionalArgumentIndices) { string arg = _arguments[argIdx]; if (ApplyArgument(targetObject, target, arg, arg, assignedArgument, logger)) { MarkAsUsed(argIdx); assignedArgument = arg; } } } // Get the appropriate prefix for this attribute string prefix = GetArgumentPrefix(target, attribute); // Get the value with the correct prefix int firstIndex; if(_argumentToFirstIndex.TryGetValue(prefix, out firstIndex)) { for(int index = firstIndex; index != -1; index = _nextArgumentIndex[index]) { // Get the argument text string argument = _arguments[index]; // Get the text for this value string valueText; if(attribute.Value != null) { valueText = attribute.Value; } else if(_flagArguments.Get(index)) { valueText = "true"; } else { valueText = argument.Substring(prefix.Length); } // Apply the value to the field if(attribute.ListSeparator == 0) { if(ApplyArgument(targetObject, target, argument, valueText, assignedArgument, logger)) { assignedArgument = argument; } } else { foreach(string itemValueText in valueText.Split(attribute.ListSeparator)) { if(ApplyArgument(targetObject, target, argument, itemValueText, assignedArgument, logger)) { assignedArgument = argument; } } } // Mark this argument as used if (attribute.MarkUsed) { _usedArguments.Set(index, true); } } } // If this attribute is marked as required, keep track of it so we can warn if the field is not assigned to if(attribute.Required && requiredPrefix == null) { requiredPrefix = prefix; } } // Make sure that this field has been assigned to if(assignedArgument == null && requiredPrefix != null) { missingArguments.Add(requiredPrefix); } } // If any arguments were missing, print an error about them if(missingArguments.Count > 0) { if(missingArguments.Count == 1) { throw new CommandLineArgumentException(String.Format("Missing {0} argument", missingArguments[0].Replace("=", "=...", StringComparison.Ordinal))); } else { throw new CommandLineArgumentException(String.Format("Missing {0} arguments", StringUtils.FormatList(missingArguments.Select(x => x.Replace("=", "=...", StringComparison.Ordinal))))); } } } /// /// Applies these arguments to fields with the [CommandLine] attribute in the given object. /// /// Sink for error/warning messages public T ApplyTo(ILogger logger) where T : new() { T obj = new T(); ApplyTo(obj, logger); return obj; } private static readonly Dictionary> s_cachedArgumentTargetsForType = new Dictionary>(); static List GetArgumentTargetsForType(Type? targetType) { List targets = []; if (targetType == null || targetType == typeof(object)) { return targets; } lock (s_cachedArgumentTargetsForType) { List? cachedTargets; if (s_cachedArgumentTargetsForType.TryGetValue(targetType, out cachedTargets)) { return cachedTargets; } } foreach (FieldInfo fieldInfo in targetType.GetFields(BindingFlags.Instance | BindingFlags.GetField | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { IEnumerable attributes = fieldInfo.GetCustomAttributes(); if (attributes.Any()) { targets.Add(new ArgumentTarget(fieldInfo, fieldInfo.FieldType, fieldInfo.SetValue, fieldInfo.GetValue, attributes.ToArray())); } } foreach (PropertyInfo propertyInfo in targetType.GetProperties(BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { IEnumerable attributes = propertyInfo.GetCustomAttributes(); if (attributes.Any()) { targets.Add(new ArgumentTarget(propertyInfo, propertyInfo.PropertyType, propertyInfo.SetValue, propertyInfo.GetValue, attributes.ToArray())); } } List baseTargets = GetArgumentTargetsForType(targetType.BaseType); if (baseTargets.Count > 0) { if (targets.Count > 0) { targets.AddRange(baseTargets); } else { targets = baseTargets; } } lock (s_cachedArgumentTargetsForType) { s_cachedArgumentTargetsForType[targetType] = targets; } return targets; } /// /// Gets help text for the arguments of a given type /// /// The type to find parameters for /// List of parameters public static List> GetParameters(Type type) { List> parameters = []; List targets = GetArgumentTargetsForType(type); foreach (ArgumentTarget target in targets) { StringBuilder descriptionBuilder = new StringBuilder(); foreach (DescriptionAttribute attribute in target.Member.GetCustomAttributes()) { if(descriptionBuilder.Length > 0) { descriptionBuilder.Append('\n'); } descriptionBuilder.Append(attribute.Description); } foreach (CommandLineAttribute attribute in target.Member.GetCustomAttributes().Where(x => x.Description != null)) { if (descriptionBuilder.Length > 0) { descriptionBuilder.Append('\n'); } descriptionBuilder.Append(attribute.Description); } string description = descriptionBuilder.ToString(); if (description.Length == 0) { description = "No description available."; } foreach (CommandLineAttribute attribute in target.Attributes) { string prefix = GetArgumentPrefix(target, attribute); if(prefix.EndsWith('=')) { prefix += "..."; } parameters.Add(new KeyValuePair(prefix, description)); } } return parameters; } /// /// Quotes a command line argument, if necessary /// /// The argument that may need quoting /// Argument which is safe to pass on the command line public static string Quote(string argument) { // See if the entire string is quoted correctly bool bInQuotes = false; for (int idx = 0;;idx++) { if (idx == argument.Length) { return argument; } else if (argument[idx] == '\"') { bInQuotes ^= true; } else if (argument[idx] == ' ') { break; } } // Try to insert a quote after the argument string if (argument[0] == '-') { for(int idx = 1; idx < argument.Length && argument[idx] != ' '; idx++) { if (argument[idx] == '=') { return String.Format("{0}=\"{1}\"", argument.Substring(0, idx), argument.Substring(idx + 1).Replace("\"", "\\\"", StringComparison.Ordinal)); } } } // Quote the whole thing return "\"" + argument.Replace("\"", "\\\"", StringComparison.Ordinal) + "\""; } /// /// Joins the given arguments into a command line /// /// List of command line arguments /// Joined command line public static string Join(IEnumerable arguments) { StringBuilder result = new StringBuilder(); foreach (string argument in arguments) { if(result.Length > 0) { result.Append(' '); } result.Append(Quote(argument)); } return result.ToString(); } /// /// Splits a command line into individual arguments /// /// The command line text /// Array of arguments public static string[] Split(string commandLine) { StringBuilder argument = new StringBuilder(); List arguments = []; // First do a pass leaving all quotes in the arguments, they will be removed later for(int idx = 0; idx < commandLine.Length; idx++) { if(!Char.IsWhiteSpace(commandLine[idx])) { argument.Clear(); for(bool bInQuotes = false; idx < commandLine.Length; idx++) { if (commandLine[idx] == '\"') { bInQuotes ^= true; } else if(!bInQuotes && Char.IsWhiteSpace(commandLine[idx])) { break; } argument.Append(commandLine[idx]); } arguments.Add(argument.ToString()); } } // Remove quotes from arguments except where only the value is quoted in -Define (-Define:KEY="VALUE") for (int idx = 0; idx < arguments.Count; idx++) { string arg = arguments[idx]; if (arg.StartsWith("-Define:", StringComparison.OrdinalIgnoreCase) && !arg.StartsWith("-Define:\"", StringComparison.OrdinalIgnoreCase)) { continue; } arguments[idx] = arg.Replace("\"", "", StringComparison.OrdinalIgnoreCase); } return [.. arguments]; } /// /// Appends the given arguments to the current argument list /// /// The arguments to add /// New argument list public CommandLineArguments Append(IEnumerable appendArguments) { CommandLineArguments newArguments = new CommandLineArguments(Enumerable.Concat(_arguments, appendArguments).ToArray()); for(int idx = 0; idx < _arguments.Length; idx++) { if(HasBeenUsed(idx)) { newArguments.MarkAsUsed(idx); } } return newArguments; } /// /// Retrieves all arguments with the given prefix, and returns the remaining a list of strings /// /// Prefix for the arguments to remove /// Receives a list of values with the given prefix /// New argument list public CommandLineArguments Remove(string prefix, out List values) { values = []; // Split the arguments into the values array and an array of new arguments int[] newArgumentIndex = new int[_arguments.Length]; List newArgumentList = new List(_arguments.Length); for(int idx = 0; idx < _arguments.Length; idx++) { string argument = _arguments[idx]; if(argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { newArgumentIndex[idx] = -1; values.Add(argument.Substring(prefix.Length)); } else { newArgumentIndex[idx] = newArgumentList.Count; newArgumentList.Add(argument); } } // Create the new argument list, and mark the same arguments as used CommandLineArguments newArguments = new CommandLineArguments([.. newArgumentList]); for(int idx = 0; idx < _arguments.Length; idx++) { if(HasBeenUsed(idx) && newArgumentIndex[idx] != -1) { newArguments.MarkAsUsed(newArgumentIndex[idx]); } } return newArguments; } /// /// Checks that there are no unused arguments (and warns if there are) /// public void CheckAllArgumentsUsed() { CheckAllArgumentsUsed(Log.Logger); } /// /// Checks that there are no unused arguments (and warns if there are) /// public void CheckAllArgumentsUsed(ILogger logger) { // Find all the unused arguments List remainingArguments = []; for(int idx = 0; idx < _arguments.Length; idx++) { if(!_usedArguments[idx]) { remainingArguments.Add(_arguments[idx]); } } // Output a warning if(remainingArguments.Count > 0) { if(remainingArguments.Count == 1) { logger.LogWarning("Invalid argument: {Argument}", remainingArguments[0]); } else { logger.LogWarning("Invalid arguments:\n{Arguments}", String.Join("\n", remainingArguments)); } } } /// /// Checks to see if any arguments are used /// /// public bool AreAnyArgumentsUsed() { return _usedArguments.Cast().Any(b => b); } /// /// Checks to see if any arguments are used /// /// public IEnumerable GetUnusedArguments() { for(int idx = 0; idx < _arguments.Length; idx++) { if (!_usedArguments[idx]) { yield return _arguments[idx]; } } } /// /// Count the number of value (non-flag) arguments on the command line /// /// public int CountValueArguments() { return _flagArguments.Cast().Count(b => !b); } /// /// Checks that a given string is a valid argument prefix /// /// The prefix to check private static void CheckValidPrefix(string prefix) { if(prefix.Length == 0) { throw new ArgumentException("Argument prefix cannot be empty."); } else if(prefix[0] != '-') { throw new ArgumentException("Argument prefix must begin with a hyphen."); } else if(!s_valueSeparators.Contains(prefix[^1])) { throw new ArgumentException(String.Format("Argument prefix must end with '{0}'", String.Join("' or '", s_valueSeparators))); } } /// /// Parses and assigns a value to a field /// /// The target object to assign values to /// The target to assign the value to /// The full argument text /// Argument text /// The previous text used to configure this field /// Logger for error/warning messages /// True if the value was assigned to the field, false otherwise private static bool ApplyArgument(object targetObject, ArgumentTarget target, string argumentText, string valueText, string? previousArgumentText, ILogger logger) { Type valueType = target.ValueType; // Check if the field type implements ICollection<>. If so, we can take multiple values. Type? collectionType = null; foreach (Type interfaceType in valueType.GetInterfaces()) { if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>)) { valueType = interfaceType.GetGenericArguments()[0]; collectionType = interfaceType; break; } } // Try to parse the value object? value; if(!TryParseValue(valueType, valueText, out value)) { logger.LogWarning("Unable to parse value for argument '{Argument}'.", argumentText); return false; } // Try to assign values to the target field if (collectionType == null) { // Check if this field has already been assigned to. Output a warning if the previous value is in conflict with the new one. if(previousArgumentText != null) { object? previousValue = target.GetValue(targetObject); if(!Object.Equals(previousValue, value)) { logger.LogWarning("Argument '{Argument}' conflicts with '{PrevArgument}'; ignoring.", argumentText, previousArgumentText); } return false; } // Set the value on the target object target.SetValue(targetObject, value); return true; } else { // Call the 'Add' method on the collection collectionType.InvokeMember("Add", BindingFlags.InvokeMethod, null, target.GetValue(targetObject), [value]); return true; } } /// /// Attempts to parse the given string to a value /// /// Type of the field to convert to /// The value text /// On success, contains the parsed object /// True if the text could be parsed, false otherwise private static bool TryParseValue(Type fieldType, string text, [NotNullWhen(true)] out object? value) { if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>)) { // Try to parse the inner type instead return TryParseValue(fieldType.GetGenericArguments()[0], text, out value); } else if (fieldType.IsEnum) { // Special handling for enums; parse the value ignoring case. try { value = Enum.Parse(fieldType, text, true); return true; } catch (ArgumentException) { value = null; return false; } } else if (fieldType == typeof(FileReference)) { // Construct a file reference from the string try { value = new FileReference(text); return true; } catch { value = null; return false; } } else if (fieldType == typeof(DirectoryReference)) { // Construct a file reference from the string try { value = new DirectoryReference(text); return true; } catch { value = null; return false; } } else if (fieldType == typeof(TimeSpan)) { // Construct a time span form the string double floatValue; if (text.EndsWith("h", StringComparison.OrdinalIgnoreCase) && Double.TryParse(text.Substring(0, text.Length - 1), out floatValue)) { value = TimeSpan.FromHours(floatValue); return true; } else if (text.EndsWith("m", StringComparison.OrdinalIgnoreCase) && Double.TryParse(text.Substring(0, text.Length - 1), out floatValue)) { value = TimeSpan.FromMinutes(floatValue); return true; } else if (text.EndsWith("s", StringComparison.OrdinalIgnoreCase) && Double.TryParse(text.Substring(0, text.Length - 1), out floatValue)) { value = TimeSpan.FromSeconds(floatValue); return true; } TimeSpan timeSpanValue; if (TimeSpan.TryParse(text, out timeSpanValue)) { value = timeSpanValue; return true; } else { value = null; return false; } } else { // First check for a TypeConverter TypeConverter typeConverter = TypeDescriptor.GetConverter(fieldType); if (typeConverter.CanConvertFrom(typeof(string))) { value = typeConverter.ConvertFrom(text)!; return true; } // Otherwise let the framework convert between types try { value = Convert.ChangeType(text, fieldType); return true; } catch (InvalidCastException) { value = null; return false; } } } /// /// Obtains an enumerator for the argument list /// /// IEnumerator interface IEnumerator IEnumerable.GetEnumerator() { return _arguments.GetEnumerator(); } /// /// Obtains an enumerator for the argument list /// /// Generic IEnumerator interface public IEnumerator GetEnumerator() { return ((IEnumerable)_arguments).GetEnumerator(); } /// /// Gets the raw argument array /// /// Array of arguments public string[] GetRawArray() { return _arguments; } /// /// Takes a command line argument and adds quotes if necessary /// /// /// The command line argument /// The command line argument with quotes inserted to escape it if necessary public static void Append(StringBuilder commandLine, string argument) { if(commandLine.Length > 0) { commandLine.Append(' '); } int spaceIdx = argument.IndexOf(' ', StringComparison.Ordinal); if(spaceIdx == -1) { commandLine.Append(argument); } else { int equalsIdx = argument.IndexOf('=', StringComparison.Ordinal); if(equalsIdx == -1 || equalsIdx > spaceIdx) { commandLine.AppendFormat("\"{0}\"", argument); } else { commandLine.AppendFormat("{0}\"{1}\"", argument.Substring(0, equalsIdx + 1), argument.Substring(equalsIdx + 1)); } } } /// /// Converts this string to /// /// public override string ToString() { StringBuilder result = new StringBuilder(); foreach(string argument in _arguments) { Append(result, argument); } return result.ToString(); } } }