#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.Collections.Generic;
namespace Nuclex.Support.Parsing {
partial class CommandLine {
/// Parses command line strings
private class Parser {
/// Initializes a new command line parser
/// Whether the / character initiates an argument
private Parser(bool windowsMode) {
this.windowsMode = windowsMode;
this.arguments = new List();
}
/// 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 List Parse(
string commandLineString, bool windowsMode
) {
Parser theParser = new Parser(windowsMode);
theParser.parseFullCommandLine(commandLineString);
return theParser.arguments;
}
///
/// 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 parseFullCommandLine(string commandLineString) {
if(commandLineString == 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 < commandLineString.Length; ) {
// Look for the next non-whitespace character
index = StringHelper.IndexNotOfAny(
commandLineString, WhitespaceCharacters, index
);
if(index == -1) {
break;
}
// Parse the chunk of characters at this location and advance the index
// to the next location after the chunk of characters
parseChunk(commandLineString, ref 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 void parseChunk(string commandLineString, ref 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(new StringSegment(commandLineString, startIndex, 1));
break;
}
// Does another '-' follow? Might be a unix style option or a loose "--"
if(commandLineString[index] == '-') {
++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;
parsePotentialOption(commandLineString, startIndex, ref index);
break;
}
// Quoted loose value
case '"': {
parseQuotedValue(commandLineString, ref index);
break;
}
// Unquoted loose value
default: {
parseNakedValue(commandLineString, ref index);
break;
}
}
}
/// 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 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(
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
if(commandLineString[index] != commandLineString[initiatorStartIndex]) {
index = commandLineString.IndexOfAny(NameEndingCharacters, nameStartIndex);
if(index == -1) {
index = commandLineString.Length;
}
}
// 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((index == nameStartIndex)/* && !isAssignmentCharacter(commandLineString[index])*/) {
index = commandLineString.IndexOfAny(WhitespaceCharacters, index);
if(index == -1) {
index = commandLineString.Length;
}
addValue(
new StringSegment(
commandLineString, initiatorStartIndex, index - initiatorStartIndex
)
);
return;
}
parsePotentialOptionAssignment(
commandLineString, initiatorStartIndex, nameStartIndex, ref index
);
}
/// 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 parsePotentialOptionAssignment(
string commandLineString, int initiatorStartIndex, int nameStartIndex, ref int index
) {
int nameEndIndex = index;
int valueStartIndex;
int valueEndIndex;
// See if this is an assignment character. If it is, the assigned value
// should follow to the right.
bool isAssignment =
(index < commandLineString.Length) &&
isAssignmentCharacter(commandLineString[index]);
// If it's an assignment, we can proceed parsing the assigned value
if(isAssignment) {
++index;
parseOptionValue(commandLineString, initiatorStartIndex, nameStartIndex, ref index);
return;
} else { // No, it's an option name without an assignment
bool isModifier =
(commandLineString[index - 1] == '+') ||
(commandLineString[index - 1] == '-');
if(isModifier) {
valueStartIndex = index - 1;
valueEndIndex = index;
--nameEndIndex;
} else {
valueStartIndex = -1;
valueEndIndex = -1;
}
}
int argumentLength = index - initiatorStartIndex;
this.arguments.Add(
new Argument(
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 - 1;
int valueStartIndex, valueEndIndex;
// Does the string end after the suspected assignment character?
bool argumentEndReached = (index == commandLineString.Length);
if(argumentEndReached) {
valueStartIndex = -1;
valueEndIndex = -1;
} else {
char nextCharacter = commandLineString[index];
// Is this a quoted assignment
if(nextCharacter == '"') {
++index;
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
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.arguments.Add(
new Argument(
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
private void parseQuotedValue(string commandLineString, ref int index) {
int startIndex = index;
char quoteCharacter = commandLineString[index];
int valueIndex = startIndex + 1;
// Search for the closing quote
index = commandLineString.IndexOf(quoteCharacter, valueIndex);
if(index == -1) {
index = commandLineString.Length; // value ends at string end
this.arguments.Add(
Argument.ValueOnly(
new StringSegment(commandLineString, startIndex, index - startIndex),
valueIndex, index - valueIndex
)
);
} else { // A closing quote was found
this.arguments.Add(
Argument.ValueOnly(
new StringSegment(commandLineString, startIndex, index - startIndex + 1),
valueIndex, index - valueIndex
)
);
++index; // Skip the closing quote
}
}
/// Parses a plain, unquoted value from the input string
/// String containing the value to be parsed
/// Index at which the value begins
private void parseNakedValue(string commandLineString, ref int index) {
int startIndex = index;
index = commandLineString.IndexOfAny(WhitespaceCharacters, index);
if(index == -1) {
index = commandLineString.Length;
}
addValue(new StringSegment(commandLineString, startIndex, index - startIndex));
}
/// Adds a loose value to the command line
/// Value taht will be added
private void addValue(StringSegment value) {
this.arguments.Add(
Argument.ValueOnly(value, value.Offset, value.Count)
);
}
///
/// Determines whether the specified character indicates an assignment
///
///
/// Character that will be checked for being an assignemnt
///
///
/// True if the specified character indicated an assignment, otherwise false
///
private static bool isAssignmentCharacter(char character) {
return (character == ':') || (character == '=');
}
/// Characters which end an option name when they are encountered
private static readonly char[] NameEndingCharacters = new char[] {
' ', '\t', '=', ':', '"'
};
/// Characters the parser considers to be whitespace
private static readonly char[] WhitespaceCharacters = new char[] { ' ', '\t' };
/// Argument list being filled by the parser
private List arguments;
/// Whether the '/' character initiates an argument
private bool windowsMode;
}
}
} // namespace Nuclex.Support.Parsing