// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.
using System;
using System.IO;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.ComponentModel;
namespace MGCB
{
///
/// Adapted from this generic command line argument parser:
/// http://blogs.msdn.com/b/shawnhar/archive/2012/04/20/a-reusable-reflection-based-command-line-parser.aspx
///
public class MGBuildParser
{
public static MGBuildParser Instance;
#region Supporting Types
public class PreprocessorProperty
{
public string Name;
public string CurrentValue;
public PreprocessorProperty()
{
Name = string.Empty;
CurrentValue = string.Empty;
}
}
public class PreprocessorPropertyCollection
{
private readonly List _properties;
public PreprocessorPropertyCollection()
{
_properties = new List();
}
public string this[string name]
{
get
{
foreach (var i in _properties)
{
if (i.Name.Equals(name))
return i.CurrentValue;
}
return null;
}
set
{
foreach (var i in _properties)
{
if (i.Name.Equals(name))
{
i.CurrentValue = value;
return;
}
}
var prop = new PreprocessorProperty()
{
Name = name,
CurrentValue = value,
};
_properties.Add(prop);
}
}
}
#endregion
private readonly object _optionsObject;
private readonly Queue _requiredOptions;
private readonly Dictionary _optionalOptions;
private readonly Dictionary _flags;
private readonly List _requiredUsageHelp;
public readonly PreprocessorPropertyCollection _properties;
public delegate void ErrorCallback(string msg, object[] args);
public event ErrorCallback OnError;
public MGBuildParser(object optionsObject)
{
Instance = this;
_optionsObject = optionsObject;
_requiredOptions = new Queue();
_optionalOptions = new Dictionary();
_requiredUsageHelp = new List();
_properties = new PreprocessorPropertyCollection();
// Reflect to find what commandline options are available...
// Fields
foreach (var field in optionsObject.GetType().GetFields())
{
var param = GetAttribute(field);
if (param == null)
continue;
CheckReservedPrefixes(param.Name);
if (param.Required)
{
// Record a required option.
_requiredOptions.Enqueue(field);
_requiredUsageHelp.Add(string.Format("<{0}>", param.Name));
}
else
{
// Record an optional option.
_optionalOptions.Add(param.Name.ToLowerInvariant(), field);
}
}
// Properties
foreach (var property in optionsObject.GetType().GetProperties())
{
var param = GetAttribute(property);
if (param == null)
continue;
CheckReservedPrefixes(param.Name);
if (param.Required)
{
// Record a required option.
_requiredOptions.Enqueue(property);
_requiredUsageHelp.Add(string.Format("<{0}>", param.Name));
}
else
{
// Record an optional option.
_optionalOptions.Add(param.Name.ToLowerInvariant(), property);
}
}
// Methods
foreach (var method in optionsObject.GetType().GetMethods())
{
var param = GetAttribute(method);
if (param == null)
continue;
CheckReservedPrefixes(param.Name);
// Only accept methods that take less than 1 parameter.
if (method.GetParameters().Length > 1)
throw new NotSupportedException("Methods must have one or zero parameters.");
if (param.Required)
{
// Record a required option.
_requiredOptions.Enqueue(method);
_requiredUsageHelp.Add(string.Format("<{0}>", param.Name));
}
else
{
// Record an optional option.
_optionalOptions.Add(param.Name.ToLowerInvariant(), method);
}
}
_flags = new Dictionary();
foreach(var pair in _optionalOptions)
{
var fi = GetAttribute(pair.Value);
if(!string.IsNullOrEmpty(fi.Flag))
_flags.Add(fi.Flag, fi.Name);
}
}
public bool Parse(IEnumerable args)
{
args = Preprocess(args);
var showUsage = true;
var success = true;
foreach (var arg in args)
{
showUsage = false;
if (!ParseFlags(arg))
{
success = false;
break;
}
}
var missingRequiredOption = _requiredOptions.FirstOrDefault(field => !IsList(field) || GetList(field).Count == 0);
if (missingRequiredOption != null)
{
ShowError("Missing argument '{0}'", GetAttribute(missingRequiredOption).Name);
return false;
}
if (showUsage)
ShowError(null);
return success;
}
private IEnumerable Preprocess(IEnumerable args)
{
var output = new List();
var lines = new List(args);
var ifstack = new Stack>();
var fileStack = new Stack();
while (lines.Count > 0)
{
var arg = lines[0];
lines.RemoveAt(0);
if (arg.StartsWith("# Begin:"))
{
var file = arg.Substring(8);
fileStack.Push(file);
continue;
}
if (arg.StartsWith("# End:"))
{
fileStack.Pop();
continue;
}
if (arg.StartsWith("$endif"))
{
ifstack.Pop();
continue;
}
if (ifstack.Count > 0)
{
var skip = false;
foreach (var i in ifstack)
{
var val = _properties[i.Item1];
if (!(i.Item2).Equals(val))
{
skip = true;
break;
}
}
if (skip)
continue;
}
if (arg.StartsWith("$set"))
{
var words = arg.Substring(5).Split('=');
var name = words[0];
var value = words[1];
_properties[name] = value;
continue;
}
if (arg.StartsWith("$if"))
{
if (fileStack.Count == 0)
throw new Exception("$if is invalid outside of a response file.");
var words = arg.Substring(4).Split('=');
var name = words[0];
var value = words[1];
var condition = new Tuple(name, value);
ifstack.Push(condition);
continue;
}
if (arg.StartsWith("/define:") || arg.StartsWith("--define:"))
{
var words = arg.Substring(8).Split('=');
var name = words[0];
var value = words[1];
_properties[name] = value;
continue;
}
if (arg.StartsWith("/@") || arg.StartsWith("--@") || arg.StartsWith("-@")
|| (arg.EndsWith(".mgcb") && File.Exists(arg)))
{
var file = arg;
if (!File.Exists(arg))
file = arg.Substring(arg.StartsWith("--@") ? 4 : 3);
var commands = File.ReadAllLines(file);
var offset = 0;
lines.Insert(0, string.Format("# Begin:{0} ", file));
offset++;
for (var j = 0; j < commands.Length; j++)
{
var line = commands[j];
if (string.IsNullOrEmpty(line))
continue;
if (line.StartsWith("#"))
continue;
lines.Insert(offset, line);
offset++;
}
lines.Insert(offset, string.Format("# End:{0}", file));
continue;
}
output.Add(arg);
}
return output.ToArray();
}
private bool ParseFlags(string arg)
{
// Filename detected, redo with a build command
if (File.Exists(arg))
return ParseFlags("/build=" + arg);
// Only one flag
if (arg.Length >= 3 &&
(arg[0] == '-' || arg[0] == '/') &&
(arg[2] == '=' || arg[2] == ':'))
{
string name;
if (!_flags.TryGetValue(arg[1].ToString(), out name))
{
ShowError("Unknown option '{0}'", arg[1].ToString());
return false;
}
ParseArgument("/" + name + arg.Substring(2));
return true;
}
// Multiple flags
if (arg.Length >= 2 &&
((arg[0] == '-' && arg[1] != '-') || arg[0] == '/') &&
!arg.Contains(":") && !arg.Contains("=") &&
!_optionalOptions.ContainsKey(arg.Substring(1)))
{
for (int i = 1; i < arg.Length; i++)
{
string name;
if (!_flags.TryGetValue(arg[i].ToString(), out name))
{
ShowError("Unknown option '{0}'", arg[i].ToString());
break;
}
ParseArgument("/" + name);
}
return true;
}
// Not a flag, parse argument
return ParseArgument(arg);
}
private bool ParseArgument(string arg)
{
if (arg.StartsWith("/") || arg.StartsWith("--"))
{
// After the first escaped argument we can no
// longer read non-escaped arguments.
if (_requiredOptions.Count > 0)
return false;
// Parse an optional argument.
char[] separators = { ':', '=' };
var split = arg.Substring(arg.StartsWith("/") ? 1 : 2).Split(separators, 2, StringSplitOptions.None);
var name = split[0];
var value = (split.Length > 1) ? split[1] : "true";
MemberInfo member;
if (!_optionalOptions.TryGetValue(name.ToLowerInvariant(), out member))
{
ShowError("Unknown option '{0}'", name);
return false;
}
return SetOption(member, value);
}
if (_requiredOptions.Count > 0)
{
// Parse the next non escaped argument.
var field = _requiredOptions.Peek();
if (!IsList(field))
_requiredOptions.Dequeue();
return SetOption(field, arg);
}
ShowError("Too many arguments");
return false;
}
bool SetOption(MemberInfo member, string value)
{
try
{
if (IsList(member))
{
// Append this value to a list of options.
GetList(member).Add(ChangeType(value, ListElementType(member)));
}
else
{
// Set the value of a single option.
if (member is MethodInfo)
{
var method = member as MethodInfo;
var parameters = method.GetParameters();
if (parameters.Length == 0)
method.Invoke(_optionsObject, null);
else
method.Invoke(_optionsObject, new[] { ChangeType(value, parameters[0].ParameterType) });
}
else if (member is FieldInfo)
{
var field = member as FieldInfo;
field.SetValue(_optionsObject, ChangeType(value, field.FieldType));
}
else
{
var property = member as PropertyInfo;
property.SetValue(_optionsObject, ChangeType(value, property.PropertyType), null);
}
}
return true;
}
catch
{
ShowError("Invalid value '{0}' for option '{1}'", value, GetAttribute(member).Name);
return false;
}
}
static readonly string[] ReservedPrefixes = new[]
{
"$",
"/",
"#",
"--",
"-"
};
static void CheckReservedPrefixes(string str)
{
foreach (var i in ReservedPrefixes)
{
if (str.StartsWith(i))
throw new Exception(string.Format("'{0}' is a reserved prefix and cannot be used at the start of an argument name.", i));
}
}
static object ChangeType(string value, Type type)
{
var converter = TypeDescriptor.GetConverter(type);
return converter.ConvertFromInvariantString(value);
}
static bool IsList(MemberInfo member)
{
if (member is MethodInfo)
return false;
if (member is FieldInfo)
return typeof(IList).IsAssignableFrom((member as FieldInfo).FieldType);
return typeof(IList).IsAssignableFrom((member as PropertyInfo).PropertyType);
}
IList GetList(MemberInfo member)
{
if (member is PropertyInfo)
return (IList)(member as PropertyInfo).GetValue(_optionsObject, null);
if (member is FieldInfo)
return (IList)(member as FieldInfo).GetValue(_optionsObject);
throw new Exception();
}
static Type ListElementType(MemberInfo member)
{
if (member is FieldInfo)
{
var field = member as FieldInfo;
var interfaces = from i in field.FieldType.GetInterfaces()
where i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IEnumerable<>)
select i;
return interfaces.First().GetGenericArguments()[0];
}
if (member is PropertyInfo)
{
var property = member as PropertyInfo;
var interfaces = from i in property.PropertyType.GetInterfaces()
where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)
select i;
return interfaces.First().GetGenericArguments()[0];
}
throw new ArgumentException("Only FieldInfo and PropertyInfo are valid arguments.", "member");
}
public string Title { get; set; }
bool IsWindows()
{
switch (Environment.OSVersion.Platform)
{
case PlatformID.Win32NT:
case PlatformID.Win32S:
case PlatformID.Win32Windows:
case PlatformID.WinCE:
return true;
}
return false;
}
public void ShowError(string message, params object[] args)
{
if (!string.IsNullOrEmpty(message) && OnError != null)
{
OnError(message, args);
return;
}
var name = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName);
if (!string.IsNullOrEmpty(Title))
{
Console.Error.WriteLine(Title);
Console.Error.WriteLine();
}
if (!string.IsNullOrEmpty(message))
{
Console.Error.WriteLine(message, args);
Console.Error.WriteLine();
}
var defaultParamPrefix = IsWindows() ? "/" : "--";
Console.Error.WriteLine("Usage: {0} {1}{2}",
name,
_requiredUsageHelp.Count > 0 ? string.Join(" ", _requiredUsageHelp) + " " : string.Empty,
_optionalOptions.Count > 0 ? "" : string.Empty);
if (_optionalOptions.Count > 0)
{
Console.Error.WriteLine();
Console.Error.WriteLine("Options:");
var data = _optionalOptions.Values.ToList();
data.Sort((x, y) => {
var px = GetAttribute(x);
var py = GetAttribute(y);
return px.Name.CompareTo(py.Name);
});
foreach(var d in data)
{
var attr = GetAttribute(d);
var field = d as FieldInfo;
var prop = d as PropertyInfo;
var method = d as MethodInfo;
var hasValue = false;
if (field != null && field.FieldType != typeof (bool))
hasValue = true;
if (prop != null && prop.PropertyType != typeof (bool))
hasValue = true;
if (method != null && method.GetParameters().Length != 0)
hasValue = true;
var s = " ";
s += (!string.IsNullOrEmpty(attr.Flag)) ? (IsWindows() ? "/" : "-") + attr.Flag + "," : " ";
s += " " + defaultParamPrefix + attr.Name;
if (hasValue)
{
if (IsWindows())
s += ":<" + attr.ValueName + ">";
else
s += "=" + attr.ValueName.Replace("=", ":").ToUpper();
}
s = s.PadRight(35, ' ');
// Wrap text description
var bw = Math.Max(60, Console.BufferWidth);
var desc = attr.Description.Split(' ');
foreach(var dw in desc)
{
if (s.Length + dw.Length >= bw)
{
Console.WriteLine(s);
s = string.Empty.PadRight(37, ' ');
}
s += " " + dw;
}
Console.WriteLine(s);
}
}
}
static T GetAttribute(ICustomAttributeProvider provider) where T : Attribute
{
return provider.GetCustomAttributes(typeof(T), false).OfType().FirstOrDefault();
}
}
// Used on an optionsObject field or method to rename the corresponding commandline option.
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property)]
public sealed class CommandLineParameterAttribute : Attribute
{
public CommandLineParameterAttribute()
{
ValueName = "value";
}
public string Name { get; set; }
public string Flag { get; set; }
public bool Required { get; set; }
public string ValueName { get; set; }
public string Description { get; set; }
}
}