Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/AutomationUtils/FilePattern.cs
Ben Marsh 7f06f93ff9 Copying //UE4/Dev-Build to //UE4/Dev-Main (Source: //UE4/Dev-Build @ 3209300)
#lockdown Nick.Penwarden
#rb none

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3146735 on 2016/09/30 by Ben.Marsh

	EC: Add the standard postprocessor onto conform job steps.

Change 3147190 on 2016/09/30 by Ben.Marsh

	UBT: Add a whitelist for circular dependencies between modules, and output a warning if any new circular dependencies are added.

Change 3148611 on 2016/10/03 by Matthew.Griffin

	Added list of Dependant modules to EULA check
	#jira UE-29432

Change 3149098 on 2016/10/03 by Ben.Marsh

	PR #2821: Fix to stop BuildGraph-created files from being tracked by Git (*.manifest and 'LocalBuilds/') (Contributed by BrodyHiggerson)

Change 3149395 on 2016/10/03 by Ben.Marsh

	UBT: Don't execute pre- and post-build steps when generating project files.

Change 3150398 on 2016/10/04 by Ben.Marsh

	UBT: Fix support for the OptimizeCode setting on Mac, HTML5, Android, Linux, iOS, and XboxOne, and use it to control optimization level in DebugGame configurations. Also determine whether optimization should be enabled for a module at the UEBuildModule level, rather than deriving it (inconsistently) from the ModuleRules enum in the toolchain.

	#jira UE-18652

Change 3150569 on 2016/10/04 by Ben.Marsh

	UBT: Remove the NativeBuildEnvironmentConfiguration and NativeBuildEnvironmentConfiguration.TargetInfo classes; store the platform/configuration/architecture on the CPPEnvironment and LinkEnvironment directly.

Change 3150606 on 2016/10/04 by Ben.Marsh

	UBT: Remove support for C++/CLR modules. There's quite a lot of baggage to support it, and I suspect it's already rotted since we don't use it.

Change 3150628 on 2016/10/04 by Ben.Marsh

	UBT: Remove .NET framework assembly paths for C++ modules.

Change 3150640 on 2016/10/04 by Ben.Marsh

	UBT: Move functionality for finding headers into its own class (CPPHeaders), rather than hijacking CPPEnvironment.

Change 3152101 on 2016/10/05 by Ben.Marsh

	UBT: Always force include PCHs, even if they're the first header in the file. Clang already has to work this way, as do shared pchs on Windows, and it's simpler to use the same system universally.

Change 3153231 on 2016/10/06 by Ben.Marsh

	UBT: Write arguments for the Visual C++ toolchain to the response file on separate lines, for easier debugging.

Change 3154868 on 2016/10/07 by Richard.Fawcett

	Ensure that child instances of UAT invoked by BuildGraph honor p4 and submit properties

Change 3155017 on 2016/10/07 by Ben.Marsh

	Lightmass: Add a prefix to all SSE macros to distinguish from overlapping implementations in Core.

Change 3156159 on 2016/10/08 by Ben.Marsh

	UBT: Rewrite code to generate/consume shared PCHs in a way that is compatible with Clang platforms (and which doesn't require supressing warnings on Windows)

	* Per-module defines are now set via a generated header rather than the command line (Definitions.<ModuleName>.h). This header is force-included AFTER the shared PCH header.
	* Shared PCHs are now built using the public compile environment only, eliminating situations where private include paths and definitions from the first module using the shared PCH were being passed to the compiler.
	* Shared PCHs can now be generated in separate optimized/unoptimized variants if necessary due to per-module optimization settings
	* Names of shared PCHs now follow the pattern "SharedPCH.<ModuleName>.h" to distinguish from private PCHs
	* Enabled shared PCHs for Mac, iOS, Linux.

	Reduces UE4Editor Mac build times by ~25% (~21m vs ~28m).

Change 3163040 on 2016/10/14 by Ben.Marsh

	UBT: Add an option to export a target's properties, modules, and binaries for external analyzers (-jsonexport and -jsonexport=<filename>). Also add an option to specifically supress building a target (-skipbuild).

Change 3165028 on 2016/10/17 by Ben.Marsh

	PR #2799: Set Windows exe properties from ini (Contributed by projectgheist)

Change 3165076 on 2016/10/17 by Ben.Marsh

	Build: Remove run conditions from agent setup steps. Should fix issues where an agent doesn't run because the preconditions for one of its nodes failed, but which doesn't prevent another node from running. (Also: being able to see an expanded list of job steps is a useful feature, and it doesn't take much time to run if it's a no-op).

Change 3167773 on 2016/10/19 by Ben.Marsh

	BuildGraph: More flexible <Copy> task for BuildGraph. Now takes "From" and "To" attributes, which may take individual files as well as wildcards, and permits renaming as part of copies. Separate "Files" attribute is still supported, but is now optional, and is used to filter the list of source files.

	Examples, taken from Engine/Build/Graph/TagsAndFiles.xml:

	    <!-- Source and destination are treated as directories due to presence of 'Files' attribute -->
	    <Copy Files="..." From="Engine/Build" To="Output0" />

	    <!-- Single file -->
	    <Copy From="Engine/Build/Build.version" To="Output1/Output.version" />

	    <!-- Output treated as directory -->
	    <Copy From="Engine/Build/.../*.txt" To="Output2" />
	    <Copy From="Engine/Build/.../*.txt" To="Output3/" />
	    <Copy From="Engine/Build/....txt" To="Output4" />

	    <!-- With rename -->
	    <Copy From="Engine/Build/....txt" To="Output5/....old" />

	    <!-- Copy only subdirectories of Engine/Build, but maintain directory structure -->
	    <Copy From="Engine/Build/*/....txt" To="Output6/" />

	    <!-- Match bat*/.../*.txt -->
	    <Copy From="Engine/Build/bat...txt" To="Output7" />

	    <!-- Copy only PS4 subfolders -->
	    <Copy From="Engine/Build/.../ps4/..." To="Output8" />

Change 3167852 on 2016/10/19 by Ben.Marsh

	BuildGraph: Add a <Move> task, which can move or rename files using the same syntax as the <Copy> task.

Change 3168034 on 2016/10/19 by Ben.Marsh

	BuildGraph: Add support for multi-line properties, declared inside a <Property> tag.

	Mutli-line properties can be useful for making lists for filtering. Each non-empty line inside the property tag is stripped of leading and trailing whitespace, and appended to the property value separated by a semicolon. For example, the following two properties have an identical value:

	    <Property Name="Prop1" Value="One;Two;Three;One hundred"/>

	    <Property Name="Prop2">
	        <!-- Some numbers -->
	        One
	        Two
	        Three

	        <!-- Values are delimited by newlines; spaces within a line are preserved -->
	        One hundred
	    </Property>

Change 3169256 on 2016/10/20 by Ben.Marsh

	UBT: Fix chronic algorithmic complexity of StableTopologicalSort(). Was recursively building lists of dependencies for each module, scanning linearly to find dependencies, and only caching pairs of modules being checked. Now caches a flat set of dependencies for each module.

	Previously took >10s to run on my machine, now takes < 0.2s.

Change 3169271 on 2016/10/20 by Ben.Marsh

	BuildGraph: Modify implementation of task merging. Instead of combining multiple tasks together, allow any task to supply a proxy executor instance deriving from ITaskExecutor, to which other tasks can be added. Keeps the in-memory representation closer to the script representation, and makes it easier to re-export preprocessed scripts and do in-memory analysis of the graph.

Change 3179662 on 2016/10/31 by Matthew.Griffin

	Fixed last remaining issues with building QAGameEditor as a monolithic executable
	Added a node for building mono editor in CIS so that we catch any new issues
	#jira UE-32712

Change 3184857 on 2016/11/03 by Matthew.Griffin

	Removing CopyVisualizers now that UE4.natvis is included in solution and we're not supporting VS2013
	#jira UE-35628

Change 3187232 on 2016/11/04 by Ben.Marsh

	UGS: Fix editor .target file being deleted when doing a content-only sync, and causing a prompt to rebuild the editor when trying to launch.

Change 3188413 on 2016/11/07 by Matthew.Griffin

	Added Switch to Installed Build so that it matches pattern for other confidential platforms

Change 3188426 on 2016/11/07 by Matthew.Griffin

	Changed BuildPlugin command so that it reads from installed platform data to get list of target platforms
	Some hard coding remains so as to not change functionality for code users, Mac only built on Mac etc.
	#jira UE-36205

Change 3189363 on 2016/11/07 by Ben.Marsh

	Consolidate functionality for determining the path to MSBuild.exe to use for compiling UE4 tools into a single batch file (GetMSBuildToolPath) and fix "Clean" not working on PS4 due to include/library paths being set to something by the Visual Studio environment.

Change 3191372 on 2016/11/09 by Ben.Marsh

	UGS: Ensure project config file remains valid even if sync is aborted due to files needing resolve. Prevents user configuration from not being applied onto build steps.

Change 3191381 on 2016/11/09 by Ben.Marsh

	UGS: Allow Ctrl-A to select all in the log window.

	#jira UE-38378

Change 3193388 on 2016/11/10 by Ben.Marsh

	Change installed plugins to be disabled by default, but display a notification in the editor the first time you load a project with a new one. Installed plugins which are new to this project will be adorned with a "NEW!" badge in the plugin browser.

Change 3193677 on 2016/11/10 by Ben.Marsh

	UBT: Remove global static instance of ActionGraph. Instance is now instantiated and passed around to functions that require it.

Change 3193942 on 2016/11/10 by Ben.Marsh

	UBT: Store the include cache as an instanced object on each target, rather than looking up separate caches in a global variable.

Change 3198296 on 2016/11/15 by Ben.Marsh

	UBT: Include .modules files in target receipts and manifests.

Change 3200284 on 2016/11/16 by Matthew.Griffin

	Move Sample game projects into a different solution folder like Templates

Change 3205168 on 2016/11/19 by Ben.Marsh

	Update strings to refer to Visual Studio "15" as Visual Studio 2017.

Change 3206333 on 2016/11/21 by Ben.Marsh

	Merge fix to detection of VS2017 RC from 4.14 release.

Change 3206786 on 2016/11/21 by Ben.Marsh

	BuildGraph: Spawn child processes to embed source server information into PDB files in parallel.

Change 3207588 on 2016/11/22 by Ben.Marsh

	UBT: Reduce the number of resource files needed to compile local builds. ModuleVersionResource.rc.inl is now always only compiled once, and linked into each output binary. The default PCLaunch.rc file is also only compiled once, expect when making formal builds (where -formal is passed on the command line, or a changelist is set in Build.version). This ensures that the OriginalFileName metadata is still set for output binaries in binary releases.

[CL 3209331 by Ben Marsh in Main branch]
2016-11-23 15:34:07 -05:00

426 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnrealBuildTool;
namespace AutomationTool
{
/// <summary>
/// Encapsulates a pattern containing the '?', '*', and '...' wildcards.
/// </summary>
public class FilePattern
{
/// <summary>
/// Base directory for all matched files
/// </summary>
public readonly DirectoryReference BaseDirectory;
/// <summary>
/// List of tokens in the pattern. Every second token is a wildcard, other tokens are string fragments. Always has an odd number of elements. Path separators are normalized to the host platform format.
/// </summary>
public readonly List<string> Tokens = new List<string>();
/// <summary>
/// Constructs a file pattern which matches a single file
/// </summary>
/// <param name="File">Location of the file</param>
public FilePattern(FileReference File)
{
BaseDirectory = File.Directory;
Tokens.Add(File.GetFileName());
}
/// <summary>
/// Constructs a file pattern from the given string, resolving relative paths to the given directory.
/// </summary>
/// <param name="RootDirectory">If a relative path is specified by the pattern, the root directory used to turn it into an absolute path</param>
/// <param name="Pattern">The pattern to match. If the pattern ends with a directory separator, an implicit '...' is appended.</param>
public FilePattern(DirectoryReference RootDirectory, string Pattern)
{
// Normalize the path separators
StringBuilder Text = new StringBuilder(Pattern);
if(Path.DirectorySeparatorChar != '\\')
{
Text.Replace('\\', Path.DirectorySeparatorChar);
}
if(Path.DirectorySeparatorChar != '/')
{
Text.Replace('/', Path.DirectorySeparatorChar);
}
// Find the base directory, stopping when we hit a wildcard. The source directory must end with a path specification.
int BaseDirectoryLen = 0;
for(int Idx = 0; Idx < Text.Length; Idx++)
{
if(Text[Idx] == Path.DirectorySeparatorChar)
{
BaseDirectoryLen = Idx + 1;
}
else if(Text[Idx] == '?' || Text[Idx] == '*' || (Idx + 2 < Text.Length && Text[Idx] == '.' && Text[Idx + 1] == '.' && Text[Idx + 2] == '.'))
{
break;
}
}
// Extract the base directory
BaseDirectory = DirectoryReference.Combine(RootDirectory, Text.ToString(0, BaseDirectoryLen));
// Convert any directory wildcards ("...") into complete directory wildcards ("\\...\\"). We internally treat use "...\\" as the wildcard
// token so we can correctly match zero directories. Patterns such as "foo...bar" should require at least one directory separator, so
// should be converted to "foo*\\...\\*bar".
for(int Idx = BaseDirectoryLen; Idx < Text.Length; Idx++)
{
if(Text[Idx] == '.' && Text[Idx + 1] == '.' && Text[Idx + 2] == '.')
{
// Insert a directory separator before
if(Idx > BaseDirectoryLen && Text[Idx - 1] != Path.DirectorySeparatorChar)
{
Text.Insert(Idx++, '*');
Text.Insert(Idx++, Path.DirectorySeparatorChar);
}
// Skip past the ellipsis
Idx += 3;
// Insert a directory separator after
if(Idx == Text.Length || Text[Idx] != Path.DirectorySeparatorChar)
{
Text.Insert(Idx++, Path.DirectorySeparatorChar);
Text.Insert(Idx++, '*');
}
}
}
// Parse the tokens
int LastIdx = BaseDirectoryLen;
for(int Idx = BaseDirectoryLen; Idx < Text.Length; Idx++)
{
if(Text[Idx] == '?' || Text[Idx] == '*')
{
Tokens.Add(Text.ToString(LastIdx, Idx - LastIdx));
Tokens.Add(Text.ToString(Idx, 1));
LastIdx = Idx + 1;
}
else if(Idx - 3 >= BaseDirectoryLen && Text[Idx] == Path.DirectorySeparatorChar && Text[Idx - 1] == '.' && Text[Idx - 2] == '.' && Text[Idx - 3] == '.')
{
Tokens.Add(Text.ToString(LastIdx, Idx - 3 - LastIdx));
Tokens.Add(Text.ToString(Idx - 3, 4));
LastIdx = Idx + 1;
}
}
Tokens.Add(Text.ToString(LastIdx, Text.Length - LastIdx));
}
/// <summary>
/// A pattern without wildcards may match either a single file or directory based on context. This pattern resolves to the later as necessary, producing a new pattern.
/// </summary>
/// <returns>Pattern which matches a directory</returns>
public FilePattern AsDirectoryPattern()
{
if(ContainsWildcards())
{
return this;
}
else
{
return new FilePattern(BaseDirectory, String.Join("", Tokens) + Path.DirectorySeparatorChar + "...");
}
}
/// <summary>
/// For a pattern that does not contain wildcards, returns the single file location
/// </summary>
/// <returns>Location of the referenced file</returns>
public FileReference GetSingleFile()
{
if(Tokens.Count == 1)
{
return FileReference.Combine(BaseDirectory, Tokens[0]);
}
else
{
throw new InvalidOperationException("File pattern does not reference a single file");
}
}
/// <summary>
/// Checks whether this pattern is explicitly a directory, ie. is terminated with a directory separator
/// </summary>
/// <returns>True if the pattern is a directory</returns>
public bool EndsWithDirectorySeparator()
{
string LastToken = Tokens[Tokens.Count - 1];
return LastToken.Length > 0 && LastToken[LastToken.Length - 1] == Path.DirectorySeparatorChar;
}
/// <summary>
/// Determines whether the pattern contains wildcards
/// </summary>
/// <returns>True if the pattern contains wildcards, false otherwise.</returns>
public bool ContainsWildcards()
{
return Tokens.Count > 1;
}
/// <summary>
/// Tests whether a pattern is compatible with another pattern (that is, that the number and type of wildcards match)
/// </summary>
/// <param name="Other">Pattern to compare against</param>
/// <returns>Whether the patterns are compatible.</returns>
public bool IsCompatibleWith(FilePattern Other)
{
// Check there are the same number of tokens in each pattern
if(Tokens.Count != Other.Tokens.Count)
{
return false;
}
// Check all the wildcard tokens match
for(int Idx = 1; Idx < Tokens.Count; Idx += 2)
{
if(Tokens[Idx] != Other.Tokens[Idx])
{
return false;
}
}
return true;
}
/// <summary>
/// Converts this pattern to a C# regex format string, which matches paths relative to the base directory formatted with native directory separators
/// </summary>
/// <returns>The regex pattern</returns>
public string GetRegexPattern()
{
StringBuilder Pattern = new StringBuilder("^");
Pattern.Append(Regex.Escape(Tokens[0]));
for(int Idx = 1; Idx < Tokens.Count; Idx += 2)
{
// Append the wildcard expression
if(Tokens[Idx] == "?")
{
Pattern.Append("([^\\/])");
}
else if(Tokens[Idx] == "*")
{
Pattern.Append("([^\\/]*)");
}
else
{
Pattern.AppendFormat("((?:.+{0})?)", Regex.Escape(Path.DirectorySeparatorChar.ToString()));
}
// Append the next sequence of characters to match
Pattern.Append(Regex.Escape(Tokens[Idx + 1]));
}
Pattern.Append("$");
return Pattern.ToString();
}
/// <summary>
/// Creates a regex replacement pattern
/// </summary>
/// <returns>String representing the regex replacement pattern</returns>
public string GetRegexReplacementPattern()
{
StringBuilder Pattern = new StringBuilder();
for(int Idx = 0;;Idx += 2)
{
// Append the escaped replacement character
Pattern.Append(Tokens[Idx].Replace("$", "$$"));
// Check if we've reached the end of the string
if(Idx == Tokens.Count - 1)
{
break;
}
// Insert the capture
Pattern.AppendFormat("${0}", (Idx / 2) + 1);
}
return Pattern.ToString();
}
/// <summary>
/// Creates a file mapping between a set of source patterns and a target pattern. All patterns should have a matching order and number of wildcards.
/// </summary>
/// <param name="Files">Files to use for the mapping</param>
/// <param name="SourcePatterns">List of source patterns</param>
/// <param name="TargetPattern">Matching output pattern</param>
/// <param name="Filter">Filter to apply to source files</param>
/// <param name="TargetFileToSourceFile">Dictionary to receive a mapping from target file to source file. An exception is thrown if multiple source files map to one target file, or a source file is also used as a target file.</param>
public static bool TryCreateMapping(HashSet<FileReference> Files, FilePattern SourcePattern, FilePattern TargetPattern, out Dictionary<FileReference, FileReference> OutTargetFileToSourceFile)
{
bool bResult = true;
// If the source pattern ends in a directory separator, or a set of input files are specified and it doesn't contain wildcards, treat it as a full directory match
if(SourcePattern.EndsWithDirectorySeparator())
{
SourcePattern = new FilePattern(SourcePattern.BaseDirectory, String.Join("", SourcePattern.Tokens) + "...");
}
else if(Files != null)
{
SourcePattern = SourcePattern.AsDirectoryPattern();
}
// If we have multiple potential source files, but no wildcards in the output pattern, assume it's a directory and append the pattern from the source.
if(SourcePattern.ContainsWildcards() && !TargetPattern.ContainsWildcards())
{
StringBuilder NewPattern = new StringBuilder();
foreach(string Token in TargetPattern.Tokens)
{
NewPattern.Append(Token);
}
if(NewPattern.Length > 0 && NewPattern[NewPattern.Length - 1] != Path.DirectorySeparatorChar)
{
NewPattern.Append(Path.DirectorySeparatorChar);
}
foreach(string Token in SourcePattern.Tokens)
{
NewPattern.Append(Token);
}
TargetPattern = new FilePattern(TargetPattern.BaseDirectory, NewPattern.ToString());
}
// If the target pattern ends with a directory separator, treat it as a full directory match if it has wildcards, or a copy of the source pattern if not
if(TargetPattern.EndsWithDirectorySeparator())
{
TargetPattern = new FilePattern(TargetPattern.BaseDirectory, String.Join("", TargetPattern.Tokens) + "...");
}
// Handle the case where source and target pattern are both individual files
Dictionary<FileReference, FileReference> TargetFileToSourceFile = new Dictionary<FileReference, FileReference>();
if(SourcePattern.ContainsWildcards() || TargetPattern.ContainsWildcards())
{
// Check the two patterns are compatible
if(!SourcePattern.IsCompatibleWith(TargetPattern))
{
CommandUtils.LogError("File patterns '{0}' and '{1}' do not have matching wildcards", SourcePattern, TargetPattern);
OutTargetFileToSourceFile = null;
return false;
}
// Create a filter to match the source files
FileFilter Filter = new FileFilter(FileFilterType.Exclude);
Filter.Include(String.Join("", SourcePattern.Tokens));
// Apply it to the source directory
List<FileReference> SourceFiles;
if(Files == null)
{
SourceFiles = Filter.ApplyToDirectory(SourcePattern.BaseDirectory, true);
}
else
{
SourceFiles = CheckInputFiles(Files, SourcePattern.BaseDirectory, ref bResult);
}
// Map them onto output files
FileReference[] TargetFiles = new FileReference[SourceFiles.Count];
// Get the source and target regexes
string SourceRegex = SourcePattern.GetRegexPattern();
string TargetRegex = TargetPattern.GetRegexReplacementPattern();
for(int Idx = 0; Idx < SourceFiles.Count; Idx++)
{
string SourceRelativePath = SourceFiles[Idx].MakeRelativeTo(SourcePattern.BaseDirectory);
string TargetRelativePath = Regex.Replace(SourceRelativePath, SourceRegex, TargetRegex);
TargetFiles[Idx] = FileReference.Combine(TargetPattern.BaseDirectory, TargetRelativePath);
}
// Add them to the output map
for(int Idx = 0; Idx < TargetFiles.Length; Idx++)
{
FileReference ExistingSourceFile;
if(TargetFileToSourceFile.TryGetValue(TargetFiles[Idx], out ExistingSourceFile) && ExistingSourceFile != SourceFiles[Idx])
{
CommandUtils.LogError("Output file '{0}' is mapped from '{1}' and '{2}'", TargetFiles[Idx], ExistingSourceFile, SourceFiles[Idx]);
bResult = false;
}
TargetFileToSourceFile[TargetFiles[Idx]] = SourceFiles[Idx];
}
}
else
{
// Just copy a single file
FileReference SourceFile = SourcePattern.GetSingleFile();
if(SourceFile.Exists())
{
FileReference TargetFile = TargetPattern.GetSingleFile();
TargetFileToSourceFile[TargetFile] = SourceFile;
}
else
{
CommandUtils.LogError("Source file '{0}' does not exist", SourceFile);
bResult = false;
}
}
// Check that no source file is also destination file
foreach(FileReference SourceFile in TargetFileToSourceFile.Values)
{
if(TargetFileToSourceFile.ContainsKey(SourceFile))
{
CommandUtils.LogError("'{0}' is listed as a source and target file", SourceFile);
bResult = false;
}
}
// Set the output map
if(bResult)
{
OutTargetFileToSourceFile = TargetFileToSourceFile;
return true;
}
else
{
OutTargetFileToSourceFile = null;
return false;
}
}
/// <summary>
/// Checks that the given input files all exist and are under the given base directory
/// </summary>
/// <param name="InputFiles">Input files to check</param>
/// <param name="BaseDirectory">Base directory for files</param>
/// <param name="bResult">Set to false if a check fails, otherwise unmodified</param>
/// <returns>List of valid files</returns>
public static List<FileReference> CheckInputFiles(IEnumerable<FileReference> InputFiles, DirectoryReference BaseDirectory, ref bool bResult)
{
List<FileReference> Files = new List<FileReference>();
foreach(FileReference InputFile in InputFiles)
{
if(!InputFile.IsUnderDirectory(BaseDirectory))
{
CommandUtils.LogError("Source file '{0}' is not under '{1}'", InputFile, BaseDirectory);
bResult = false;
}
else if(!InputFile.Exists())
{
CommandUtils.LogError("Source file '{0}' does not exist", InputFile);
bResult = false;
}
else
{
Files.Add(InputFile);
}
}
return Files;
}
/// <summary>
/// Formats the pattern as a string
/// </summary>
/// <returns>The original representation of this pattern</returns>
public override string ToString()
{
return BaseDirectory.ToString() + Path.DirectorySeparatorChar + String.Join("", Tokens);
}
}
}