// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AutomationTool { /// /// Exception class thrown due to type and syntax errors in condition expressions /// class ConditionException : Exception { /// /// Constructor; formats the exception message with the given String.Format() style parameters. /// /// Formatting string, in String.Format syntax /// Optional arguments for the string public ConditionException(string Format, params object[] Args) : base(String.Format(Format, Args)) { } } /// /// Class to evaluate condition expressions in build scripts, following this grammar: /// /// or-expression ::= and-expression /// | or-expression "Or" and-expression; /// /// and-expression ::= comparison /// | and-expression "And" comparison; /// /// comparison ::= scalar /// | scalar "==" scalar /// | scalar "!=" scalar /// | scalar "<" scalar /// | scalar "<=" scalar; /// | scalar ">" scalar /// | scalar ">=" scalar; /// /// scalar ::= "(" or-expression ")" /// | "!" scalar /// | "Exists" "(" scalar ")" /// | "HasTrailingSlash" "(" scalar ")" /// | string /// | identifier; /// /// string ::= any sequence of characters terminated by single quotes (') or double quotes ("). Not escaped. /// identifier ::= any sequence of letters, digits, or underscore characters. /// /// The type of each subexpression is always a scalar, which are converted to expression-specific types (eg. booleans, integers) as required. /// Scalar values are case-insensitive strings. The identifier 'true' and the strings "true" and "True" are all identical scalars. /// static class Condition { /// /// Sentinel added to the end of a sequence of tokens. /// const string EndToken = ""; /// /// Evaluates the given string as a condition. Throws a ConditionException on a type or syntax error. /// /// The condition text /// The result of evaluating the condition public static bool Evaluate(string Text) { List Tokens = new List(); Tokenize(Text, Tokens); bool bResult = true; if(Tokens.Count > 1) { int Idx = 0; string Result = EvaluateOr(Tokens, ref Idx); if(Tokens[Idx] != EndToken) { throw new ConditionException("Garbage after expression: {0}", String.Join("", Tokens.Skip(Idx))); } bResult = CoerceToBool(Result); } return bResult; } /// /// Evaluates an "or-expression" production. /// /// List of tokens in the expression /// Current position in the token stream. Will be incremented as tokens are consumed. /// A scalar representing the result of evaluating the expression. static string EvaluateOr(List Tokens, ref int Idx) { // Or Or... string Result = EvaluateAnd(Tokens, ref Idx); while(String.Compare(Tokens[Idx], "Or", true) == 0) { // Evaluate this condition. We use a binary OR here, because we want to parse everything rather than short-circuit it. Idx++; string Lhs = Result; string Rhs = EvaluateAnd(Tokens, ref Idx); Result = (CoerceToBool(Lhs) | CoerceToBool(Rhs))? "true" : "false"; } return Result; } /// /// Evaluates an "and-expression" production. /// /// List of tokens in the expression /// Current position in the token stream. Will be incremented as tokens are consumed. /// A scalar representing the result of evaluating the expression. static string EvaluateAnd(List Tokens, ref int Idx) { // And And... string Result = EvaluateComparison(Tokens, ref Idx); while(String.Compare(Tokens[Idx], "And", true) == 0) { // Evaluate this condition. We use a binary AND here, because we want to parse everything rather than short-circuit it. Idx++; string Lhs = Result; string Rhs = EvaluateComparison(Tokens, ref Idx); Result = (CoerceToBool(Lhs) & CoerceToBool(Rhs))? "true" : "false"; } return Result; } /// /// Evaluates a "comparison" production. /// /// List of tokens in the expression /// Current position in the token stream. Will be incremented as tokens are consumed. /// The result of evaluating the expression static string EvaluateComparison(List Tokens, ref int Idx) { // scalar // scalar == scalar // scalar != scalar // scalar < scalar // scalar <= scalar // scalar > scalar // scalar >= scalar string Result = EvaluateScalar(Tokens, ref Idx); if(Tokens[Idx] == "==") { // Compare two scalars for equality Idx++; string Lhs = Result; string Rhs = EvaluateScalar(Tokens, ref Idx); Result = (String.Compare(Lhs, Rhs, true) == 0)? "true" : "false"; } else if(Tokens[Idx] == "!=") { // Compare two scalars for inequality Idx++; string Lhs = Result; string Rhs = EvaluateScalar(Tokens, ref Idx); Result = (String.Compare(Lhs, Rhs, true) != 0)? "true" : "false"; } else if(Tokens[Idx] == "<") { // Compares whether the first integer is less than the second Idx++; int Lhs = CoerceToInteger(Result); int Rhs = CoerceToInteger(EvaluateScalar(Tokens, ref Idx)); Result = (Lhs < Rhs)? "true" : "false"; } else if(Tokens[Idx] == "<=") { // Compares whether the first integer is less than the second Idx++; int Lhs = CoerceToInteger(Result); int Rhs = CoerceToInteger(EvaluateScalar(Tokens, ref Idx)); Result = (Lhs <= Rhs)? "true" : "false"; } else if(Tokens[Idx] == ">") { // Compares whether the first integer is less than the second Idx++; int Lhs = CoerceToInteger(Result); int Rhs = CoerceToInteger(EvaluateScalar(Tokens, ref Idx)); Result = (Lhs > Rhs)? "true" : "false"; } else if(Tokens[Idx] == ">=") { // Compares whether the first integer is less than the second Idx++; int Lhs = CoerceToInteger(Result); int Rhs = CoerceToInteger(EvaluateScalar(Tokens, ref Idx)); Result = (Lhs >= Rhs)? "true" : "false"; } return Result; } /// /// Evaluates a "scalar" production. /// /// List of tokens in the expression /// Current position in the token stream. Will be incremented as tokens are consumed. /// The result of evaluating the expression static string EvaluateScalar(List Tokens, ref int Idx) { string Result; if(Tokens[Idx] == "(") { // Subexpression Idx++; Result = EvaluateOr(Tokens, ref Idx); if(Tokens[Idx] != ")") { throw new ConditionException("Expected ')'"); } Idx++; } else if(Tokens[Idx] == "!") { // Logical not Idx++; string Rhs = EvaluateScalar(Tokens, ref Idx); Result = CoerceToBool(Rhs)? "false" : "true"; } else if(String.Compare(Tokens[Idx], "Exists", true) == 0 && Tokens[Idx + 1] == "(") { // Check whether file or directory exists. Evaluate the argument as a subexpression. Idx++; string Argument = EvaluateScalar(Tokens, ref Idx); Result = Exists(Argument)? "true" : "false"; } else if(String.Compare(Tokens[Idx], "HasTrailingSlash", true) == 0 && Tokens[Idx + 1] == "(") { // Check whether the given string ends with a slash Idx++; string Argument = EvaluateScalar(Tokens, ref Idx); Result = (Argument.Length > 0 && (Argument[Argument.Length - 1] == Path.DirectorySeparatorChar || Argument[Argument.Length - 1] == Path.AltDirectorySeparatorChar))? "true" : "false"; } else { // Raw scalar. Remove quotes from strings, and allow literals and simple identifiers to pass through directly. string Token = Tokens[Idx]; if(Token.Length >= 2 && (Token[0] == '\'' || Token[0] == '\"') && Token[Token.Length - 1] == Token[0]) { Result = Token.Substring(1, Token.Length - 2); Idx++; } else if(Char.IsLetterOrDigit(Token[0]) || Token[0] == '_') { Result = Token; Idx++; } else { throw new ConditionException("Token '{0}' is not a valid scalar", Token); } } return Result; } /// /// Checks whether a path exists /// /// The path to check for /// True if the path exists, false otherwise. static bool Exists(string Scalar) { try { string FullPath = Path.Combine(CommandUtils.RootDirectory.FullName, Scalar); return CommandUtils.FileExists(FullPath) || CommandUtils.DirectoryExists(FullPath); } catch { return false; } } /// /// Converts a scalar to a boolean value. /// /// The scalar to convert /// The scalar converted to a boolean value. static bool CoerceToBool(string Scalar) { bool Result; if(String.Compare(Scalar, "true", true) == 0) { Result = true; } else if(String.Compare(Scalar, "false", true) == 0) { Result = false; } else { throw new ConditionException("Token '{0}' cannot be coerced to a bool", Scalar); } return Result; } /// /// Converts a scalar to a boolean value. /// /// The scalar to convert /// The scalar converted to an integer value. static int CoerceToInteger(string Scalar) { int Value; if(!Int32.TryParse(Scalar, out Value)) { throw new ConditionException("Token '{0}' cannot be coerced to an integer", Scalar); } return Value; } /// /// Splits an input string up into expression tokens. /// /// Text to be converted into tokens /// List to receive a list of tokens static void Tokenize(string Text, List Tokens) { int Idx = 0; while(Idx < Text.Length) { int EndIdx = Idx + 1; if(!Char.IsWhiteSpace(Text[Idx])) { // Scan to the end of the current token if(Char.IsNumber(Text[Idx])) { // Number while(EndIdx < Text.Length && Char.IsNumber(Text[EndIdx])) { EndIdx++; } } else if(Char.IsLetter(Text[Idx]) || Text[Idx] == '_') { // Identifier while(EndIdx < Text.Length && (Char.IsLetterOrDigit(Text[EndIdx]) || Text[EndIdx] == '_')) { EndIdx++; } } else if(Text[Idx] == '!' || Text[Idx] == '<' || Text[Idx] == '>' || Text[Idx] == '=') { // Operator that can be followed by an equals character if(EndIdx < Text.Length && Text[EndIdx] == '=') { EndIdx++; } } else if(Text[Idx] == '\'' || Text[Idx] == '\"') { // String if(EndIdx < Text.Length) { EndIdx++; while(EndIdx < Text.Length && Text[EndIdx - 1] != Text[Idx]) EndIdx++; } } Tokens.Add(Text.Substring(Idx, EndIdx - Idx)); } Idx = EndIdx; } Tokens.Add(EndToken); } /// /// Test cases for conditions. /// public static void TestConditions() { TestCondition("1 == 2", false); TestCondition("1 == 1", true); TestCondition("1 != 2", true); TestCondition("1 != 1", false); TestCondition("'hello' == 'hello'", true); TestCondition("'hello' == ('hello')", true); TestCondition("'hello' == 'world'", false); TestCondition("'hello' != ('world')", true); TestCondition("true == ('true')", true); TestCondition("true == ('True')", true); TestCondition("true == ('false')", false); TestCondition("true == !('False')", true); TestCondition("true == 'true' and 'false' == 'False'", true); TestCondition("true == 'true' and 'false' == 'true'", false); TestCondition("true == 'false' or 'false' == 'false'", true); TestCondition("true == 'false' or 'false' == 'true'", true); } /// /// Helper method to evaluate a condition and check it's the expected result /// /// Condition to evaluate /// The expected result static void TestCondition(string Condition, bool ExpectedResult) { bool Result = Evaluate(Condition); Console.WriteLine("{0}: {1} = {2}", (Result == ExpectedResult)? "PASS" : "FAIL", Condition, Result); } } }