#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