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 {