Skip to content

Commit 2cc4959

Browse files
committed
(PowerShellGH-879) Use globbing when enumerating workspace files
Previously the Workspace.EnumeratePSFiles method could only filter files based on file extension (*.ps1, *.psm1, *.psd1). However editor settings tend to use file glob patterns, but Editor Services did not have a library that could parse them. This commit: * Updates Editor Services to use the Microsoft.Extensions.FileSystemGlobbing library * Updated the build process to include the new FileSystemGlobbing DLL * The FileSystemGlobbing library uses an abstract file system to search, not an actual System.IO.FileSystem object. So to implement the same error handling and maximum depth recursion, a WorkspaceFileSystemWrapperFactory is used to create the Directory and File objects needed for the globbing library The WorkspaceFileSystemWrapperFactory can filter on: - Maximum recursion depth - Reparse points (Note that these aren't strictly Symlinks on windows. There are many other types of filesystem items which are reparse points - File system extension - Gracefully ignores any file access errors * The EnumeratePSFiles has two method signatures. One with no arguments which uses the Workspace object's default values and another where all arguments must be specified when enumerating the files * Adds tests for the EnumeratePSFiles method to ensure that it filters on glob and recursion depth.
1 parent cc2fda0 commit 2cc4959

File tree

5 files changed

+438
-118
lines changed

5 files changed

+438
-118
lines changed

PowerShellEditorServices.build.ps1

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ $script:RequiredBuildAssets = @{
5757
'publish/Serilog.Sinks.Async.dll',
5858
'publish/Serilog.Sinks.Console.dll',
5959
'publish/Serilog.Sinks.File.dll',
60+
'publish/Microsoft.Extensions.FileSystemGlobbing.dll',
6061
'Microsoft.PowerShell.EditorServices.dll',
6162
'Microsoft.PowerShell.EditorServices.pdb'
6263
)

src/PowerShellEditorServices/PowerShellEditorServices.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
2121
<PackageReference Include="Serilog.Sinks.Async" Version="1.3.0" />
2222
<PackageReference Include="System.Runtime.Extensions" Version="4.3.0" />
23+
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="2.2.0" />
2324
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
2425
</ItemGroup>
2526
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">

src/PowerShellEditorServices/Workspace/Workspace.cs

+78-118
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using System.Security;
1212
using System.Text;
1313
using System.Runtime.InteropServices;
14+
using Microsoft.Extensions.FileSystemGlobbing;
15+
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
1416

1517
namespace Microsoft.PowerShell.EditorServices
1618
{
@@ -22,11 +24,28 @@ public class Workspace
2224
{
2325
#region Private Fields
2426

25-
private static readonly string[] s_psFilePatterns = new []
27+
private static readonly string[] s_psFileExtensions =
2628
{
27-
"*.ps1",
28-
"*.psm1",
29-
"*.psd1"
29+
".ps1",
30+
".psm1",
31+
".psd1"
32+
};
33+
34+
// .Net Core doesn't appear to use the same three letter pattern matching rule although the docs
35+
// suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'.
36+
// ref https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netcore-2.1#System_IO_Directory_GetFiles_System_String_System_String_System_IO_EnumerationOptions_
37+
private static readonly string[] s_psFileExtensionsDotNetFramework =
38+
{
39+
".ps1",
40+
".psm1",
41+
".psd1",
42+
".ps1xml"
43+
};
44+
45+
// An array of globs which includes everything.
46+
private static readonly string[] s_psIncludeAllGlob = new []
47+
{
48+
"**/*"
3049
};
3150

3251
private ILogger logger;
@@ -42,6 +61,16 @@ public class Workspace
4261
/// </summary>
4362
public string WorkspacePath { get; set; }
4463

64+
/// <summary>
65+
/// Gets or sets the default list of file globs to exclude during workspace searches.
66+
/// </summary>
67+
public List<string> ExcludeFilesGlob { get; set; }
68+
69+
/// <summary>
70+
/// Gets or sets whether the workspace should follow symlinks in search operations.
71+
/// </summary>
72+
public bool FollowSymlinks { get; set; }
73+
4574
#endregion
4675

4776
#region Constructors
@@ -55,6 +84,8 @@ public Workspace(Version powerShellVersion, ILogger logger)
5584
{
5685
this.powerShellVersion = powerShellVersion;
5786
this.logger = logger;
87+
this.ExcludeFilesGlob = new List<string>();
88+
this.FollowSymlinks = true;
5889
}
5990

6091
#endregion
@@ -282,135 +313,64 @@ public string GetRelativePath(string filePath)
282313
}
283314

284315
/// <summary>
285-
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner
316+
/// Whether the current runtime is Dot Net Framework as opposed to Core or Native
286317
/// </summary>
287-
/// <returns>An enumerator over the PowerShell files found in the workspace</returns>
288-
public IEnumerable<string> EnumeratePSFiles()
318+
private bool IsDotNetFrameWork()
289319
{
290-
if (WorkspacePath == null || !Directory.Exists(WorkspacePath))
291-
{
292-
return Enumerable.Empty<string>();
293-
}
294-
295-
var foundFiles = new List<string>();
296-
this.RecursivelyEnumerateFiles(WorkspacePath, ref foundFiles);
297-
return foundFiles;
320+
return RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.Ordinal);
298321
}
299322

300-
#endregion
301-
302-
#region Private Methods
303-
304323
/// <summary>
305-
/// Find PowerShell files recursively down from a given directory path.
306-
/// Currently collects files in depth-first order.
307-
/// Directory.GetFiles(folderPath, pattern, SearchOption.AllDirectories) would provide this,
308-
/// but a cycle in the filesystem will cause that to enter an infinite loop.
324+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner, using default values.
309325
/// </summary>
310-
/// <param name="folderPath">The path of the current directory to find files in</param>
311-
/// <param name="foundFiles">The accumulator for files found so far.</param>
312-
/// <param name="currDepth">The current depth of the recursion from the original base directory.</param>
313-
private void RecursivelyEnumerateFiles(string folderPath, ref List<string> foundFiles, int currDepth = 0)
326+
/// <returns>An enumerator over the PowerShell files found in the workspace.</returns>
327+
public IEnumerable<string> EnumeratePSFiles()
314328
{
315-
const int recursionDepthLimit = 64;
316-
317-
// Look for any PowerShell files in the current directory
318-
foreach (string pattern in s_psFilePatterns)
319-
{
320-
string[] psFiles;
321-
try
322-
{
323-
psFiles = Directory.GetFiles(folderPath, pattern, SearchOption.TopDirectoryOnly);
324-
}
325-
catch (DirectoryNotFoundException e)
326-
{
327-
this.logger.WriteHandledException(
328-
$"Could not enumerate files in the path '{folderPath}' due to it being an invalid path",
329-
e);
330-
331-
continue;
332-
}
333-
catch (PathTooLongException e)
334-
{
335-
this.logger.WriteHandledException(
336-
$"Could not enumerate files in the path '{folderPath}' due to the path being too long",
337-
e);
338-
339-
continue;
340-
}
341-
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
342-
{
343-
this.logger.WriteHandledException(
344-
$"Could not enumerate files in the path '{folderPath}' due to the path not being accessible",
345-
e);
346-
347-
continue;
348-
}
349-
catch (Exception e)
350-
{
351-
this.logger.WriteHandledException(
352-
$"Could not enumerate files in the path '{folderPath}' due to an exception",
353-
e);
354-
355-
continue;
356-
}
357-
358-
foundFiles.AddRange(psFiles);
359-
}
360-
361-
// Prevent unbounded recursion here
362-
if (currDepth >= recursionDepthLimit)
363-
{
364-
this.logger.Write(LogLevel.Warning, $"Recursion depth limit hit for path {folderPath}");
365-
return;
366-
}
367-
368-
// Add the recursive directories to search next
369-
string[] subDirs;
370-
try
371-
{
372-
subDirs = Directory.GetDirectories(folderPath);
373-
}
374-
catch (DirectoryNotFoundException e)
375-
{
376-
this.logger.WriteHandledException(
377-
$"Could not enumerate directories in the path '{folderPath}' due to it being an invalid path",
378-
e);
379-
380-
return;
381-
}
382-
catch (PathTooLongException e)
383-
{
384-
this.logger.WriteHandledException(
385-
$"Could not enumerate directories in the path '{folderPath}' due to the path being too long",
386-
e);
387-
388-
return;
389-
}
390-
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
391-
{
392-
this.logger.WriteHandledException(
393-
$"Could not enumerate directories in the path '{folderPath}' due to the path not being accessible",
394-
e);
329+
return EnumeratePSFiles(
330+
ExcludeFilesGlob.ToArray(),
331+
s_psIncludeAllGlob,
332+
maxDepth: 64,
333+
ignoreReparsePoints: !FollowSymlinks
334+
);
335+
}
395336

396-
return;
397-
}
398-
catch (Exception e)
337+
/// <summary>
338+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner.
339+
/// </summary>
340+
/// <returns>An enumerator over the PowerShell files found in the workspace.</returns>
341+
public IEnumerable<string> EnumeratePSFiles(
342+
string[] excludeGlobs,
343+
string[] includeGlobs,
344+
int maxDepth,
345+
bool ignoreReparsePoints
346+
)
347+
{
348+
if (WorkspacePath == null || !Directory.Exists(WorkspacePath))
399349
{
400-
this.logger.WriteHandledException(
401-
$"Could not enumerate directories in the path '{folderPath}' due to an exception",
402-
e);
403-
404-
return;
350+
yield break;
405351
}
406352

407-
408-
foreach (string subDir in subDirs)
353+
var matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher();
354+
foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); }
355+
foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); }
356+
357+
var fsFactory = new WorkspaceFileSystemWrapperFactory(
358+
WorkspacePath,
359+
maxDepth,
360+
this.IsDotNetFrameWork() ? s_psFileExtensionsDotNetFramework : s_psFileExtensions,
361+
ignoreReparsePoints,
362+
this.logger
363+
);
364+
var result = matcher.Execute(fsFactory.RootDirectory);
365+
foreach (FilePatternMatch item in result.Files)
409366
{
410-
RecursivelyEnumerateFiles(subDir, ref foundFiles, currDepth: currDepth + 1);
367+
yield return Path.Combine(WorkspacePath, item.Path.Replace('/', Path.DirectorySeparatorChar));
411368
}
412369
}
413370

371+
#endregion
372+
373+
#region Private Methods
414374
/// <summary>
415375
/// Recusrively searches through referencedFiles in scriptFiles
416376
/// and builds a Dictonary of the file references

0 commit comments

Comments
 (0)