diff --git a/Source/Parsing/BrokenCommandLineParser.Test.cs b/Source/Parsing/BrokenCommandLineParser.Test.cs
index be9f45d..1a65557 100644
--- a/Source/Parsing/BrokenCommandLineParser.Test.cs
+++ b/Source/Parsing/BrokenCommandLineParser.Test.cs
@@ -120,7 +120,7 @@ namespace Nuclex.Support.Parsing {
///
/// Tests whether the string constructor recognizes an unfinished argument
- /// (that is, and argument that gets 'nothing' assigned)
+ /// (that is, an argument that gets 'nothing' assigned)
///
[Test]
public void TestStringConstructorWithUnfinishedAssignment() {
@@ -128,6 +128,16 @@ namespace Nuclex.Support.Parsing {
Assert.AreEqual(0, parser.Values.Count);
}
+ ///
+ /// Tests whether the string constructor recognizes an argument with a space before
+ /// its assigned value
+ ///
+ [Test]
+ public void TestStringConstructorWithSpacedAssignment() {
+ CommandLineParser parser = new CommandLineParser("--hello= world");
+ Assert.AreEqual(1, parser.Values.Count);
+ }
+
}
} // namespace Nuclex.Support.Parsing
diff --git a/Source/Parsing/CommandLine.Parser.cs b/Source/Parsing/CommandLine.Parser.cs
index 55f07a9..69ae653 100644
--- a/Source/Parsing/CommandLine.Parser.cs
+++ b/Source/Parsing/CommandLine.Parser.cs
@@ -20,74 +20,236 @@ License along with this library
using System;
using System.Collections.Generic;
+using System.Diagnostics;
namespace Nuclex.Support.Parsing {
partial class CommandLine {
/// Parses command line strings
- private static class Parser {
+ private class Parser {
+
+ /// Initializes a new command line parser
+ private Parser() {
+ this.commandLine = new CommandLine();
+ }
/// Parses a string containing command line arguments
/// String that will be parsed
/// The parsed command line arguments from the string
public static CommandLine Parse(string commandLineString) {
- CommandLine commandLine = new CommandLine();
+ Parser theParser = new Parser();
+ theParser.parse(commandLineString);
+ return theParser.commandLine;
+ }
+
+ ///
+ /// Parses the provided string and adds the parameters found to
+ /// the command line representation
+ ///
+ ///
+ /// String containing the command line arguments that will be parsed
+ ///
+ private void parse(string commandLineString) {
if(commandLineString == null) {
- return commandLine;
+ return;
}
-/*
+ // Walk through the command line character by character and gather
+ // the parameters and values to build the command line representation from
for(int index = 0; index < commandLineString.Length; ) {
- char currentCharacter = commandLineString[index];
- // We ignore whitespaces outside of quoted values
- if(char.IsWhiteSpace(currentCharacter)) {
- continue;
+ // Look for the next non-whitespace character
+ index = StringHelper.IndexNotOfAny(
+ commandLineString, WhitespaceCharacters, index
+ );
+ if(index == -1) {
+ break;
}
- switch(currentCharacter) {
- case '-':
- case '/': {
- parseArgument(commandLine, commandLineString, ref index);
- break;
- }
- case '"': {
- parseQuotedValue(commandLine, commandLineString, ref index);
- break;
- }
- default: {
- parseUnquotedValue(commandLine, commandLineString, ref index);
- break;
- }
- }
+ // 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);
}
-*/
- return null;
}
-/*
- private static void parseArgument(
- CommandLine commandLine, string commandLineString, ref int index
- ) {
+ ///
+ /// Parses a chunk of characters and adds it as an option or a loose value to
+ /// the command line representation we're building
+ ///
+ ///
+ /// String containing the chunk of characters that will be parsed
+ ///
+ /// Index in the string at which to begin parsing
+ /// The number of characters that were consumed
+ private int parseCharacterChunk(string commandLineString, int index) {
+ int startIndex = index;
+
+ char currentCharacter = commandLineString[index];
+ switch(currentCharacter) {
+
+ // Unix style argument using either '-' or "--" as its initiator
+ case '-': {
+ ++index;
+
+ // Does the string end here? Stop parsing.
+ if(index >= commandLineString.Length) {
+ addValue("-");
+ 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);
+ }
+
+ break;
+ }
+
+ // Windows style argument using '/' as its initiator
+ case '/': {
+ ++index;
+ index += parsePotentialOption(commandLineString, startIndex, index);
+ break;
+ }
+
+ // Quoted loose value
+ case '"': {
+ StringSegment value = parseQuotedValue(commandLineString, index);
+ index += value.Count + 1;
+ break;
+ }
+
+ // Unquoted loose value
+ default: {
+ StringSegment value = parseNakedValue(commandLineString, index);
+ index += value.Count;
+ break;
+ }
+
+ }
+
+ return index - startIndex;
}
- private static void parseQuotedValue(
- CommandLine commandLine, string commandLineString, ref int index
+ /// Parses a potential command line option
+ /// String containing the command line arguments
+ ///
+ /// Index of the option's initiator ('-' or '--' or '/')
+ ///
+ ///
+ /// 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
) {
+
+ // If the string ends here this can only be considered as a loose value
+ if(index >= commandLineString.Length) {
+ addValue(commandLineString.Substring(initiatorStartIndex));
+ return 0;
+ }
+
+ // 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;
+ }
+
+ // If the first character of the supposed option is not valid for an option,
+ // 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;
+ }
+ */
+ }
+
+ Console.WriteLine(
+ "Argument name: " + commandLineString.Substring(index, nameEndIndex - index)
+ );
+
+ // TODO: Parse argument value (if provided) here!!
+
+ return nameEndIndex - index;
}
- private static void parseUnquotedValue(
- CommandLine commandLine, string commandLineString, ref int index
- ) {
+ static readonly char[] OptionNameEndingCharacters = new char[] {
+ ' ', '\t', '=', ':', '/', '-', '+', '"'
+ };
+
+ /// 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) {
+ char quoteCharacter = commandLineString[index];
+ ++index;
+
+ int endIndex = commandLineString.IndexOf(quoteCharacter, index);
+ if(endIndex == -1) {
+ endIndex = commandLineString.Length;
+ }
+
+ // 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);
-
- StringSegment argument = new StringSegment(commandLineString, index, endIndex - index);
-
+ if(endIndex == -1) {
+ endIndex = 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 + "'");
+ }
+
+
+ /// 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;
+
}
}
diff --git a/Source/Parsing/CommandLine.Test.cs b/Source/Parsing/CommandLine.Test.cs
index 47222ca..e3ea8fb 100644
--- a/Source/Parsing/CommandLine.Test.cs
+++ b/Source/Parsing/CommandLine.Test.cs
@@ -31,6 +31,74 @@ 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;
+ }
+*/
+ /// Validates that a single argument without quotes can be parsed
+ [Test]
+ public void TestParseSingleNakedArgument() {
+ CommandLine.Parse("Hello");
+ }
+
+ ///
+ /// Validates that the parser can handle a single argument initator without
+ /// a following argument
+ ///
+ [Test]
+ public void TestParseLoneArgumentInitiator() {
+ CommandLine.Parse("/");
+ CommandLine.Parse("-");
+ CommandLine.Parse("--");
+ }
+
+ ///
+ /// Validates that the parser can handle multiple lone argument initators without
+ /// a following argument
+ ///
+ [Test]
+ public void TestParseMultipleLoneArgumentInitiators() {
+ CommandLine.Parse("/ // /");
+ CommandLine.Parse("- -- -");
+ CommandLine.Parse("-- --- --");
+ }
+
+ ///
+ /// Validates that the parser can handle multiple lone argument initators without
+ /// a following argument
+ ///
+ [Test]
+ public void TestParseArgumentInitiatorsWithInvalidNames() {
+ CommandLine.Parse("/=:");
+ CommandLine.Parse("-/=");
+ CommandLine.Parse("--:/");
+ }
+
+ ///
+ /// Validates that the parser can handle an command line consisting of only spaces
+ ///
+ [Test]
+ public void TestParseSpacesOnly() {
+ CommandLine.Parse(" \t ");
+ }
+
+ ///
+ /// Validates that the parser can handle a quoted argument that's missing
+ /// the closing quote
+ ///
+ [Test]
+ public void TestParseQuoteArgumentWithoutClosingQuote() {
+ CommandLine.Parse("\"Quoted argument");
+ }
/// Validates that normal arguments can be parsed
[Test]
diff --git a/Source/Parsing/CommandLine.cs b/Source/Parsing/CommandLine.cs
index c8d6497..f4b0392 100644
--- a/Source/Parsing/CommandLine.cs
+++ b/Source/Parsing/CommandLine.cs
@@ -71,6 +71,11 @@ 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 {