/**************************************************************************** ** ** Copyright (C) 2017 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt VS Tools. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.IO; namespace QtVsTools.Core.CommandLine { using IVSMacroExpander = QtMsBuild.IVSMacroExpander; public class Parser { List<Option> commandLineOptionList = new List<Option>(); Dictionary<string, int> nameHash = new Dictionary<string, int>(); Dictionary<int, List<string>> optionValuesHash = new Dictionary<int, List<string>>(); List<string> optionNames = new List<string>(); List<string> positionalArgumentList = new List<string>(); List<string> unknownOptionNames = new List<string>(); bool needsParsing = true; public enum SingleDashWordOptionMode { ParseAsCompactedShortOptions = 0, ParseAsLongOptions = 1 } SingleDashWordOptionMode singleDashWordOptionMode = 0; public enum OptionsAfterPositionalArgumentsMode { ParseAsOptions = 0, ParseAsPositionalArguments = 1 } OptionsAfterPositionalArgumentsMode optionsAfterPositionalArgumentsMode = 0; public Parser() { } public string ApplicationDescription { get; set; } public string ErrorText { get { throw new NotImplementedException(); } } public string HelpText { get { throw new NotImplementedException(); } } public IEnumerable<string> PositionalArguments { get { CheckParsed("PositionalArguments"); return positionalArgumentList; } } public IEnumerable<string> OptionNames { get { CheckParsed("OptionNames"); return optionNames; } } public IEnumerable<string> UnknownOptionNames { get { CheckParsed("UnknownOptionNames"); return unknownOptionNames; } } IEnumerable<string> Aliases(string optionName) { int optionIndex; if (!nameHash.TryGetValue(optionName, out optionIndex)) { return new List<string>(); } return commandLineOptionList[optionIndex].Names; } public void SetSingleDashWordOptionMode(SingleDashWordOptionMode singleDashWordOptionMode) { this.singleDashWordOptionMode = singleDashWordOptionMode; } public void SetOptionsAfterPositionalArgumentsMode( OptionsAfterPositionalArgumentsMode parsingMode) { this.optionsAfterPositionalArgumentsMode = parsingMode; } public bool AddOption(Option option) { if (option.Names.Any()) { foreach (var name in option.Names) { if (nameHash.ContainsKey(name)) return false; } commandLineOptionList.Add(option); int offset = commandLineOptionList.Count() - 1; foreach (var name in option.Names) nameHash.Add(name, offset); return true; } return false; } public bool AddOptions(IEnumerable<Option> options) { bool result = true; foreach (var option in options) result &= AddOption(option); return result; } public Option AddVersionOption() { Option opt = new Option(new[] { "v", "version" }); AddOption(opt); return opt; } public Option AddHelpOption() { Option opt = new Option(new[] { "?", "h", "help" }); AddOption(opt); return opt; } public void AddPositionalArgument(string name, string description, string syntax) { throw new NotImplementedException(); } public void ClearPositionalArguments() { throw new NotImplementedException(); } bool RegisterFoundOption(string optionName) { if (nameHash.ContainsKey(optionName)) { optionNames.Add(optionName); return true; } else { unknownOptionNames.Add(optionName); return false; } } bool ParseOptionValue(string optionName, string argument, IEnumerator<string> argumentEnumerator, ref bool atEnd) { const char assignChar = '='; int optionOffset; if (nameHash.TryGetValue(optionName, out optionOffset)) { int assignPos = argument.IndexOf(assignChar); bool withValue = !string.IsNullOrEmpty( commandLineOptionList[optionOffset].ValueName); if (withValue) { if (assignPos == -1) { if (atEnd = (!argumentEnumerator.MoveNext())) { return false; } if (!optionValuesHash.ContainsKey(optionOffset)) optionValuesHash.Add(optionOffset, new List<string>()); optionValuesHash[optionOffset].Add(argumentEnumerator.Current); } else { if (!optionValuesHash.ContainsKey(optionOffset)) optionValuesHash.Add(optionOffset, new List<string>()); optionValuesHash[optionOffset].Add(argument.Substring(assignPos + 1)); } } else { if (assignPos != -1) { return false; } } } return true; } void CheckParsed(string method) { if (needsParsing) Trace.TraceWarning("CommandLineParser: Parse() before {0}", method); } List<string> TokenizeArgs(string commandLine, IVSMacroExpander macros, string execName = "") { List<string> arguments = new List<string>(); StringBuilder arg = new StringBuilder(); bool foundExec = string.IsNullOrEmpty(execName); foreach (Match token in Lexer.Tokenize(commandLine + " ")) { // Additional " " ensures loop will always end with whitespace processing if (!foundExec) { if (!token.TokenText() .EndsWith(execName, StringComparison.InvariantCultureIgnoreCase)) { continue; } foundExec = true; } var tokenType = token.TokenType(); if (tokenType == Token.Whitespace || tokenType == Token.Newline) { // This will always run at the end of the loop if (arg.Length > 0) { var argData = arg.ToString(); arg.Clear(); if (argData.StartsWith("@")) { var workingDir = macros.ExpandString("$(MSBuildProjectDirectory)"); var optFilePath = macros.ExpandString(argData.Substring(1)); string[] additionalArgs = File.ReadAllLines( Path.Combine(workingDir, optFilePath)); if (additionalArgs != null) { var additionalArgsString = string.Join(" ", additionalArgs .Select(x => "\"" + x.Replace("\"", "\\\"") + "\"")); arguments.AddRange(TokenizeArgs(additionalArgsString, macros)); } } else { arguments.Add(argData); } } if (tokenType == Token.Newline) break; } else { arg.Append(token.TokenText()); } } return arguments; } public bool Parse(string commandLine, IVSMacroExpander macros, string execName) { List<string> args = null; try { args = TokenizeArgs(commandLine, macros, execName); } catch { return false; } return Parse(args); } public bool Parse(IEnumerable<string> args) { needsParsing = false; bool error = false; const string doubleDashString = "--"; const char dashChar = '-'; const char assignChar = '='; bool forcePositional = false; positionalArgumentList.Clear(); optionNames.Clear(); unknownOptionNames.Clear(); optionValuesHash.Clear(); if (args == null || !args.Any()) { return false; } var argumentIterator = args.GetEnumerator(); bool atEnd = false; while (!atEnd && argumentIterator.MoveNext()) { var argument = argumentIterator.Current; if (forcePositional) { positionalArgumentList.Add(argument); } else if (argument.StartsWith(doubleDashString)) { if (argument.Length > 2) { var optionName = argument.Substring(2).Split(new char[] { assignChar })[0]; if (RegisterFoundOption(optionName)) { if (!ParseOptionValue( optionName, argument, argumentIterator, ref atEnd)) { error = true; } } else { error = true; } } else { forcePositional = true; } } else if (argument.StartsWith(dashChar.ToString())) { if (argument.Length == 1) { // single dash ("stdin") positionalArgumentList.Add(argument); continue; } string optionName = ""; switch (singleDashWordOptionMode) { case SingleDashWordOptionMode.ParseAsCompactedShortOptions: bool valueFound = false; for (int pos = 1; pos < argument.Length; ++pos) { optionName = argument.Substring(pos, 1); if (!RegisterFoundOption(optionName)) { error = true; } else { int optionOffset; Trace.Assert(nameHash.TryGetValue( optionName, out optionOffset)); bool withValue = !string.IsNullOrEmpty( commandLineOptionList[optionOffset].ValueName); if (withValue) { if (pos + 1 < argument.Length) { if (argument[pos + 1] == assignChar) ++pos; if (!optionValuesHash.ContainsKey(optionOffset)) { optionValuesHash.Add( optionOffset, new List<string>()); } optionValuesHash[optionOffset].Add( argument.Substring(pos + 1)); valueFound = true; } break; } if (pos + 1 < argument.Length && argument[pos + 1] == assignChar) { break; } } } if (!valueFound && !ParseOptionValue( optionName, argument, argumentIterator, ref atEnd)) { error = true; } break; case SingleDashWordOptionMode.ParseAsLongOptions: if (argument.Length > 2) { string possibleShortOptionStyleName = argument.Substring(1, 1); int shortOptionIdx; if (nameHash.TryGetValue( possibleShortOptionStyleName, out shortOptionIdx)) { var arg = commandLineOptionList[shortOptionIdx]; if ((arg.Flags & Option.Flag.ShortOptionStyle) != 0) { RegisterFoundOption(possibleShortOptionStyleName); if (!optionValuesHash.ContainsKey(shortOptionIdx)) { optionValuesHash.Add( shortOptionIdx, new List<string>()); } optionValuesHash[shortOptionIdx].Add( argument.Substring(2)); break; } } } optionName = argument.Substring(1).Split(new char[] { assignChar })[0]; if (RegisterFoundOption(optionName)) { if (!ParseOptionValue( optionName, argument, argumentIterator, ref atEnd)) { error = true; } } else { error = true; } break; } } else { positionalArgumentList.Add(argument); if (optionsAfterPositionalArgumentsMode == OptionsAfterPositionalArgumentsMode.ParseAsPositionalArguments) { forcePositional = true; } } } return !error; } public bool IsSet(string name) { CheckParsed("IsSet"); if (optionNames.Contains(name)) return true; var aliases = Aliases(name); foreach (var optionName in optionNames) { if (aliases.Contains(optionName)) return true; } return false; } public string Value(string optionName) { CheckParsed("Value"); var valueList = Values(optionName); if (valueList.Any()) return valueList.Last(); return ""; } public IEnumerable<string> Values(string optionName) { CheckParsed("Values"); int optionOffset; if (nameHash.TryGetValue(optionName, out optionOffset)) { var values = optionValuesHash[optionOffset]; return values; } Trace.TraceWarning("QCommandLineParser: option not defined: \"{0}\"", optionName); return new List<string>(); } public bool IsSet(Option option) { return option.Names.Any() && IsSet(option.Names.First()); } public string Value(Option option) { return Value(option.Names.FirstOrDefault()); } public IEnumerable<string> Values(Option option) { return Values(option.Names.FirstOrDefault()); } } public class Option { [Flags] public enum Flag { HiddenFromHelp = 0x1, ShortOptionStyle = 0x2 } public Option(string name, string valueName = "") { Names = new[] { name }; ValueName = valueName; Flags = 0; } public Option(IEnumerable<string> names, string valueName = "") { Names = names; ValueName = valueName; Flags = 0; } public Option(Option other) { Names = other.Names; ValueName = other.ValueName; Flags = other.Flags; } public IEnumerable<string> Names { get; private set; } public string ValueName { get; set; } public Flag Flags { get; set; } public override string ToString() { return Names.Last(); } } enum Token { Unknown = 0, Newline = 1, Unquoted = 2, Quoted = 3, Whitespace = 4 }; static class Lexer { static Regex lexer = new Regex( /* Newline */ @"(\n)" + /* Unquoted */ @"|((?:(?:[^\s\""])|(?:(?<=\\)\""))+)" + /* Quoted */ @"|(?:\""((?:(?:[^\""])|(?:(?<=\\)\""))+)\"")" + /* Whitespace */ @"|(\s+)"); public static Token TokenType(this Match token) { for (int i = 1; i < token.Groups.Count; i++) { if (!string.IsNullOrEmpty(token.Groups[i].Value)) return (Token)i; } return Token.Unknown; } public static string TokenText(this Match token) { Token t = TokenType(token); if (t != Token.Unknown) return token.Groups[(int)t].Value.Replace("\\\"", "\""); return ""; } public static MatchCollection Tokenize(string commandLine) { return lexer.Matches(commandLine); } } }