/****************************************************************************
**
** 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 commandLineOptionList = new List ();
Dictionary nameHash = new Dictionary();
Dictionary> optionValuesHash = new Dictionary>();
List optionNames = new List();
List positionalArgumentList = new List();
List unknownOptionNames = new List();
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 PositionalArguments
{
get
{
CheckParsed("PositionalArguments");
return positionalArgumentList;
}
}
public IEnumerable OptionNames
{
get
{
CheckParsed("OptionNames");
return optionNames;
}
}
public IEnumerable UnknownOptionNames
{
get
{
CheckParsed("UnknownOptionNames");
return unknownOptionNames;
}
}
IEnumerable Aliases(string optionName)
{
int optionIndex;
if (!nameHash.TryGetValue(optionName, out optionIndex)) {
return new List();
}
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 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 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());
optionValuesHash[optionOffset].Add(argumentEnumerator.Current);
} else {
if (!optionValuesHash.ContainsKey(optionOffset))
optionValuesHash.Add(optionOffset, new List());
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 TokenizeArgs(string commandLine, IVSMacroExpander macros, string execName = "")
{
List arguments = new List();
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 args = null;
try {
args = TokenizeArgs(commandLine, macros, execName);
} catch {
return false;
}
return Parse(args);
}
public bool Parse(IEnumerable 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());
}
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());
}
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 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();
}
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 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 names, string valueName = "")
{
Names = names;
ValueName = valueName;
Flags = 0;
}
public Option(Option other)
{
Names = other.Names;
ValueName = other.ValueName;
Flags = other.Flags;
}
public IEnumerable 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);
}
}
}