|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.ML.Data;
using Microsoft.ML.Internal.Utilities;
using Microsoft.ML.Runtime;
namespace Microsoft.ML.CommandLine
{
/// <summary>
/// A delegate used in error reporting.
/// </summary>
internal delegate void ErrorReporter(string message);
[Flags]
[BestFriend]
internal enum SettingsFlags
{
None = 0x00,
ShortNames = 0x01,
NoSlashes = 0x02,
NoUnparse = 0x04,
Default = ShortNames | NoSlashes
}
/// <summary>
/// An IComponentFactory that is used in the command line.
///
/// This allows components to be created by name, signature type, and a settings string.
/// </summary>
[BestFriend]
internal interface ICommandLineComponentFactory : IComponentFactory
{
Type SignatureType { get; }
string Name { get; }
string GetSettingsString();
}
//////////////////////////////////////////////////////////////////////////////
// Command Line Argument Parser
// ----------------------------
// Usage
// -----
//
// Parsing command line arguments to a console application is a common problem.
// This library handles the common task of reading arguments from a command line
// and filling in the values in a type.
//
// To use this library, define a class whose fields represent the data that your
// application wants to receive from arguments on the command line. Then call
// CommandLine.ParseArguments() to fill the object with the data
// from the command line. Each field in the class defines a command line argument.
// The type of the field is used to validate the data read from the command line.
// The name of the field defines the name of the command line option.
//
// The parser can handle fields of the following types:
//
// - string
// - int
// - uint
// - bool
// - enum
// - array of the above type
//
// For example, suppose you want to read in the argument list for wc (word count).
// wc takes three optional boolean arguments: -l, -w, and -c and a list of files.
//
// You could parse these arguments using the following code:
//
// class WCArguments
// {
// public bool lines;
// public bool words;
// public bool chars;
// public string[] files;
// }
//
// class WC
// {
// static void Main(string[] args)
// {
// if (CommandLine.ParseArgumentsWithUsage(args, parsedArgs))
// {
// // insert application code here
// }
// }
// }
//
// So you could call this application with the following command line to count
// lines in the foo and bar files:
//
// wc.exe /lines /files:foo /files:bar
//
// The program will display the following usage message when bad command line
// arguments are used:
//
// wc.exe -x
//
// Unrecognized command line argument '-x'
// /lines[+|-] short form /l
// /words[+|-] short form /w
// /chars[+|-] short form /c
// /files=<string> short form /f
// @<file> Read response file for more options
//
// That was pretty easy. However, you really want to omit the "/files:" for the
// list of files. The details of field parsing can be controlled using custom
// attributes. The attributes which control parsing behavior are:
//
// ArgumentAttribute
// - controls short name, long name, required, allow duplicates, default value
// and help text
// DefaultArgumentAttribute
// - allows omission of the "/name".
// - This attribute is allowed on only one field in the argument class.
//
// So for the wc.exe program we want this:
//
// using System;
// using Utilities;
//
// class WCArguments
// {
// [Argument(ArgumentType.AtMostOnce, HelpText="Count number of lines in the input text.")]
// public bool lines;
// [Argument(ArgumentType.AtMostOnce, HelpText="Count number of words in the input text.")]
// public bool words;
// [Argument(ArgumentType.AtMostOnce, HelpText="Count number of chars in the input text.")]
// public bool chars;
// [DefaultArgument(ArgumentType.MultipleUnique, HelpText="Input files to count.")]
// public string[] files;
// }
//
// class WC
// {
// static void Main(string[] args)
// {
// WCArguments parsedArgs = new WCArguments();
// if (CommandLine.ParseArgumentsWithUsage(args, parsedArgs))
// {
// // insert application code here
// }
// }
// }
//
//
//
// So now we have the command line we want:
//
// wc.exe /lines foo bar
//
// This will set lines to true and will set files to an array containing the
// strings "foo" and "bar".
//
// The new usage message becomes:
//
// wc.exe -x
//
// Unrecognized command line argument '-x'
// /lines[+|-] Count number of lines in the input text. (short form /l)
// /words[+|-] Count number of words in the input text. (short form /w)
// /chars[+|-] Count number of chars in the input text. (short form /c)
// @<file> Read response file for more options
// <files> Input files to count. (short form /f)
//
// If you want more control over how error messages are reported, how /help is
// dealt with, etc you can instantiate the CommandLine.Parser class.
/// <summary>
/// Parser for command line arguments.
///
/// The parser specification is inferred from the instance fields of the object
/// specified as the destination of the parse.
/// Valid argument types are: int, uint, string, bool, enums
/// Also argument types of Array of the above types are also valid.
///
/// Error checking options can be controlled by adding a ArgumentAttribute
/// to the instance fields of the destination object.
///
/// At most one field may be marked with the DefaultArgumentAttribute
/// indicating that arguments without a '-' or '/' prefix will be parsed as that argument.
///
/// If not specified then the parser will infer default options for parsing each
/// instance field. The default long name of the argument is the field name. The
/// default short name is the first character of the long name. Long names and explicitly
/// specified short names must be unique. Default short names will be used provided that
/// the default short name does not conflict with a long name or an explicitly
/// specified short name.
///
/// Arguments which are array types are collection arguments. Collection
/// arguments can be specified multiple times.
/// </summary>
[BestFriend]
internal sealed class CmdParser
{
private const int SpaceBeforeParam = 2;
private readonly ErrorReporter _reporter;
private readonly IHost _host;
/// <summary>
/// Parses a command line. This assumes that the .exe name has been stripped off.
/// Errors are output on Console.Error.
/// Use ArgumentAttributes to control parsing behavior.
/// </summary>
/// <param name="env"> The host environment</param>
/// <param name="settings">The command line</param>
/// <param name="destination">The object to receive the options</param>
/// <returns>true if no errors were detected</returns>
public static bool ParseArguments(IHostEnvironment env, string settings, object destination)
{
return ParseArguments(env, settings, destination, Console.Error.WriteLine);
}
/// <summary>
/// Parses a command line. This assumes that the .exe name has been stripped off.
/// Use ArgumentAttributes to control parsing behavior.
/// </summary>
/// <param name="env"> The host environment</param>
/// <param name="settings">The command line</param>
/// <param name="destination">The object to receive the options</param>
/// <param name="destinationType">The type of 'destination'</param>
/// <param name="reporter"> The destination for parse errors. </param>
/// <returns>true if no errors were detected</returns>
public static bool ParseArguments(IHostEnvironment env, string settings, object destination, Type destinationType, ErrorReporter reporter)
{
Contracts.CheckValue(env, nameof(env));
env.CheckValue(destination, nameof(destination));
env.CheckValue(destinationType, nameof(destinationType));
env.Check(destinationType.IsInstanceOfType(destination));
env.CheckValue(reporter, nameof(reporter));
string[] strs;
if (!LexString(settings, out strs))
{
reporter("Error: Unbalanced quoting in command line arguments");
return false;
}
var info = GetArgumentInfo(destinationType, null);
CmdParser parser = new CmdParser(env, reporter);
return parser.Parse(info, strs, destination);
}
private static bool ParseArguments(string settings, object destination, CmdParser parser)
{
string[] strs;
var destinationType = destination.GetType();
if (!LexString(settings, out strs))
{
parser.Report("Error: Unbalanced quoting in command line arguments");
return false;
}
var info = GetArgumentInfo(destinationType, null);
return parser.Parse(info, strs, destination);
}
/// <summary>
/// Parses a command line. This assumes that the exe name has been stripped off.
/// Use ArgumentAttributes to control parsing behavior.
/// </summary>
/// <param name="env"> The host environment</param>
/// <param name="settings">The command line</param>
/// <param name="destination">The object to receive the options</param>
/// <param name="reporter"> The destination for parse errors. </param>
/// <returns>true if no errors were detected</returns>
public static bool ParseArguments(IHostEnvironment env, string settings, object destination, ErrorReporter reporter)
{
Contracts.CheckValue(destination, nameof(destination));
return ParseArguments(env, settings, destination, destination.GetType(), reporter);
}
public static bool ParseArguments(IHostEnvironment env, string settings, object destination, out string helpText)
{
return ParseArguments(env, settings, destination, Console.Error.WriteLine, out helpText);
}
public static bool ParseArguments(IHostEnvironment env, string settings, object destination, ErrorReporter reporter, out string helpText)
{
Contracts.CheckValue(env, nameof(env));
env.CheckValue(reporter, nameof(reporter));
var info = GetArgumentInfo(destination.GetType(), destination);
CmdParser parser = new CmdParser(env, reporter);
string[] strs;
if (!LexString(settings, out strs))
reporter("Error: Unbalanced quoting in command line arguments");
else if (parser.Parse(info, strs, destination))
{
helpText = null;
return true;
}
helpText = parser.GetUsageString(env, info);
return false;
}
public static string CombineSettings(string[] settings)
{
if (settings == null || settings.Length == 0)
return null;
StringBuilder sb = new StringBuilder();
foreach (var s in settings)
{
string inner = CmdLexer.UnquoteValue(s);
inner = inner.Trim();
if (inner.Length > 0)
{
if (sb.Length > 0)
sb.Append(' ');
sb.Append(inner);
}
}
return sb.ToString();
}
// REVIEW: Add a method for cloning arguments, instead of going to text and back.
public static string GetSettings(IHostEnvironment env, object values, object defaults, SettingsFlags flags = SettingsFlags.Default)
{
Type t1 = values.GetType();
Type t2 = defaults.GetType();
Type t;
if (t1 == t2)
t = t1;
else if (t1.IsAssignableFrom(t2))
t = t1;
else if (t2.IsAssignableFrom(t1))
t = t2;
else
return null;
var info = GetArgumentInfo(t, defaults);
return GetSettingsCore(env, info, values, flags);
}
public static IEnumerable<KeyValuePair<string, string>> GetSettingPairs(IHostEnvironment env, object values, object defaults, SettingsFlags flags = SettingsFlags.None)
{
Type t1 = values.GetType();
Type t2 = defaults.GetType();
Type t;
if (t1 == t2)
t = t1;
else if (t1.IsAssignableFrom(t2))
t = t1;
else if (t2.IsAssignableFrom(t1))
t = t2;
else
return null;
var info = GetArgumentInfo(t, defaults);
var parser = new CmdParser(env);
return parser.GetSettingPairsCore(env, info, values, flags);
}
public static IEnumerable<KeyValuePair<string, string>> GetSettingPairs(IHostEnvironment env, object values, SettingsFlags flags = SettingsFlags.None)
{
var info = GetArgumentInfo(values.GetType(), null);
var parser = new CmdParser(env);
return parser.GetSettingPairsCore(env, info, values, flags);
}
/// <summary>
/// Check whether a certain type is numeric.
/// </summary>
public static bool IsNumericType(Type type)
{
return
type == typeof(double) ||
type == typeof(float) ||
type == typeof(int) ||
type == typeof(long) ||
type == typeof(short) ||
type == typeof(uint) ||
type == typeof(ulong) ||
type == typeof(ushort);
}
private void Report(string str)
{
_reporter?.Invoke(str);
}
private void Report(string fmt, params object[] args)
{
_reporter?.Invoke(string.Format(fmt, args));
}
/// <summary>
/// Returns a Usage string for command line argument parsing.
/// Use ArgumentAttributes to control parsing behavior.
/// </summary>
/// <param name="env"> The host environment. </param>
/// <param name="type"> The type of the arguments to display usage for. </param>
/// <param name="defaults"> The default values. </param>
/// <param name="showRsp"> Whether to show the @file item. </param>
/// <param name="columns"> The number of columns to format the output to. </param>
/// <returns> Printable string containing a user friendly description of command line arguments. </returns>
public static string ArgumentsUsage(IHostEnvironment env, Type type, object defaults, bool showRsp = false, int? columns = null)
{
var info = GetArgumentInfo(type, defaults);
var parser = new CmdParser(env);
return parser.GetUsageString(env, info, showRsp, columns);
}
private CmdParser(IHostEnvironment env)
{
_host = env.Register("CmdParser");
_reporter = Console.Error.WriteLine;
}
private CmdParser(IHostEnvironment env, ErrorReporter reporter)
{
Contracts.AssertValueOrNull(reporter);
_host = env.Register("CmdParser");
_reporter = reporter;
}
public static ArgInfo GetArgInfo(Type type, object defaults)
{
return ArgInfo.Arg.GetInfo(type, defaults);
}
private static string ArgCase(string name)
{
if (string.IsNullOrEmpty(name))
return name;
if (!char.IsUpper(name[0]))
return name;
if (name.Length == 1)
return name.ToLowerInvariant();
if (!char.IsUpper(name[1]))
return name.Substring(0, 1).ToLowerInvariant() + name.Substring(1);
int firstNonUpper;
for (firstNonUpper = 0; firstNonUpper < name.Length && char.IsUpper(name[firstNonUpper]); ++firstNonUpper)
;
Contracts.Assert(1 < firstNonUpper && firstNonUpper <= name.Length);
if (firstNonUpper == name.Length)
return name.ToLowerInvariant();
--firstNonUpper;
return name.Substring(0, firstNonUpper).ToLowerInvariant() + name.Substring(firstNonUpper);
}
private static ArgumentInfo GetArgumentInfo(Type type, object defaults)
{
Contracts.CheckValue(type, nameof(type));
Contracts.CheckParam(type.IsClass, nameof(type));
var args = new List<Argument>();
var map = new Dictionary<string, Argument>();
Argument def = null;
foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
ArgumentAttribute attr = GetAttribute(field);
if (attr != null)
{
Contracts.Check(!field.IsStatic && !field.IsInitOnly && !field.IsLiteral);
bool isDefault = attr is DefaultArgumentAttribute;
if (isDefault && def != null)
throw Contracts.Except($"Duplicate default argument '{def.LongName}' vs '{field.Name}'");
string name = ArgCase(attr.Name ?? field.Name);
string[] nicks;
// Semantics of ShortName:
// The string provided represents an array of names separated by commas and spaces, once empty entries are removed.
// 'null' or a singleton array with containing only the long field name means "use the default short name",
// and is represented by the null 'nicks' array.
// 'String.Empty' or a string containing only spaces and commas means "no short name", and is represented by an empty 'nicks' array.
if (attr.ShortName == null)
nicks = null;
else
{
nicks = attr.ShortName.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (nicks.Length == 0 || nicks.Length == 1 && nicks[0].ToLowerInvariant() == name.ToLowerInvariant())
nicks = null;
}
Contracts.Assert(!isDefault || nicks == null);
if (map.ContainsKey(name.ToLowerInvariant()))
throw Contracts.Except($"Duplicate name '{name}' in argument type '{type.Name}'");
if (nicks != null)
{
foreach (var nick in nicks)
{
if (map.ContainsKey(nick.ToLowerInvariant()))
throw Contracts.Except($"Duplicate name '{nick}' in argument type '{type.Name}'");
}
}
var arg = new Argument(isDefault ? -1 : args.Count, name, nicks, defaults, attr, field);
// Note that we put the default arg in the map to ensure that no other args use the same name.
map.Add(name.ToLowerInvariant(), arg);
if (nicks != null)
{
foreach (var nick in nicks)
map.Add(nick.ToLowerInvariant(), arg);
}
if (isDefault)
def = arg;
else
args.Add(arg);
}
}
// If there is a default argument, remove it from the _map.
if (def != null)
{
Contracts.Assert(def.ShortNames == null);
string name = def.LongName.ToLowerInvariant();
Contracts.Assert(map.ContainsKey(name) && map[name] == def);
map.Remove(name);
}
return new ArgumentInfo(type, def, args.ToArray(), map);
}
private static ArgumentAttribute GetAttribute(FieldInfo field)
{
var argumentAttribute = field.GetCustomAttribute<ArgumentAttribute>(false);
if (argumentAttribute?.Visibility == ArgumentAttribute.VisibilityType.EntryPointsOnly)
return null;
return argumentAttribute;
}
private void ReportUnrecognizedArgument(string argument)
{
Report("Unrecognized command line argument '{0}'", argument);
}
/// <summary>
/// Parses an argument list into an object
/// </summary>
/// <param name="info"></param>
/// <param name="strs"></param>
/// <param name="destination"></param>
/// <param name="values"></param>
/// <returns> true if successful </returns>
private bool ParseArgumentList(ArgumentInfo info, string[] strs, object destination, ArgValue[] values)
{
if (strs == null)
return true;
bool hadError = false;
for (int i = 0; i < strs.Length; i++)
{
string str = strs[i];
Contracts.Assert(!string.IsNullOrEmpty(str));
Argument arg;
string option;
string value;
string tag;
switch (str[0])
{
case '@':
string[] nested;
hadError |= !LexFileArguments(str.Substring(1), out nested);
hadError |= !ParseArgumentList(info, nested, destination, values);
continue;
case '-':
case '/':
if (!TryGetOptionValue(info, str.Substring(1), out arg, out option, out tag, out value))
{
ReportUnrecognizedArgument(option);
hadError = true;
if (value == null && i + 2 < strs.Length && strs[i + 1] == "=")
i += 2;
continue;
}
break;
default:
// No switch. See if it looks like a switch or a default value.
if (TryGetOptionValue(info, str, out arg, out option, out tag, out value))
{
if (info.ArgDef == null)
break;
// There is a default argument, so if the arg looks like a filename, assume it is
// a default argument.
if (option.Length > 1 || str.Length <= 1 || str[1] != ':')
break;
}
if (info.ArgDef != null)
{
str = CmdLexer.UnquoteValue(str);
hadError |= !info.ArgDef.SetValue(this, ref values[info.Args.Length], str, "", destination);
}
else
{
ReportUnrecognizedArgument(option);
hadError = true;
if (value == null && i + 2 < strs.Length && strs[i + 1] == "=")
i += 2;
}
continue;
}
Contracts.AssertValue(arg);
Contracts.Assert(arg != info.ArgDef);
Contracts.Assert(0 <= arg.Index && arg.Index < info.Args.Length);
if (tag != null && !arg.IsTaggedCollection)
{
hadError = true;
Report("Error: Tag not allowed for option '{0}'", arg.LongName);
tag = null;
}
tag = tag ?? "";
if (value == null)
{
if (i + 2 < strs.Length && strs[i + 1] == "=")
value = strs[i += 2];
else if (arg.ItemValueType == typeof(bool))
{
// The option is a boolean without embedded value, so its value is true.
value = "+";
}
else if (i + 1 < strs.Length)
value = strs[++i];
else
{
hadError = true;
Report("Error: Need a value for option '{0}'", arg.LongName);
continue;
}
}
if (arg.IsComponentFactory)
{
ComponentCatalog.ComponentInfo component;
if (IsCurlyGroup(value) && value.Length == 2)
arg.Field.SetValue(destination, null);
else if (!arg.IsCollection &&
_host.ComponentCatalog.TryFindComponentCaseInsensitive(arg.Field.FieldType, value, out component))
{
var activator = Activator.CreateInstance(component.ArgumentType);
if (!IsCurlyGroup(value) && i + 1 < strs.Length && IsCurlyGroup(strs[i + 1]))
{
hadError |= !ParseArguments(CmdLexer.UnquoteValue(strs[i + 1]), activator, this);
i++;
}
if (!hadError)
arg.Field.SetValue(destination, activator);
}
else
{
hadError |= !arg.SetValue(this, ref values[arg.Index], value, tag, destination);
if (!IsCurlyGroup(value) && i + 1 < strs.Length && IsCurlyGroup(strs[i + 1]))
hadError |= !arg.SetValue(this, ref values[arg.Index], strs[++i], "", destination);
}
continue;
}
if (arg.IsCustomItemType)
{
hadError |= !arg.SetValue(this, ref values[arg.Index], value, tag, destination);
continue;
}
if (IsCurlyGroup(value))
{
value = CmdLexer.UnquoteValue(value);
hadError |= !arg.SetValue(this, ref values[arg.Index], value, tag, destination);
continue;
}
// Collections of value types (enum or number) can be specified as a colon separated list.
if (!arg.IsCollection || value == null || !arg.ItemValueType.IsSubclassOf(typeof(System.ValueType)) ||
arg.ItemValueType == typeof(char) || !value.Contains(":"))
{
hadError |= !arg.SetValue(this, ref values[arg.Index], value, tag, destination);
continue;
}
foreach (string val in value.Split(':'))
hadError |= !arg.SetValue(this, ref values[arg.Index], val, "", destination);
}
return !hadError;
}
// Decompose str into the option, tag (if present) and value (if an embedded value). Get the Argument from the option,
// or return false if there isn't a corresponding Argument.
private bool TryGetOptionValue(ArgumentInfo info, string str, out Argument arg, out string option, out string tag, out string value)
{
// See if it contains a value.
int ichLim = str.IndexOfAny(new char[] { ':', '+', '-' });
if (ichLim > 0 && str[ichLim] == ':')
{
option = str.Substring(0, ichLim);
value = str.Substring(ichLim + 1);
}
else if (ichLim > 0 && ichLim == str.Length - 1)
{
option = str.Substring(0, ichLim);
value = str.Substring(ichLim);
}
else
{
option = str;
value = null;
}
// See if option contains a tag
tag = null;
var open = option.IndexOf('[');
if (open > 0)
{
var close = option.IndexOf(']');
if (close == option.Length - 1)
{
tag = option.Substring(open, close - open + 1);
option = option.Substring(0, open);
}
}
return info.Map.TryGetValue(option.ToLowerInvariant(), out arg);
}
private static bool IsCurlyGroup(string str)
{
return str != null && str.StartsWith("{") && str.EndsWith("}");
}
/// <summary>
/// Parses an argument list.
/// </summary>
/// <param name="info"></param>
/// <param name="strs"> The arguments to parse. </param>
/// <param name="destination"> The destination of the parsed arguments. </param>
/// <returns> true if no parse errors were encountered. </returns>
private bool Parse(ArgumentInfo info, string[] strs, object destination)
{
var values = new ArgValue[info.Args.Length + 1];
bool hadError = !ParseArgumentList(info, strs, destination, values);
// check for missing required arguments
for (int i = 0; i < info.Args.Length; i++)
{
var arg = info.Args[i];
hadError |= arg.Finish(this, values[arg.Index], destination);
}
if (info.ArgDef != null)
hadError |= info.ArgDef.Finish(this, values[info.Args.Length], destination);
return !hadError;
}
private static string GetSettingsCore(IHostEnvironment env, ArgumentInfo info, object values, SettingsFlags flags)
{
StringBuilder sb = new StringBuilder();
if (info.ArgDef != null)
{
var val = info.ArgDef.GetValue(values);
info.ArgDef.AppendSetting(env, sb, val, flags);
}
foreach (Argument arg in info.Args)
{
var val = arg.GetValue(values);
arg.AppendSetting(env, sb, val, flags);
}
return sb.ToString();
}
/// <summary>
/// GetSettingsCore handles the top-level case. This handles the nested custom record case.
/// It deals with custom "unparse" functionality, as well as quoting. It also appends to a StringBuilder
/// instead of returning a string.
/// </summary>
private static void AppendCustomItem(IHostEnvironment env, ArgumentInfo info, object values, SettingsFlags flags, StringBuilder sb)
{
int ich = sb.Length;
// We always call unparse, even when NoUnparse is specified, since Unparse can "cleanse", which
// we also want done in the full case.
if (info.TryUnparse(values, sb))
{
// Make sure it doesn't need quoted.
if ((flags & SettingsFlags.NoUnparse) != 0 || !CmdQuoter.NeedsQuoting(sb, ich))
return;
}
sb.Length = ich;
if (info.ArgDef != null)
{
var val = info.ArgDef.GetValue(values);
info.ArgDef.AppendSetting(env, sb, val, flags);
}
foreach (Argument arg in info.Args)
{
var val = arg.GetValue(values);
arg.AppendSetting(env, sb, val, flags);
}
string str = sb.ToString(ich, sb.Length - ich);
sb.Length = ich;
CmdQuoter.QuoteValue(str, sb, force: true);
}
private IEnumerable<KeyValuePair<string, string>> GetSettingPairsCore(IHostEnvironment env, ArgumentInfo info, object values, SettingsFlags flags)
{
StringBuilder buffer = new StringBuilder();
foreach (Argument arg in info.Args)
{
string key = arg.GetKey(flags);
object value = arg.GetValue(values);
foreach (string val in arg.GetSettingStrings(env, value, buffer))
yield return new KeyValuePair<string, string>(key, val);
}
}
private readonly struct ArgumentHelpStrings
{
public readonly string Syntax;
public readonly string Help;
public ArgumentHelpStrings(string syntax, string help)
{
Syntax = syntax;
Help = help;
}
}
/// <summary>
/// A user friendly usage string describing the command line argument syntax.
/// </summary>
private string GetUsageString(IHostEnvironment env, ArgumentInfo info, bool showRsp = true, int? columns = null)
{
int screenWidth = columns ?? Console.BufferWidth;
if (screenWidth <= 0)
screenWidth = 80;
ArgumentHelpStrings[] strings = GetAllHelpStrings(env, info, showRsp);
int maxParamLen = 0;
foreach (ArgumentHelpStrings helpString in strings)
{
maxParamLen = Math.Max(maxParamLen, helpString.Syntax.Length);
}
const int minimumNumberOfCharsForHelpText = 10;
const int minimumHelpTextColumn = 5;
const int minimumScreenWidth = minimumHelpTextColumn + minimumNumberOfCharsForHelpText;
int helpTextColumn;
int idealMinimumHelpTextColumn = maxParamLen + SpaceBeforeParam;
screenWidth = Math.Max(screenWidth, minimumScreenWidth);
if (screenWidth < (idealMinimumHelpTextColumn + minimumNumberOfCharsForHelpText))
helpTextColumn = minimumHelpTextColumn;
else
helpTextColumn = idealMinimumHelpTextColumn;
const string newLine = "\r\n";
StringBuilder builder = new StringBuilder();
foreach (ArgumentHelpStrings helpStrings in strings)
{
// add syntax string
int syntaxLength = helpStrings.Syntax.Length;
builder.Append(helpStrings.Syntax);
// start help text on new line if syntax string is too long
int currentColumn = syntaxLength;
if (syntaxLength >= helpTextColumn)
{
builder.Append(newLine);
currentColumn = 0;
}
// add help text broken on spaces
int charsPerLine = screenWidth - helpTextColumn;
int index = 0;
while (index < helpStrings.Help.Length)
{
// tab to start column
builder.Append(' ', helpTextColumn - currentColumn);
currentColumn = helpTextColumn;
// find number of chars to display on this line
int endIndex = index + charsPerLine;
if (endIndex >= helpStrings.Help.Length)
{
// rest of text fits on this line
endIndex = helpStrings.Help.Length;
}
else
{
endIndex = helpStrings.Help.LastIndexOf(' ', endIndex - 1, Math.Min(endIndex - index, charsPerLine));
if (endIndex <= index)
{
// no spaces on this line, append full set of chars
endIndex = index + charsPerLine;
}
}
// add chars
builder.Append(helpStrings.Help, index, endIndex - index);
index = endIndex;
// do new line
AddNewLine(newLine, builder, ref currentColumn);
// don't start a new line with spaces
while (index < helpStrings.Help.Length && helpStrings.Help[index] == ' ')
index++;
}
// add newline if there's no help text
if (helpStrings.Help.Length == 0)
{
builder.Append(newLine);
}
}
return builder.ToString();
}
private static void AddNewLine(string newLine, StringBuilder builder, ref int currentColumn)
{
builder.Append(newLine);
currentColumn = 0;
}
private ArgumentHelpStrings[] GetAllHelpStrings(IHostEnvironment env, ArgumentInfo info, bool showRsp)
{
List<ArgumentHelpStrings> strings = new List<ArgumentHelpStrings>();
if (info.ArgDef != null)
strings.Add(GetHelpStrings(env, info.ArgDef));
foreach (Argument arg in info.Args)
{
if (!arg.IsHidden)
strings.Add(GetHelpStrings(env, arg));
}
if (showRsp)
strings.Add(new ArgumentHelpStrings("@<file>", "Read response file for more options"));
return strings.ToArray();
}
private ArgumentHelpStrings GetHelpStrings(IHostEnvironment env, Argument arg)
{
return new ArgumentHelpStrings(arg.GetSyntaxHelp(), arg.GetFullHelpText(env));
}
private bool LexFileArguments(string fileName, out string[] arguments)
{
string args;
try
{
using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
args = (new StreamReader(file)).ReadToEnd();
}
}
catch (Exception e)
{
Report("Error: Can't open command line argument file '{0}' : '{1}'", fileName, e.Message);
arguments = null;
return false;
}
if (!LexString(args, out arguments))
{
Report("Error: Unbalanced '\"' in command line argument file '{0}'", fileName);
return false;
}
return true;
}
public static string TrimExePath(string args, out string exe)
{
if (string.IsNullOrEmpty(args))
{
exe = "";
return args;
}
CharCursor curs = new CharCursor(args);
// The exe part shouldn't consider \ as an escaping character.
CmdLexer lex = new CmdLexer(curs, false);
// REVIEW: Should this care about the error flag?
StringBuilder sb = new StringBuilder();
lex.GetToken(sb);
lex.SkipWhiteSpace();
exe = sb.ToString();
return curs.GetRest();
}
public static bool TryGetFirstToken(string text, out string token, out string rest)
{
if (string.IsNullOrWhiteSpace(text))
{
token = null;
rest = null;
return false;
}
StringBuilder bldr = new StringBuilder();
CharCursor curs = new CharCursor(text);
CmdLexer lex = new CmdLexer(curs);
int ich = curs.IchCur;
lex.GetToken(bldr);
Contracts.Assert(ich < curs.IchCur || curs.Eof);
token = bldr.ToString();
rest = text.Substring(curs.IchCur);
if (lex.Error || token.Length < 2 || token[0] != '@')
return !lex.Error;
// The first token is an rsp file, so need to drill into it.
string path = token.Substring(1);
string nested;
try
{
using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
nested = (new StreamReader(file)).ReadToEnd();
}
}
catch (Exception e)
{
Console.WriteLine("Error: Can't open command line argument file '{0}' : '{1}'", path, e.Message);
token = null;
rest = null;
return false;
}
// Rsp files can contain other rsp files, so we simply recurse....
if (!TryGetFirstToken(nested, out token, out nested))
return false;
// Concatenate the rest of the rsp with the rest of the original string.
rest = nested + "\r\n" + rest;
return true;
}
public static bool LexString(string text, out string[] arguments)
{
if (string.IsNullOrEmpty(text))
{
arguments = new string[0];
return true;
}
List<string> args = new List<string>();
StringBuilder bldr = new StringBuilder();
CharCursor curs = new CharCursor(text);
CmdLexer lex = new CmdLexer(curs);
while (!curs.Eof)
{
bldr.Clear();
int ich = curs.IchCur;
lex.GetToken(bldr);
Contracts.Assert(ich < curs.IchCur || curs.Eof);
if (bldr.Length > 0)
args.Add(bldr.ToString());
}
arguments = args.ToArray();
return !lex.Error;
}
private static bool IsNullableType(Type type)
{
return type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}
private static bool IsValidItemType(Type type)
{
if (type == null)
return false;
if (type == typeof(string))
return true;
Type typeBase = type;
if (IsNullableType(type))
typeBase = type.GetGenericArguments()[0];
return
typeBase == typeof(int) ||
typeBase == typeof(uint) ||
typeBase == typeof(short) ||
typeBase == typeof(ushort) ||
typeBase == typeof(long) ||
typeBase == typeof(ulong) ||
typeBase == typeof(byte) ||
typeBase == typeof(sbyte) ||
typeBase == typeof(float) ||
typeBase == typeof(double) ||
typeBase == typeof(Guid) ||
typeBase == typeof(DateTime) ||
typeBase == typeof(char) ||
typeBase == typeof(decimal) ||
typeBase == typeof(bool) ||
typeBase.IsEnum;
}
/// <summary>
/// Creates an ICommandLineComponentFactory given the factory type, signature type,
/// and a command line string.
/// </summary>
public static ICommandLineComponentFactory CreateComponentFactory(
Type factoryType,
Type signatureType,
string settings)
{
ParseComponentStrings(settings, out string name, out string args);
string[] argsArray = string.IsNullOrEmpty(args) ? Array.Empty<string>() : new string[] { args };
return ComponentFactoryFactory.CreateComponentFactory(factoryType, signatureType, name, argsArray);
}
private static void ParseComponentStrings(string str, out string kind, out string args)
{
kind = args = null;
if (string.IsNullOrWhiteSpace(str))
return;
str = str.Trim();
int ich = str.IndexOf('{');
if (ich < 0)
{
kind = str;
return;
}
if (ich == 0 || str[str.Length - 1] != '}')
throw Contracts.Except("Invalid Component string: mismatched braces, or empty component name.");
kind = str.Substring(0, ich);
args = CmdLexer.UnquoteValue(str.Substring(ich));
}
private sealed class ArgValue
{
public readonly string FirstValue;
public List<KeyValuePair<string, object>> Values; // Key=tag.
public ArgValue(string first)
{
Contracts.AssertValueOrNull(first);
FirstValue = first;
}
}
public sealed class ArgInfo
{
public readonly Arg[] Args;
private readonly ArgumentInfo _info;
private ArgInfo(ArgumentInfo info, Arg[] args)
{
Contracts.AssertValue(info);
Contracts.AssertValue(args);
Args = args;
_info = info;
}
public bool TryGetArg(string name, out Arg arg)
{
Argument argument;
if (string.IsNullOrWhiteSpace(name) || !_info.Map.TryGetValue(name.ToLowerInvariant(), out argument))
{
arg = null;
return false;
}
// Given that we find it in the map, there should be precisely one match.
Contracts.Assert(Args.Where(a => a.Index == argument.Index).Count() == 1);
arg = Args.FirstOrDefault(a => a.Index == argument.Index);
Contracts.AssertValue(arg);
return true;
}
public sealed class Arg
{
/// <summary>
/// This class exposes those parts of this wrapped <see cref="Argument"/> appropriate
/// for public consumption.
/// </summary>
private readonly Argument _arg;
public int Index { get { return _arg.Index; } }
public FieldInfo Field { get { return _arg.Field; } }
public string LongName { get { return _arg.LongName; } }
public string[] ShortNames { get { return _arg.ShortNames; } }
public string HelpText { get { return _arg.HelpText; } }
public Type ItemType { get { return _arg.ItemType; } }
public double SortOrder { get { return _arg.SortOrder; } }
public string NullName { get { return _arg.NullName; } }
// If tagged collection, this is the 'value' of the KeyValuePair. Otherwise, equal to ItemType.
public Type ItemValueType { get { return _arg.ItemValueType; } }
public ArgumentType Kind { get { return _arg.Kind; } }
public bool IsDefault { get { return _arg.IsDefault; } }
public bool IsHidden { get { return _arg.IsHidden; } }
public bool IsCollection { get { return _arg.IsCollection; } }
public bool IsTaggedCollection { get { return _arg.IsTaggedCollection; } }
// Used for help and composing settings strings.
public object DefaultValue { get { return _arg.DefaultValue; } }
public bool IsRequired
{
get { return ArgumentType.Required == (Kind & ArgumentType.Required); }
}
public string ItemTypeString { get { return TypeToString(ItemValueType); } }
private Arg(Argument arg)
{
Contracts.AssertValue(arg);
_arg = arg;
}
internal static ArgInfo GetInfo(Type type, object defaults = null)
{
Contracts.CheckValue(type, nameof(type));
Contracts.CheckValueOrNull(defaults);
if (defaults == null)
defaults = Activator.CreateInstance(type);
ArgumentInfo argumentInfo = GetArgumentInfo(type, defaults);
Arg[] args = Utils.BuildArray(argumentInfo.Args.Length, i => new Arg(argumentInfo.Args[i]));
Array.Sort(args, 0, args.Length, Comparer<Arg>.Create((x, y) => x.SortOrder.CompareTo(y.SortOrder)));
return new ArgInfo(argumentInfo, args);
}
private static string TypeToString(Type type)
{
Contracts.AssertValue(type);
bool isGeneric = type.IsGenericType;
if (type.IsGenericEx(typeof(Nullable<>)))
{
Contracts.Assert(isGeneric);
var genArgs = type.GetGenericTypeArgumentsEx();
Contracts.Assert(Utils.Size(genArgs) == 1);
return TypeToString(genArgs[0]) + "?";
}
if (type == typeof(int))
return "int";
else if (type == typeof(float))
return "float";
else if (type == typeof(double))
return "double";
else if (type == typeof(string))
return "string";
else if (type == typeof(uint))
return "uint";
else if (type == typeof(byte))
return "byte";
else if (type == typeof(sbyte))
return "sbyte";
else if (type == typeof(short))
return "short";
else if (type == typeof(ushort))
return "ushort";
else if (type == typeof(long))
return "long";
else if (type == typeof(ulong))
return "ulong";
else if (type == typeof(bool))
return "bool";
else if (type == typeof(decimal))
return "decimal";
else if (type == typeof(char))
return "char";
else if (type == typeof(System.Guid))
return "guid";
else if (type == typeof(System.DateTime))
return "datetime";
else if (type.IsEnum)
{
var bldr = new StringBuilder();
bldr.Append("[");
string sep = "";
foreach (FieldInfo field in type.GetFields())
{
if (!field.IsStatic || field.FieldType != type)
continue;
if (field.GetCustomAttribute<HideEnumValueAttribute>() != null)
continue;
bldr.Append(sep).Append(field.Name);
sep = " | ";
}
bldr.Append(']');
return bldr.ToString();
}
else
return type.Name;
}
}
}
private sealed class ArgumentInfo
{
public readonly Argument ArgDef;
public readonly Argument[] Args;
public readonly Dictionary<string, Argument> Map;
public readonly MethodInfo ParseCustom;
public readonly MethodInfo TryUnparseCustom;
public ArgumentInfo(Type type, Argument argDef, Argument[] args, Dictionary<string, Argument> map)
{
Contracts.Assert(argDef == null || argDef.Index == -1);
Contracts.AssertValue(args);
Contracts.AssertValue(map);
Contracts.Assert(map.Count >= args.Length);
Contracts.Assert(args.Select((arg, index) => arg.Index == index).All(b => b));
ArgDef = argDef;
Args = args;
Map = map;
// See if there is a custom parse method.
// REVIEW: Is there a better way to handle parsing and unparsing of custom forms?
// For unparse, we could use an interface or base class. That doesn't work for parsing though,
// since we don't have an instance at parse time (Parse is a static method). Perhaps we could
// first construct an instance, then call an instance method to try the parsing. Unfortunately,
// if parsing fails, we'd have to toss that instance and create another into which to do the
// field-by-field parsing.
ParseCustom = GetParseMethod(type);
TryUnparseCustom = GetUnparseMethod(type);
}
private static MethodInfo GetParseMethod(Type type)
{
Contracts.AssertValue(type);
var meth = type.GetMethod("Parse", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public, binder: null, new[] { typeof(string) }, null);
if (meth != null && meth.IsStatic && !meth.IsPrivate && meth.ReturnType == type)
return meth;
return null;
}
private static MethodInfo GetUnparseMethod(Type type)
{
Contracts.AssertValue(type);
var meth = type.GetMethod("TryUnparse", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, binder: null, new[] { typeof(StringBuilder) }, null);
if (meth != null && !meth.IsPrivate && meth.ReturnType == typeof(bool))
return meth;
return null;
}
public bool TryUnparse(object obj, StringBuilder sb)
{
Contracts.AssertValue(sb);
if (TryUnparseCustom == null)
return false;
return (bool)TryUnparseCustom.Invoke(obj, new object[] { sb });
}
}
private sealed class Argument
{
public readonly int Index;
public readonly FieldInfo Field;
public readonly string LongName;
public readonly string[] ShortNames;
public readonly string HelpText;
public readonly Type ItemType;
public readonly double SortOrder;
public readonly string NullName;
// If tagged collection, this is the 'value' of the KeyValuePair. Otherwise, equal to ItemType.
public readonly Type ItemValueType;
public readonly ArgumentType Kind;
public readonly bool IsDefault;
public readonly bool IsHidden;
public readonly bool IsCollection;
public readonly bool IsTaggedCollection;
public readonly bool IsComponentFactory;
// Used for help and composing settings strings.
public readonly object DefaultValue;
private readonly Type _signatureType;
// For custom types.
private readonly ArgumentInfo _infoCustom;
private readonly ConstructorInfo _ctorCustom;
public Argument(int index, string name, string[] nicks, object defaults, ArgumentAttribute attr, FieldInfo field)
{
Contracts.Assert(index >= -1);
Contracts.Assert(!string.IsNullOrWhiteSpace(name));
Contracts.Check(nicks == null || nicks.All(nick => !string.IsNullOrWhiteSpace(nick)));
Contracts.AssertValueOrNull(defaults);
Contracts.AssertValue(attr);
Contracts.AssertValue(field);
Index = index;
Field = field;
LongName = name;
ShortNames = nicks;
HelpText = attr.HelpText;
SortOrder = attr.SortOrder;
NullName = attr.NullName;
if (string.IsNullOrWhiteSpace(HelpText))
HelpText = null;
Kind = attr.Type;
IsDefault = attr is DefaultArgumentAttribute;
Contracts.Assert(!IsDefault || Utils.Size(ShortNames) == 0);
IsHidden = attr.Hide;
_signatureType = attr.SignatureType;
if (field.FieldType.IsArray)
{
IsCollection = true;
ItemType = field.FieldType.GetElementType();
IsTaggedCollection = IsKeyValuePair(ItemType);
ItemValueType = IsTaggedCollection ? ItemType.GenericTypeArguments[1] : ItemType;
}
else
{
IsCollection = IsTaggedCollection = false;
ItemValueType = ItemType = field.FieldType;
}
if (defaults != null && !IsRequired)
DefaultValue = field.GetValue(defaults);
if (typeof(IComponentFactory).IsAssignableFrom(ItemValueType))
IsComponentFactory = true;
else if (!IsValidItemType(ItemValueType))
{
object def;
try
{
_ctorCustom = ItemValueType.GetConstructor(Type.EmptyTypes);
def = _ctorCustom != null ? _ctorCustom.Invoke(null) : null;
}
catch
{
def = null;
}
if (def != null)
{
_infoCustom = GetArgumentInfo(ItemValueType, def);
if (_infoCustom.ArgDef == null && _infoCustom.Args.Length == 0)
_infoCustom = null;
}
if (_infoCustom == null)
throw Contracts.Except("Invalid argument type: '{0}'", ItemValueType.Name);
}
Contracts.Check(!IsCollection || AllowMultiple, "Collection arguments must allow multiple");
Contracts.Check(!Unique || IsCollection, "Unique only applicable to collection arguments");
}
private bool IsKeyValuePair(Type type)
{
Contracts.AssertValue(type);
if (!type.IsGenericEx(typeof(KeyValuePair<,>)))
return false;
Contracts.AssertValue(type.GenericTypeArguments);
if (type.GenericTypeArguments.Length != 2)
return false;
return type.GenericTypeArguments[0] == typeof(string);
}
public bool Finish(CmdParser owner, ArgValue val, object destination)
{
if (val == null)
{
// Make sure all collections have a non-null.
// REVIEW: Should we really do this? Or should all code be able to handle null arrays?
// Should we also set null strings to ""?
if (IsCollection && Field.GetValue(destination) == null)
Field.SetValue(destination, Array.CreateInstance(ItemType, 0));
return ReportMissingRequiredArgument(owner, val);
}
var values = val.Values;
bool error = false;
if (IsSingleComponentFactory)
{
bool haveName = false;
string name = null;
string[] settings = null;
for (int i = 0; i < Utils.Size(values);)
{
string str = (string)values[i].Value;
if (str.StartsWith("{"))
{
i++;
continue;
}
if (haveName)
{
owner.Report("Duplicate component kind for argument {0}", LongName);
error = true;
}
name = str;
haveName = true;
values.RemoveAt(i);
}
if (Utils.Size(values) > 0)
settings = values.Select(x => (string)x.Value).ToArray();
Contracts.Check(_signatureType != null, "ComponentFactory Arguments need a SignatureType set.");
if (ComponentFactoryFactory.TryCreateComponentFactory(
ItemType,
_signatureType,
name,
settings,
out ICommandLineComponentFactory factory))
{
Field.SetValue(destination, factory);
}
else
{
owner.Report("There was an error creating the ComponentFactory. Ensure '{0}' is configured correctly.", LongName);
error = true;
}
}
else if (IsMultiComponentFactory)
{
// REVIEW: the kind should not be separated from settings: everything related
// to one item should go into one value, not multiple values
if (IsTaggedCollection)
{
// Tagged collection of IComponentFactory
var comList = new List<KeyValuePair<string, IComponentFactory>>();
for (int i = 0; i < Utils.Size(values);)
{
string tag = values[i].Key;
string name = (string)values[i++].Value;
string[] settings = null;
if (i < values.Count && IsCurlyGroup((string)values[i].Value) && string.IsNullOrEmpty(values[i].Key))
settings = new string[] { (string)values[i++].Value };
if (ComponentFactoryFactory.TryCreateComponentFactory(
ItemValueType,
_signatureType,
name,
settings,
out ICommandLineComponentFactory factory))
{
comList.Add(new KeyValuePair<string, IComponentFactory>(tag, factory));
}
else
{
owner.Report("There was an error creating the ComponentFactory. Ensure '{0}' is configured correctly.", LongName);
error = true;
}
}
var arr = Array.CreateInstance(ItemType, comList.Count);
for (int i = 0; i < arr.Length; i++)
{
var kvp = Activator.CreateInstance(ItemType, comList[i].Key, comList[i].Value);
arr.SetValue(kvp, i);
}
Field.SetValue(destination, arr);
}
else
{
// Collection of IComponentFactory
var comList = new List<IComponentFactory>();
for (int i = 0; i < Utils.Size(values);)
{
string name = (string)values[i++].Value;
string[] settings = null;
if (i < values.Count && IsCurlyGroup((string)values[i].Value))
settings = new string[] { (string)values[i++].Value };
if (ComponentFactoryFactory.TryCreateComponentFactory(
ItemValueType,
_signatureType,
name,
settings,
out ICommandLineComponentFactory factory))
{
comList.Add(factory);
}
else
{
owner.Report("There was an error creating the ComponentFactory. Ensure '{0}' is configured correctly.", LongName);
error = true;
}
}
var arr = Array.CreateInstance(ItemValueType, comList.Count);
for (int i = 0; i < arr.Length; i++)
arr.SetValue(comList[i], i);
Field.SetValue(destination, arr);
}
}
else if (IsTaggedCollection)
{
var res = Array.CreateInstance(ItemType, Utils.Size(values));
for (int i = 0; i < res.Length; i++)
{
var kvp = Activator.CreateInstance(ItemType, values[i].Key, values[i].Value);
res.SetValue(kvp, i);
}
Field.SetValue(destination, res);
}
else if (IsCollection)
{
var res = Array.CreateInstance(ItemType, Utils.Size(values));
for (int i = 0; i < res.Length; i++)
res.SetValue(values[i].Value, i);
Field.SetValue(destination, res);
}
return error;
}
private bool ReportMissingRequiredArgument(CmdParser owner, ArgValue val)
{
if (!IsRequired || val != null)
return false;
if (IsDefault)
owner.Report("Missing required argument '<{0}>'.", LongName);
else
owner.Report("Missing required argument '{0}'.", LongName);
return true;
}
private void ReportDuplicateArgumentValue(CmdParser owner, string value)
{
owner.Report("Duplicate '{0}' argument '{1}'", LongName, value);
}
public bool SetValue(CmdParser owner, ref ArgValue val, string value, string tag, object destination)
{
if (val == null)
val = new ArgValue(value);
else if (!AllowMultiple)
{
owner.Report("Duplicate '{0}' argument: '{1}' then '{2}'", LongName, val.FirstValue, value);
return false;
}
Contracts.Assert(string.IsNullOrEmpty(tag) || tag.StartsWith("[") && tag.EndsWith("]"));
bool hasTag = tag != null && tag.Length > 2;
tag = hasTag ? tag.Substring(1, tag.Length - 2) : "";
if (hasTag && !IsTaggedCollection)
{
owner.Report("Tags aren't allowed for '{0}' argument", LongName);
return false;
}
object newValue;
if (!ParseValue(owner, value, out newValue))
return false;
if (IsCollection)
{
if (val.Values == null)
val.Values = new List<KeyValuePair<string, object>>();
else if (Unique && val.Values.Any(x => x.Value.Equals(newValue)))
{
ReportDuplicateArgumentValue(owner, value);
return false;
}
val.Values.Add(new KeyValuePair<string, object>(tag, newValue));
}
else if (IsComponentFactory)
{
Contracts.Assert(newValue is string || newValue == null);
Contracts.Assert((string)newValue != "");
if (newValue != null && (string)newValue != "{}")
Utils.Add(ref val.Values, new KeyValuePair<string, object>("", newValue));
}
else
Field.SetValue(destination, newValue);
return true;
}
public object GetValue(object source)
{
return Field.GetValue(source);
}
public string GetKey(SettingsFlags flags)
{
if ((flags & SettingsFlags.ShortNames) != 0)
return Utils.Size(ShortNames) != 0 ? ShortNames[0] : LongName;
return LongName;
}
private void ReportBadArgumentValue(CmdParser owner, string value)
{
owner.Report("'{0}' is not a valid value for the '{1}' command line option", value, LongName);
}
private bool ParseValue(CmdParser owner, string data, out object value)
{
Type type = ItemValueType;
// Treat empty string the same as null. Null is only valid for nullable types, strings,
// and sub components.
if (string.IsNullOrEmpty(data))
{
value = null;
if (IsNullableType(type))
return true;
if (type == typeof(string))
return true;
ReportBadArgumentValue(owner, data);
return false;
}
if (IsComponentFactory)
{
value = data;
return true;
}
if (IsCustomItemType)
{
object res;
if (!IsCurlyGroup(data))
{
if (_infoCustom.ParseCustom != null &&
(res = _infoCustom.ParseCustom.Invoke(null, new string[] { data })) != null)
{
value = res;
return true;
}
owner.Report("Expected arguments in curly braces for '{0}'", LongName);
value = null;
return false;
}
// Need to unquote.
data = CmdLexer.UnquoteValue(data);
res = _ctorCustom.Invoke(null);
string[] strs;
if (!LexString(data, out strs))
{
value = null;
return false;
}
if (!owner.Parse(_infoCustom, strs, res))
{
value = null;
return false;
}
value = res;
return true;
}
if (IsNullableType(type))
type = type.GetGenericArguments()[0];
try
{
if (type == typeof(string))
{
value = data;
return true;
}
else if (type == typeof(bool))
{
if (data == "+")
{
value = true;
return true;
}
else if (data == "-")
{
value = false;
return true;
}
}
else if (type == typeof(int))
{
value = int.Parse(data);
return true;
}
else if (type == typeof(uint))
{
value = uint.Parse(data);
return true;
}
else if (type == typeof(short))
{
value = short.Parse(data);
return true;
}
else if (type == typeof(ushort))
{
value = ushort.Parse(data);
return true;
}
else if (type == typeof(byte))
{
value = byte.Parse(data);
return true;
}
else if (type == typeof(sbyte))
{
value = sbyte.Parse(data);
return true;
}
else if (type == typeof(long))
{
value = long.Parse(data);
return true;
}
else if (type == typeof(ulong))
{
value = ulong.Parse(data);
return true;
}
else if (type == typeof(float))
{
value = float.Parse(data, CultureInfo.InvariantCulture);
return true;
}
else if (type == typeof(double))
{
value = double.Parse(data, CultureInfo.InvariantCulture);
return true;
}
else if (type == typeof(decimal))
{
value = decimal.Parse(data);
return true;
}
else if (type == typeof(char))
{
value = char.Parse(data);
return true;
}
else if (type == typeof(System.Guid))
{
value = new System.Guid(data);
return true;
}
else if (type == typeof(System.DateTime))
{
value = System.DateTime.Parse(data);
return true;
}
else
{
Contracts.Assert(type.IsEnum);
value = Enum.Parse(type, data, true);
return true;
}
}
catch
{
// catch parse errors
}
ReportBadArgumentValue(owner, data);
value = null;
return false;
}
private void AppendHelpValue(IHostEnvironment env, StringBuilder builder, object value)
{
if (value == null)
builder.Append("{}");
else if (value is bool)
builder.Append((bool)value ? "+" : "-");
else if (value is Array)
{
string pre = "";
foreach (object o in (System.Array)value)
{
builder.Append(pre);
AppendHelpValue(env, builder, o);
pre = ", ";
}
}
else if (value is IComponentFactory)
{
string name;
var catalog = env.ComponentCatalog;
var type = value.GetType();
bool success = catalog.TryGetComponentShortName(type, out name);
Contracts.Assert(success);
var settings = GetSettings(env, value, Activator.CreateInstance(type));
builder.Append(name);
if (!string.IsNullOrWhiteSpace(settings))
{
StringBuilder sb = new StringBuilder();
CmdQuoter.QuoteValue(settings, sb, true);
builder.Append(sb);
}
}
else
{
// REVIEW: This isn't necessarily correct for string - may need to quote!
builder.Append(value.ToString());
}
}
// If value differs from the default, appends the setting to sb.
public void AppendSetting(IHostEnvironment env, StringBuilder sb, object value, SettingsFlags flags)
{
object def = DefaultValue;
if (!IsCollection)
{
if (value == null)
{
if (def != null || IsRequired)
AppendSettingCore(env, sb, "", flags);
}
else if (def == null || !value.Equals(def))
{
var buffer = new StringBuilder();
if (!(value is IComponentFactory) || (GetString(env, value, buffer) != GetString(env, def, buffer)))
AppendSettingCore(env, sb, value, flags);
}
return;
}
Contracts.Assert(value == null || value is Array);
IList vals = (Array)value;
if (vals == null || vals.Count == 0)
{
// REVIEW: Is there any way to represent an empty array?
return;
}
// See if vals matches defs.
IList defs = (Array)def;
if (defs != null && vals.Count == defs.Count)
{
for (int i = 0; ; i++)
{
if (i >= vals.Count)
return;
if (!vals[i].Equals(defs[i]))
break;
}
}
foreach (object x in vals)
AppendSettingCore(env, sb, x, flags);
}
private void AppendSettingCore(IHostEnvironment env, StringBuilder sb, object value, SettingsFlags flags)
{
if (sb.Length > 0)
sb.Append(" ");
if (!IsDefault)
{
if ((flags & SettingsFlags.NoSlashes) == 0)
sb.Append('/');
sb.Append(GetKey(flags));
string tag;
ExtractTag(value, out tag, out value);
sb.Append(tag);
sb.Append('=');
}
if (value is string)
CmdQuoter.QuoteValue(value.ToString(), sb);
else if (value is bool)
sb.Append((bool)value ? "+" : "-");
else if (IsCustomItemType)
AppendCustomItem(env, _infoCustom, value, flags, sb);
else if (IsComponentFactory)
{
var buffer = new StringBuilder();
sb.Append(GetString(env, value, buffer));
}
else if (value is Data.InternalDataKind kind)
{
sb.Append(kind.GetString());
}
else
sb.Append(value.ToString());
}
private void ExtractTag(object value, out string tag, out object newValue)
{
if (!IsTaggedCollection)
{
newValue = value;
tag = "";
return;
}
var type = value.GetType();
Contracts.Assert(IsKeyValuePair(type));
tag = (string)type.GetProperty("Key").GetValue(value);
if (!string.IsNullOrEmpty(tag))
tag = string.Format("[{0}]", tag);
newValue = type.GetProperty("Value").GetValue(value);
}
// If value differs from the default, return the string representation of 'value',
// or an IEnumerable of string representations if 'value' is an array.
public IEnumerable<string> GetSettingStrings(IHostEnvironment env, object value, StringBuilder buffer)
{
object def = DefaultValue;
if (!IsCollection)
{
if (value == null)
{
if (def != null || IsRequired)
yield return GetString(env, value, buffer);
}
else if (def == null || !value.Equals(def))
{
if (!(value is IComponentFactory) || (GetString(env, value, buffer) != GetString(env, def, buffer)))
yield return GetString(env, value, buffer);
}
yield break;
}
Contracts.Assert(value == null || value is Array);
IList vals = (Array)value;
if (vals == null || vals.Count == 0)
{
// REVIEW: Is there any way to represent an empty array?
yield break;
}
// See if vals matches defs.
IList defs = (Array)def;
if (defs != null && vals.Count == defs.Count)
{
for (int i = 0; ; i++)
{
if (i >= vals.Count)
yield break;
if (!vals[i].Equals(defs[i]))
break;
}
}
foreach (object x in vals)
yield return GetString(env, x, buffer);
}
private string GetString(IHostEnvironment env, object value, StringBuilder buffer)
{
if (value == null)
return "{}";
if (value is string)
{
buffer.Clear();
CmdQuoter.QuoteValue(value.ToString(), buffer);
return buffer.ToString();
}
if (value is bool)
return (bool)value ? "+" : "-";
if (value is IComponentFactory)
{
string name;
var catalog = env.ComponentCatalog;
var type = value.GetType();
bool isModuleComponent = catalog.TryGetComponentShortName(type, out name);
if (isModuleComponent)
{
var settings = GetSettings(env, value, Activator.CreateInstance(type));
buffer.Clear();
buffer.Append(name);
if (!string.IsNullOrWhiteSpace(settings))
{
StringBuilder sb = new StringBuilder();
CmdQuoter.QuoteValue(settings, sb, true);
buffer.Append(sb);
}
return buffer.ToString();
}
}
return value.ToString();
}
public string GetFullHelpText(IHostEnvironment env)
{
if (IsHidden)
return null;
StringBuilder builder = new StringBuilder();
if (HelpText != null)
{
builder.Append(HelpText);
// REVIEW: Add this code.
switch (HelpText[HelpText.Length - 1])
{
case '.':
case '?':
case '!':
break;
default:
//builder.Append(".");
break;
}
}
if (DefaultValue != null && !"".Equals(DefaultValue))
{
if (builder.Length > 0)
builder.Append(" ");
builder.Append("Default value:'");
AppendHelpValue(env, builder, DefaultValue);
builder.Append('\'');
}
if (Utils.Size(ShortNames) != 0)
{
if (builder.Length > 0)
builder.Append(" ");
// REVIEW: This hides all short names except the first, which may cause user confusion.
builder.Append("(short form ").Append(ShortNames[0]).Append(")");
}
return builder.ToString();
}
public string GetSyntaxHelp()
{
if (IsHidden)
return null;
StringBuilder bldr = new StringBuilder();
if (IsDefault)
bldr.Append("<").Append(LongName).Append(">");
else
{
bldr.Append(LongName);
Type type = ItemValueType;
if (IsNullableType(type))
type = type.GetGenericArguments()[0];
if (IsTaggedCollection)
bldr.Append("[<tag>]");
if (type == typeof(int))
bldr.Append("=<int>");
else if (type == typeof(uint))
bldr.Append("=<uint>");
else if (type == typeof(byte))
bldr.Append("=<byte>");
else if (type == typeof(sbyte))
bldr.Append("=<sbyte>");
else if (type == typeof(short))
bldr.Append("=<short>");
else if (type == typeof(ushort))
bldr.Append("=<ushort>");
else if (type == typeof(long))
bldr.Append("=<long>");
else if (type == typeof(ulong))
bldr.Append("=<ulong>");
else if (type == typeof(bool))
bldr.Append("=[+|-]");
else if (type == typeof(string))
bldr.Append("=<string>");
else if (type == typeof(float))
bldr.Append("=<float>");
else if (type == typeof(double))
bldr.Append("=<double>");
else if (type == typeof(decimal))
bldr.Append("=<decimal>");
else if (type == typeof(char))
bldr.Append("=<char>");
else if (type == typeof(System.Guid))
bldr.Append("=<guid>");
else if (type == typeof(System.DateTime))
bldr.Append("=<datetime>");
else if (IsComponentFactory)
bldr.Append("=<name>{<options>}");
else if (IsCustomItemType)
bldr.Append("={<options>}");
else if (type.IsEnum)
{
bldr.Append("=[");
string sep = "";
foreach (FieldInfo field in type.GetFields())
{
if (!field.IsStatic || field.FieldType != type)
continue;
if (field.GetCustomAttribute<HideEnumValueAttribute>() != null)
continue;
bldr.Append(sep).Append(field.Name);
sep = "|";
}
bldr.Append(']');
}
else
Contracts.Assert(false);
}
return bldr.ToString();
}
public bool IsRequired
{
get { return 0 != (Kind & ArgumentType.Required); }
}
public bool AllowMultiple
{
get { return 0 != (Kind & ArgumentType.Multiple); }
}
public bool Unique
{
get { return 0 != (Kind & ArgumentType.Unique); }
}
public bool IsSingleComponentFactory
{
get { return IsComponentFactory && !Field.FieldType.IsArray; }
}
public bool IsMultiComponentFactory
{
get { return IsComponentFactory && Field.FieldType.IsArray; }
}
public bool IsCustomItemType
{
get { return _infoCustom != null; }
}
}
/// <summary>
/// A factory class for creating IComponentFactory instances.
/// </summary>
private static class ComponentFactoryFactory
{
public static ICommandLineComponentFactory CreateComponentFactory(
Type factoryType,
Type signatureType,
string name,
string[] settings)
{
if (!TryCreateComponentFactory(factoryType, signatureType, name, settings, out ICommandLineComponentFactory factory))
{
throw Contracts.ExceptNotImpl("ComponentFactoryFactory can only create IComponentFactory<> types with 4 or less type args.");
}
return factory;
}
public static bool TryCreateComponentFactory(
Type factoryType,
Type signatureType,
string name,
string[] settings,
out ICommandLineComponentFactory factory)
{
if (factoryType == null ||
!typeof(IComponentFactory).IsAssignableFrom(factoryType) ||
!factoryType.IsGenericType)
{
factory = null;
return false;
}
Type componentFactoryType;
switch (factoryType.GenericTypeArguments.Length)
{
case 1: componentFactoryType = typeof(ComponentFactory<>); break;
case 2: componentFactoryType = typeof(ComponentFactory<,>); break;
case 3: componentFactoryType = typeof(ComponentFactory<,,>); break;
case 4: componentFactoryType = typeof(ComponentFactory<,,,>); break;
default:
factory = null;
return false;
}
factory = (ICommandLineComponentFactory)Activator.CreateInstance(
componentFactoryType.MakeGenericType(factoryType.GenericTypeArguments),
signatureType,
name,
settings);
return true;
}
private abstract class ComponentFactory : ICommandLineComponentFactory
{
public Type SignatureType { get; }
public string Name { get; }
private string[] Settings { get; }
protected ComponentFactory(Type signatureType, string name, string[] settings)
{
SignatureType = signatureType;
Name = name;
if (settings == null || (settings.Length == 1 && string.IsNullOrEmpty(settings[0])))
{
settings = Array.Empty<string>();
}
Settings = settings;
}
public string GetSettingsString()
{
return CombineSettings(Settings);
}
public override string ToString()
{
if (string.IsNullOrEmpty(Name) && Settings.Length == 0)
return "{}";
if (Settings.Length == 0)
return Name;
string str = CombineSettings(Settings);
StringBuilder sb = new StringBuilder();
CmdQuoter.QuoteValue(str, sb, true);
return Name + sb.ToString();
}
}
private class ComponentFactory<TComponent> : ComponentFactory, IComponentFactory<TComponent>
where TComponent : class
{
public ComponentFactory(Type signatureType, string name, string[] settings)
: base(signatureType, name, settings)
{
}
public TComponent CreateComponent(IHostEnvironment env)
{
return ComponentCatalog.CreateInstance<TComponent>(
env,
SignatureType,
Name,
GetSettingsString());
}
}
private class ComponentFactory<TArg1, TComponent> : ComponentFactory, IComponentFactory<TArg1, TComponent>
where TComponent : class
{
public ComponentFactory(Type signatureType, string name, string[] settings)
: base(signatureType, name, settings)
{
}
public TComponent CreateComponent(IHostEnvironment env, TArg1 argument1)
{
return ComponentCatalog.CreateInstance<TComponent>(
env,
SignatureType,
Name,
GetSettingsString(),
argument1);
}
}
private class ComponentFactory<TArg1, TArg2, TComponent> : ComponentFactory, IComponentFactory<TArg1, TArg2, TComponent>
where TComponent : class
{
public ComponentFactory(Type signatureType, string name, string[] settings)
: base(signatureType, name, settings)
{
}
public TComponent CreateComponent(IHostEnvironment env, TArg1 argument1, TArg2 argument2)
{
return ComponentCatalog.CreateInstance<TComponent>(
env,
SignatureType,
Name,
GetSettingsString(),
argument1,
argument2);
}
}
private class ComponentFactory<TArg1, TArg2, TArg3, TComponent> : ComponentFactory, IComponentFactory<TArg1, TArg2, TArg3, TComponent>
where TComponent : class
{
public ComponentFactory(Type signatureType, string name, string[] settings)
: base(signatureType, name, settings)
{
}
public TComponent CreateComponent(IHostEnvironment env, TArg1 argument1, TArg2 argument2, TArg3 argument3)
{
return ComponentCatalog.CreateInstance<TComponent>(
env,
SignatureType,
Name,
GetSettingsString(),
argument1,
argument2,
argument3);
}
}
}
}
}
|