Skip to content

Commit 5f1c377

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 5f1c377

File tree

5 files changed

+439
-117
lines changed

5 files changed

+439
-117
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

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

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

3250
private ILogger logger;
@@ -42,6 +60,16 @@ public class Workspace
4260
/// </summary>
4361
public string WorkspacePath { get; set; }
4462

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

4775
#region Constructors
@@ -55,6 +83,8 @@ public Workspace(Version powerShellVersion, ILogger logger)
5583
{
5684
this.powerShellVersion = powerShellVersion;
5785
this.logger = logger;
86+
this.ExcludeFilesGlob = new List<string>();
87+
this.FollowSymlinks = true;
5888
}
5989

6090
#endregion
@@ -282,135 +312,64 @@ public string GetRelativePath(string filePath)
282312
}
283313

284314
/// <summary>
285-
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner
315+
/// Whether the current runtime is Dot Net Framework as opposed to Core or Native
316+
/// </summary>
317+
private bool IsDotNetFrameWork()
318+
{
319+
return RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework");
320+
}
321+
322+
/// <summary>
323+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner, using default values
286324
/// </summary>
287325
/// <returns>An enumerator over the PowerShell files found in the workspace</returns>
288326
public IEnumerable<string> EnumeratePSFiles()
289327
{
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;
328+
return EnumeratePSFiles(
329+
ExcludeFilesGlob.ToArray(),
330+
s_psIncludeAllGlob,
331+
maxDepth: 64,
332+
ignoreReparsePoints: !FollowSymlinks
333+
);
298334
}
299335

300-
#endregion
301-
302-
#region Private Methods
303-
304336
/// <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.
337+
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner
309338
/// </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)
339+
/// <returns>An enumerator over the PowerShell files found in the workspace</returns>
340+
public IEnumerable<string> EnumeratePSFiles(
341+
string[] excludeGlobs,
342+
string[] includeGlobs,
343+
int maxDepth,
344+
bool ignoreReparsePoints
345+
)
314346
{
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)
347+
if (WorkspacePath == null || !Directory.Exists(WorkspacePath))
399348
{
400-
this.logger.WriteHandledException(
401-
$"Could not enumerate directories in the path '{folderPath}' due to an exception",
402-
e);
403-
404-
return;
349+
yield break;
405350
}
406351

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

370+
#endregion
371+
372+
#region Private Methods
414373
/// <summary>
415374
/// Recusrively searches through referencedFiles in scriptFiles
416375
/// and builds a Dictonary of the file references

0 commit comments

Comments
 (0)