// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Tools.DotNETCommon; namespace UnrealBuildTool { /// /// Functions for parsing command line arguments /// public static class CommandLine { /// /// Stores information about the field to receive command line arguments /// class Parameter { /// /// Prefix for the argument. /// public string Prefix; /// /// Field to receive values for this argument /// public FieldInfo FieldInfo; /// /// The attribute containing this argument's info /// public CommandLineAttribute Attribute; } /// /// Parse the given list of arguments and apply them to the given object /// /// List of arguments. Parsed arguments will be removed from this list when the function returns. /// Object to receive the parsed arguments. Fields in this object should be marked up with CommandLineArgumentAttribute's to indicate how they should be parsed. public static void ParseArguments(IEnumerable Arguments, object TargetObject) { ParseAndRemoveArguments(Arguments.ToList(), TargetObject); } /// /// Parse the given list of arguments, and remove any that are parsed successfully /// /// List of arguments. Parsed arguments will be removed from this list when the function returns. /// Object to receive the parsed arguments. Fields in this object should be marked up with CommandLineArgumentAttribute's to indicate how they should be parsed. public static void ParseAndRemoveArguments(List Arguments, object TargetObject) { // Build a mapping from name to field and attribute for this object Dictionary PrefixToParameter = new Dictionary(StringComparer.InvariantCultureIgnoreCase); 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)) { IEnumerable Attributes = FieldInfo.GetCustomAttributes(); foreach(CommandLineAttribute Attribute in Attributes) { 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 + "="; } } PrefixToParameter.Add(Prefix, new Parameter { Prefix = Prefix, Attribute = Attribute, FieldInfo = FieldInfo }); } } } // Step through the arguments, and remove those that we can parse Dictionary AssignedFieldToParameter = new Dictionary(); for(int Idx = 0; Idx < Arguments.Count; Idx++) { string Argument = Arguments[Idx]; if(Argument.Length > 0 && Argument[0] == '-') { // Get the length of the argument prefix int EqualsIdx = Argument.IndexOfAny(new char[]{ '=', ':' }); string Prefix = (EqualsIdx == -1)? Argument : Argument.Substring(0, EqualsIdx + 1); // Check if there's a matching argument registered Parameter Parameter; if(PrefixToParameter.TryGetValue(Prefix, out Parameter)) { int NextIdx = Idx + 1; // Parse the value if(Parameter.Attribute.Value != null) { if(EqualsIdx != -1) { Log.WriteLine(LogEventType.Warning, "Cannot specify a value for {0}", Parameter.Prefix); } else { AssignValue(Parameter, Parameter.Attribute.Value, TargetObject, AssignedFieldToParameter); } } else if(EqualsIdx != -1) { AssignValue(Parameter, Argument.Substring(EqualsIdx + 1), TargetObject, AssignedFieldToParameter); } else if(Parameter.FieldInfo.FieldType == typeof(bool)) { AssignValue(Parameter, "true", TargetObject, AssignedFieldToParameter); } else { Log.WriteLine(LogEventType.Warning, "Missing value for {0}", Parameter.Prefix); } // Remove the argument from the list Arguments.RemoveRange(Idx, NextIdx - Idx); Idx--; } } } // Make sure there are no required parameters that are missing Dictionary MissingFieldToParameter = new Dictionary(); foreach(Parameter Parameter in PrefixToParameter.Values) { if(Parameter.Attribute.Required && !AssignedFieldToParameter.ContainsKey(Parameter.FieldInfo) && !MissingFieldToParameter.ContainsKey(Parameter.FieldInfo)) { MissingFieldToParameter.Add(Parameter.FieldInfo, Parameter); } } if(MissingFieldToParameter.Count > 0) { if(MissingFieldToParameter.Count == 1) { throw new BuildException("Missing {0} argument", MissingFieldToParameter.First().Value.Prefix.Replace("=", "=...")); } else { throw new BuildException("Missing {0} arguments", StringUtils.FormatList(MissingFieldToParameter.Values.Select(x => x.Prefix.Replace("=", "=...")))); } } } /// /// Checks that the list of arguments is empty. If not, throws an exception listing them. /// /// List of remaining arguments public static void CheckNoRemainingArguments(List RemainingArguments) { 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)); } } } /// /// Parses and assigns a value to a field /// /// The parameter being parsed /// Argument text /// The target object to assign values to /// Maps assigned fields to the parameter that wrote to it. Used to detect duplicate and conflicting arguments. static void AssignValue(Parameter Parameter, string Text, object TargetObject, Dictionary AssignedFieldToParameter) { // Check if the field type implements ICollection<>. If so, we can take multiple values. Type CollectionType = null; foreach (Type InterfaceType in Parameter.FieldInfo.FieldType.GetInterfaces()) { if (InterfaceType.IsGenericType && InterfaceType.GetGenericTypeDefinition() == typeof(ICollection<>)) { CollectionType = InterfaceType; break; } } // Try to assign values to the target field if (CollectionType == null) { // Try to parse the value object Value; if(!TryParseValue(Parameter.FieldInfo.FieldType, Text, out Value)) { Log.WriteLine(LogEventType.Warning, "Invalid value for {0}... - ignoring {1}", Parameter.Prefix, Text); return; } // Check if this field has already been assigned to. Output a warning if the previous value is in conflict with the new one. Parameter PreviousParameter; if(AssignedFieldToParameter.TryGetValue(Parameter.FieldInfo, out PreviousParameter)) { object PreviousValue = Parameter.FieldInfo.GetValue(TargetObject); if(!PreviousValue.Equals(Value)) { if(PreviousParameter.Prefix == Parameter.Prefix) { Log.WriteLine(LogEventType.Warning, "Conflicting {0} arguments - ignoring", Parameter.Prefix); } else { Log.WriteLine(LogEventType.Warning, "{0} conflicts with {1} - ignoring", Parameter.Prefix, PreviousParameter.Prefix); } } return; } // Set the value on the target object Parameter.FieldInfo.SetValue(TargetObject, Value); AssignedFieldToParameter.Add(Parameter.FieldInfo, Parameter); } else { // Split the text into an array of values if necessary string[] ItemArray; if(Parameter.Attribute.ListSeparator == 0) { ItemArray = new string[] { Text }; } else { ItemArray = Text.Split(Parameter.Attribute.ListSeparator); } // Parse each of the argument values separately foreach(string Item in ItemArray) { object Value; if(TryParseValue(CollectionType.GenericTypeArguments[0], Item, out Value)) { CollectionType.InvokeMember("Add", BindingFlags.InvokeMethod, null, Parameter.FieldInfo.GetValue(TargetObject), new object[] { Value }); } else { Log.WriteLine(LogEventType.Warning, "'{0}' is not a valid value for -{1}=... - ignoring", Item, Parameter.Prefix); } } } } /// /// 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 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 { // Otherwise let the framework convert between types try { Value = Convert.ChangeType(Text, FieldType); return true; } catch(InvalidCastException) { Value = null; return false; } } } } }