// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Tools.DotNETCommon { /// /// Helper class to visualize an argument list /// class CommandLineArgumentListView { /// /// The list of arguments /// [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public string[] Arguments; /// /// 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 { /// /// The raw array of arguments /// string[] Arguments; /// /// Bitmask indicating which arguments are flags rather than values /// BitArray FlagArguments; /// /// Bitmask indicating which arguments have been used, via calls to GetOption(), GetValues() etc... /// BitArray UsedArguments; /// /// Dictionary of argument names (or prefixes, in the case of "-Foo=" style arguments) to their index into the arguments array. /// Dictionary ArgumentToFirstIndex; /// /// For each argument which is seen more than once, keeps a list of indices for the second and subsequent arguments. /// int[] NextArgumentIndex; /// /// Array of characters that separate argument names from values /// static readonly char[] ValueSeparators = { '=', ':' }; /// /// Constructor /// /// The raw list of arguments public CommandLineArguments(string[] Arguments) { this.Arguments = Arguments; this.FlagArguments = new BitArray(Arguments.Length); this.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++) { int SeparatorIdx = Arguments[Idx].IndexOfAny(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; } } } } /// /// The number of arguments in this list /// public int Count { get { return Arguments.Length; } } /// /// Access an argument by index /// /// Index of the argument /// The argument at the given index public string this[int Index] { get { return 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, true); } /// /// 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 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 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 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 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, 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 = int.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, 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, 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; } } } /// /// Applies these arguments to fields with the [CommandLine] attribute in the given object. /// /// The object to configure public void ApplyTo(object TargetObject) { // Build a mapping from name to field and attribute for this object List MissingArguments = new List(); for(Type TargetType = TargetObject.GetType(); TargetType != typeof(object); TargetType = TargetType.BaseType) { foreach(FieldInfo FieldInfo in TargetType.GetFields(BindingFlags.Instance | BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { // 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 IEnumerable Attributes = FieldInfo.GetCustomAttributes(); foreach(CommandLineAttribute Attribute in Attributes) { // Get the appropriate prefix for this attribute string Prefix = Attribute.Prefix; if(Prefix == null) { if(FieldInfo.FieldType == typeof(bool)) { Prefix = String.Format("-{0}", FieldInfo.Name); } else { Prefix = String.Format("-{0}=", FieldInfo.Name); } } else { if(FieldInfo.FieldType != typeof(bool) && Attribute.Value == null && !Prefix.EndsWith("=") && !Prefix.EndsWith(":")) { Prefix = Prefix + "="; } } // 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(ApplyArgument(TargetObject, FieldInfo, Argument, ValueText, AssignedArgument)) { AssignedArgument = Argument; } // Mark this argument as used 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("=", "=..."))); } else { throw new CommandLineArgumentException(String.Format("Missing {0} arguments", StringUtils.FormatList(MissingArguments.Select(x => x.Replace("=", "=..."))))); } } } /// /// 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 = new List(); 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; } else { Argument.Append(CommandLine[Idx]); } } Arguments.Add(Argument.ToString()); } } return Arguments.ToArray(); } /// /// 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 = new List(); // 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.ToArray()); 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() { // Find all the unused arguments List RemainingArguments = new List(); 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) { Log.TraceWarning("Invalid argument: {0}", RemainingArguments[0]); } else { Log.TraceWarning("Invalid arguments:\n{0}", String.Join("\n", RemainingArguments)); } } } /// /// 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(!ValueSeparators.Contains(Prefix[Prefix.Length - 1])) { throw new ArgumentException(String.Format("Argument prefix must end with '{0}'", String.Join("' or '", ValueSeparators))); } } /// /// Parses and assigns a value to a field /// /// The target object to assign values to /// The field to assign the value to /// The full argument text /// Argument text /// The previous text used to configure this field /// True if the value was assigned to the field, false otherwise private static bool ApplyArgument(object TargetObject, FieldInfo Field, string ArgumentText, string ValueText, string PreviousArgumentText) { // The value type for items of this field Type ValueType = Field.FieldType; // Check if the field type implements ICollection<>. If so, we can take multiple values. Type CollectionType = null; foreach (Type InterfaceType in Field.FieldType.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)) { Log.TraceWarning("Unable to parse value for argument '{0}'.", 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 = Field.GetValue(TargetObject); if(!PreviousValue.Equals(Value)) { Log.TraceWarning("Argument '{0}' conflicts with '{1}'; ignoring.", ArgumentText, PreviousArgumentText); } return false; } // Set the value on the target object Field.SetValue(TargetObject, Value); return true; } else { // Call the 'Add' method on the collection CollectionType.InvokeMember("Add", BindingFlags.InvokeMethod, null, Field.GetValue(TargetObject), new object[] { 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, out object Value) { 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 { // 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(' '); if(SpaceIdx == -1) { CommandLine.Append(Argument); } else { int EqualsIdx = Argument.IndexOf('='); if(EqualsIdx == -1) { 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(); } } }