Wrote more unit tests for the new command line parser; actually implemented a good part of the new command line parser; wrote one more test to show that the old command line parser is broken
git-svn-id: file:///srv/devel/repo-conversion/nusu@106 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
parent
f0d76f988f
commit
ffba112786
|
@ -120,7 +120,7 @@ namespace Nuclex.Support.Parsing {
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests whether the string constructor recognizes an unfinished argument
|
/// 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)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void TestStringConstructorWithUnfinishedAssignment() {
|
public void TestStringConstructorWithUnfinishedAssignment() {
|
||||||
|
@ -128,6 +128,16 @@ namespace Nuclex.Support.Parsing {
|
||||||
Assert.AreEqual(0, parser.Values.Count);
|
Assert.AreEqual(0, parser.Values.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests whether the string constructor recognizes an argument with a space before
|
||||||
|
/// its assigned value
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestStringConstructorWithSpacedAssignment() {
|
||||||
|
CommandLineParser parser = new CommandLineParser("--hello= world");
|
||||||
|
Assert.AreEqual(1, parser.Values.Count);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Nuclex.Support.Parsing
|
} // namespace Nuclex.Support.Parsing
|
||||||
|
|
|
@ -20,74 +20,236 @@ License along with this library
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace Nuclex.Support.Parsing {
|
namespace Nuclex.Support.Parsing {
|
||||||
|
|
||||||
partial class CommandLine {
|
partial class CommandLine {
|
||||||
|
|
||||||
/// <summary>Parses command line strings</summary>
|
/// <summary>Parses command line strings</summary>
|
||||||
private static class Parser {
|
private class Parser {
|
||||||
|
|
||||||
|
/// <summary>Initializes a new command line parser</summary>
|
||||||
|
private Parser() {
|
||||||
|
this.commandLine = new CommandLine();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Parses a string containing command line arguments</summary>
|
/// <summary>Parses a string containing command line arguments</summary>
|
||||||
/// <param name="commandLineString">String that will be parsed</param>
|
/// <param name="commandLineString">String that will be parsed</param>
|
||||||
/// <returns>The parsed command line arguments from the string</returns>
|
/// <returns>The parsed command line arguments from the string</returns>
|
||||||
public static CommandLine Parse(string commandLineString) {
|
public static CommandLine Parse(string commandLineString) {
|
||||||
CommandLine commandLine = new CommandLine();
|
Parser theParser = new Parser();
|
||||||
|
theParser.parse(commandLineString);
|
||||||
|
return theParser.commandLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses the provided string and adds the parameters found to
|
||||||
|
/// the command line representation
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandLineString">
|
||||||
|
/// String containing the command line arguments that will be parsed
|
||||||
|
/// </param>
|
||||||
|
private void parse(string commandLineString) {
|
||||||
if(commandLineString == null) {
|
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; ) {
|
for(int index = 0; index < commandLineString.Length; ) {
|
||||||
char currentCharacter = commandLineString[index];
|
|
||||||
|
|
||||||
// We ignore whitespaces outside of quoted values
|
// Look for the next non-whitespace character
|
||||||
if(char.IsWhiteSpace(currentCharacter)) {
|
index = StringHelper.IndexNotOfAny(
|
||||||
continue;
|
commandLineString, WhitespaceCharacters, index
|
||||||
|
);
|
||||||
|
if(index == -1) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(currentCharacter) {
|
// Parse the chunk of characters at this location and advance the index
|
||||||
case '-':
|
// to the next location after the chunk of characters
|
||||||
case '/': {
|
index += parseCharacterChunk(commandLineString, index);
|
||||||
parseArgument(commandLine, commandLineString, ref index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '"': {
|
|
||||||
parseQuotedValue(commandLine, commandLineString, ref index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
parseUnquotedValue(commandLine, commandLineString, ref index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/// <summary>
|
||||||
private static void parseArgument(
|
/// Parses a chunk of characters and adds it as an option or a loose value to
|
||||||
CommandLine commandLine, string commandLineString, ref int index
|
/// the command line representation we're building
|
||||||
) {
|
/// </summary>
|
||||||
|
/// <param name="commandLineString">
|
||||||
|
/// String containing the chunk of characters that will be parsed
|
||||||
|
/// </param>
|
||||||
|
/// <param name="index">Index in the string at which to begin parsing</param>
|
||||||
|
/// <returns>The number of characters that were consumed</returns>
|
||||||
|
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(
|
/// <summary>Parses a potential command line option</summary>
|
||||||
CommandLine commandLine, string commandLineString, ref int index
|
/// <param name="commandLineString">String containing the command line arguments</param>
|
||||||
|
/// <param name="initiatorStartIndex">
|
||||||
|
/// Index of the option's initiator ('-' or '--' or '/')
|
||||||
|
/// </param>
|
||||||
|
/// <param name="index">
|
||||||
|
/// Index at which the option name is supposed start (if it's an actual option)
|
||||||
|
/// </param>
|
||||||
|
/// <returns>The number of characters consumed</returns>
|
||||||
|
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(
|
static readonly char[] OptionNameEndingCharacters = new char[] {
|
||||||
CommandLine commandLine, string commandLineString, ref int index
|
' ', '\t', '=', ':', '/', '-', '+', '"'
|
||||||
) {
|
};
|
||||||
|
|
||||||
|
/// <summary>Parses a quoted value from the input string</summary>
|
||||||
|
/// <param name="commandLineString">String the quoted value is parsed from</param>
|
||||||
|
/// <param name="index">Index at which the quoted value begins</param>
|
||||||
|
/// <returns>A string segment containing the parsed quoted value</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// The returned string segment does not include the quotes.
|
||||||
|
/// </remarks>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses a plain, unquoted value from the input string</summary>
|
||||||
|
/// <param name="commandLineString">String containing the value to be parsed</param>
|
||||||
|
/// <param name="index">Index at which the value begins</param>
|
||||||
|
/// <returns>A string segment containing the parsed value</returns>
|
||||||
|
private static StringSegment parseNakedValue(string commandLineString, int index) {
|
||||||
int endIndex = commandLineString.IndexOfAny(WhitespaceCharacters, index);
|
int endIndex = commandLineString.IndexOfAny(WhitespaceCharacters, index);
|
||||||
|
if(endIndex == -1) {
|
||||||
|
endIndex = commandLineString.Length;
|
||||||
|
}
|
||||||
|
|
||||||
StringSegment argument = new StringSegment(commandLineString, index, endIndex - index);
|
return new StringSegment(commandLineString, index, endIndex - index);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the specified character is valid as the first character
|
||||||
|
/// in an option
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="character">Character that will be tested for validity</param>
|
||||||
|
/// <returns>True if the character is valid as the first character in an option</returns>
|
||||||
|
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 + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Characters the parser considers to be whitespace</summary>
|
||||||
private static readonly char[] WhitespaceCharacters = new char[] { ' ', '\t' };
|
private static readonly char[] WhitespaceCharacters = new char[] { ' ', '\t' };
|
||||||
*/
|
|
||||||
|
/// <summary>Command line currently being built by the parser</summary>
|
||||||
|
private CommandLine commandLine;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,74 @@ namespace Nuclex.Support.Parsing {
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class CommandLineTest {
|
public class CommandLineTest {
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct CommandLine {
|
||||||
|
[Option]
|
||||||
|
bool? Option;
|
||||||
|
[Option]
|
||||||
|
int? Width;
|
||||||
|
[Option]
|
||||||
|
TypeCode Code;
|
||||||
|
[Values]
|
||||||
|
string[] Values;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/// <summary>Validates that a single argument without quotes can be parsed</summary>
|
||||||
|
[Test]
|
||||||
|
public void TestParseSingleNakedArgument() {
|
||||||
|
CommandLine.Parse("Hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the parser can handle a single argument initator without
|
||||||
|
/// a following argument
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestParseLoneArgumentInitiator() {
|
||||||
|
CommandLine.Parse("/");
|
||||||
|
CommandLine.Parse("-");
|
||||||
|
CommandLine.Parse("--");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the parser can handle multiple lone argument initators without
|
||||||
|
/// a following argument
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestParseMultipleLoneArgumentInitiators() {
|
||||||
|
CommandLine.Parse("/ // /");
|
||||||
|
CommandLine.Parse("- -- -");
|
||||||
|
CommandLine.Parse("-- --- --");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the parser can handle multiple lone argument initators without
|
||||||
|
/// a following argument
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestParseArgumentInitiatorsWithInvalidNames() {
|
||||||
|
CommandLine.Parse("/=:");
|
||||||
|
CommandLine.Parse("-/=");
|
||||||
|
CommandLine.Parse("--:/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the parser can handle an command line consisting of only spaces
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestParseSpacesOnly() {
|
||||||
|
CommandLine.Parse(" \t ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the parser can handle a quoted argument that's missing
|
||||||
|
/// the closing quote
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestParseQuoteArgumentWithoutClosingQuote() {
|
||||||
|
CommandLine.Parse("\"Quoted argument");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Validates that normal arguments can be parsed</summary>
|
/// <summary>Validates that normal arguments can be parsed</summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void TestParseOptions() {
|
public void TestParseOptions() {
|
||||||
|
|
|
@ -71,6 +71,11 @@ namespace Nuclex.Support.Parsing {
|
||||||
/// </item>
|
/// </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// 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.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public partial class CommandLine {
|
public partial class CommandLine {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user