/// <summary> /// Parses the command line into an arguments object. Option definitions are determined by the attributes on the properties of <paramref name="argumentsObject"/>. This method will call <see cref="IOptionArguments.Validate"/>. /// </summary> /// <typeparam name="T">The type of arguments object to initialize.</typeparam> /// <param name="argumentsObject">The arguments object that is initialized. May not be <c>null</c>.</param> /// <param name="commandLine">The command line to parse, not including the process name. If <c>null</c>, the process' command line is lexed by <see cref="ConsoleCommandLineLexer"/>.</param> /// <param name="parserCollection">A parser collection to use for parsing, or <c>null</c> to use the default parsers.</param> /// <param name="stringComparer">The string comparison to use when parsing options. If <c>null</c>, then the string comparer for the current culture is used.</param> public static void Parse <T>(this T argumentsObject, IEnumerable <string> commandLine = null, SimpleParserCollection parserCollection = null, StringComparer stringComparer = null) where T : class, IOptionArguments { Contract.Requires(argumentsObject != null); if (parserCollection == null) { parserCollection = new SimpleParserCollection(); } if (stringComparer == null) { stringComparer = StringComparer.CurrentCulture; } // Generate option definitions from OptionAttributes on the arguments object. var options = new Dictionary <OptionDefinition, Action <string> >(); var positionalArguments = new List <Action <string> >(); Action <string> remainingPositionalArguments = null; var argumentsObjectType = argumentsObject.GetType(); foreach (var property in argumentsObjectType.GetProperties()) { var localProperty = property; // If the property specifies a [SimpleParser], then create a parser for that property. var parserOverrideAttribute = property.GetCustomAttributes(typeof(SimpleParserAttribute), true).OfType <SimpleParserAttribute>().FirstOrDefault(); var parserOverride = ((parserOverrideAttribute == null) ? null : Activator.CreateInstance(parserOverrideAttribute.ParserType)) as ISimpleParser; foreach (var attribute in property.GetCustomAttributes(true)) { // Handle [Option] attributes. var optionAttribute = attribute as OptionAttribute; if (optionAttribute != null) { var optionDefinition = new OptionDefinition { LongName = optionAttribute.LongName, ShortName = optionAttribute.ShortName, Argument = optionAttribute.Argument }; if (optionDefinition.Argument == OptionArgument.None) { // If the option takes no arguments, it must be applied to a boolean property. if (localProperty.PropertyType != typeof(bool)) { throw new InvalidOperationException("An OptionAttribute with no Argument may only be applied to a boolean property."); } // If the option is specified, set the property to true. options.Add(optionDefinition, _ => localProperty.SetOptionProperty(argumentsObject, true)); } else { // If the option takes an argument, then attempt to parse it to the correct type. options.Add(optionDefinition, parameter => { if (parameter == null) { return; } var value = parserOverride != null ? parserOverride.TryParse(parameter) : parserCollection.TryParse(parameter, localProperty.PropertyType); if (value == null) { throw new OptionParsingException.OptionArgumentException("Could not parse " + parameter + " as " + FriendlyTypeName(Nullable.GetUnderlyingType(localProperty.PropertyType) ?? localProperty.PropertyType)); } localProperty.SetOptionProperty(argumentsObject, value); }); } } // Handle [PositionalArgument] attributes. var positionalArgumentAttribute = attribute as PositionalArgumentAttribute; if (positionalArgumentAttribute != null) { if (positionalArguments.Count <= positionalArgumentAttribute.Index) { positionalArguments.AddRange(new Action <string> [positionalArgumentAttribute.Index - positionalArguments.Count + 1]); Contract.Assume(positionalArguments.Count > positionalArgumentAttribute.Index); } if (positionalArguments[positionalArgumentAttribute.Index] != null) { throw new InvalidOperationException("More than one property has a PositionalArgumentAttribute.Index of " + positionalArgumentAttribute.Index + "."); } // If the positional argument is specified, then attempt to parse it to the correct type. positionalArguments[positionalArgumentAttribute.Index] = parameter => { Contract.Assume(parameter != null); var value = parserOverride != null?parserOverride.TryParse(parameter) : parserCollection.TryParse(parameter, localProperty.PropertyType); if (value == null) { throw new OptionParsingException.OptionArgumentException("Could not parse " + parameter + " as " + FriendlyTypeName(Nullable.GetUnderlyingType(localProperty.PropertyType) ?? localProperty.PropertyType)); } localProperty.SetOptionProperty(argumentsObject, value); }; } // Handle [PositionalArguments] attributes. var positionalArgumentsAttribute = attribute as PositionalArgumentsAttribute; if (positionalArgumentsAttribute != null) { if (remainingPositionalArguments != null) { throw new InvalidOperationException("More than one property has a PositionalArgumentsAttribute."); } var addMethods = localProperty.PropertyType.GetMethods().Where(x => x.Name == "Add" && x.GetParameters().Length == 1); if (!addMethods.Any()) { throw new InvalidOperationException("Property with PositionalArgumentsAttribute does not implement an Add method taking exactly one parameter."); } if (addMethods.Count() != 1) { throw new InvalidOperationException("Property with PositionalArgumentsAttribute has more than one Add method taking exactly one parameter."); } var addMethod = addMethods.First(); // As the remaining positional arguments are specified, then attempt to parse it to the correct type and add it to the collection. remainingPositionalArguments = parameter => { Contract.Assume(parameter != null); var value = parserOverride != null?parserOverride.TryParse(parameter) : parserCollection.TryParse(parameter, addMethod.GetParameters()[0].ParameterType); if (value == null) { throw new OptionParsingException.OptionArgumentException("Could not parse " + parameter + " as " + FriendlyTypeName(Nullable.GetUnderlyingType(addMethod.GetParameters()[0].ParameterType) ?? addMethod.GetParameters()[0].ParameterType)); } addMethod.Invoke(localProperty.GetValue(argumentsObject, null), new[] { value }); }; } } } // Handle [OptionPresent] attributes. var optionDefinitions = options.Keys; foreach (var property in argumentsObjectType.GetProperties()) { var localProperty = property; var optionPresentAttribute = property.GetCustomAttributes(typeof(OptionPresentAttribute), true).OfType <OptionPresentAttribute>().FirstOrDefault(); if (optionPresentAttribute == null) { continue; } // This attribute must be applied to a boolean property. if (localProperty.PropertyType != typeof(bool)) { throw new InvalidOperationException("An OptionPresentAttribute may only be applied to a boolean property."); } OptionDefinition optionDefinition = null; if (optionPresentAttribute.LongName != null) { optionDefinition = optionDefinitions.FirstOrDefault(x => stringComparer.Equals(x.LongName, optionPresentAttribute.LongName)); } else { optionDefinition = optionDefinitions.FirstOrDefault(x => stringComparer.Equals(x.ShortNameAsString, optionPresentAttribute.ShortNameAsString)); } if (optionDefinition == null) { throw new InvalidOperationException("OptionPresentAttribute does not refer to an existing OptionAttribute for option " + optionPresentAttribute.Name); } // If the option is specified, set the property to true. options[optionDefinition] = (Action <string>)Delegate.Combine(options[optionDefinition], (Action <string>)(_ => localProperty.SetOptionProperty(argumentsObject, true))); } // Verify options. for (int i = 0; i != positionalArguments.Count; ++i) { if (positionalArguments[i] == null) { throw new InvalidOperationException("No property has a PositionalArgumentAttribute with Index of " + i + "."); } } if (remainingPositionalArguments == null) { throw new InvalidOperationException("No property has a PositionalArgumentsAttribute."); } // Parse the command line, filling in the property values. var parser = OptionParser.Parse(commandLine, optionDefinitions, stringComparer); int positionalArgumentIndex = 0; foreach (var option in parser) { Contract.Assume(option != null); if (option.Definition == null) { if (positionalArgumentIndex < positionalArguments.Count) { var action = positionalArguments[positionalArgumentIndex]; Contract.Assume(action != null); action(option.Argument); } else { remainingPositionalArguments(option.Argument); } ++positionalArgumentIndex; } else { var action = options[option.Definition]; Contract.Assume(action != null); action(option.Argument); } } // Have the arguments object perform its own validation. argumentsObject.Validate(); }
/// <summary> /// Parses the command line into options. /// </summary> /// <returns>The sequence of options specified on the command line.</returns> public IEnumerator <Option> GetEnumerator() { foreach (var value in this.commandLine) { // If the option parsing is done, then all remaining command line elements are positional arguments. if (this.done) { yield return(new Option { Argument = value }); continue; } if (this.lastOption != null && this.lastOption.Argument == OptionArgument.Required) { // Argument for a preceding option yield return(new Option { Definition = this.lastOption, Argument = value }); this.lastOption = null; continue; } // Either there is no last option, or the last option's argument is optional. if (value == "--") { // End-of-options marker if (this.lastOption != null) { // The last option was an option that takes an optional argument, without an argument. yield return(new Option { Definition = this.lastOption }); this.lastOption = null; } // All future parameters are positional arguments. this.done = true; continue; } if (value.StartsWith("--")) { // Long option if (this.lastOption != null) { // The last option was an option that takes an optional argument, without an argument. yield return(new Option { Definition = this.lastOption }); } string option = null; string argument = null; int argumentIndex = value.IndexOfAny(ArgumentDelimiters, 2); if (argumentIndex == -1) { // No argument delimiters were found; the command line element is an option. option = value.Substring(2); } else { // An argument delimiter was found; split the command line element into an option and its argument. option = value.Substring(2, argumentIndex - 2); argument = value.Substring(argumentIndex + 1); } // Find the option in the option definitions. this.lastOption = this.definitions.FirstOrDefault(x => this.stringComparer.Equals(x.LongName, option)); if (this.lastOption == null) { throw new OptionParsingException.UnknownOptionException("Unknown option " + option + " in parameter " + value); } if (argument != null) { // There is an argument with this long option, so it is complete. if (this.lastOption.Argument == OptionArgument.None) { throw new OptionParsingException.OptionArgumentException("Option " + option + " cannot take an argument in parameter " + value); } yield return(new Option { Definition = this.lastOption, Argument = argument }); this.lastOption = null; continue; } if (this.lastOption.Argument == OptionArgument.None) { // This long option does not take an argument, so it is complete. yield return(new Option { Definition = this.lastOption }); this.lastOption = null; continue; } // This long option does take an argument. continue; } if (value.StartsWith("-")) { // Short option or short option run if (this.lastOption != null) { // The last option was an option that takes an optional argument, without an argument. yield return(new Option { Definition = this.lastOption }); } if (value.Length < 2) { throw new OptionParsingException.InvalidParameterException("Invalid parameter " + value); } string option = value[1].ToString(); this.lastOption = this.definitions.FirstOrDefault(x => this.stringComparer.Equals(x.ShortNameAsString, option)); if (this.lastOption == null) { throw new OptionParsingException.UnknownOptionException("Unknown option " + option + " in parameter " + value); } // The first short option may either have an argument or start a short option run int argumentIndex = value.IndexOfAny(ArgumentDelimiters, 2); if (argumentIndex == 2) { // The first short option has an argument. if (this.lastOption.Argument == OptionArgument.None) { throw new OptionParsingException.OptionArgumentException("Option " + option + " cannot take an argument in parameter " + value); } yield return(new Option { Definition = this.lastOption, Argument = value.Substring(3) }); this.lastOption = null; } else if (argumentIndex != -1) { // There is an argument delimiter somewhere else in the parameter string. throw new OptionParsingException.InvalidParameterException("Invalid parameter " + value); } else if (value.Length == 2) { // The first short option is the only one. if (this.lastOption.Argument == OptionArgument.None) { yield return(new Option { Definition = this.lastOption }); this.lastOption = null; } } else { // This is a short option run; they must not take arguments. for (int i = 1; i != value.Length; ++i) { option = value[i].ToString(); this.lastOption = this.definitions.FirstOrDefault(x => this.stringComparer.Equals(x.ShortNameAsString, option)); if (this.lastOption == null) { throw new OptionParsingException.UnknownOptionException("Unknown option " + option + " in parameter " + value); } if (this.lastOption.Argument == OptionArgument.Required) { throw new OptionParsingException.OptionArgumentException("Option " + option + " cannot be in a short option run (because it takes an argument) in parameter " + value); } yield return(new Option { Definition = this.lastOption }); this.lastOption = null; } } continue; } if (value.StartsWith("/")) { // Short or long option if (this.lastOption != null) { // The last option was an option that takes an optional argument, without an argument. yield return(new Option { Definition = this.lastOption }); } if (value.Length < 2) { throw new OptionParsingException.InvalidParameterException("Invalid parameter " + value); } string option = null; string argument = null; int argumentIndex = value.IndexOfAny(ArgumentDelimiters, 2); if (argumentIndex == -1) { option = value.Substring(1); } else { option = value.Substring(1, argumentIndex - 1); argument = value.Substring(argumentIndex + 1); } this.lastOption = this.definitions.FirstOrDefault(x => this.stringComparer.Equals(x.LongName, option) || this.stringComparer.Equals(x.ShortNameAsString, option)); if (this.lastOption == null) { throw new OptionParsingException.UnknownOptionException("Unknown option " + option + " in parameter " + value); } if (argument != null) { // There is an argument with this option, so it is complete. if (this.lastOption.Argument == OptionArgument.None) { throw new OptionParsingException.OptionArgumentException("Option " + option + " cannot take an argument in parameter " + value); } yield return(new Option { Definition = this.lastOption, Argument = argument }); this.lastOption = null; continue; } if (this.lastOption.Argument == OptionArgument.None) { // This option does not take an argument, so it is complete. yield return(new Option { Definition = this.lastOption }); this.lastOption = null; continue; } // This option does take an argument. continue; } if (this.lastOption != null) { // The last option was an option that takes an optional argument, with an argument. yield return(new Option { Definition = this.lastOption, Argument = value }); this.lastOption = null; continue; } // The first positional argument this.done = true; yield return(new Option { Argument = value }); continue; } if (this.lastOption != null) { if (this.lastOption.Argument == OptionArgument.Required) { throw new OptionParsingException.OptionArgumentException("Missing argument for option " + this.lastOption.Name); } yield return(new Option { Definition = this.lastOption }); } }