Files
Chris Gagnon 1dd3e0189f Merging //UE4/Dev-Main to Dev-Editor (//UE4/Dev-Editor)
#rb none

[CL 4730305 by Chris Gagnon in Dev-Editor branch]
2019-01-15 18:47:22 -05:00

316 lines
10 KiB
C#

// 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
{
/// <summary>
/// Functions for parsing command line arguments
/// </summary>
public static class CommandLine
{
/// <summary>
/// Stores information about the field to receive command line arguments
/// </summary>
class Parameter
{
/// <summary>
/// Prefix for the argument.
/// </summary>
public string Prefix;
/// <summary>
/// Field to receive values for this argument
/// </summary>
public FieldInfo FieldInfo;
/// <summary>
/// The attribute containing this argument's info
/// </summary>
public CommandLineAttribute Attribute;
}
/// <summary>
/// Parse the given list of arguments and apply them to the given object
/// </summary>
/// <param name="Arguments">List of arguments. Parsed arguments will be removed from this list when the function returns.</param>
/// <param name="TargetObject">Object to receive the parsed arguments. Fields in this object should be marked up with CommandLineArgumentAttribute's to indicate how they should be parsed.</param>
public static void ParseArguments(IEnumerable<string> Arguments, object TargetObject)
{
ParseAndRemoveArguments(Arguments.ToList(), TargetObject);
}
/// <summary>
/// Parse the given list of arguments, and remove any that are parsed successfully
/// </summary>
/// <param name="Arguments">List of arguments. Parsed arguments will be removed from this list when the function returns.</param>
/// <param name="TargetObject">Object to receive the parsed arguments. Fields in this object should be marked up with CommandLineArgumentAttribute's to indicate how they should be parsed.</param>
public static void ParseAndRemoveArguments(List<string> Arguments, object TargetObject)
{
// Build a mapping from name to field and attribute for this object
Dictionary<string, Parameter> PrefixToParameter = new Dictionary<string, Parameter>(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<CommandLineAttribute> Attributes = FieldInfo.GetCustomAttributes<CommandLineAttribute>();
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<FieldInfo, Parameter> AssignedFieldToParameter = new Dictionary<FieldInfo, Parameter>();
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<FieldInfo, Parameter> MissingFieldToParameter = new Dictionary<FieldInfo, Parameter>();
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("=", "=..."))));
}
}
}
/// <summary>
/// Checks that the list of arguments is empty. If not, throws an exception listing them.
/// </summary>
/// <param name="RemainingArguments">List of remaining arguments</param>
public static void CheckNoRemainingArguments(List<string> 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));
}
}
}
/// <summary>
/// Parses and assigns a value to a field
/// </summary>
/// <param name="Parameter">The parameter being parsed</param>
/// <param name="Text">Argument text</param>
/// <param name="TargetObject">The target object to assign values to</param>
/// <param name="AssignedFieldToParameter">Maps assigned fields to the parameter that wrote to it. Used to detect duplicate and conflicting arguments.</param>
static void AssignValue(Parameter Parameter, string Text, object TargetObject, Dictionary<FieldInfo, Parameter> 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);
}
}
}
}
/// <summary>
/// Attempts to parse the given string to a value
/// </summary>
/// <param name="FieldType">Type of the field to convert to</param>
/// <param name="Text">The value text</param>
/// <param name="Value">On success, contains the parsed object</param>
/// <returns>True if the text could be parsed, false otherwise</returns>
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;
}
}
}
}
}