Removed the old command line parser since its replacement is fully functional by now; the CommandLine class now provides a HasArgument() method for convenience; added experimental parsing for pre-split command lines (as provided by .NET's Main() method) and disabled it because it turned out to be a bad idea

git-svn-id: file:///srv/devel/repo-conversion/nusu@130 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
Markus Ewald 2009-04-15 19:54:01 +00:00
parent 47b0039137
commit a2331b95c1
7 changed files with 182 additions and 389 deletions

View File

@ -157,10 +157,6 @@
<Compile Include="Source\Parsing\CommandLine.Test.cs">
<DependentUpon>CommandLine.cs</DependentUpon>
</Compile>
<Compile Include="Source\Parsing\BrokenCommandLineParser.cs" />
<Compile Include="Source\Parsing\BrokenCommandLineParser.Test.cs">
<DependentUpon>BrokenCommandLineParser.cs</DependentUpon>
</Compile>
<Compile Include="Source\Parsing\CommandLine.cs" />
<Compile Include="Source\Parsing\CommandLine.Parser.cs">
<DependentUpon>CommandLine.cs</DependentUpon>

View File

@ -139,10 +139,6 @@
<Compile Include="Source\Parsing\CommandLine.Test.cs">
<DependentUpon>CommandLine.cs</DependentUpon>
</Compile>
<Compile Include="Source\Parsing\BrokenCommandLineParser.cs" />
<Compile Include="Source\Parsing\BrokenCommandLineParser.Test.cs">
<DependentUpon>BrokenCommandLineParser.cs</DependentUpon>
</Compile>
<Compile Include="Source\Parsing\CommandLine.cs" />
<Compile Include="Source\Parsing\CommandLine.Parser.cs">
<DependentUpon>CommandLine.cs</DependentUpon>

View File

@ -1,147 +0,0 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System;
using System.Collections.Generic;
using System.Text;
#if UNITTEST
using NUnit.Framework;
#if ENABLE_BROKEN_COMMAND_LINE_PARSER // Too bugged. 100% test coverage not possible.
namespace Nuclex.Support.Parsing {
/// <summary>Ensures that the command line parser is working properly</summary>
[TestFixture]
public class CommandLineParserTest {
/// <summary>Validates that normal arguments can be parsed</summary>
[Test]
public void TestArrayConstructorWithPlainArguments() {
Assert.IsTrue(
new CommandLineParser(new string[] { "-hello" }).HasArgument("hello"),
"Argument with minus sign is recognized"
);
Assert.IsTrue(
new CommandLineParser(new string[] { "--hello" }).HasArgument("hello"),
"Argument with double minus sign is recognized"
);
Assert.IsTrue(
new CommandLineParser(new string[] { "/hello" }).HasArgument("hello"),
"Argument with slash is recognized"
);
}
/// <summary>Validates that argument assignments are working</summary>
[Test]
public void TestArrayConstructorWithAssignments() {
Assert.AreEqual(
"world",
new CommandLineParser(new string[] { "-hello:world" })["hello"],
"Argument can be assigned with a double colon"
);
Assert.AreEqual(
"world",
new CommandLineParser(new string[] { "-hello=world" })["hello"],
"Argument can be assigned with a equality sign"
);
Assert.AreEqual(
"world",
new CommandLineParser(new string[] { "-hello", "world" })["hello"],
"Argument can be assigned with a space"
);
}
/// <summary>
/// Validates that loosely specified values are recognized by the parser
/// </summary>
[Test]
public void TestArrayConstructorWithLooseValues() {
Assert.IsTrue(
new CommandLineParser(new string[] { "hello" }).Values.Contains("hello"),
"Plain loose value is recognized"
);
Assert.IsTrue(
new CommandLineParser(new string[] { "-hello:world", "foo" }).Values.Contains("foo"),
"Loose value following an assignment is recognized"
);
}
/// <summary>
/// Tests whether the parser can parse the processes current command line if
/// the default constructor is used
/// </summary>
[Test]
public void TestDefaultConstructor() {
new CommandLineParser();
}
/// <summary>
/// Tests whether the string constructor works for simple arguments being
/// specified on the command line
/// </summary>
[Test]
public void TestStringConstructorWithSimpleArguments() {
CommandLineParser parser = new CommandLineParser("argument1 argument2");
Assert.AreEqual("argument1", parser.Values[0]);
Assert.AreEqual("argument2", parser.Values[1]);
}
// TODO: This test fails!!
#if FAILED_TEST
/// <summary>
/// Bullshit
/// </summary>
[Test]
public void TestStringConstructorWithQuotedArguments() {
CommandLineParser parser = new CommandLineParser("\"this is a single argument\"");
Assert.AreEqual("this is a single argument", parser.Values[0]);
}
#endif
/// <summary>
/// Tests whether the string constructor recognizes an unfinished argument
/// (that is, an argument that gets 'nothing' assigned)
/// </summary>
[Test]
public void TestStringConstructorWithUnfinishedAssignment() {
CommandLineParser parser = new CommandLineParser("--hello= --world=");
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
#endif // ENABLE_BROKEN_COMMAND_LINE_PARSER
#endif // UNITTEST

View File

@ -1,185 +0,0 @@
#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
using System.Collections.Specialized;
using System.Text.RegularExpressions;
#if ENABLE_BROKEN_COMMAND_LINE_PARSER // Too bugged. 100% test coverage not possible.
namespace Nuclex.Support.Parsing {
/// <summary>Parses an application's command line</summary>
/// <remarks>
/// <para>
/// Based on an article Richard Lopes published on "The Code Project" at
/// http://www.codeproject.com/csharp/command_line.asp
/// </para>
/// <para>
/// Valid forms for command line arguments: {-|/|--}param[{ |=|:}[{"|'}]value[{"|'}]]
/// </para>
/// <example>
/// <code>
/// -param1 value1
/// --param2
/// /param3:"Test-:-work"
/// /param4=happy
/// -param5 '--=nice=--'
/// </code>
/// </example>
/// </remarks>
public class CommandLineParser {
/// <summary>
/// Initializes a new command line parser using the running program's command line
/// </summary>
public CommandLineParser() : this(System.Environment.CommandLine) { }
/// <summary>Initializes a new command line parser</summary>
/// <param name="arguments">All supplied command line arguments as a single string</param>
public CommandLineParser(string arguments)
: this(arguments.Split(new char[] { ' ', '\t' })) { }
/// <summary>Initializes a new command line parser</summary>
/// <param name="arguments">Arguments that have been passed in the command line</param>
public CommandLineParser(string[] arguments) {
this.arguments = new StringDictionary();
this.values = new StringCollection();
string activeParameter = null;
foreach(string argument in arguments) {
// Look for arguments ('-', '/', '--') with their assignments ('=', ':')
string[] parts = splitter.Split(argument, 3);
switch(parts.Length) {
// Value found without an argument being specified (eg. file name)
case 1: {
if(activeParameter != null) {
if(!this.arguments.ContainsKey(activeParameter)) {
parts[0] = remover.Replace(parts[0], "$1");
this.arguments.Add(activeParameter, parts[0]);
}
activeParameter = null;
} else {
this.values.Add(parts[0]);
}
// Error: No argument is waiting for a value. Skip this argument.
break;
}
// Found an argument with no value assignment
case 2: {
// In case the previous argument is still waiting for a value we need to finish
// it up before switching to the argument we just found.
if(activeParameter != null)
if(!this.arguments.ContainsKey(activeParameter))
this.arguments.Add(activeParameter, null);
// Remember argument to allow for a later value assignment
activeParameter = parts[1];
break;
}
// Found an argument with a proper assignment declaration
case 3: {
// In case the previous argument is still waiting for a value we need to finish
// it up before switching to the argument we just found.
if(activeParameter != null)
if(!this.arguments.ContainsKey(activeParameter))
this.arguments.Add(activeParameter, null);
activeParameter = parts[1];
// Remove any quotes that might be enclosing this argument (",')
if(!this.arguments.ContainsKey(activeParameter)) {
parts[2] = remover.Replace(parts[2], "$1");
this.arguments.Add(activeParameter, parts[2]);
}
activeParameter = null;
break;
}
}
}
// In case the previous argument is still waiting for a value we need to finish
// it up before leaving the parsing method.
if(activeParameter != null) {
if(!this.arguments.ContainsKey(activeParameter)) {
this.arguments.Add(activeParameter, null);
}
}
}
/// <summary>Returns the value of an argument by the argument's name</summary>
/// <param name="argumentName">Name of the argument whose value will be returned</param>
/// <returns>The value of the argument with the specified name</returns>
public string this[string argumentName] {
get { return this.arguments[argumentName]; }
}
/// <summary>
/// Checks whether the specified argument was specified on the command line
/// </summary>
/// <param name="argumentName">Name of the argument to check</param>
/// <returns>True if the specified command was given on the command line</returns>
public bool HasArgument(string argumentName) {
return this.arguments.ContainsKey(argumentName);
}
/// <summary>
/// Any values loosely specified on the command line without being assigned
/// to an argument.
/// </summary>
public StringCollection Values {
get { return this.values; }
}
/// <summary>
/// Regular Expression used to split the arguments and their assigned values
/// </summary>
private static Regex splitter =
new Regex(@"^-{1,2}|^/|=|:", RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Regular Expression used to remove quotations around an argument's value
/// </summary>
private static Regex remover =
new Regex(@"^['""]?(.*?)['""]?$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>Stores the parsed arguments</summary>
private StringDictionary arguments;
/// <summary>
/// Stores any values passed on the command line without assigning an argument
/// </summary>
private StringCollection values;
}
} // namespace Nuclex.Support.Parsing
#endif // ENABLE_BROKEN_COMMAND_LINE_PARSER

View File

@ -34,20 +34,35 @@ namespace Nuclex.Support.Parsing {
/// <param name="windowsMode">Whether the / character initiates an argument</param>
private Parser(bool windowsMode) {
this.windowsMode = windowsMode;
this.commandLine = new CommandLine();
this.arguments = new List<CommandLine.Argument>();
}
/// <summary>Parses a string containing command line arguments</summary>
/// <param name="commandLineString">String that will be parsed</param>
/// <param name="windowsMode">Whether the / character initiates an argument</param>
/// <returns>The parsed command line arguments from the string</returns>
public static CommandLine Parse(string commandLineString, bool windowsMode) {
Console.WriteLine("Parsing '" + commandLineString + "'");
public static List<CommandLine.Argument> Parse(
string commandLineString, bool windowsMode
) {
Parser theParser = new Parser(windowsMode);
theParser.parseFullCommandLine(commandLineString);
return theParser.commandLine;
return theParser.arguments;
}
#if ENABLE_TOKENIZED_COMMAND_LINE_PARSING // don't enable, it's broken!
/// <summary>Parses a string containing command line arguments</summary>
/// <param name="commandLineArguments">Command line tokens that will be parsed</param>
/// <param name="windowsMode">Whether the / character initiates an argument</param>
/// <returns>The parsed command line arguments from the string</returns>
public static List<CommandLine.Argument> Parse(
string[] commandLineArguments, bool windowsMode
) {
Parser theParser = new Parser(windowsMode);
theParser.parseSplitCommandLine(commandLineArguments);
return theParser.arguments;
}
#endif // ENABLE_TOKENIZED_COMMAND_LINE_PARSING
/// <summary>
/// Parses the provided string and adds the parameters found to
/// the command line representation
@ -79,6 +94,31 @@ namespace Nuclex.Support.Parsing {
}
}
#if ENABLE_TOKENIZED_COMMAND_LINE_PARSING // don't enable, it's broken!
/// <summary>
/// Parses the command line from a series pre-split argument tokens
/// </summary>
/// <param name="commandLineParts">Split argument tokens that will be parsed</param>
private void parseSplitCommandLine(string[] commandLineParts) {
if(commandLineParts == null) {
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 < commandLineParts.Length; ++index) {
if(commandLineParts[index] != null) {
int characterIndex = 0;
parseChunk(commandLineParts[index], ref characterIndex);
Debug.Assert(characterIndex == commandLineParts[index].Length);
}
}
}
#endif // ENABLE_TOKENIZED_COMMAND_LINE_PARSING
/// <summary>
/// Parses a chunk of characters and adds it as an option or a loose value to
/// the command line representation we're building
@ -100,7 +140,7 @@ namespace Nuclex.Support.Parsing {
// Does the string end here? Stop parsing.
if(index >= commandLineString.Length) {
this.commandLine.addValue(new StringSegment(commandLineString, startIndex, 1));
addValue(new StringSegment(commandLineString, startIndex, 1));
break;
}
@ -153,12 +193,12 @@ namespace Nuclex.Support.Parsing {
/// </param>
/// <returns>The number of characters consumed</returns>
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
if(index == commandLineString.Length) {
this.commandLine.addValue(
addValue(
new StringSegment(
commandLineString,
initiatorStartIndex,
@ -187,7 +227,7 @@ namespace Nuclex.Support.Parsing {
index = commandLineString.Length;
}
commandLine.addValue(
addValue(
new StringSegment(
commandLineString, initiatorStartIndex, index - initiatorStartIndex
)
@ -211,7 +251,7 @@ namespace Nuclex.Support.Parsing {
/// </param>
/// <param name="index">Index at which the option name ended</param>
private void parsePotentialOptionAssignment(
string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index
string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index
) {
int nameEndIndex = index;
int valueStartIndex;
@ -245,7 +285,7 @@ namespace Nuclex.Support.Parsing {
}
int argumentLength = index - initiatorStartIndex;
this.commandLine.addArgument(
this.arguments.Add(
new Argument(
new StringSegment(commandLineString, initiatorStartIndex, argumentLength),
nameStartIndex, nameEndIndex - nameStartIndex,
@ -264,7 +304,7 @@ namespace Nuclex.Support.Parsing {
/// </param>
/// <param name="index">Index at which the option name ended</param>
private void parseOptionValue(
string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index
string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index
) {
int nameEndIndex = index - 1;
int valueStartIndex, valueEndIndex;
@ -308,7 +348,7 @@ namespace Nuclex.Support.Parsing {
}
int argumentLength = index - initiatorStartIndex;
this.commandLine.addArgument(
this.arguments.Add(
new Argument(
new StringSegment(commandLineString, initiatorStartIndex, argumentLength),
nameStartIndex, nameEndIndex - nameStartIndex,
@ -329,14 +369,14 @@ namespace Nuclex.Support.Parsing {
index = commandLineString.IndexOf(quoteCharacter, valueIndex);
if(index == -1) {
index = commandLineString.Length; // value ends at string end
commandLine.addArgument(
this.arguments.Add(
Argument.ValueOnly(
new StringSegment(commandLineString, startIndex, index - startIndex),
valueIndex, index - valueIndex
)
);
} else { // A closing quote was found
commandLine.addArgument(
this.arguments.Add(
Argument.ValueOnly(
new StringSegment(commandLineString, startIndex, index - startIndex + 1),
valueIndex, index - valueIndex
@ -357,8 +397,14 @@ namespace Nuclex.Support.Parsing {
index = commandLineString.Length;
}
commandLine.addValue(
new StringSegment(commandLineString, startIndex, index - startIndex)
addValue(new StringSegment(commandLineString, startIndex, index - startIndex));
}
/// <summary>Adds a loose value to the command line</summary>
/// <param name="value">Value taht will be added</param>
private void addValue(StringSegment value) {
this.arguments.Add(
Argument.ValueOnly(value, value.Offset, value.Count)
);
}
@ -383,8 +429,8 @@ namespace Nuclex.Support.Parsing {
/// <summary>Characters the parser considers to be whitespace</summary>
private static readonly char[] WhitespaceCharacters = new char[] { ' ', '\t' };
/// <summary>Command line currently being built by the parser</summary>
private CommandLine commandLine;
/// <summary>Argument list being filled by the parser</summary>
private List<CommandLine.Argument> arguments;
/// <summary>Whether the '/' character initiates an argument</summary>
private bool windowsMode;

View File

@ -194,6 +194,14 @@ namespace Nuclex.Support.Parsing {
#endregion // class ArgumentTest
/// <summary>Verifies that the default constructor is working</summary>
[Test]
public void TestDefaultConstructor() {
CommandLine commandLine = new CommandLine();
Assert.AreEqual(0, commandLine.Arguments.Count);
}
/// <summary>
/// Validates that the parser can handle an argument initiator with an
/// assignment that is missing a name
@ -302,7 +310,7 @@ namespace Nuclex.Support.Parsing {
/// <summary>Validates that null can be parsed</summary>
[Test]
public void TestParseNull() {
CommandLine commandLine = CommandLine.Parse(null);
CommandLine commandLine = CommandLine.Parse((string)null);
Assert.AreEqual(0, commandLine.Arguments.Count);
}
@ -558,6 +566,19 @@ namespace Nuclex.Support.Parsing {
Assert.AreEqual("//world", commandLine.Arguments[1].Value);
}
/// <summary>
/// Tests whether the existence of named arguments can be checked
/// </summary>
[Test]
public void TestHasArgument() {
CommandLine test = CommandLine.Parse("/first:x /second:y /second:z third");
Assert.IsTrue(test.HasArgument("first"));
Assert.IsTrue(test.HasArgument("second"));
Assert.IsFalse(test.HasArgument("third"));
Assert.IsFalse(test.HasArgument("fourth"));
}
}
} // namespace Nuclex.Support.Parsing

View File

@ -21,6 +21,7 @@ License along with this library
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Nuclex.Support.Collections;
@ -82,55 +83,120 @@ namespace Nuclex.Support.Parsing {
public partial class CommandLine {
/// <summary>Initializes a new command line</summary>
public CommandLine() {
this.arguments = new List<Argument>();
public CommandLine() : this(new List<Argument>()) { }
/// <summary>Initializes a new command line</summary>
/// <param name="argumentList">List containing the parsed arguments</param>
private CommandLine(List<Argument> argumentList) {
this.arguments = argumentList;
}
#if ENABLE_TOKENIZED_COMMAND_LINE_PARSING // don't enable, it's broken!
/// <summary>Parses the command line arguments from the provided string</summary>
/// <param name="commandLineArguments">Command line tokens that will be parsed</param>
/// <returns>The parsed command line</returns>
public static CommandLine Parse(string[] commandLineArguments) {
bool windowsMode = (Path.DirectorySeparatorChar != '/');
return Parse(commandLineArguments, windowsMode);
}
/// <summary>Parses the command line arguments from the provided string</summary>
/// <param name="commandLineArguments">Command line tokens that will be parsed</param>
/// <param name="windowsMode">Whether the / character initiates an argument</param>
/// <returns>The parsed command line</returns>
public static CommandLine Parse(string[] commandLineArguments, bool windowsMode) {
return new CommandLine(
Parser.Parse(commandLineArguments, windowsMode)
);
}
#endif // ENABLE_TOKENIZED_COMMAND_LINE_PARSING
/// <summary>Parses the command line arguments from the provided string</summary>
/// <param name="commandLineString">String containing the command line arguments</param>
/// <returns>The parsed command line</returns>
/// <remarks>
/// You should always pass Environment.CommandLine to this methods to avoid
/// some problems with the build-in command line tokenizer in .NET
/// (which splits '--test"hello world"/v' into '--testhello world/v')
/// </remarks>
public static CommandLine Parse(string commandLineString) {
bool windowsMode = (Path.DirectorySeparatorChar != '/');
return Parser.Parse(commandLineString, windowsMode);
return Parse(commandLineString, windowsMode);
}
/// <summary>Parses the command line arguments from the provided string</summary>
/// <param name="commandLineString">String containing the command line arguments</param>
/// <param name="windowsMode">Whether the / character initiates an argument</param>
/// <returns>The parsed command line</returns>
/// <remarks>
/// You should always pass Environment.CommandLine to this methods to avoid
/// some problems with the build-in command line tokenizer in .NET
/// (which splits '--test"hello world"/v' into '--testhello world/v')
/// </remarks>
public static CommandLine Parse(string commandLineString, bool windowsMode) {
return Parser.Parse(commandLineString, windowsMode);
}
#region To Be Removed
/// <summary>Adds a loose value to the command line</summary>
/// <param name="value">Value taht will be added</param>
internal void addValue(StringSegment value) {
/*
Console.WriteLine("Discovered loose value: '" + value.ToString() + "'");
*/
this.arguments.Add(
Argument.ValueOnly(value, value.Offset, value.Count)
return new CommandLine(
Parser.Parse(commandLineString, windowsMode)
);
}
/// <summary>Adds an argument to the command line</summary>
/// <param name="argument">Argument that will be added</param>
internal void addArgument(Argument argument) {
/*
Console.WriteLine("Discovered option: '" + argument.Raw.ToString() + "'");
Console.WriteLine(" Name: '" + argument.Name + "'");
if(argument.Value != null) {
Console.WriteLine(" Value: '" + argument.Value + "'");
}
*/
this.arguments.Add(argument);
/// <summary>Returns whether an argument with the specified name exists</summary>
/// <param name="name">Name of the argument whose existence will be checked</param>
/// <returns>True if an argument with the specified name exists</returns>
public bool HasArgument(string name) {
return (indexOfArgument(name) != -1);
}
#if false
/// <summary>Retrieves the value of the specified argument</summary>
/// <param name="name">Name of the argument whose value will be retrieved</param>
/// <returns>The value of the specified argument</returns>
public string GetValue(string name) {
int index = indexOfArgument(name);
if(index == -1) {
return null;
}
// Does this argument have a value?
Argument argument = this.arguments[index];
if(argument.Value != null) {
return argument.Value;
} else { // No, it might be a spaced argument
// See if anything more follows this argument
++index;
if(index < this.arguments.Count) {
// If something follows the argument, and it is not an option of its own,
// use its value as the value for the preceding argument
argument = this.arguments[index];
if(argument.Name == null) {
return argument.Value;
}
}
}
// No argument found
return null;
}
#endif
/// <summary>Retrieves the index of the argument with the specified name</summary>
/// <param name="name">Name of the argument whose index will be returned</param>
/// <returns>
/// The index of the indicated argument of -1 if no argument with that name exists
/// </returns>
private int indexOfArgument(string name) {
for(int index = 0; index < this.arguments.Count; ++index) {
if(this.arguments[index].Name == name) {
return index;
}
}
return -1;
}
#endregion // To Be Removed
/// <summary>Options that were specified on the command line</summary>
public IList<Argument> Arguments {