mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
597 lines
22 KiB
C#
597 lines
22 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
#nullable enable
|
|
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
namespace System.Management.Automation
|
|
{
|
|
/// <summary>
|
|
/// Used to enumerate the commands on the system that match the specified
|
|
/// command name.
|
|
/// </summary>
|
|
internal class CommandPathSearch : IEnumerable<string>, IEnumerator<string>
|
|
{
|
|
[TraceSource("CommandSearch", "CommandSearch")]
|
|
private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer("CommandSearch", "CommandSearch");
|
|
|
|
/// <summary>
|
|
/// Constructs a command searching enumerator that resolves the location
|
|
/// of a command using the PATH environment variable.
|
|
/// </summary>
|
|
/// <param name="commandName">
|
|
/// The command name to search for in the path.
|
|
/// </param>
|
|
/// <param name="lookupPaths">
|
|
/// The paths to directories in which to lookup the command.
|
|
/// Ex.null: paths from PATH environment variable.
|
|
/// </param>
|
|
/// <param name="context">
|
|
/// The execution context for the current engine instance.
|
|
/// </param>
|
|
/// <param name="acceptableCommandNames">
|
|
/// The patterns to search for in the paths.
|
|
/// </param>
|
|
/// <param name="fuzzyMatcher">
|
|
/// The fuzzy matcher to use for fuzzy searching.
|
|
/// </param>
|
|
internal CommandPathSearch(
|
|
string commandName,
|
|
LookupPathCollection lookupPaths,
|
|
ExecutionContext context,
|
|
Collection<string>? acceptableCommandNames,
|
|
FuzzyMatcher? fuzzyMatcher)
|
|
{
|
|
_fuzzyMatcher = fuzzyMatcher;
|
|
string[] commandPatterns;
|
|
if (acceptableCommandNames != null)
|
|
{
|
|
// The name passed in is not a pattern. To minimize enumerating the file system, we
|
|
// turn the command name into a pattern and then match against extensions in PATHEXT.
|
|
// The old code would enumerate the file system many more times, once per possible extension.
|
|
if (Platform.IsWindows)
|
|
{
|
|
commandPatterns = new[] { commandName + ".*" };
|
|
}
|
|
else
|
|
{
|
|
// Porting note: on non-Windows platforms, we want to always allow just 'commandName'
|
|
// as an acceptable command name. However, we also want to allow commands to be
|
|
// called with the .ps1 extension, so that 'script.ps1' can be called by 'script'.
|
|
commandPatterns = new[] { commandName, commandName + ".ps1" };
|
|
}
|
|
|
|
_postProcessEnumeratedFiles = CheckAgainstAcceptableCommandNames;
|
|
_acceptableCommandNames = acceptableCommandNames;
|
|
}
|
|
else
|
|
{
|
|
commandPatterns = new[] { commandName };
|
|
_postProcessEnumeratedFiles = JustCheckExtensions;
|
|
}
|
|
|
|
// Note, discovery must be set before resolving the current directory
|
|
_context = context;
|
|
_patterns = commandPatterns;
|
|
_lookupPaths = lookupPaths;
|
|
ResolveCurrentDirectoryInLookupPaths();
|
|
|
|
_orderedPathExt = CommandDiscovery.PathExtensionsWithPs1Prepended;
|
|
|
|
// The same as in this.Reset()
|
|
_lookupPathsEnumerator = _lookupPaths.GetEnumerator();
|
|
_patternEnumerator = _patterns.GetEnumerator();
|
|
_currentDirectoryResults = Array.Empty<string>();
|
|
_currentDirectoryResultsEnumerator = _currentDirectoryResults.GetEnumerator();
|
|
_justReset = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures that all the paths in the lookupPaths member are absolute
|
|
/// file system paths.
|
|
/// </summary>
|
|
private void ResolveCurrentDirectoryInLookupPaths()
|
|
{
|
|
var indexesToRemove = new SortedDictionary<int, int>();
|
|
int removalListCount = 0;
|
|
|
|
string fileSystemProviderName = _context.ProviderNames.FileSystem;
|
|
|
|
SessionStateInternal sessionState = _context.EngineSessionState;
|
|
|
|
// Only use the directory if it gets resolved by the FileSystemProvider
|
|
bool isCurrentDriveValid =
|
|
sessionState.CurrentDrive != null &&
|
|
sessionState.CurrentDrive.Provider.NameEquals(fileSystemProviderName) &&
|
|
sessionState.IsProviderLoaded(fileSystemProviderName);
|
|
|
|
string? environmentCurrentDirectory = null;
|
|
|
|
try
|
|
{
|
|
environmentCurrentDirectory = Directory.GetCurrentDirectory();
|
|
}
|
|
catch (FileNotFoundException)
|
|
{
|
|
// This can happen if the current working directory is deleted by another process on non-Windows
|
|
// In this case, we'll just ignore it and continue on with the current directory as null
|
|
}
|
|
|
|
LocationGlobber pathResolver = _context.LocationGlobber;
|
|
|
|
// Loop through the relative paths and resolve them
|
|
|
|
foreach (int index in _lookupPaths.IndexOfRelativePath())
|
|
{
|
|
string? resolvedDirectory = null;
|
|
string? resolvedPath = null;
|
|
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Lookup directory \"{0}\" appears to be a relative path. Attempting resolution...",
|
|
_lookupPaths[index]);
|
|
|
|
if (isCurrentDriveValid)
|
|
{
|
|
try
|
|
{
|
|
ProviderInfo provider;
|
|
resolvedPath =
|
|
pathResolver.GetProviderPath(
|
|
_lookupPaths[index],
|
|
out provider);
|
|
}
|
|
catch (ProviderInvocationException providerInvocationException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"The relative path '{0}', could not be resolved because the provider threw an exception: '{1}'",
|
|
_lookupPaths[index],
|
|
providerInvocationException.Message);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"The relative path '{0}', could not resolve a home directory for the provider",
|
|
_lookupPaths[index]);
|
|
}
|
|
|
|
// Note, if the directory resolves to multiple paths, only the first is used.
|
|
|
|
if (!string.IsNullOrEmpty(resolvedPath))
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The relative path resolved to: {0}",
|
|
resolvedPath);
|
|
|
|
resolvedDirectory = resolvedPath;
|
|
}
|
|
else
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"The relative path was not a file system path. {0}",
|
|
_lookupPaths[index]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceWarning(
|
|
"The current drive is not set, using the process current directory: {0}",
|
|
environmentCurrentDirectory);
|
|
|
|
resolvedDirectory = environmentCurrentDirectory;
|
|
}
|
|
|
|
// If we successfully resolved the path, make sure it is unique. Remove
|
|
// any duplicates found after the first occurrence of the path.
|
|
|
|
if (resolvedDirectory != null)
|
|
{
|
|
int existingIndex = _lookupPaths.IndexOf(resolvedDirectory);
|
|
|
|
if (existingIndex != -1)
|
|
{
|
|
if (existingIndex > index)
|
|
{
|
|
// The relative path index is less than the explicit path,
|
|
// so remove the explicit path.
|
|
|
|
indexesToRemove.Add(removalListCount++, existingIndex);
|
|
_lookupPaths[index] = resolvedDirectory;
|
|
}
|
|
else
|
|
{
|
|
// The explicit path index is less than the relative path
|
|
// index, so remove the relative path.
|
|
|
|
indexesToRemove.Add(removalListCount++, index);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Change the relative path to the resolved path.
|
|
|
|
_lookupPaths[index] = resolvedDirectory;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The directory couldn't be resolved so remove it from the
|
|
// lookup paths.
|
|
|
|
indexesToRemove.Add(removalListCount++, index);
|
|
}
|
|
}
|
|
|
|
// Now remove all the duplicates starting from the back of the collection.
|
|
// As each element is removed, elements that follow are moved up to occupy
|
|
// the emptied index.
|
|
|
|
for (int removeIndex = indexesToRemove.Count; removeIndex > 0; --removeIndex)
|
|
{
|
|
int indexToRemove = indexesToRemove[removeIndex - 1];
|
|
_lookupPaths.RemoveAt(indexToRemove);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an instance of a command enumerator.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// An instance of this class as IEnumerator.
|
|
/// </returns>
|
|
IEnumerator<string> IEnumerable<string>.GetEnumerator()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an instance of a command enumerator.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// An instance of this class as IEnumerator.
|
|
/// </returns>
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the enumerator to the next command match.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true if there was another command that matches, false otherwise.
|
|
/// </returns>
|
|
public bool MoveNext()
|
|
{
|
|
bool result = false;
|
|
|
|
if (_justReset)
|
|
{
|
|
_justReset = false;
|
|
|
|
if (!_patternEnumerator.MoveNext())
|
|
{
|
|
s_tracer.TraceError("No patterns were specified");
|
|
return false;
|
|
}
|
|
|
|
if (!_lookupPathsEnumerator.MoveNext())
|
|
{
|
|
s_tracer.TraceError("No lookup paths were specified");
|
|
return false;
|
|
}
|
|
|
|
GetNewDirectoryResults(_patternEnumerator.Current, _lookupPathsEnumerator.Current);
|
|
}
|
|
|
|
while (true) // while lookupPathsEnumerator is valid
|
|
{
|
|
while (true) // while patternEnumerator is valid
|
|
{
|
|
// Try moving to the next path in the current results
|
|
|
|
if (!_currentDirectoryResultsEnumerator.MoveNext())
|
|
{
|
|
s_tracer.WriteLine("Current directory results are invalid");
|
|
|
|
// Since a path was not found in the current result,
|
|
// advance the pattern and try again
|
|
|
|
if (!_patternEnumerator.MoveNext())
|
|
{
|
|
s_tracer.WriteLine("Current patterns exhausted in current directory: {0}", _lookupPathsEnumerator.Current);
|
|
break;
|
|
}
|
|
|
|
// Get the results of the next pattern
|
|
|
|
GetNewDirectoryResults(_patternEnumerator.Current, _lookupPathsEnumerator.Current);
|
|
}
|
|
else
|
|
{
|
|
s_tracer.WriteLine("Next path found: {0}", _currentDirectoryResultsEnumerator.Current);
|
|
result = true;
|
|
break;
|
|
}
|
|
|
|
// Since we have reset the results, loop again to find the next result.
|
|
}
|
|
|
|
if (result)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Since the path was not found in the current results, and all patterns were exhausted,
|
|
// advance the path and continue
|
|
|
|
if (!_lookupPathsEnumerator.MoveNext())
|
|
{
|
|
s_tracer.WriteLine("All lookup paths exhausted, no more matches can be found");
|
|
break;
|
|
}
|
|
|
|
// Reset the pattern enumerator and get new results using the new lookup path
|
|
|
|
_patternEnumerator = _patterns.GetEnumerator();
|
|
|
|
if (!_patternEnumerator.MoveNext())
|
|
{
|
|
s_tracer.WriteLine("All patterns exhausted, no more matches can be found");
|
|
break;
|
|
}
|
|
|
|
GetNewDirectoryResults(_patternEnumerator.Current, _lookupPathsEnumerator.Current);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the enumerator to before the first command match.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
_lookupPathsEnumerator.Dispose();
|
|
_lookupPathsEnumerator = _lookupPaths.GetEnumerator();
|
|
_patternEnumerator.Dispose();
|
|
_patternEnumerator = _patterns.GetEnumerator();
|
|
_currentDirectoryResults = Array.Empty<string>();
|
|
_currentDirectoryResultsEnumerator.Dispose();
|
|
_currentDirectoryResultsEnumerator = _currentDirectoryResults.GetEnumerator();
|
|
_justReset = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the path to the current command match.
|
|
/// </summary>
|
|
/// <value></value>
|
|
/// <exception cref="InvalidOperationException">
|
|
/// The enumerator is positioned before the first element of
|
|
/// the collection or after the last element.
|
|
/// </exception>
|
|
string IEnumerator<string>.Current
|
|
{
|
|
get
|
|
{
|
|
if (_currentDirectoryResults == null)
|
|
{
|
|
throw PSTraceSource.NewInvalidOperationException();
|
|
}
|
|
|
|
return _currentDirectoryResultsEnumerator.Current;
|
|
}
|
|
}
|
|
|
|
object IEnumerator.Current
|
|
{
|
|
get
|
|
{
|
|
return ((IEnumerator<string>)this).Current;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Required by the IEnumerator generic interface.
|
|
/// Resets the searcher.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Reset();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
#region private members
|
|
|
|
/// <summary>
|
|
/// Gets the matching files in the specified directories and resets
|
|
/// the currentDirectoryResultsEnumerator to this new set of results.
|
|
/// </summary>
|
|
/// <param name="pattern">
|
|
/// The pattern used to find the matching files in the specified directory.
|
|
/// </param>
|
|
/// <param name="directory">
|
|
/// The path to the directory to find the files in.
|
|
/// </param>
|
|
private void GetNewDirectoryResults(string pattern, string directory)
|
|
{
|
|
IEnumerable<string>? result = null;
|
|
try
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine("Looking for {0} in {1}", pattern, directory);
|
|
|
|
// Get the matching files in the directory
|
|
if (Directory.Exists(directory))
|
|
{
|
|
// Win8 bug 92113: Directory.GetFiles() regressed in NET4
|
|
// Directory.GetFiles(directory, ".") used to return null with CLR 2.
|
|
// but with CLR4 it started acting like "*". This is a appcompat bug in CLR4
|
|
// but they cannot fix it as CLR4 is already RTMd by the time this was reported.
|
|
// If they revert it, it will become a CLR4 appcompat issue. So, using the workaround
|
|
// to forcefully use null if pattern is "."
|
|
if (pattern.Length != 1 || pattern[0] != '.')
|
|
{
|
|
if (_fuzzyMatcher is not null)
|
|
{
|
|
var files = new List<string>();
|
|
var matchingFiles = Directory.EnumerateFiles(directory);
|
|
foreach (string file in matchingFiles)
|
|
{
|
|
if (_fuzzyMatcher.IsFuzzyMatch(Path.GetFileName(file), pattern))
|
|
{
|
|
files.Add(file);
|
|
}
|
|
}
|
|
|
|
result = _postProcessEnumeratedFiles != null
|
|
? _postProcessEnumeratedFiles(files.ToArray())
|
|
: files;
|
|
}
|
|
else
|
|
{
|
|
var matchingFiles = Directory.EnumerateFiles(directory, pattern);
|
|
result = _postProcessEnumeratedFiles != null
|
|
? _postProcessEnumeratedFiles(matchingFiles.ToArray())
|
|
: matchingFiles;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// The pattern contained illegal file system characters
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// A directory specified in the lookup path was not
|
|
// accessible
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
// A directory specified in the lookup path was not
|
|
// accessible
|
|
}
|
|
catch (NotSupportedException)
|
|
{
|
|
// A directory specified in the lookup path was not
|
|
// accessible
|
|
}
|
|
|
|
_currentDirectoryResults = result ?? Array.Empty<string>();
|
|
_currentDirectoryResultsEnumerator = _currentDirectoryResults.GetEnumerator();
|
|
}
|
|
|
|
private IEnumerable<string>? CheckAgainstAcceptableCommandNames(string[] fileNames)
|
|
{
|
|
var baseNames = fileNames.Select(Path.GetFileName).ToArray();
|
|
|
|
// Result must be ordered by PATHEXT order of precedence.
|
|
// acceptableCommandNames is in this order, so
|
|
|
|
// Porting note: allow files with executable bit on non-Windows platforms
|
|
|
|
Collection<string>? result = null;
|
|
if (baseNames.Length > 0 && _acceptableCommandNames != null)
|
|
{
|
|
foreach (var name in _acceptableCommandNames)
|
|
{
|
|
for (int i = 0; i < baseNames.Length; i++)
|
|
{
|
|
if (name.Equals(baseNames[i], StringComparison.OrdinalIgnoreCase)
|
|
|| (!Platform.IsWindows && Platform.NonWindowsIsExecutable(name)))
|
|
{
|
|
result ??= new Collection<string>();
|
|
result.Add(fileNames[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private IEnumerable<string>? JustCheckExtensions(string[] fileNames)
|
|
{
|
|
// Warning: pretty duplicated code
|
|
// Result must be ordered by PATHEXT order of precedence.
|
|
|
|
// Porting note: allow files with executable bit on non-Windows platforms
|
|
|
|
Collection<string>? result = null;
|
|
foreach (var allowedExt in _orderedPathExt)
|
|
{
|
|
foreach (var fileName in fileNames)
|
|
{
|
|
if (fileName.EndsWith(allowedExt, StringComparison.OrdinalIgnoreCase)
|
|
|| (!Platform.IsWindows && Platform.NonWindowsIsExecutable(fileName)))
|
|
{
|
|
result ??= new Collection<string>();
|
|
result.Add(fileName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The directory paths in which to look for commands.
|
|
/// This is derived from the PATH environment variable.
|
|
/// </summary>
|
|
private readonly LookupPathCollection _lookupPaths;
|
|
|
|
/// <summary>
|
|
/// The enumerator for the lookup paths.
|
|
/// </summary>
|
|
private IEnumerator<string> _lookupPathsEnumerator;
|
|
|
|
/// <summary>
|
|
/// The list of results matching the pattern in the current
|
|
/// path lookup directory. Resets to null.
|
|
/// </summary>
|
|
private IEnumerable<string> _currentDirectoryResults;
|
|
|
|
/// <summary>
|
|
/// The enumerator for the list of results.
|
|
/// </summary>
|
|
private IEnumerator<string> _currentDirectoryResultsEnumerator;
|
|
|
|
/// <summary>
|
|
/// The command name to search for.
|
|
/// </summary>
|
|
private readonly IEnumerable<string> _patterns;
|
|
|
|
/// <summary>
|
|
/// The enumerator for the patterns.
|
|
/// </summary>
|
|
private IEnumerator<string> _patternEnumerator;
|
|
|
|
/// <summary>
|
|
/// A reference to the execution context for this runspace.
|
|
/// </summary>
|
|
private readonly ExecutionContext _context;
|
|
|
|
/// <summary>
|
|
/// When reset is called, this gets set to true. Once MoveNext
|
|
/// is called, this gets set to false.
|
|
/// </summary>
|
|
private bool _justReset;
|
|
|
|
/// <summary>
|
|
/// If not null, called with the enumerated files for further processing.
|
|
/// </summary>
|
|
private readonly Func<string[], IEnumerable<string>?> _postProcessEnumeratedFiles;
|
|
|
|
private readonly string[] _orderedPathExt;
|
|
private readonly Collection<string>? _acceptableCommandNames;
|
|
|
|
private readonly FuzzyMatcher? _fuzzyMatcher;
|
|
|
|
#endregion private members
|
|
}
|
|
}
|