From 9c40abe10a33d2552875a5b53f21d65fa0f26c4c Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Thu, 11 Dec 2008 20:15:21 +0000 Subject: [PATCH] Completed the command line parser, then decided while adding result validation to the unit tests that the parser should be greedy (and accept option initiators within option names) - I don't like the way the parser code turned out anyway, so I'll rewrite soon git-svn-id: file:///srv/devel/repo-conversion/nusu@107 d2e56fa2-650e-0410-a79f-9358c0239efd --- Documents/CommandLine.txt | 13 ++ Nuclex.Support.csproj | 1 + Source/Parsing/CommandLine.Option.cs | 116 ++++++++-- Source/Parsing/CommandLine.Parser.cs | 297 +++++++++++++++++------- Source/Parsing/CommandLine.Test.cs | 329 +++++++++++++++++++++++---- Source/Parsing/CommandLine.cs | 68 +++++- 6 files changed, 676 insertions(+), 148 deletions(-) create mode 100644 Documents/CommandLine.txt diff --git a/Documents/CommandLine.txt b/Documents/CommandLine.txt new file mode 100644 index 0000000..ef8da36 --- /dev/null +++ b/Documents/CommandLine.txt @@ -0,0 +1,13 @@ + /* + struct CommandLine { + [Option] + bool? Option; + [Option] + int? Width; + [Option] + TypeCode Code; + [Values] + string[] Values; + } +*/ + diff --git a/Nuclex.Support.csproj b/Nuclex.Support.csproj index 3259424..d4df1d2 100644 --- a/Nuclex.Support.csproj +++ b/Nuclex.Support.csproj @@ -246,6 +246,7 @@ + diff --git a/Source/Parsing/CommandLine.Option.cs b/Source/Parsing/CommandLine.Option.cs index dc4895f..fc5b56d 100644 --- a/Source/Parsing/CommandLine.Option.cs +++ b/Source/Parsing/CommandLine.Option.cs @@ -20,28 +20,114 @@ License along with this library using System; using System.Collections.Generic; +using System.Diagnostics; namespace Nuclex.Support.Parsing { partial class CommandLine { - /// Option that can be specified on an application's command line - public struct Option { + /// Option being specified on an application's command line + public class Option { + + /// Initializes a new option with only a name + /// + /// String segment containing the entire option as it was given on the command line + /// + /// Absolute index the option name starts at + /// Number of characters in the option name + /// The newly created option + internal Option( + StringSegment raw, + int nameStart, int nameLength + ) + : this(raw, nameStart, nameLength, -1, -1) { } + + /// Creates a new option with a name and an assigned value + /// + /// String segment containing the entire option as it was given on the command line + /// + /// Absolute index the option name starts at + /// Number of characters in the option name + /// Absolute index the value starts at + /// Number of characters in the value + /// The newly created option + internal Option( + StringSegment raw, + int nameStart, int nameLength, + int valueStart, int valueLength + ) { + this.raw = raw; + this.nameStart = nameStart; + 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 - public string Raw; // TODO: ToString() instead -/* - /// Method used to specify the argument (either '-', '--' or '/') - public string Method; -*/ - /// Name of the command line argument - public string Name; - /// Value that has been assigned to the command line argument - public string Value; -/* - /// Method used to assign the value (either '=', ':' or ' ') - public string Assignment; -*/ + public string Raw { + get { return this.raw.ToString(); } + } + + /// Characters used to initiate this option + public string Initiator { + get { + 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); + } + } + + /// Characters used to associate a value to this option + public string Associator { + get { + int associatorStart = this.nameStart + this.nameLength; + + if(this.valueStart == -1) { + int characterCount = (this.raw.Offset + this.raw.Count) - associatorStart; + if(characterCount == 0) { + return null; + } + } + + return this.raw.Text.Substring(associatorStart, 1); + } + } + + /// Name of the command line option + public string Value { + get { + if(this.valueStart == -1) { + return null; + } else { + return this.raw.Text.Substring(this.valueStart, this.valueLength); + } + } + } + + /// + /// Contains the entire option as it was specified on the command line + /// + private StringSegment raw; + + /// Absolute index in the raw string the option name starts at + private int nameStart; + /// Number of characters in the option name + private int nameLength; + /// Absolute index in the raw string the value starts at + private int valueStart; + /// Number of characters in the value + private int valueLength; + } } diff --git a/Source/Parsing/CommandLine.Parser.cs b/Source/Parsing/CommandLine.Parser.cs index 69ae653..c28ef64 100644 --- a/Source/Parsing/CommandLine.Parser.cs +++ b/Source/Parsing/CommandLine.Parser.cs @@ -21,6 +21,7 @@ License along with this library using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; namespace Nuclex.Support.Parsing { @@ -30,16 +31,20 @@ namespace Nuclex.Support.Parsing { private class Parser { /// Initializes a new command line parser - private Parser() { + /// Whether the / character initiates an argument + private Parser(bool windowsMode) { + this.windowsMode = windowsMode; this.commandLine = new CommandLine(); } /// Parses a string containing command line arguments /// String that will be parsed + /// Whether the / character initiates an argument /// The parsed command line arguments from the string - public static CommandLine Parse(string commandLineString) { - Parser theParser = new Parser(); - theParser.parse(commandLineString); + public static CommandLine Parse(string commandLineString, bool windowsMode) { + Console.WriteLine("Parsing '" + commandLineString + "'"); + Parser theParser = new Parser(windowsMode); + theParser.parseFullCommandLine(commandLineString); return theParser.commandLine; } @@ -50,7 +55,7 @@ namespace Nuclex.Support.Parsing { /// /// String containing the command line arguments that will be parsed /// - private void parse(string commandLineString) { + private void parseFullCommandLine(string commandLineString) { if(commandLineString == null) { return; } @@ -69,7 +74,8 @@ namespace Nuclex.Support.Parsing { // Parse the chunk of characters at this location and advance the index // to the next location after the chunk of characters - index += parseCharacterChunk(commandLineString, index); + parseChunk(commandLineString, ref index); + } } @@ -82,7 +88,7 @@ namespace Nuclex.Support.Parsing { /// /// Index in the string at which to begin parsing /// The number of characters that were consumed - private int parseCharacterChunk(string commandLineString, int index) { + private void parseChunk(string commandLineString, ref int index) { int startIndex = index; char currentCharacter = commandLineString[index]; @@ -94,45 +100,47 @@ namespace Nuclex.Support.Parsing { // Does the string end here? Stop parsing. if(index >= commandLineString.Length) { - addValue("-"); + this.commandLine.addValue(new StringSegment(commandLineString, startIndex, 1)); break; } // Does another '-' follow? Might be a unix style option or a loose "--" if(commandLineString[index] == '-') { ++index; - index += parsePotentialOption(commandLineString, startIndex, index); - } else { // Nope, it's a normal option or a loose '-' - index += parsePotentialOption(commandLineString, startIndex, index); } + parsePotentialOption(commandLineString, startIndex, ref index); + break; } // 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 + // operating system or whether uniform behavior across platforms is desired. + if(!this.windowsMode) { + goto default; + } + ++index; - index += parsePotentialOption(commandLineString, startIndex, index); + parsePotentialOption(commandLineString, startIndex, ref index); break; } // Quoted loose value case '"': { - StringSegment value = parseQuotedValue(commandLineString, index); - index += value.Count + 1; + parseQuotedValue(commandLineString, ref index); break; } // Unquoted loose value default: { - StringSegment value = parseNakedValue(commandLineString, index); - index += value.Count; + parseNakedValue(commandLineString, ref index); break; } } - - return index - startIndex; } /// Parses a potential command line option @@ -144,111 +152,226 @@ namespace Nuclex.Support.Parsing { /// Index at which the option name is supposed start (if it's an actual option) /// /// The number of characters consumed - private int parsePotentialOption( - string commandLineString, int initiatorStartIndex, int index + private void parsePotentialOption( + string commandLineString, int initiatorStartIndex, ref int index ) { // If the string ends here this can only be considered as a loose value - if(index >= commandLineString.Length) { - addValue(commandLineString.Substring(initiatorStartIndex)); - return 0; + if(index == commandLineString.Length) { + this.commandLine.addValue( + new StringSegment( + commandLineString, + initiatorStartIndex, + commandLineString.Length - initiatorStartIndex + ) + ); + return; } + int nameStartIndex = index; + // Look for the first character that ends the option. If it is not an actual option, // the very first character might be the end - int nameEndIndex = commandLineString.IndexOfAny(OptionNameEndingCharacters, index); - if(nameEndIndex == -1) { - nameEndIndex = commandLineString.Length; + index = commandLineString.IndexOfAny(OptionNameEndingCharacters, nameStartIndex); + if(index == -1) { + index = commandLineString.Length; } - - // If the first character of the supposed option is not valid for an option, + + // 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(nameEndIndex == index) { - // Parse normal unquoted value - //parseNakedValue(commandLineString, initiatorStartIndex).Count; - /* - int endIndex = commandLineString.IndexOfAny(WhitespaceCharacters, index); - if(endIndex == -1) { - addValue(commandLineString.Substring(initiatorStartIndex)); - return commandLineString.Length - index; - } else { - addValue( - commandLineString.Substring(initiatorStartIndex, endIndex - initiatorStartIndex) - ); - return endIndex - index; + if(index == nameStartIndex) { + index = commandLineString.IndexOfAny(WhitespaceCharacters, index); + if(index == -1) { + index = commandLineString.Length; } - */ + + commandLine.addValue( + new StringSegment( + commandLineString, initiatorStartIndex, index - initiatorStartIndex + ) + ); + return; } - - Console.WriteLine( - "Argument name: " + commandLineString.Substring(index, nameEndIndex - index) + + parseOptionAssignment( + commandLineString, initiatorStartIndex, nameStartIndex, ref index ); - - // TODO: Parse argument value (if provided) here!! - - return nameEndIndex - index; } - static readonly char[] OptionNameEndingCharacters = new char[] { - ' ', '\t', '=', ':', '/', '-', '+', '"' - }; + /// Parses the value assignment in a command line option + /// String containing the command line arguments + /// + /// Position of the character that started the option + /// + /// + /// 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 + ) { + int nameEndIndex = index; + int valueStartIndex; + int valueEndIndex; + + if(index == commandLineString.Length) { + valueStartIndex = -1; + valueEndIndex = -1; + } else { + + char currentCharacter = commandLineString[index]; + bool isAssignment = + (currentCharacter == ':') || + (currentCharacter == '='); + + // Does the string end after the suspected assignment character? + bool argumentEndReached = ((index + 1) == commandLineString.Length); + + if(isAssignment) { + parseOptionValue(commandLineString, initiatorStartIndex, nameStartIndex, ref index); + return; + } else { + + bool isModifier = + (currentCharacter == '+') || + (currentCharacter == '-'); + + if(isModifier) { + valueStartIndex = index; + ++index; + valueEndIndex = index; + } else { + valueStartIndex = -1; + valueEndIndex = -1; + } + } + } + + int argumentLength = index - initiatorStartIndex; + this.commandLine.addOption( + new Option( + new StringSegment(commandLineString, initiatorStartIndex, argumentLength), + nameStartIndex, nameEndIndex - nameStartIndex, + valueStartIndex, valueEndIndex - valueStartIndex + ) + ); + } + + /// Parses the value assignment in a command line option + /// String containing the command line arguments + /// + /// Position of the character that started the option + /// + /// + /// Position of the first character in the option's name + /// + /// Index at which the option name ended + private void parseOptionValue( + string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index + ) { + int nameEndIndex = index; + int valueStartIndex, valueEndIndex; + + // Does the string end after the suspected assignment character? + bool argumentEndReached = ((index + 1) == commandLineString.Length); + + if(argumentEndReached) { + ++index; + valueStartIndex = -1; + valueEndIndex = -1; + } else { + char nextCharacter = commandLineString[index + 1]; + + // Is this a quoted assignment + if(nextCharacter == '"') { + index += 2; + valueStartIndex = index; + index = commandLineString.IndexOf('"', index); + if(index == -1) { + index = commandLineString.Length; + valueEndIndex = index; + } else { + valueEndIndex = index; + ++index; + } + } else { // Nope, assuming unquoted assignment or empty assignment + ++index; + valueStartIndex = index; + index = commandLineString.IndexOfAny(WhitespaceCharacters, index); + if(index == -1) { + index = commandLineString.Length; + valueEndIndex = index; + } else { + if(index == valueStartIndex) { + valueStartIndex = -1; + valueEndIndex = -1; + } else { + valueEndIndex = index; + } + } + } + } + + int argumentLength = index - initiatorStartIndex; + this.commandLine.addOption( + new Option( + new StringSegment(commandLineString, initiatorStartIndex, argumentLength), + nameStartIndex, nameEndIndex - nameStartIndex, + valueStartIndex, valueEndIndex - valueStartIndex + ) + ); + } /// Parses a quoted value from the input string /// String the quoted value is parsed from /// Index at which the quoted value begins - /// A string segment containing the parsed quoted value - /// - /// The returned string segment does not include the quotes. - /// - private static StringSegment parseQuotedValue(string commandLineString, int index) { + private void parseQuotedValue(string commandLineString, ref int index) { char quoteCharacter = commandLineString[index]; - ++index; + int startIndex = index + 1; - int endIndex = commandLineString.IndexOf(quoteCharacter, index); - if(endIndex == -1) { - endIndex = commandLineString.Length; + // Search for the closing quote + index = commandLineString.IndexOf(quoteCharacter, startIndex); + if(index == -1) { + index = commandLineString.Length; // value ends at string end + commandLine.addValue( + new StringSegment(commandLineString, startIndex, index - startIndex) + ); + } else { // A closing quote was found + commandLine.addValue( + new StringSegment(commandLineString, startIndex, index - startIndex) + ); + ++index; // Skip the closing quote } - - // TODO: We don't skip the closing quote, the callee would have to detect it himself - - return new StringSegment(commandLineString, index, endIndex - index); } /// Parses a plain, unquoted value from the input string /// String containing the value to be parsed /// Index at which the value begins - /// A string segment containing the parsed value - private static StringSegment parseNakedValue(string commandLineString, int index) { - int endIndex = commandLineString.IndexOfAny(WhitespaceCharacters, index); - if(endIndex == -1) { - endIndex = commandLineString.Length; + private void parseNakedValue(string commandLineString, ref int index) { + int startIndex = index; + + index = commandLineString.IndexOfAny(WhitespaceCharacters, index); + if(index == -1) { + index = commandLineString.Length; } - return new StringSegment(commandLineString, index, endIndex - index); - } - - /// - /// Determines whether the specified character is valid as the first character - /// in an option - /// - /// Character that will be tested for validity - /// True if the character is valid as the first character in an option - private static bool isValidFirstCharacterInOption(char character) { - const string InvalidCharacters = " \t=:/-+\""; - return (InvalidCharacters.IndexOf(character) == -1); - } - - - private void addValue(string value) { - Console.WriteLine("Added Value: '" + value + "'"); + commandLine.addValue( + new StringSegment(commandLineString, startIndex, index - startIndex) + ); } + /// Characters which end an option name when they are encountered + private static readonly char[] OptionNameEndingCharacters = new char[] { + ' ', '\t', '=', ':', '/', '-', '+', '"' + }; /// Characters the parser considers to be whitespace private static readonly char[] WhitespaceCharacters = new char[] { ' ', '\t' }; /// Command line currently being built by the parser private CommandLine commandLine; + /// Whether the '/' character initiates an argument + private bool windowsMode; } diff --git a/Source/Parsing/CommandLine.Test.cs b/Source/Parsing/CommandLine.Test.cs index e3ea8fb..6b92da2 100644 --- a/Source/Parsing/CommandLine.Test.cs +++ b/Source/Parsing/CommandLine.Test.cs @@ -31,34 +31,218 @@ namespace Nuclex.Support.Parsing { /// Ensures that the command line parser is working properly [TestFixture] public class CommandLineTest { - - /* - struct CommandLine { - [Option] - bool? Option; - [Option] - int? Width; - [Option] - TypeCode Code; - [Values] - string[] Values; + + #region class OptionTest + + /// Unit test for the command line option class + [TestFixture] + public class OptionTest { + + /// + /// Verifies that the name of a command line option without a value can be extracted + /// + [Test] + public void TestNameExtraction() { + CommandLine.Option option = new CommandLine.Option( + 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); + } + + /// + /// 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 + /// + [Test] + public void TestNameExtractionFromSubstring() { + CommandLine.Option option = new CommandLine.Option( + 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); + } + + /// + /// Varifies that the name and value of a command line option can be extracted + /// + [Test] + public void TestValueExtraction() { + CommandLine.Option option = new CommandLine.Option( + 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); + } + + /// + /// 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 + /// + [Test] + public void TestValueExtractionFromSubstring() { + CommandLine.Option option = new CommandLine.Option( + 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); + } + + /// + /// Varifies that the name and value of a command line option can be extracted + /// when the option is assigned a quoted value + /// + [Test] + public void TestQuotedValueExtraction() { + CommandLine.Option option = new CommandLine.Option( + 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); + } + + /// + /// Varifies that the associator of a command line option with an open ended value + /// assignment can be retrieved + /// + [Test] + public void TestValuelessAssociatorRetrieval() { + CommandLine.Option option = new CommandLine.Option( + 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); + } + + /// + /// Varifies that the associator of a command line option with an open ended value + /// assignment can be retrieved when the option is contained in a substring of + /// a larger string + /// + [Test] + public void TestValuelessAssociatorRetrievalFromSubstring() { + CommandLine.Option option = new CommandLine.Option( + new StringSegment("||--test=||", 2, 7), 4, 4//, 9, -1 + ); + + Assert.AreEqual("--test=", option.Raw); + Assert.AreEqual("--", option.Initiator); + Assert.AreEqual("test", option.Name); + Assert.AreEqual("=", option.Associator); + Assert.IsNull(option.Value); + } + } -*/ - /// Validates that a single argument without quotes can be parsed + + #endregion // class OptionTest + + /// + /// Validates that the parser can handle an argument initiator without an obvious name + /// [Test] - public void TestParseSingleNakedArgument() { - CommandLine.Parse("Hello"); + 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); } /// - /// Validates that the parser can handle a single argument initator without + /// Validates that the parser can handle multiple lone argument initators without /// a following argument /// [Test] - public void TestParseLoneArgumentInitiator() { - CommandLine.Parse("/"); - CommandLine.Parse("-"); - CommandLine.Parse("--"); + 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]); + } + + /// Validates that quoted arguments can be parsed + [Test] + public void TestParseQuotedOption() { + 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); + } + + /// Validates that null can be parsed + [Test] + public void TestParseNull() { + CommandLine commandLine = CommandLine.Parse(null); + + Assert.AreEqual(0, commandLine.Values.Count); + Assert.AreEqual(0, commandLine.Options.Count); + } + + /// Validates that a single argument without quotes can be parsed + [Test] + 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]); + } + + /// + /// Validates that the parser can handle a quoted argument that's missing + /// the closing quote + /// + [Test] + 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]); + } + + /// + /// Validates that the parser can handle an command line consisting of only spaces + /// + [Test] + public void TestParseSpacesOnly() { + CommandLine commandLine = CommandLine.Parse(" \t "); + + Assert.AreEqual(0, commandLine.Values.Count); + Assert.AreEqual(0, commandLine.Options.Count); } /// @@ -67,49 +251,110 @@ namespace Nuclex.Support.Parsing { /// [Test] public void TestParseMultipleLoneArgumentInitiators() { - CommandLine.Parse("/ // /"); - CommandLine.Parse("- -- -"); - CommandLine.Parse("-- --- --"); + 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); } /// - /// Validates that the parser can handle multiple lone argument initators without - /// a following argument + /// Verifies that the parser correctly handles options with embedded option initiators /// [Test] - public void TestParseArgumentInitiatorsWithInvalidNames() { - CommandLine.Parse("/=:"); - CommandLine.Parse("-/="); - CommandLine.Parse("--:/"); + 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); } /// - /// Validates that the parser can handle an command line consisting of only spaces + /// Validates that arguments and values without spaces inbetween can be parsed /// [Test] - public void TestParseSpacesOnly() { - CommandLine.Parse(" \t "); + 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]); } /// - /// Validates that the parser can handle a quoted argument that's missing - /// the closing quote + /// Validates that options with modifiers at the end of the command line + /// are parsed successfully /// [Test] - public void TestParseQuoteArgumentWithoutClosingQuote() { - CommandLine.Parse("\"Quoted argument"); + 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); } - /// Validates that normal arguments can be parsed + /// + /// Validates that options with values assigned to them are parsed successfully + /// [Test] - public void TestParseOptions() { - CommandLine.Parse("Hello -World /This --Is \"a test\""); + public void TestParseOptionWithAssignment() { + CommandLine commandLine = CommandLine.Parse("-hello:123 -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); } - /// Validates that null can be parsed + /// + /// Validates that options with an empty value at the end of the command line + /// string are parsed successfully + /// [Test] - public void TestParseNull() { - Assert.IsNotNull(CommandLine.Parse(null)); + 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); + } + + /// + /// Verifies that the parser can recognize windows command line options if + /// configured to windows mode + /// + [Test] + 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); + } + + /// + /// Verifies that the parser ignores windows command line options if + /// configured to non-windows mode + /// + [Test] + 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]); } } diff --git a/Source/Parsing/CommandLine.cs b/Source/Parsing/CommandLine.cs index f4b0392..bdb4aa6 100644 --- a/Source/Parsing/CommandLine.cs +++ b/Source/Parsing/CommandLine.cs @@ -20,7 +20,9 @@ License along with this library using System; using System.Collections.Generic; -using System.Text; +using System.IO; + +using Nuclex.Support.Collections; namespace Nuclex.Support.Parsing { @@ -55,7 +57,14 @@ namespace Nuclex.Support.Parsing { /// /// /// - /// Option / Argument + /// Argument + /// + /// Either an option or a loose value (see below) that being specified on + /// the command line + /// + /// + /// + /// Option /// /// Can be specified on the command line and typically alters the behavior /// of the application or changes a setting. For example, '--normalize' or @@ -80,15 +89,66 @@ namespace Nuclex.Support.Parsing { public partial class CommandLine { /// Initializes a new command line - public CommandLine() { } + public CommandLine() { + this.options = new List