diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md
index ba030b55c7..9c11d37b06 100644
--- a/docs/CHANGELOG-v3.md
+++ b/docs/CHANGELOG-v3.md
@@ -38,6 +38,8 @@ What's changed since pre-release v3.0.0-B0351:
- Bug fixes:
- Fixed string formatting of semantic version and constraints by @BernieWhite.
[#1828](https://github.com/microsoft/PSRule/issues/1828)
+ - Fixed directory handling of input paths without trailing slash by @BernieWhite.
+ [#1842](https://github.com/microsoft/PSRule/issues/1842)
## v3.0.0-B0351 (pre-release)
diff --git a/src/PSRule/Pipeline/InputPathBuilder.cs b/src/PSRule/Pipeline/InputPathBuilder.cs
index cace5fb033..257867bc37 100644
--- a/src/PSRule/Pipeline/InputPathBuilder.cs
+++ b/src/PSRule/Pipeline/InputPathBuilder.cs
@@ -3,8 +3,10 @@
namespace PSRule.Pipeline;
-internal sealed class InputPathBuilder : PathBuilder
+///
+/// A builder for input paths.
+///
+internal sealed class InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
+ : PathBuilder(logger, basePath, searchPattern, filter, required)
{
- public InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
- : base(logger, basePath, searchPattern, filter, required) { }
}
diff --git a/src/PSRule/Pipeline/PathBuilder.cs b/src/PSRule/Pipeline/PathBuilder.cs
index c9ea0c730e..67309aa990 100644
--- a/src/PSRule/Pipeline/PathBuilder.cs
+++ b/src/PSRule/Pipeline/PathBuilder.cs
@@ -7,18 +7,7 @@
namespace PSRule.Pipeline;
-//public interface IPathBuilder
-//{
-// void Add(string path);
-
-// void Add(FileInfo[] fileInfo);
-
-// void Add(PathInfo[] pathInfo);
-
-// InputFileInfo[] Build();
-//}
-
-internal abstract class PathBuilder
+internal abstract class PathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
{
// Path separators
private const char Slash = '/';
@@ -28,27 +17,16 @@ internal abstract class PathBuilder
private const string CurrentPath = ".";
private const string RecursiveSearchOperator = "**";
- private static readonly char[] PathLiteralStopCharacters = new char[] { '*', '[', '?' };
- private static readonly char[] PathSeparatorCharacters = new char[] { '\\', '/' };
+ private static readonly char[] PathLiteralStopCharacters = ['*', '[', '?'];
+ private static readonly char[] PathSeparatorCharacters = ['\\', '/'];
- private readonly IPipelineWriter _Logger;
- private readonly List _Files;
- private readonly HashSet _Paths;
- private readonly string _BasePath;
- private readonly string _DefaultSearchPattern;
- private readonly PathFilter _GlobalFilter;
- private readonly PathFilter _Required;
-
- protected PathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
- {
- _Logger = logger;
- _Files = [];
- _Paths = [];
- _BasePath = NormalizePath(Environment.GetRootedBasePath(basePath));
- _DefaultSearchPattern = searchPattern;
- _GlobalFilter = filter;
- _Required = required;
- }
+ private readonly IPipelineWriter _Logger = logger;
+ private readonly List _Files = [];
+ private readonly HashSet _Paths = [];
+ private readonly string _BasePath = NormalizePath(Environment.GetRootedBasePath(basePath));
+ private readonly string _DefaultSearchPattern = searchPattern;
+ private readonly PathFilter _GlobalFilter = filter;
+ private readonly PathFilter _Required = required;
///
/// The number of files found.
@@ -89,7 +67,7 @@ public InputFileInfo[] Build()
{
try
{
- return _Files.ToArray();
+ return [.. _Files];
}
finally
{
@@ -105,9 +83,14 @@ private void FindFiles(string path)
var pathLiteral = GetSearchParameters(path, out var searchPattern, out var searchOption, out var filter);
var files = Directory.EnumerateFiles(pathLiteral, searchPattern, searchOption);
+
foreach (var file in files)
+ {
if (ShouldInclude(file, filter))
+ {
AddFile(file);
+ }
+ }
}
private bool TryUrl(string path)
@@ -128,9 +111,7 @@ private bool TryPath(string path, out string normalPath)
var rootedPath = GetRootedPath(path);
if (Directory.Exists(rootedPath) || path == CurrentPath)
{
- if (IsBasePath(rootedPath))
- normalPath = CurrentPath;
-
+ normalPath = IsBasePath(rootedPath) ? CurrentPath : NormalizeDirectoryPath(path);
return false;
}
if (!File.Exists(rootedPath))
@@ -144,8 +125,7 @@ private bool TryPath(string path, out string normalPath)
private bool IsBasePath(string path)
{
- path = IsSeparator(path[path.Length - 1]) ? path : string.Concat(path, Path.DirectorySeparatorChar);
- return NormalizePath(path) == _BasePath;
+ return NormalizeDirectoryPath(path) == _BasePath;
}
private void ErrorNotFound(string path)
@@ -251,7 +231,7 @@ private static bool IsSeparator(char c)
[DebuggerStepThrough]
private static bool UseSimpleSearch(string s)
{
- return s.IndexOf(RecursiveSearchOperator, System.StringComparison.OrdinalIgnoreCase) == -1;
+ return s.IndexOf(RecursiveSearchOperator, StringComparison.OrdinalIgnoreCase) == -1;
}
[DebuggerStepThrough]
@@ -259,4 +239,12 @@ private static string NormalizePath(string path)
{
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}
+
+ [DebuggerStepThrough]
+ private static string NormalizeDirectoryPath(string path)
+ {
+ return NormalizePath(
+ IsSeparator(path[path.Length - 1]) ? path : string.Concat(path, Path.DirectorySeparatorChar)
+ );
+ }
}
diff --git a/tests/PSRule.Tests/InputPathBuilderTests.cs b/tests/PSRule.Tests/InputPathBuilderTests.cs
deleted file mode 100644
index 732cbf64aa..0000000000
--- a/tests/PSRule.Tests/InputPathBuilderTests.cs
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System;
-using System.IO;
-using System.Linq;
-using PSRule.Pipeline;
-
-namespace PSRule;
-
-public sealed class InputPathBuilderTests
-{
- [Fact]
- public void GetPath()
- {
- var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, null);
- builder.Add(".");
- var actual = builder.Build();
- Assert.True(actual.Length > 100);
-
- builder.Add(GetWorkingPath());
- actual = builder.Build();
- Assert.True(actual.Length > 100);
-
- builder.Add("./src");
- actual = builder.Build();
- Assert.True(actual.Length == 0);
-
- builder.Add("./src/");
- actual = builder.Build();
- Assert.True(actual.Length > 100);
-
- builder.Add("./");
- actual = builder.Build();
- Assert.True(actual.Length > 100);
-
- builder.Add("./.github/*.yml");
- actual = builder.Build();
- Assert.Single(actual);
-
- builder.Add("./.github/**/*.yaml");
- actual = builder.Build();
- Assert.Equal(9, actual.Length);
-
- builder.Add("./.github/");
- actual = builder.Build();
- Assert.Equal(12, actual.Length);
-
- builder.Add(".github/");
- actual = builder.Build();
- Assert.Equal(12, actual.Length);
-
- builder.Add("./*.json");
- actual = builder.Build();
- Assert.Equal(8, actual.Length);
-
- builder.Add("src/");
- actual = builder.Build();
- Assert.True(actual.Length > 100);
-
- // Check error handling
- var writer = new TestWriter(new Configuration.PSRuleOption());
- builder = new InputPathBuilder(writer, GetWorkingPath(), "*", null, null);
- builder.Add("ZZ://not/path");
- actual = builder.Build();
- Assert.Empty(actual);
- Assert.True(writer.Errors.Count(r => r.FullyQualifiedErrorId == "PSRule.ReadInputFailed") == 1);
- }
-
- [Fact]
- public void GetPathRequired()
- {
- var required = PathFilter.Create(GetWorkingPath(), new string[] { "README.md" });
- var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required);
- builder.Add(".");
- var actual = builder.Build();
- Assert.True(actual.Length == 1);
-
- builder.Add(GetWorkingPath());
- actual = builder.Build();
- Assert.True(actual.Length == 1);
-
- required = PathFilter.Create(GetWorkingPath(), new string[] { "**" });
- builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required);
- builder.Add(".");
- actual = builder.Build();
- Assert.True(actual.Length > 100);
- }
-
- #region Helper methods
-
- private static string GetWorkingPath()
- {
- return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../../.."));
- }
-
- #endregion Helper methods
-}
diff --git a/tests/PSRule.Tests/Pipeline/InputPathBuilderTests.cs b/tests/PSRule.Tests/Pipeline/InputPathBuilderTests.cs
new file mode 100644
index 0000000000..02828a6fc6
--- /dev/null
+++ b/tests/PSRule.Tests/Pipeline/InputPathBuilderTests.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Linq;
+
+namespace PSRule.Pipeline;
+
+///
+/// Tests for .
+///
+public sealed class InputPathBuilderTests
+{
+ [Theory]
+ [InlineData("./.github/*.yml", 1)]
+ [InlineData("./.github/**/*.yaml", 9)]
+ [InlineData("./.github/", 12)]
+ [InlineData(".github/", 12)]
+ [InlineData(".github", 12)]
+ [InlineData("./*.json", 8)]
+ public void Build_WithValidPathAdded_ShouldReturnFiles(string path, int expected)
+ {
+ var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, null);
+ builder.Add(path);
+ var actual = builder.Build();
+
+ Assert.Equal(expected, actual.Length);
+ }
+
+ [Theory]
+ [InlineData(".")]
+ [InlineData("./")]
+ [InlineData("./src")]
+ [InlineData("./src/")]
+ [InlineData("src/")]
+ public void Build_WithValidPathAdded_ShouldReturnManyFiles(string path)
+ {
+ var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, null);
+ builder.Add(path);
+ var actual = builder.Build();
+
+ Assert.True(actual.Length > 100);
+ }
+
+ [Fact]
+ public void Build_WithWorkingPathAdded_ShouldReturnFiles()
+ {
+ var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, null);
+ builder.Add(GetWorkingPath());
+ var actual = builder.Build();
+
+ Assert.True(actual.Length > 100);
+ }
+
+ ///
+ /// Test that an invalid path is handled correctly.
+ /// Should not return any files, and should log an error.
+ ///
+ [Fact]
+ public void Build_WithInvalidPathAdded_ShouldReturnEmpty()
+ {
+ var writer = new TestWriter(new Configuration.PSRuleOption());
+ var builder = new InputPathBuilder(writer, GetWorkingPath(), "*", null, null);
+ builder.Add("ZZ://not/path");
+ var actual = builder.Build();
+
+ Assert.Empty(actual);
+ Assert.True(writer.Errors.Count(r => r.FullyQualifiedErrorId == "PSRule.ReadInputFailed") == 1);
+ }
+
+ [Fact]
+ public void GetPathRequired()
+ {
+ var required = PathFilter.Create(GetWorkingPath(), ["README.md"]);
+ var builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required);
+ builder.Add(".");
+ var actual = builder.Build();
+ Assert.True(actual.Length == 1);
+
+ builder.Add(GetWorkingPath());
+ actual = builder.Build();
+ Assert.True(actual.Length == 1);
+
+ required = PathFilter.Create(GetWorkingPath(), ["**"]);
+ builder = new InputPathBuilder(null, GetWorkingPath(), "*", null, required);
+ builder.Add(".");
+ actual = builder.Build();
+ Assert.True(actual.Length > 100);
+ }
+
+ #region Helper methods
+
+ private static string GetWorkingPath()
+ {
+ return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../../.."));
+ }
+
+ #endregion Helper methods
+}