MegaBites-AI/Windows-powershell
0308
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4#nullable enable5 6using System.Collections;7using System.Collections.Generic;8using System.Collections.ObjectModel;9using System.IO;10using System.Linq;11 12namespace System.Management.Automation13{14 /// <summary>15 /// Used to enumerate the commands on the system that match the specified16 /// command name.17 /// </summary>18 internal class CommandPathSearch : IEnumerable<string>, IEnumerator<string>19 {20 [TraceSource("CommandSearch", "CommandSearch")]21 private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer("CommandSearch", "CommandSearch");22 23 /// <summary>24 /// Constructs a command searching enumerator that resolves the location25 /// of a command using the PATH environment variable.26 /// </summary>27 /// <param name="commandName">28 /// The command name to search for in the path.29 /// </param>30 /// <param name="lookupPaths">31 /// The paths to directories in which to lookup the command.32 /// Ex.null: paths from PATH environment variable.33 /// </param>34 /// <param name="context">35 /// The execution context for the current engine instance.36 /// </param>37 /// <param name="acceptableCommandNames">38 /// The patterns to search for in the paths.39 /// </param>40 /// <param name="fuzzyMatcher">41 /// The fuzzy matcher to use for fuzzy searching.42 /// </param>43 internal CommandPathSearch(44 string commandName,45 LookupPathCollection lookupPaths,46 ExecutionContext context,47 Collection<string>? acceptableCommandNames,48 FuzzyMatcher? fuzzyMatcher)49 {50 _fuzzyMatcher = fuzzyMatcher;51 string[] commandPatterns;52 if (acceptableCommandNames != null)53 {54 // The name passed in is not a pattern. To minimize enumerating the file system, we55 // turn the command name into a pattern and then match against extensions in PATHEXT.56 // The old code would enumerate the file system many more times, once per possible extension.57 if (Platform.IsWindows)58 {59 commandPatterns = new[] { commandName + ".*" };60 }61 else62 {63 // Porting note: on non-Windows platforms, we want to always allow just 'commandName'64 // as an acceptable command name. However, we also want to allow commands to be65 // called with the .ps1 extension, so that 'script.ps1' can be called by 'script'.66 commandPatterns = new[] { commandName, commandName + ".ps1" };67 }68 69 _postProcessEnumeratedFiles = CheckAgainstAcceptableCommandNames;70 _acceptableCommandNames = acceptableCommandNames;71 }72 else73 {74 commandPatterns = new[] { commandName };75 _postProcessEnumeratedFiles = JustCheckExtensions;76 }77 78 // Note, discovery must be set before resolving the current directory79 _context = context;80 _patterns = commandPatterns;81 _lookupPaths = lookupPaths;82 ResolveCurrentDirectoryInLookupPaths();83 84 _orderedPathExt = CommandDiscovery.PathExtensionsWithPs1Prepended;85 86 // The same as in this.Reset()87 _lookupPathsEnumerator = _lookupPaths.GetEnumerator();88 _patternEnumerator = _patterns.GetEnumerator();89 _currentDirectoryResults = Array.Empty<string>();90 _currentDirectoryResultsEnumerator = _currentDirectoryResults.GetEnumerator();91 _justReset = true;92 }93 94 /// <summary>95 /// Ensures that all the paths in the lookupPaths member are absolute96 /// file system paths.97 /// </summary>98 private void ResolveCurrentDirectoryInLookupPaths()99 {100 var indexesToRemove = new SortedDictionary<int, int>();101 int removalListCount = 0;102 103 string fileSystemProviderName = _context.ProviderNames.FileSystem;104 105 SessionStateInternal sessionState = _context.EngineSessionState;106 107 // Only use the directory if it gets resolved by the FileSystemProvider108 bool isCurrentDriveValid =109 sessionState.CurrentDrive != null &&110 sessionState.CurrentDrive.Provider.NameEquals(fileSystemProviderName) &&111 sessionState.IsProviderLoaded(fileSystemProviderName);112 113 string? environmentCurrentDirectory = null;114 115 try116 {117 environmentCurrentDirectory = Directory.GetCurrentDirectory();118 }119 catch (FileNotFoundException)120 {121 // This can happen if the current working directory is deleted by another process on non-Windows122 // In this case, we'll just ignore it and continue on with the current directory as null123 }124 125 LocationGlobber pathResolver = _context.LocationGlobber;126 127 // Loop through the relative paths and resolve them128 129 foreach (int index in _lookupPaths.IndexOfRelativePath())130 {131 string? resolvedDirectory = null;132 string? resolvedPath = null;133 134 CommandDiscovery.discoveryTracer.WriteLine(135 "Lookup directory \"{0}\" appears to be a relative path. Attempting resolution...",136 _lookupPaths[index]);137 138 if (isCurrentDriveValid)139 {140 try141 {142 ProviderInfo provider;143 resolvedPath =144 pathResolver.GetProviderPath(145 _lookupPaths[index],146 out provider);147 }148 catch (ProviderInvocationException providerInvocationException)149 {150 CommandDiscovery.discoveryTracer.WriteLine(151 "The relative path '{0}', could not be resolved because the provider threw an exception: '{1}'",152 _lookupPaths[index],153 providerInvocationException.Message);154 }155 catch (InvalidOperationException)156 {157 CommandDiscovery.discoveryTracer.WriteLine(158 "The relative path '{0}', could not resolve a home directory for the provider",159 _lookupPaths[index]);160 }161 162 // Note, if the directory resolves to multiple paths, only the first is used.163 164 if (!string.IsNullOrEmpty(resolvedPath))165 {166 CommandDiscovery.discoveryTracer.TraceError(167 "The relative path resolved to: {0}",168 resolvedPath);169 170 resolvedDirectory = resolvedPath;171 }172 else173 {174 CommandDiscovery.discoveryTracer.WriteLine(175 "The relative path was not a file system path. {0}",176 _lookupPaths[index]);177 }178 }179 else180 {181 CommandDiscovery.discoveryTracer.TraceWarning(182 "The current drive is not set, using the process current directory: {0}",183 environmentCurrentDirectory);184 185 resolvedDirectory = environmentCurrentDirectory;186 }187 188 // If we successfully resolved the path, make sure it is unique. Remove189 // any duplicates found after the first occurrence of the path.190 191 if (resolvedDirectory != null)192 {193 int existingIndex = _lookupPaths.IndexOf(resolvedDirectory);194 195 if (existingIndex != -1)196 {197 if (existingIndex > index)198 {199 // The relative path index is less than the explicit path,200 // so remove the explicit path.201 202 indexesToRemove.Add(removalListCount++, existingIndex);203 _lookupPaths[index] = resolvedDirectory;204 }205 else206 {207 // The explicit path index is less than the relative path208 // index, so remove the relative path.209 210 indexesToRemove.Add(removalListCount++, index);211 }212 }213 else214 {215 // Change the relative path to the resolved path.216 217 _lookupPaths[index] = resolvedDirectory;218 }219 }220 else221 {222 // The directory couldn't be resolved so remove it from the223 // lookup paths.224 225 indexesToRemove.Add(removalListCount++, index);226 }227 }228 229 // Now remove all the duplicates starting from the back of the collection.230 // As each element is removed, elements that follow are moved up to occupy231 // the emptied index.232 233 for (int removeIndex = indexesToRemove.Count; removeIndex > 0; --removeIndex)234 {235 int indexToRemove = indexesToRemove[removeIndex - 1];236 _lookupPaths.RemoveAt(indexToRemove);237 }238 }239 240 /// <summary>241 /// Gets an instance of a command enumerator.242 /// </summary>243 /// <returns>244 /// An instance of this class as IEnumerator.245 /// </returns>246 IEnumerator<string> IEnumerable<string>.GetEnumerator()247 {248 return this;249 }250 251 /// <summary>252 /// Gets an instance of a command enumerator.253 /// </summary>254 /// <returns>255 /// An instance of this class as IEnumerator.256 /// </returns>257 IEnumerator IEnumerable.GetEnumerator()258 {259 return this;260 }261 262 /// <summary>263 /// Moves the enumerator to the next command match.264 /// </summary>265 /// <returns>266 /// true if there was another command that matches, false otherwise.267 /// </returns>268 public bool MoveNext()269 {270 bool result = false;271 272 if (_justReset)273 {274 _justReset = false;275 276 if (!_patternEnumerator.MoveNext())277 {278 s_tracer.TraceError("No patterns were specified");279 return false;280 }281 282 if (!_lookupPathsEnumerator.MoveNext())283 {284 s_tracer.TraceError("No lookup paths were specified");285 return false;286 }287 288 GetNewDirectoryResults(_patternEnumerator.Current, _lookupPathsEnumerator.Current);289 }290 291 while (true) // while lookupPathsEnumerator is valid292 {293 while (true) // while patternEnumerator is valid294 {295 // Try moving to the next path in the current results296 297 if (!_currentDirectoryResultsEnumerator.MoveNext())298 {299 s_tracer.WriteLine("Current directory results are invalid");300 301 // Since a path was not found in the current result,302 // advance the pattern and try again303 304 if (!_patternEnumerator.MoveNext())305 {306 s_tracer.WriteLine("Current patterns exhausted in current directory: {0}", _lookupPathsEnumerator.Current);307 break;308 }309 310 // Get the results of the next pattern311 312 GetNewDirectoryResults(_patternEnumerator.Current, _lookupPathsEnumerator.Current);313 }314 else315 {316 s_tracer.WriteLine("Next path found: {0}", _currentDirectoryResultsEnumerator.Current);317 result = true;318 break;319 }320 321 // Since we have reset the results, loop again to find the next result.322 }323 324 if (result)325 {326 break;327 }328 329 // Since the path was not found in the current results, and all patterns were exhausted,330 // advance the path and continue331 332 if (!_lookupPathsEnumerator.MoveNext())333 {334 s_tracer.WriteLine("All lookup paths exhausted, no more matches can be found");335 break;336 }337 338 // Reset the pattern enumerator and get new results using the new lookup path339 340 _patternEnumerator = _patterns.GetEnumerator();341 342 if (!_patternEnumerator.MoveNext())343 {344 s_tracer.WriteLine("All patterns exhausted, no more matches can be found");345 break;346 }347 348 GetNewDirectoryResults(_patternEnumerator.Current, _lookupPathsEnumerator.Current);349 }350 351 return result;352 }353 354 /// <summary>355 /// Resets the enumerator to before the first command match.356 /// </summary>357 public void Reset()358 {359 _lookupPathsEnumerator.Dispose();360 _lookupPathsEnumerator = _lookupPaths.GetEnumerator();361 _patternEnumerator.Dispose();362 _patternEnumerator = _patterns.GetEnumerator();363 _currentDirectoryResults = Array.Empty<string>();364 _currentDirectoryResultsEnumerator.Dispose();365 _currentDirectoryResultsEnumerator = _currentDirectoryResults.GetEnumerator();366 _justReset = true;367 }368 369 /// <summary>370 /// Gets the path to the current command match.371 /// </summary>372 /// <value></value>373 /// <exception cref="InvalidOperationException">374 /// The enumerator is positioned before the first element of375 /// the collection or after the last element.376 /// </exception>377 string IEnumerator<string>.Current378 {379 get380 {381 if (_currentDirectoryResults == null)382 {383 throw PSTraceSource.NewInvalidOperationException();384 }385 386 return _currentDirectoryResultsEnumerator.Current;387 }388 }389 390 object IEnumerator.Current391 {392 get393 {394 return ((IEnumerator<string>)this).Current;395 }396 }397 398 /// <summary>399 /// Required by the IEnumerator generic interface.400 /// Resets the searcher.401 /// </summary>402 public void Dispose()403 {404 Reset();405 GC.SuppressFinalize(this);406 }407 #region private members408 409 /// <summary>410 /// Gets the matching files in the specified directories and resets411 /// the currentDirectoryResultsEnumerator to this new set of results.412 /// </summary>413 /// <param name="pattern">414 /// The pattern used to find the matching files in the specified directory.415 /// </param>416 /// <param name="directory">417 /// The path to the directory to find the files in.418 /// </param>419 private void GetNewDirectoryResults(string pattern, string directory)420 {421 IEnumerable<string>? result = null;422 try423 {424 CommandDiscovery.discoveryTracer.WriteLine("Looking for {0} in {1}", pattern, directory);425 426 // Get the matching files in the directory427 if (Directory.Exists(directory))428 {429 // Win8 bug 92113: Directory.GetFiles() regressed in NET4430 // Directory.GetFiles(directory, ".") used to return null with CLR 2.431 // but with CLR4 it started acting like "*". This is a appcompat bug in CLR4432 // but they cannot fix it as CLR4 is already RTMd by the time this was reported.433 // If they revert it, it will become a CLR4 appcompat issue. So, using the workaround434 // to forcefully use null if pattern is "."435 if (pattern.Length != 1 || pattern[0] != '.')436 {437 if (_fuzzyMatcher is not null)438 {439 var files = new List<string>();440 var matchingFiles = Directory.EnumerateFiles(directory);441 foreach (string file in matchingFiles)442 {443 if (_fuzzyMatcher.IsFuzzyMatch(Path.GetFileName(file), pattern))444 {445 files.Add(file);446 }447 }448 449 result = _postProcessEnumeratedFiles != null450 ? _postProcessEnumeratedFiles(files.ToArray())451 : files;452 }453 else454 {455 var matchingFiles = Directory.EnumerateFiles(directory, pattern);456 result = _postProcessEnumeratedFiles != null457 ? _postProcessEnumeratedFiles(matchingFiles.ToArray())458 : matchingFiles;459 }460 }461 }462 }463 catch (ArgumentException)464 {465 // The pattern contained illegal file system characters466 }467 catch (IOException)468 {469 // A directory specified in the lookup path was not470 // accessible471 }472 catch (UnauthorizedAccessException)473 {474 // A directory specified in the lookup path was not475 // accessible476 }477 catch (NotSupportedException)478 {479 // A directory specified in the lookup path was not480 // accessible481 }482 483 _currentDirectoryResults = result ?? Array.Empty<string>();484 _currentDirectoryResultsEnumerator = _currentDirectoryResults.GetEnumerator();485 }486 487 private IEnumerable<string>? CheckAgainstAcceptableCommandNames(string[] fileNames)488 {489 var baseNames = fileNames.Select(Path.GetFileName).ToArray();490 491 // Result must be ordered by PATHEXT order of precedence.492 // acceptableCommandNames is in this order, so493 494 // Porting note: allow files with executable bit on non-Windows platforms495 496 Collection<string>? result = null;497 if (baseNames.Length > 0 && _acceptableCommandNames != null)498 {499 foreach (var name in _acceptableCommandNames)500 {501 for (int i = 0; i < baseNames.Length; i++)502 {503 if (name.Equals(baseNames[i], StringComparison.OrdinalIgnoreCase)504 || (!Platform.IsWindows && Platform.NonWindowsIsExecutable(name)))505 {506 result ??= new Collection<string>();507 result.Add(fileNames[i]);508 break;509 }510 }511 }512 }513 514 return result;515 }516 517 private IEnumerable<string>? JustCheckExtensions(string[] fileNames)518 {519 // Warning: pretty duplicated code520 // Result must be ordered by PATHEXT order of precedence.521 522 // Porting note: allow files with executable bit on non-Windows platforms523 524 Collection<string>? result = null;525 foreach (var allowedExt in _orderedPathExt)526 {527 foreach (var fileName in fileNames)528 {529 if (fileName.EndsWith(allowedExt, StringComparison.OrdinalIgnoreCase)530 || (!Platform.IsWindows && Platform.NonWindowsIsExecutable(fileName)))531 {532 result ??= new Collection<string>();533 result.Add(fileName);534 }535 }536 }537 538 return result;539 }540 541 /// <summary>542 /// The directory paths in which to look for commands.543 /// This is derived from the PATH environment variable.544 /// </summary>545 private readonly LookupPathCollection _lookupPaths;546 547 /// <summary>548 /// The enumerator for the lookup paths.549 /// </summary>550 private IEnumerator<string> _lookupPathsEnumerator;551 552 /// <summary>553 /// The list of results matching the pattern in the current554 /// path lookup directory. Resets to null.555 /// </summary>556 private IEnumerable<string> _currentDirectoryResults;557 558 /// <summary>559 /// The enumerator for the list of results.560 /// </summary>561 private IEnumerator<string> _currentDirectoryResultsEnumerator;562 563 /// <summary>564 /// The command name to search for.565 /// </summary>566 private readonly IEnumerable<string> _patterns;567 568 /// <summary>569 /// The enumerator for the patterns.570 /// </summary>571 private IEnumerator<string> _patternEnumerator;572 573 /// <summary>574 /// A reference to the execution context for this runspace.575 /// </summary>576 private readonly ExecutionContext _context;577 578 /// <summary>579 /// When reset is called, this gets set to true. Once MoveNext580 /// is called, this gets set to false.581 /// </summary>582 private bool _justReset;583 584 /// <summary>585 /// If not null, called with the enumerated files for further processing.586 /// </summary>587 private readonly Func<string[], IEnumerable<string>?> _postProcessEnumeratedFiles;588 589 private readonly string[] _orderedPathExt;590 private readonly Collection<string>? _acceptableCommandNames;591 592 private readonly FuzzyMatcher? _fuzzyMatcher;593 594 #endregion private members595 }596}597 