// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using EpicGames.Core; using Microsoft.Extensions.Logging; 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; /// /// Constructor /// /// /// /// public Parameter(string Prefix, FieldInfo FieldInfo, CommandLineAttribute Attribute) { this.Prefix = Prefix; this.FieldInfo = FieldInfo; this.Attribute = 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) => ParseArguments(Arguments, TargetObject, Log.Logger); /// /// 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. /// Logger for output public static void ParseArguments(IEnumerable Arguments, object TargetObject, ILogger Logger) { ParseAndRemoveArguments(Arguments.ToList(), TargetObject, Logger); } private static Type NonNullableType(Type type) { Type? UnderlyingType = Nullable.GetUnderlyingType(type); if (UnderlyingType != null) { return UnderlyingType; } return type; } /// /// 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. /// Logger for output public static void ParseAndRemoveArguments(List Arguments, object TargetObject, ILogger Logger) { // 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 (NonNullableType(FieldInfo.FieldType) == typeof(bool)) { Prefix = String.Format("-{0}", FieldInfo.Name); } else { Prefix = String.Format("-{0}=", FieldInfo.Name); } } else { if (NonNullableType(FieldInfo.FieldType) != typeof(bool) && Attribute.Value == null && !Prefix.EndsWith("=") && !Prefix.EndsWith(":")) { Prefix += "="; } } PrefixToParameter.Add(Prefix, new Parameter(Prefix, FieldInfo, Attribute)); } } } // 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) { Logger.LogWarning("Cannot specify a value for {ParameterPrefix}", Parameter.Prefix); } else { AssignValue(Parameter, Parameter.Attribute.Value, TargetObject, AssignedFieldToParameter, Logger); } } else if (EqualsIdx != -1) { AssignValue(Parameter, Argument.Substring(EqualsIdx + 1), TargetObject, AssignedFieldToParameter, Logger); } else if (NonNullableType(Parameter.FieldInfo.FieldType) == typeof(bool)) { AssignValue(Parameter, "true", TargetObject, AssignedFieldToParameter, Logger); } else { Logger.LogWarning("Missing value for {ParameterPrefix}", 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 /// Logger for output public static void CheckNoRemainingArguments(List RemainingArguments, ILogger Logger) { if (RemainingArguments.Count > 0) { if (RemainingArguments.Count == 1) { Logger.LogWarning("Invalid argument: {Arg}", RemainingArguments[0]); } else { Logger.LogWarning("Invalid arguments:\n{Args}", 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. /// Logger for output static void AssignValue(Parameter Parameter, string Text, object TargetObject, Dictionary AssignedFieldToParameter, ILogger Logger) { // 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)) { Logger.LogWarning("Invalid value for {ParameterPrefix}... - ignoring {Text}", 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 (!Object.Equals(PreviousValue, Value)) { if (PreviousParameter.Prefix == Parameter.Prefix) { Logger.LogWarning("Conflicting {ParameterPrefix} arguments - ignoring", Parameter.Prefix); } else { Logger.LogWarning("{ParameterPrefix} conflicts with {PreviousParameterPrefix} - 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 { Logger.LogWarning("'{Item}' is not a valid value for -{ParameterPrefix}=... - 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, [NotNullWhen(true)] out object? Value) { FieldType = NonNullableType(FieldType); 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; } } } } }