/// <summary>
        /// Resolve a list of files, tag names or file specifications as above, but preserves any directory references for further processing.
        /// </summary>
        /// <param name="DefaultDirectory">The default directory to resolve relative paths to</param>
        /// <param name="FilePatterns">List of files, tag names, or file specifications to include separated by semicolons.</param>
        /// <param name="ExcludePatterns">Set of patterns to apply to directory searches. This can greatly speed up enumeration by earlying out of recursive directory searches if large directories are excluded (eg. .../Intermediate/...).</param>
        /// <param name="TagNameToFileSet">Mapping of tag name to fileset, as passed to the Execute() method</param>
        /// <returns>Set of matching files.</returns>
        public static HashSet <FileReference> ResolveFilespecWithExcludePatterns(DirectoryReference DefaultDirectory, List <string> FilePatterns, List <string> ExcludePatterns, Dictionary <string, HashSet <FileReference> > TagNameToFileSet)
        {
            // Parse each of the patterns, and add the results into the given sets
            HashSet <FileReference> Files = new HashSet <FileReference>();

            foreach (string Pattern in FilePatterns)
            {
                // Check if it's a tag name
                if (Pattern.StartsWith("#"))
                {
                    Files.UnionWith(FindOrAddTagSet(TagNameToFileSet, Pattern));
                    continue;
                }

                // If it doesn't contain any wildcards, just add the pattern directly
                int WildcardIdx = FileFilter.FindWildcardIndex(Pattern);
                if (WildcardIdx == -1)
                {
                    Files.Add(FileReference.Combine(DefaultDirectory, Pattern));
                    continue;
                }

                // Find the base directory for the search. We construct this in a very deliberate way including the directory separator itself, so matches
                // against the OS root directory will resolve correctly both on Mac (where / is the filesystem root) and Windows (where / refers to the current drive).
                int LastDirectoryIdx       = Pattern.LastIndexOfAny(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, WildcardIdx);
                DirectoryReference BaseDir = DirectoryReference.Combine(DefaultDirectory, Pattern.Substring(0, LastDirectoryIdx + 1));

                // Construct the absolute include pattern to match against, re-inserting the resolved base directory to construct a canonical path.
                string IncludePattern = BaseDir.FullName.TrimEnd(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }) + "/" + Pattern.Substring(LastDirectoryIdx + 1);

                // Construct a filter and apply it to the directory
                if (DirectoryReference.Exists(BaseDir))
                {
                    FileFilter Filter = new FileFilter();
                    Filter.AddRule(IncludePattern, FileFilterType.Include);
                    if (ExcludePatterns != null && ExcludePatterns.Count > 0)
                    {
                        Filter.AddRules(ExcludePatterns, FileFilterType.Exclude);
                    }
                    Files.UnionWith(Filter.ApplyToDirectory(BaseDir, BaseDir.FullName, true));
                }
            }

            // If we have exclude rules, create and run a filter against all the output files to catch things that weren't added from an include
            if (ExcludePatterns != null && ExcludePatterns.Count > 0)
            {
                FileFilter Filter = new FileFilter(FileFilterType.Include);
                Filter.AddRules(ExcludePatterns, FileFilterType.Exclude);
                Files.RemoveWhere(x => !Filter.Matches(x.FullName));
            }
            return(Files);
        }