diff --git a/Nuclex.Support (Xbox 360).csproj b/Nuclex.Support (Xbox 360).csproj index 39811ff..18706a9 100644 --- a/Nuclex.Support (Xbox 360).csproj +++ b/Nuclex.Support (Xbox 360).csproj @@ -148,7 +148,7 @@ LicenseKey.cs - + CommandLine.cs diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj index 386c2d7..d963aea 100644 --- a/Nuclex.Support.csproj +++ b/Nuclex.Support.csproj @@ -130,7 +130,7 @@ LicenseKey.cs - + CommandLine.cs diff --git a/Source/Parsing/BrokenCommandLineParser.Test.cs b/Source/Parsing/BrokenCommandLineParser.Test.cs index ef06476..2e92c78 100644 --- a/Source/Parsing/BrokenCommandLineParser.Test.cs +++ b/Source/Parsing/BrokenCommandLineParser.Test.cs @@ -26,7 +26,7 @@ using System.Text; using NUnit.Framework; -#if false // Too bugged. 100% test coverage not possible. +#if ENABLE_BROKEN_COMMAND_LINE_PARSER // Too bugged. 100% test coverage not possible. namespace Nuclex.Support.Parsing { @@ -142,6 +142,6 @@ namespace Nuclex.Support.Parsing { } // namespace Nuclex.Support.Parsing -#endif +#endif // ENABLE_BROKEN_COMMAND_LINE_PARSER #endif // UNITTEST \ No newline at end of file diff --git a/Source/Parsing/BrokenCommandLineParser.cs b/Source/Parsing/BrokenCommandLineParser.cs index a8abdf3..5b37d55 100644 --- a/Source/Parsing/BrokenCommandLineParser.cs +++ b/Source/Parsing/BrokenCommandLineParser.cs @@ -21,7 +21,7 @@ License along with this library using System.Collections.Specialized; using System.Text.RegularExpressions; -#if false // Too bugged. 100% test coverage not possible. +#if ENABLE_BROKEN_COMMAND_LINE_PARSER // Too bugged. 100% test coverage not possible. namespace Nuclex.Support.Parsing { @@ -182,4 +182,4 @@ namespace Nuclex.Support.Parsing { } // namespace Nuclex.Support.Parsing -#endif +#endif // ENABLE_BROKEN_COMMAND_LINE_PARSER diff --git a/Source/Parsing/CommandLine.Option.cs b/Source/Parsing/CommandLine.Argument.cs similarity index 63% rename from Source/Parsing/CommandLine.Option.cs rename to Source/Parsing/CommandLine.Argument.cs index 4fdd4ab..1b7a914 100644 --- a/Source/Parsing/CommandLine.Option.cs +++ b/Source/Parsing/CommandLine.Argument.cs @@ -22,27 +22,40 @@ using System; using System.Collections.Generic; using System.Diagnostics; -#if ENABLE_BROKEN_COMMAND_LINE_PARSER - namespace Nuclex.Support.Parsing { partial class CommandLine { - /// Option being specified on an application's command line - public class Option { + /// Argument being specified on an application's command line + public class Argument { /// Initializes a new option with only a name /// - /// String segment containing the entire option as it was given on the command line + /// String segment with the entire argument as it was given on the command line /// - /// Absolute index the option name starts at + /// Absolute index the argument name starts at /// Number of characters in the option name /// The newly created option - internal Option( + internal static Argument OptionOnly( StringSegment raw, int nameStart, int nameLength - ) - : this(raw, nameStart, nameLength, -1, -1) { } + ) { + return new Argument(raw, nameStart, nameLength, -1, -1); + } + + /// Initializes a new argument with only a value + /// + /// String segment with the entire argument as it was given on the command line + /// + /// Absolute index the value starts at + /// Number of characters in the value + /// The newly created option + internal static Argument ValueOnly( + StringSegment raw, + int valueStart, int valueLength + ) { + return new Argument(raw, -1, -1, valueStart, valueLength); + } /// Creates a new option with a name and an assigned value /// @@ -53,7 +66,7 @@ namespace Nuclex.Support.Parsing { /// Absolute index the value starts at /// Number of characters in the value /// The newly created option - internal Option( + internal Argument( StringSegment raw, int nameStart, int nameLength, int valueStart, int valueLength @@ -63,9 +76,6 @@ namespace Nuclex.Support.Parsing { this.nameLength = nameLength; this.valueStart = valueStart; this.valueLength = valueLength; - - Debug.Assert(this.nameStart != -1, "Name start index must not be -1"); - Debug.Assert(this.nameLength != -1, "Name length must not be -1"); } /// Contains the raw string the command line argument was parsed from @@ -76,32 +86,46 @@ namespace Nuclex.Support.Parsing { /// Characters used to initiate this option public string Initiator { get { - return this.raw.Text.Substring( - this.raw.Offset, this.nameStart - this.raw.Offset - ); + if(this.nameStart == -1) { + return null; + } else { + return this.raw.Text.Substring( + this.raw.Offset, this.nameStart - this.raw.Offset + ); + } } } /// Name of the command line option public string Name { get { - return this.raw.Text.Substring(this.nameStart, this.nameLength); + if(this.nameStart == -1) { + return null; + } else { + return this.raw.Text.Substring(this.nameStart, this.nameLength); + } } } /// Characters used to associate a value to this option public string Associator { get { - int associatorStart = this.nameStart + this.nameLength; + if(this.nameStart == -1) { + return null; + } else { + int associatorStart = this.nameStart + this.nameLength; - if(this.valueStart == -1) { - int characterCount = (this.raw.Offset + this.raw.Count) - associatorStart; - if(characterCount == 0) { + if(this.valueStart == -1) { + int characterCount = (this.raw.Offset + this.raw.Count) - associatorStart; + if(characterCount == 0) { + return null; + } + } else if(this.valueStart == associatorStart) { return null; } - } - return this.raw.Text.Substring(associatorStart, 1); + return this.raw.Text.Substring(associatorStart, 1); + } } } @@ -135,5 +159,3 @@ namespace Nuclex.Support.Parsing { } } // namespace Nuclex.Support.Parsing - -#endif // ENABLE_BROKEN_COMMAND_LINE_PARSER diff --git a/Source/Parsing/CommandLine.Formatter.cs b/Source/Parsing/CommandLine.Formatter.cs index c435f2b..a654353 100644 --- a/Source/Parsing/CommandLine.Formatter.cs +++ b/Source/Parsing/CommandLine.Formatter.cs @@ -21,8 +21,6 @@ License along with this library using System; using System.Collections.Generic; -#if ENABLE_BROKEN_COMMAND_LINE_PARSER - namespace Nuclex.Support.Parsing { partial class CommandLine { @@ -32,5 +30,3 @@ namespace Nuclex.Support.Parsing { } } // namespace Nuclex.Support.Parsing - -#endif // ENABLE_BROKEN_COMMAND_LINE_PARSER diff --git a/Source/Parsing/CommandLine.Parser.cs b/Source/Parsing/CommandLine.Parser.cs index a6c840b..a889614 100644 --- a/Source/Parsing/CommandLine.Parser.cs +++ b/Source/Parsing/CommandLine.Parser.cs @@ -23,8 +23,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -#if ENABLE_BROKEN_COMMAND_LINE_PARSER - namespace Nuclex.Support.Parsing { partial class CommandLine { @@ -119,7 +117,7 @@ namespace Nuclex.Support.Parsing { // Windows style argument using '/' as its initiator case '/': { // The '/ character is only used to initiate argument on windows and can be - // toggled off. The application decides, whether this is done depending on the + // toggled off. The application decides whether this is done depending on the // operating system or whether uniform behavior across platforms is desired. if(!this.windowsMode) { goto default; @@ -155,7 +153,7 @@ namespace Nuclex.Support.Parsing { /// /// The number of characters consumed private void parsePotentialOption( - string commandLineString, int initiatorStartIndex, ref int index + string commandLineString, int initiatorStartIndex, ref int index ) { // If the string ends here this can only be considered as a loose value @@ -174,14 +172,16 @@ namespace Nuclex.Support.Parsing { // Look for the first character that ends the option. If it is not an actual option, // the very first character might be the end - index = commandLineString.IndexOfAny(OptionNameEndingCharacters, nameStartIndex); - if(index == -1) { - index = commandLineString.Length; + if(commandLineString[index] != commandLineString[initiatorStartIndex]) { + index = commandLineString.IndexOfAny(NameEndingCharacters, nameStartIndex); + if(index == -1) { + index = commandLineString.Length; + } } // If the first character of the supposed option is not valid for an option name, // we have to consider this to be a loose value - if(index == nameStartIndex) { + if((index == nameStartIndex)/* && !isAssignmentCharacter(commandLineString[index])*/) { index = commandLineString.IndexOfAny(WhitespaceCharacters, index); if(index == -1) { index = commandLineString.Length; @@ -195,9 +195,10 @@ namespace Nuclex.Support.Parsing { return; } - parseOptionAssignment( + parsePotentialOptionAssignment( commandLineString, initiatorStartIndex, nameStartIndex, ref index ); + } /// Parses the value assignment in a command line option @@ -209,49 +210,43 @@ namespace Nuclex.Support.Parsing { /// Position of the first character in the option's name /// /// Index at which the option name ended - private void parseOptionAssignment( - string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index + private void parsePotentialOptionAssignment( + string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index ) { int nameEndIndex = index; int valueStartIndex; int valueEndIndex; - if(index == commandLineString.Length) { - valueStartIndex = -1; - valueEndIndex = -1; - } else { + // See if this is an assignment character. If it is, the assigned value + // should follow to the right. + bool isAssignment = + (index < commandLineString.Length) && + isAssignmentCharacter(commandLineString[index]); - char currentCharacter = commandLineString[index]; - bool isAssignment = - (currentCharacter == ':') || - (currentCharacter == '='); + // If it's an assignment, we can proceed parsing the assigned value + if(isAssignment) { + ++index; + parseOptionValue(commandLineString, initiatorStartIndex, nameStartIndex, ref index); + return; + } else { // No, it's an option name without an assignment - // Does the string end after the suspected assignment character? - bool argumentEndReached = ((index + 1) == commandLineString.Length); + bool isModifier = + (commandLineString[index - 1] == '+') || + (commandLineString[index - 1] == '-'); - if(isAssignment) { - parseOptionValue(commandLineString, initiatorStartIndex, nameStartIndex, ref index); - return; + if(isModifier) { + valueStartIndex = index - 1; + valueEndIndex = index; + --nameEndIndex; } else { - - bool isModifier = - (currentCharacter == '+') || - (currentCharacter == '-'); - - if(isModifier) { - valueStartIndex = index; - ++index; - valueEndIndex = index; - } else { - valueStartIndex = -1; - valueEndIndex = -1; - } + valueStartIndex = -1; + valueEndIndex = -1; } } int argumentLength = index - initiatorStartIndex; - this.commandLine.addOption( - new Option( + this.commandLine.addArgument( + new Argument( new StringSegment(commandLineString, initiatorStartIndex, argumentLength), nameStartIndex, nameEndIndex - nameStartIndex, valueStartIndex, valueEndIndex - valueStartIndex @@ -269,24 +264,23 @@ namespace Nuclex.Support.Parsing { /// /// Index at which the option name ended private void parseOptionValue( - string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index + string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index ) { - int nameEndIndex = index; + int nameEndIndex = index - 1; int valueStartIndex, valueEndIndex; // Does the string end after the suspected assignment character? - bool argumentEndReached = ((index + 1) == commandLineString.Length); + bool argumentEndReached = (index == commandLineString.Length); if(argumentEndReached) { - ++index; valueStartIndex = -1; valueEndIndex = -1; } else { - char nextCharacter = commandLineString[index + 1]; + char nextCharacter = commandLineString[index]; // Is this a quoted assignment if(nextCharacter == '"') { - index += 2; + ++index; valueStartIndex = index; index = commandLineString.IndexOf('"', index); if(index == -1) { @@ -297,7 +291,6 @@ namespace Nuclex.Support.Parsing { ++index; } } else { // Nope, assuming unquoted assignment or empty assignment - ++index; valueStartIndex = index; index = commandLineString.IndexOfAny(WhitespaceCharacters, index); if(index == -1) { @@ -315,8 +308,8 @@ namespace Nuclex.Support.Parsing { } int argumentLength = index - initiatorStartIndex; - this.commandLine.addOption( - new Option( + this.commandLine.addArgument( + new Argument( new StringSegment(commandLineString, initiatorStartIndex, argumentLength), nameStartIndex, nameEndIndex - nameStartIndex, valueStartIndex, valueEndIndex - valueStartIndex @@ -328,19 +321,26 @@ namespace Nuclex.Support.Parsing { /// String the quoted value is parsed from /// Index at which the quoted value begins private void parseQuotedValue(string commandLineString, ref int index) { + int startIndex = index; char quoteCharacter = commandLineString[index]; - int startIndex = index + 1; + int valueIndex = startIndex + 1; // Search for the closing quote - index = commandLineString.IndexOf(quoteCharacter, startIndex); + index = commandLineString.IndexOf(quoteCharacter, valueIndex); if(index == -1) { index = commandLineString.Length; // value ends at string end - commandLine.addValue( - new StringSegment(commandLineString, startIndex, index - startIndex) + commandLine.addArgument( + Argument.ValueOnly( + new StringSegment(commandLineString, startIndex, index - startIndex), + valueIndex, index - valueIndex + ) ); } else { // A closing quote was found - commandLine.addValue( - new StringSegment(commandLineString, startIndex, index - startIndex) + commandLine.addArgument( + Argument.ValueOnly( + new StringSegment(commandLineString, startIndex, index - startIndex + 1), + valueIndex, index - valueIndex + ) ); ++index; // Skip the closing quote } @@ -362,9 +362,22 @@ namespace Nuclex.Support.Parsing { ); } + /// + /// Determines whether the specified character indicates an assignment + /// + /// + /// Character that will be checked for being an assignemnt + /// + /// + /// True if the specified character indicated an assignment, otherwise false + /// + private static bool isAssignmentCharacter(char character) { + return (character == ':') || (character == '='); + } + /// Characters which end an option name when they are encountered - private static readonly char[] OptionNameEndingCharacters = new char[] { - ' ', '\t', '=', ':', '/', '-', '+', '"' + private static readonly char[] NameEndingCharacters = new char[] { + ' ', '\t', '=', ':', '"' }; /// Characters the parser considers to be whitespace @@ -380,5 +393,3 @@ namespace Nuclex.Support.Parsing { } } // namespace Nuclex.Support.Parsing - -#endif // ENABLE_BROKEN_COMMAND_LINE_PARSER diff --git a/Source/Parsing/CommandLine.Test.cs b/Source/Parsing/CommandLine.Test.cs index 6b45199..dac28bb 100644 --- a/Source/Parsing/CommandLine.Test.cs +++ b/Source/Parsing/CommandLine.Test.cs @@ -26,118 +26,117 @@ using System.Text; using NUnit.Framework; -#if ENABLE_BROKEN_COMMAND_LINE_PARSER - namespace Nuclex.Support.Parsing { /// Ensures that the command line parser is working properly [TestFixture] public class CommandLineTest { - #region class OptionTest + #region class ArgumentTest /// Unit test for the command line option class [TestFixture] - public class OptionTest { + public class ArgumentTest { /// - /// Verifies that the name of a command line option without a value can be extracted + /// Verifies that the name of a command line argument without a value can + /// be extracted /// [Test] public void TestNameExtraction() { - CommandLine.Option option = new CommandLine.Option( + CommandLine.Argument argument = CommandLine.Argument.OptionOnly( new StringSegment("--test"), 2, 4 ); - Assert.AreEqual("--test", option.Raw); - Assert.AreEqual("--", option.Initiator); - Assert.AreEqual("test", option.Name); - Assert.IsNull(option.Associator); - Assert.IsNull(option.Value); + Assert.AreEqual("--test", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.IsNull(argument.Associator); + Assert.IsNull(argument.Value); } /// - /// Verifies that the name of a command line option without a value can be extracted - /// when the option is contained in a substring of a larger string + /// Verifies that the name of a command line argument without a value can be + /// extracted when the argument is contained in a substring of a larger string /// [Test] public void TestNameExtractionFromSubstring() { - CommandLine.Option option = new CommandLine.Option( + CommandLine.Argument argument = CommandLine.Argument.OptionOnly( new StringSegment("||--test||", 2, 6), 4, 4 ); - Assert.AreEqual("--test", option.Raw); - Assert.AreEqual("--", option.Initiator); - Assert.AreEqual("test", option.Name); - Assert.IsNull(option.Associator); - Assert.IsNull(option.Value); + Assert.AreEqual("--test", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.IsNull(argument.Associator); + Assert.IsNull(argument.Value); } /// - /// Varifies that the name and value of a command line option can be extracted + /// Varifies that the name and value of a command line argument can be extracted /// [Test] public void TestValueExtraction() { - CommandLine.Option option = new CommandLine.Option( + CommandLine.Argument argument = new CommandLine.Argument( new StringSegment("--test=123"), 2, 4, 7, 3 ); - Assert.AreEqual("--test=123", option.Raw); - Assert.AreEqual("--", option.Initiator); - Assert.AreEqual("test", option.Name); - Assert.AreEqual("=", option.Associator); - Assert.AreEqual("123", option.Value); + Assert.AreEqual("--test=123", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.AreEqual("=", argument.Associator); + Assert.AreEqual("123", argument.Value); } /// - /// Varifies that the name and value of a command line option can be extracted - /// when the option is contained in a substring of a larger string + /// Varifies that the name and value of a command line argument can be extracted + /// when the argument is contained in a substring of a larger string /// [Test] public void TestValueExtractionFromSubstring() { - CommandLine.Option option = new CommandLine.Option( + CommandLine.Argument argument = new CommandLine.Argument( new StringSegment("||--test=123||", 2, 10), 4, 4, 9, 3 ); - Assert.AreEqual("--test=123", option.Raw); - Assert.AreEqual("--", option.Initiator); - Assert.AreEqual("test", option.Name); - Assert.AreEqual("=", option.Associator); - Assert.AreEqual("123", option.Value); + Assert.AreEqual("--test=123", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.AreEqual("=", argument.Associator); + Assert.AreEqual("123", argument.Value); } /// - /// Varifies that the name and value of a command line option can be extracted + /// Varifies that the name and value of a command line argument can be extracted /// when the option is assigned a quoted value /// [Test] public void TestQuotedValueExtraction() { - CommandLine.Option option = new CommandLine.Option( + CommandLine.Argument argument = new CommandLine.Argument( new StringSegment("--test=\"123\"", 0, 12), 2, 4, 8, 3 ); - Assert.AreEqual("--test=\"123\"", option.Raw); - Assert.AreEqual("--", option.Initiator); - Assert.AreEqual("test", option.Name); - Assert.AreEqual("=", option.Associator); - Assert.AreEqual("123", option.Value); + Assert.AreEqual("--test=\"123\"", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.AreEqual("=", argument.Associator); + Assert.AreEqual("123", argument.Value); } /// - /// Varifies that the associator of a command line option with an open ended value - /// assignment can be retrieved + /// Varifies that the associator of a command line argument with an open ended + /// value assignment can be retrieved /// [Test] public void TestValuelessAssociatorRetrieval() { - CommandLine.Option option = new CommandLine.Option( + CommandLine.Argument argument = CommandLine.Argument.OptionOnly( new StringSegment("--test="), 2, 4 ); - Assert.AreEqual("--test=", option.Raw); - Assert.AreEqual("--", option.Initiator); - Assert.AreEqual("test", option.Name); - Assert.AreEqual("=", option.Associator); - Assert.IsNull(option.Value); + Assert.AreEqual("--test=", argument.Raw); + Assert.AreEqual("--", argument.Initiator); + Assert.AreEqual("test", argument.Name); + Assert.AreEqual("=", argument.Associator); + Assert.IsNull(argument.Value); } /// @@ -147,8 +146,8 @@ namespace Nuclex.Support.Parsing { /// [Test] public void TestValuelessAssociatorRetrievalFromSubstring() { - CommandLine.Option option = new CommandLine.Option( - new StringSegment("||--test=||", 2, 7), 4, 4//, 9, -1 + CommandLine.Argument option = CommandLine.Argument.OptionOnly( + new StringSegment("||--test=||", 2, 7), 4, 4 ); Assert.AreEqual("--test=", option.Raw); @@ -158,21 +157,91 @@ namespace Nuclex.Support.Parsing { Assert.IsNull(option.Value); } + /// + /// Varifies that a command line argument without an option name can be retrieved + /// + [Test] + public void TestNamelessValueRetrieval() { + CommandLine.Argument argument = CommandLine.Argument.ValueOnly( + new StringSegment("\"hello world\""), 1, 11 + ); + + Assert.AreEqual("\"hello world\"", argument.Raw); + Assert.IsNull(argument.Initiator); + Assert.IsNull(argument.Name); + Assert.IsNull(argument.Associator); + Assert.AreEqual("hello world", argument.Value); + } + + /// + /// Varifies that a command line argument without an option name can be retrieved + /// that is contained in a substring of larger string + /// + [Test] + public void TestNamelessValueRetrievalFromSubstring() { + CommandLine.Argument argument = CommandLine.Argument.ValueOnly( + new StringSegment("||\"hello world\"||", 2, 13), 3, 11 + ); + + Assert.AreEqual("\"hello world\"", argument.Raw); + Assert.IsNull(argument.Initiator); + Assert.IsNull(argument.Name); + Assert.IsNull(argument.Associator); + Assert.AreEqual("hello world", argument.Value); + } + } - #endregion // class OptionTest + #endregion // class ArgumentTest /// - /// Validates that the parser can handle an argument initiator without an obvious name + /// Validates that the parser can handle an argument initiator with an + /// assignment that is missing a name /// [Test] public void TestParseAmbiguousNameResolution() { CommandLine commandLine = CommandLine.Parse("--:test"); - Assert.AreEqual(0, commandLine.Values.Count); - Assert.AreEqual(1, commandLine.Options.Count); - Assert.AreEqual("-", commandLine.Options[0].Name); - Assert.AreEqual("test", commandLine.Options[0].Value); + // Without a name, this is not a valid command line option, so it will + // be parsed as a loose value instead. + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--:test", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("--:test", commandLine.Arguments[0].Value); + } + + /// + /// Verifies that a lone short argument initiator without anything behind + /// can be parsed + /// + [Test] + public void TestParseShortArgumentInitiatorOnly() { + CommandLine commandLine = CommandLine.Parse("-"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("-", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("-", commandLine.Arguments[0].Value); + } + + /// + /// Verifies that a lone long argument initiator without anything behind + /// can be parsed + /// + [Test] + public void TestParseLongArgumentInitiatorOnly() { + CommandLine commandLine = CommandLine.Parse("--"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("--", commandLine.Arguments[0].Value); } /// @@ -183,25 +252,51 @@ namespace Nuclex.Support.Parsing { public void TestParseArgumentInitiatorAtEnd() { CommandLine commandLine = CommandLine.Parse("-hello:-world -"); - Assert.AreEqual(1, commandLine.Values.Count); - Assert.AreEqual(1, commandLine.Options.Count); - Assert.AreEqual("hello", commandLine.Options[0].Name); - Assert.AreEqual("-world", commandLine.Options[0].Value); - Assert.AreEqual("-", commandLine.Values[0]); + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("-hello:-world", commandLine.Arguments[0].Raw); + Assert.AreEqual("-", commandLine.Arguments[0].Initiator); + Assert.AreEqual("hello", commandLine.Arguments[0].Name); + Assert.AreEqual(":", commandLine.Arguments[0].Associator); + Assert.AreEqual("-world", commandLine.Arguments[0].Value); + + Assert.AreEqual("-", commandLine.Arguments[1].Raw); + Assert.IsNull(commandLine.Arguments[1].Initiator); + Assert.IsNull(commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("-", commandLine.Arguments[1].Value); } /// Validates that quoted arguments can be parsed [Test] - public void TestParseQuotedOption() { + public void TestParseQuotedValue() { CommandLine commandLine = CommandLine.Parse("hello -world --this -is=\"a test\""); - Assert.AreEqual(1, commandLine.Values.Count); - Assert.AreEqual(3, commandLine.Options.Count); - Assert.AreEqual("hello", commandLine.Values[0]); - Assert.AreEqual("world", commandLine.Options[0].Name); - Assert.AreEqual("this", commandLine.Options[1].Name); - Assert.AreEqual("is", commandLine.Options[2].Name); - Assert.AreEqual("a test", commandLine.Options[2].Value); + Assert.AreEqual(4, commandLine.Arguments.Count); + + Assert.AreEqual("hello", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("hello", commandLine.Arguments[0].Value); + + Assert.AreEqual("-world", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("world", commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.IsNull(commandLine.Arguments[1].Value); + + Assert.AreEqual("--this", commandLine.Arguments[2].Raw); + Assert.AreEqual("--", commandLine.Arguments[2].Initiator); + Assert.AreEqual("this", commandLine.Arguments[2].Name); + Assert.IsNull(commandLine.Arguments[2].Associator); + Assert.IsNull(commandLine.Arguments[2].Value); + + Assert.AreEqual("-is=\"a test\"", commandLine.Arguments[3].Raw); + Assert.AreEqual("-", commandLine.Arguments[3].Initiator); + Assert.AreEqual("is", commandLine.Arguments[3].Name); + Assert.AreEqual("=", commandLine.Arguments[3].Associator); + Assert.AreEqual("a test", commandLine.Arguments[3].Value); } /// Validates that null can be parsed @@ -209,8 +304,7 @@ namespace Nuclex.Support.Parsing { public void TestParseNull() { CommandLine commandLine = CommandLine.Parse(null); - Assert.AreEqual(0, commandLine.Values.Count); - Assert.AreEqual(0, commandLine.Options.Count); + Assert.AreEqual(0, commandLine.Arguments.Count); } /// Validates that a single argument without quotes can be parsed @@ -218,9 +312,12 @@ namespace Nuclex.Support.Parsing { public void TestParseSingleNakedValue() { CommandLine commandLine = CommandLine.Parse("hello"); - Assert.AreEqual(1, commandLine.Values.Count); - Assert.AreEqual(0, commandLine.Options.Count); - Assert.AreEqual("hello", commandLine.Values[0]); + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("hello", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("hello", commandLine.Arguments[0].Value); } /// @@ -231,9 +328,28 @@ namespace Nuclex.Support.Parsing { public void TestParseQuotedArgumentWithoutClosingQuote() { CommandLine commandLine = CommandLine.Parse("\"Quoted argument"); - Assert.AreEqual(1, commandLine.Values.Count); - Assert.AreEqual(0, commandLine.Options.Count); - Assert.AreEqual("Quoted argument", commandLine.Values[0]); + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("\"Quoted argument", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("Quoted argument", commandLine.Arguments[0].Value); + } + + /// + /// Validates that the parser correctly handles a quoted value assignment that's + /// missing the closing quote + /// + [Test] + public void TestParseQuotedValueWithoutClosingQuote() { + CommandLine commandLine = CommandLine.Parse("--test=\"Quoted argument"); + + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--test=\"Quoted argument", commandLine.Arguments[0].Raw); + Assert.AreEqual("--", commandLine.Arguments[0].Initiator); + Assert.AreEqual("test", commandLine.Arguments[0].Name); + Assert.AreEqual("=", commandLine.Arguments[0].Associator); + Assert.AreEqual("Quoted argument", commandLine.Arguments[0].Value); } /// @@ -243,8 +359,23 @@ namespace Nuclex.Support.Parsing { public void TestParseSpacesOnly() { CommandLine commandLine = CommandLine.Parse(" \t "); - Assert.AreEqual(0, commandLine.Values.Count); - Assert.AreEqual(0, commandLine.Options.Count); + Assert.AreEqual(0, commandLine.Arguments.Count); + } + + /// + /// Validates that the parser can handle a quoted option + /// + [Test] + public void TestParseQuotedOption() { + CommandLine commandLine = CommandLine.Parse("--\"hello\""); + + // Quoted options are not supported, so this becomes a loose value + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--\"hello\"", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("--\"hello\"", commandLine.Arguments[0].Value); } /// @@ -255,10 +386,19 @@ namespace Nuclex.Support.Parsing { public void TestParseMultipleLoneArgumentInitiators() { CommandLine commandLine = CommandLine.Parse("--- --"); - Assert.AreEqual(0, commandLine.Values.Count); - Assert.AreEqual(2, commandLine.Options.Count); - Assert.AreEqual("-", commandLine.Options[1].Name); - Assert.AreEqual("-", commandLine.Options[2].Name); + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("---", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("---", commandLine.Arguments[0].Value); + + Assert.AreEqual("--", commandLine.Arguments[1].Raw); + Assert.IsNull(commandLine.Arguments[1].Initiator); + Assert.IsNull(commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("--", commandLine.Arguments[1].Value); } /// @@ -268,10 +408,19 @@ namespace Nuclex.Support.Parsing { public void TestParseOptionWithEmbeddedInitiator() { CommandLine commandLine = CommandLine.Parse("-hello/world=123 -test-case"); - Assert.AreEqual(0, commandLine.Values.Count); - Assert.AreEqual(2, commandLine.Options.Count); - Assert.AreEqual("hello/world", commandLine.Options[0].Name); - Assert.AreEqual("test-case", commandLine.Options[1].Name); + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("-hello/world=123", commandLine.Arguments[0].Raw); + Assert.AreEqual("-", commandLine.Arguments[0].Initiator); + Assert.AreEqual("hello/world", commandLine.Arguments[0].Name); + Assert.AreEqual("=", commandLine.Arguments[0].Associator); + Assert.AreEqual("123", commandLine.Arguments[0].Value); + + Assert.AreEqual("-test-case", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("test-case", commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.IsNull(commandLine.Arguments[1].Value); } /// @@ -281,11 +430,25 @@ namespace Nuclex.Support.Parsing { public void TestParseOptionAndValueWithoutSpaces() { CommandLine commandLine = CommandLine.Parse("\"value\"-option\"value\""); - Assert.AreEqual(2, commandLine.Values.Count); - Assert.AreEqual(1, commandLine.Options.Count); - Assert.AreEqual("value", commandLine.Values[0]); - Assert.AreEqual("option", commandLine.Options[0].Name); - Assert.AreEqual("value", commandLine.Values[1]); + Assert.AreEqual(3, commandLine.Arguments.Count); + + Assert.AreEqual("\"value\"", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("value", commandLine.Arguments[0].Value); + + Assert.AreEqual("-option", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("option", commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.IsNull(commandLine.Arguments[1].Value); + + Assert.AreEqual("\"value\"", commandLine.Arguments[2].Raw); + Assert.IsNull(commandLine.Arguments[2].Initiator); + Assert.IsNull(commandLine.Arguments[2].Name); + Assert.IsNull(commandLine.Arguments[2].Associator); + Assert.AreEqual("value", commandLine.Arguments[2].Value); } /// @@ -296,10 +459,19 @@ namespace Nuclex.Support.Parsing { public void TestParseOptionWithModifierAtEnd() { CommandLine commandLine = CommandLine.Parse("--test-value- -test+"); - Assert.AreEqual(0, commandLine.Values.Count); - Assert.AreEqual(2, commandLine.Options.Count); - Assert.AreEqual("test-value", commandLine.Options[0].Name); - Assert.AreEqual("test", commandLine.Options[1].Name); + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("--test-value-", commandLine.Arguments[0].Raw); + Assert.AreEqual("--", commandLine.Arguments[0].Initiator); + Assert.AreEqual("test-value", commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("-", commandLine.Arguments[0].Value); + + Assert.AreEqual("-test+", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("test", commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("+", commandLine.Arguments[1].Value); } /// @@ -307,14 +479,21 @@ namespace Nuclex.Support.Parsing { /// [Test] public void TestParseOptionWithAssignment() { - CommandLine commandLine = CommandLine.Parse("-hello:123 -world=321"); + CommandLine commandLine = CommandLine.Parse("-hello: -world=321"); - Assert.AreEqual(0, commandLine.Values.Count); - Assert.AreEqual(2, commandLine.Options.Count); - Assert.AreEqual("hello", commandLine.Options[0].Name); - Assert.AreEqual("123", commandLine.Options[0].Value); - Assert.AreEqual("world", commandLine.Options[1].Name); - Assert.AreEqual("321", commandLine.Options[1].Value); + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("-hello:", commandLine.Arguments[0].Raw); + Assert.AreEqual("-", commandLine.Arguments[0].Initiator); + Assert.AreEqual("hello", commandLine.Arguments[0].Name); + Assert.AreEqual(":", commandLine.Arguments[0].Associator); + Assert.IsNull(commandLine.Arguments[0].Value); + + Assert.AreEqual("-world=321", commandLine.Arguments[1].Raw); + Assert.AreEqual("-", commandLine.Arguments[1].Initiator); + Assert.AreEqual("world", commandLine.Arguments[1].Name); + Assert.AreEqual("=", commandLine.Arguments[1].Associator); + Assert.AreEqual("321", commandLine.Arguments[1].Value); } /// @@ -325,9 +504,12 @@ namespace Nuclex.Support.Parsing { public void TestParseOptionAtEndOfString() { CommandLine commandLine = CommandLine.Parse("--test:"); - Assert.AreEqual(0, commandLine.Values.Count); - Assert.AreEqual(1, commandLine.Options.Count); - Assert.AreEqual("test", commandLine.Options[0].Name); + Assert.AreEqual(1, commandLine.Arguments.Count); + Assert.AreEqual("--test:", commandLine.Arguments[0].Raw); + Assert.AreEqual("--", commandLine.Arguments[0].Initiator); + Assert.AreEqual("test", commandLine.Arguments[0].Name); + Assert.AreEqual(":", commandLine.Arguments[0].Associator); + Assert.IsNull(commandLine.Arguments[0].Value); } /// @@ -338,11 +520,19 @@ namespace Nuclex.Support.Parsing { public void TestWindowsOptionInitiator() { CommandLine commandLine = CommandLine.Parse("/hello //world", true); - Assert.AreEqual(1, commandLine.Values.Count); - Assert.AreEqual(2, commandLine.Options.Count); - Assert.AreEqual("hello", commandLine.Options[0].Name); - Assert.AreEqual("/", commandLine.Options[0].Value); - Assert.AreEqual("world", commandLine.Options[1].Name); + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("/hello", commandLine.Arguments[0].Raw); + Assert.AreEqual("/", commandLine.Arguments[0].Initiator); + Assert.AreEqual("hello", commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.IsNull(commandLine.Arguments[0].Value); + + Assert.AreEqual("//world", commandLine.Arguments[1].Raw); + Assert.IsNull(commandLine.Arguments[1].Initiator); + Assert.IsNull(commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("//world", commandLine.Arguments[1].Value); } /// @@ -353,16 +543,23 @@ namespace Nuclex.Support.Parsing { public void TestNonWindowsOptionValues() { CommandLine commandLine = CommandLine.Parse("/hello //world", false); - Assert.AreEqual(2, commandLine.Values.Count); - Assert.AreEqual(0, commandLine.Options.Count); - Assert.AreEqual("/hello", commandLine.Values[0]); - Assert.AreEqual("//world", commandLine.Values[1]); + Assert.AreEqual(2, commandLine.Arguments.Count); + + Assert.AreEqual("/hello", commandLine.Arguments[0].Raw); + Assert.IsNull(commandLine.Arguments[0].Initiator); + Assert.IsNull(commandLine.Arguments[0].Name); + Assert.IsNull(commandLine.Arguments[0].Associator); + Assert.AreEqual("/hello", commandLine.Arguments[0].Value); + + Assert.AreEqual("//world", commandLine.Arguments[1].Raw); + Assert.IsNull(commandLine.Arguments[1].Initiator); + Assert.IsNull(commandLine.Arguments[1].Name); + Assert.IsNull(commandLine.Arguments[1].Associator); + Assert.AreEqual("//world", commandLine.Arguments[1].Value); } } } // namespace Nuclex.Support.Parsing -#endif // ENABLE_BROKEN_COMMAND_LINE_PARSER - #endif // UNITTEST \ No newline at end of file diff --git a/Source/Parsing/CommandLine.cs b/Source/Parsing/CommandLine.cs index 18c1cc6..651ec0d 100644 --- a/Source/Parsing/CommandLine.cs +++ b/Source/Parsing/CommandLine.cs @@ -24,13 +24,9 @@ using System.IO; using Nuclex.Support.Collections; -#if ENABLE_BROKEN_COMMAND_LINE_PARSER - namespace Nuclex.Support.Parsing { - /// - /// Manages an application's command line parameters - /// + /// Parses and stores an application's command line parameters /// /// /// At the time of the creation of this component, there are already several command @@ -39,14 +35,14 @@ namespace Nuclex.Support.Parsing { /// arrive at their results. /// /// - /// This class does nothing more than represent the command line to the application. - /// It can parse a command line - /// parse the command line arguments. It doesn't - /// interpret them and it doesn't check them for validity. This promotes simplicity - /// and allows t - /// be unit-tested and is an ideal building block to create actual command line - /// interpreters that connect the parameters to program instructions and or fill - /// structures in code. + /// This command line parser does nothing more than represent the command line to + /// the application through a convenient interface. It parses a command line and + /// extracts the arguments, but doesn't interpret them and or check them for validity. + /// + /// + /// This design promotes simplicity and makes is an ideal building block to create + /// actual command line interpreters that connect the parameters to program + /// instructions and or fill structures in code. /// /// /// Terminology @@ -82,18 +78,12 @@ namespace Nuclex.Support.Parsing { /// /// /// - /// - /// What this parser doesn't support is spaced assignments (eg. '--format png') since - /// these are ambiguous if the parser doesn't know beforehand whether "format" accepts - /// a non-optional argument. - /// /// public partial class CommandLine { /// Initializes a new command line public CommandLine() { - this.options = new List