Skip to content

Commit 8fb6bba

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 5588ba4 commit 8fb6bba

File tree

5 files changed

+518
-125
lines changed

5 files changed

+518
-125
lines changed

PowerShellEditorServices.build.ps1

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

src/PowerShellEditorServices/PowerShellEditorServices.csproj

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

src/PowerShellEditorServices/Workspace/Workspace.cs

+71-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,29 @@ public class Workspace
2224
{
2325
#region Private Fields
2426

25-
private static readonly string[] s_psFilePatterns = new []
27+
// List of all file extensions considered PowerShell files in the .Net Core Framework.
28+
private static readonly string[] s_psFileExtensionsCoreFramework =
2629
{
27-
"*.ps1",
28-
"*.psm1",
29-
"*.psd1"
30+
".ps1",
31+
".psm1",
32+
".psd1"
33+
};
34+
35+
// .Net Core doesn't appear to use the same three letter pattern matching rule although the docs
36+
// suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'.
37+
// 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_
38+
private static readonly string[] s_psFileExtensionsFullFramework =
39+
{
40+
".ps1",
41+
".psm1",
42+
".psd1",
43+
".ps1xml"
44+
};
45+
46+
// An array of globs which includes everything.
47+
private static readonly string[] s_psIncludeAllGlob = new []
48+
{
49+
"**/*"
3050
};
3151

3252
private ILogger logger;
@@ -42,6 +62,16 @@ public class Workspace
4262
/// </summary>
4363
public string WorkspacePath { get; set; }
4464

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

4777
#region Constructors
@@ -55,6 +85,8 @@ public Workspace(Version powerShellVersion, ILogger logger)
5585
{
5686
this.powerShellVersion = powerShellVersion;
5787
this.logger = logger;
88+
this.ExcludeFilesGlob = new List<string>();
89+
this.FollowSymlinks = true;
5890
}
5991

6092
#endregion
@@ -282,135 +314,56 @@ public string GetRelativePath(string filePath)
282314
}
283315

284316
/// <summary>
285-
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner
317+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner, using default values.
286318
/// </summary>
287-
/// <returns>An enumerator over the PowerShell files found in the workspace</returns>
319+
/// <returns>An enumerator over the PowerShell files found in the workspace.</returns>
288320
public IEnumerable<string> EnumeratePSFiles()
289321
{
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;
322+
return EnumeratePSFiles(
323+
ExcludeFilesGlob.ToArray(),
324+
s_psIncludeAllGlob,
325+
maxDepth: 64,
326+
ignoreReparsePoints: !FollowSymlinks
327+
);
298328
}
299329

300-
#endregion
301-
302-
#region Private Methods
303-
304330
/// <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.
331+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner.
309332
/// </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)
333+
/// <returns>An enumerator over the PowerShell files found in the workspace.</returns>
334+
public IEnumerable<string> EnumeratePSFiles(
335+
string[] excludeGlobs,
336+
string[] includeGlobs,
337+
int maxDepth,
338+
bool ignoreReparsePoints
339+
)
314340
{
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);
395-
396-
return;
397-
}
398-
catch (Exception e)
341+
if (WorkspacePath == null || !Directory.Exists(WorkspacePath))
399342
{
400-
this.logger.WriteHandledException(
401-
$"Could not enumerate directories in the path '{folderPath}' due to an exception",
402-
e);
403-
404-
return;
343+
yield break;
405344
}
406345

407-
408-
foreach (string subDir in subDirs)
346+
var matcher = new Microsoft.Extensions.FileSystemGlobbing.Matcher();
347+
foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); }
348+
foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); }
349+
350+
var fsFactory = new WorkspaceFileSystemWrapperFactory(
351+
WorkspacePath,
352+
maxDepth,
353+
Utils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework,
354+
ignoreReparsePoints,
355+
logger
356+
);
357+
var fileMatchResult = matcher.Execute(fsFactory.RootDirectory);
358+
foreach (FilePatternMatch item in fileMatchResult.Files)
409359
{
410-
RecursivelyEnumerateFiles(subDir, ref foundFiles, currDepth: currDepth + 1);
360+
yield return Path.Combine(WorkspacePath, item.Path.Replace('/', Path.DirectorySeparatorChar));
411361
}
412362
}
413363

364+
#endregion
365+
366+
#region Private Methods
414367
/// <summary>
415368
/// Recusrively searches through referencedFiles in scriptFiles
416369
/// and builds a Dictonary of the file references

0 commit comments

Comments
 (0)