/// <summary> /// Splits the option and argument into two separate strings. /// </summary> /// <param name="argument"> /// The input argument to split. /// </param> /// <param name="option"> /// The option part of the <paramref name="argument"/>. /// </param> /// <param name="value"> /// The value part of the <paramref name="argument"/>, or <see cref="string.Empty"/> if /// there is no value. /// </param> /// <exception cref="ArgumentNullException"> /// <para><paramref name="argument"/> is <c>null</c> or empty.</para> /// </exception> private static void SplitOptionAndArgument(string argument, out string option, out string value) { if (StringEx.IsNullOrWhiteSpace(argument)) { throw new ArgumentNullException("argument"); } if (argument.StartsWith("--", StringComparison.Ordinal)) { int valueIndex = argument.IndexOf('='); if (valueIndex < 0) { valueIndex = argument.IndexOf(':'); } if (valueIndex < 0) { option = argument; value = string.Empty; } else { option = argument.Substring(0, valueIndex).Trim(); value = argument.Substring(valueIndex + 1); } } else { option = argument.Substring(0, 2); value = argument.Substring(2); if (value.StartsWith("=") || value.StartsWith(":")) { value = value.Substring(1); } } }
/// <summary> /// Maps the arguments onto properties of the container. /// </summary> /// <param name="arguments"> /// The collection of arguments to map onto properties on the <paramref name="container"/>. /// </param> /// <param name="container"> /// The container object to map the <paramref name="arguments"/> onto. /// </param> /// <returns> /// Any leftover non-option arguments that couldn't be mapped. Note that if the container has /// a string collection property tagged with the <see cref="ArgumentsAttribute"/> attribute, the /// leftover arguments will be added to that property, and none will be returned from this method. /// </returns> /// <exception cref="ArgumentNullException"> /// <para><paramref name="arguments"/> is <c>null</c>.</para> /// <para>- or -</para> /// <para><paramref name="container"/> is <c>null</c>.</para> /// </exception> /// <exception cref="InvalidOperationException"> /// <para><paramref name="container"/> is not the same <see cref="ContainerType"/> as the original type given to this <see cref="PropertyMap"/>.</para> /// </exception> /// <exception cref="UnknownOptionException"> /// An unknown option was specified on the command line, one that was not declared in the container type. /// </exception> /// <exception cref="OptionSyntaxException"> /// Argument starts with three or more minus signs, this is not legal. /// </exception> public string[] Map(IEnumerable <string> arguments, object container) { if (arguments == null) { throw new ArgumentNullException("arguments"); } if (container == null) { throw new ArgumentNullException("container"); } if (container.GetType() != _ContainerType) { throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "The given container was not the same type as the original type given to PropertyMap, this is an internal error (aka bug), original type was {0}, container type was {1}", _ContainerType, container.GetType())); } var leftovers = new List <string>(); int argumentIndex = 0; using (IEnumerator <string> argumentEnumerable = arguments.GetEnumerator()) { while (argumentEnumerable.MoveNext()) { string argument = argumentEnumerable.Current; if ((argument.StartsWith("--", StringComparison.Ordinal) || argument.StartsWith("-", StringComparison.Ordinal)) && !argument.StartsWith("---", StringComparison.Ordinal)) { string option; string value; SplitOptionAndArgument(argument, out option, out value); KeyValuePair <PropertyInfo, BaseOptionAttribute> entry; if (_Properties.TryGetValue(option, out entry)) { if (entry.Value.RequiresArgument) { if (StringEx.IsNullOrWhiteSpace(value)) { if (!argumentEnumerable.MoveNext()) { throw new OptionSyntaxException(string.Format(CultureInfo.InvariantCulture, "option {0} requires an argument but none was provided", option)); } if (argumentEnumerable.Current.StartsWith("-")) { string possibleOption; string possibleArgument; SplitOptionAndArgument(argumentEnumerable.Current, out possibleOption, out possibleArgument); if (_Properties.ContainsKey(possibleOption)) { throw new OptionSyntaxException(string.Format(CultureInfo.InvariantCulture, "option {0} requires an argument but none was provided", option)); } } value = argumentEnumerable.Current; } } entry.Value.AssignValueToProperty(container, entry.Key, value); } else { throw new UnknownOptionException(string.Format(CultureInfo.InvariantCulture, "Unknown option {0}", argument)); } } else if (argument.StartsWith("-", StringComparison.Ordinal)) { throw new OptionSyntaxException("Argument starts with three or more minus signs, this is not legal"); } else if (argumentIndex < _ArgumentProperties.Count) { _ArgumentProperties[argumentIndex].SetValue(container, argument, null); argumentIndex++; } else { leftovers.Add(argument); } } } if (_ArgumentsProperty != null) { var argumentsProperty = _ArgumentsProperty.GetValue(container, null) as Collection <string>; if (argumentsProperty == null) { if (_ArgumentsProperty.CanWrite) { argumentsProperty = new Collection <string>(new List <string>()); _ArgumentsProperty.SetValue(container, argumentsProperty, null); } else { throw new InvalidOperationException("The container has a property that has the ArgumentsAttribute attribute, but this property returns a null collection reference, and is not writeable"); } } foreach (string leftover in leftovers) { argumentsProperty.Add(leftover); } return(new string[0]); } return(leftovers.ToArray()); }
/// <summary> /// Extracts the command from the given arguments, and returns its position, in order to be able /// to remove the command from the argument stream before parsing the arguments for the /// given command object. The command is per definition the first non-option argument. /// </summary> /// <param name="arguments"> /// The arguments to parse. /// </param> /// <param name="indexOfCommand"> /// The index of the command found, or -1 if no command was found. /// </param> /// <returns> /// The command name, or <see cref="string.Empty"/> if no command was found. /// </returns> public string ExtractCommand(IEnumerable <string> arguments, out int indexOfCommand) { if (arguments == null) { throw new ArgumentNullException("arguments"); } indexOfCommand = -1; using (IEnumerator <string> argumentEnumerable = arguments.GetEnumerator()) { while (argumentEnumerable.MoveNext()) { indexOfCommand++; string argument = argumentEnumerable.Current; if ((argument.StartsWith("--", StringComparison.Ordinal) || argument.StartsWith("-", StringComparison.Ordinal)) && !argument.StartsWith("---", StringComparison.Ordinal)) { string option; string value; SplitOptionAndArgument(argument, out option, out value); KeyValuePair <PropertyInfo, BaseOptionAttribute> entry; if (_Properties.TryGetValue(option, out entry)) { if (entry.Value.RequiresArgument) { if (StringEx.IsNullOrWhiteSpace(value)) { if (!argumentEnumerable.MoveNext()) { throw new OptionSyntaxException(string.Format(CultureInfo.InvariantCulture, "option {0} requires an argument but none was provided", option)); } indexOfCommand++; if (argumentEnumerable.Current.StartsWith("-")) { string possibleOption; string possibleArgument; SplitOptionAndArgument(argumentEnumerable.Current, out possibleOption, out possibleArgument); if (_Properties.ContainsKey(possibleOption)) { throw new OptionSyntaxException(string.Format(CultureInfo.InvariantCulture, "option {0} requires an argument but none was provided", option)); } } value = argumentEnumerable.Current; } } } else { throw new UnknownOptionException(string.Format(CultureInfo.InvariantCulture, "Unknown option {0}", argument)); } } else if (argument.StartsWith("-", StringComparison.Ordinal)) { throw new OptionSyntaxException("Argument starts with three or more minus signs, this is not legal"); } else { return(argument); } } } indexOfCommand = -1; return(string.Empty); }
/// <summary> /// Retrieves the help text for the given options container type. /// </summary> /// <param name="containerType"> /// The type of options container to retrieve the help text for. /// </param> /// <returns> /// A collection of text lines, the help text. /// </returns> /// <exception cref="ArgumentNullException"> /// <para><paramref name="containerType"/> is <c>null</c>.</para> /// </exception> public static IEnumerable <string> GetHelp(Type containerType) { if (containerType == null) { throw new ArgumentNullException("containerType"); } var map = new PropertyMap(containerType); // Command line help var parts = new List <string>(); int argumentIndex = 1; foreach (PropertyInfo prop in map.ArgumentProperties) { var attr = (ArgumentAttribute)prop.GetCustomAttributes(typeof(ArgumentAttribute), true)[0]; string name = attr.Name; if (StringEx.IsNullOrWhiteSpace(name)) { name = "ARG" + argumentIndex; } if (attr.Optional) { parts.Add("[" + name + "]"); } else { parts.Add(name); } argumentIndex++; } if (map.ArgumentsProperty != null) { parts.Add("[ARG]..."); } if (map.MappedProperties.Any()) { parts.Add("[OPTIONS]..."); } yield return(Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location).ToLower() + " " + string.Join(" ", parts.ToArray())); yield return(string.Empty); string[] containerHelp = GetHelpTextFor(containerType).ToArray(); if (containerHelp.Length > 0) { foreach (string line in containerHelp) { yield return(line); } yield return(string.Empty); } var argumentsWithDescription = (from entry in map.ArgumentProperties.Select((property, index) => new { property, index }) let descriptionAttr = entry.property.GetCustomAttributes(typeof(DescriptionAttribute), true).FirstOrDefault() as DescriptionAttribute where descriptionAttr != null let attr = (ArgumentAttribute)entry.property.GetCustomAttributes(typeof(ArgumentAttribute), true)[0] let argName = !StringEx.IsNullOrWhiteSpace(attr.Name) ? attr.Name : ("ARG" + (entry.index + 1)) select new { argName, text = StringEx.SplitLines(descriptionAttr.Description).ToArray() }).ToArray(); if (argumentsWithDescription.Length > 0) { yield return("arguments:"); yield return(string.Empty); int maxLongLength = argumentsWithDescription.Max(p => p.argName.Length); foreach (var arg in argumentsWithDescription) { int lines = arg.text.Length; if (arg.argName.Length > 20) { yield return(" " + arg.argName); var indent = new string(' ', maxLongLength + 3); foreach (string line in arg.text) { yield return(indent + line); } } else { yield return(" " + arg.argName.PadRight(maxLongLength, ' ') + " " + arg.text[0]); var indent = new string(' ', maxLongLength + 3); foreach (string line in arg.text.Skip(1)) { yield return(indent + line); } } } yield return(string.Empty); } var propertiesWithHelpText = (from propMap in map.MappedProperties let text = GetHelpTextFor(propMap.Key).ToArray() where text.Length > 0 select new { prop = propMap.Key, option = propMap.Value.Option, parameter = propMap.Value.ParameterName, text } into entry group entry by entry.prop into g select new { options = g.Select(e => e.option).OrderBy(o => o.Length).ToArray(), g.First().text, g.First().parameter } into entry2 orderby(entry2.options.First() == "-h" || entry2.options.First() == "--help") ? 0 : 1, entry2.options.First() let shortOption = entry2.options.Where(o => o.Length == 2).FirstOrDefault() ?? string.Empty let longOption = entry2.options.Where(o => o.Length > 2).FirstOrDefault() ?? string.Empty select new { shortOption, longOption, entry2.options, entry2.parameter, entry2.text }).ToArray(); if (propertiesWithHelpText.Length > 0) { yield return("options:"); yield return(string.Empty); int maxLongLength = propertiesWithHelpText.Max(p => p.longOption.Length + p.parameter.Length); foreach (var prop in propertiesWithHelpText) { int lines = prop.text.Length; if (prop.longOption.Length > 20) { yield return(" " + (prop.shortOption.PadRight(2, ' ') + " " + prop.longOption + " " + prop.parameter).Trim()); var indent = new string(' ', 27); foreach (string line in prop.text) { yield return(indent + line); } } else { yield return(" " + prop.shortOption.PadRight(2, ' ') + " " + (prop.longOption + " " + prop.parameter).PadRight(20, ' ') + " " + prop.text[0]); var indent = new string(' ', 27); foreach (string line in prop.text.Skip(1)) { yield return(indent + line); } } } } }