mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
1734 lines
64 KiB
C#
1734 lines
64 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.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Management.Automation.Internal;
|
|
|
|
using Dbg = System.Management.Automation.Diagnostics;
|
|
|
|
namespace System.Management.Automation
|
|
{
|
|
/// <summary>
|
|
/// Used to enumerate the commands on the system that match the specified
|
|
/// command name.
|
|
/// </summary>
|
|
internal class CommandSearcher : IEnumerable<CommandInfo>, IEnumerator<CommandInfo>
|
|
{
|
|
/// <summary>
|
|
/// Constructs a command searching enumerator that resolves the location
|
|
/// to a command using a standard algorithm.
|
|
/// </summary>
|
|
/// <param name="commandName">The name of the command to look for.</param>
|
|
/// <param name="options">Determines which types of commands glob resolution of the name will take place on.</param>
|
|
/// <param name="commandTypes">The types of commands to look for.</param>
|
|
/// <param name="context">The execution context for this engine instance.</param>
|
|
/// <param name="fuzzyMatcher">The fuzzy matcher to use for fuzzy searching.</param>
|
|
///
|
|
/// <exception cref="ArgumentNullException">If <paramref name="context"/> is null.</exception>
|
|
/// <exception cref="PSArgumentException">If <paramref name="commandName"/> is null or empty.</exception>
|
|
internal CommandSearcher(
|
|
string commandName,
|
|
SearchResolutionOptions options,
|
|
CommandTypes commandTypes,
|
|
ExecutionContext context,
|
|
FuzzyMatcher? fuzzyMatcher = null)
|
|
{
|
|
Diagnostics.Assert(context != null, "caller to verify context is not null");
|
|
Diagnostics.Assert(!string.IsNullOrEmpty(commandName), "caller to verify commandName is valid");
|
|
|
|
_commandName = commandName;
|
|
_context = context;
|
|
_commandResolutionOptions = options;
|
|
_commandTypes = commandTypes;
|
|
_fuzzyMatcher = fuzzyMatcher;
|
|
|
|
// Initialize the enumerators
|
|
this.Reset();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an instance of a command enumerator.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// An instance of this class as IEnumerator.
|
|
/// </returns>
|
|
IEnumerator<CommandInfo> IEnumerable<CommandInfo>.GetEnumerator()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the enumerator to the next command match. Public for IEnumerable.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// true if there was another command that matches, false otherwise.
|
|
/// </returns>
|
|
public bool MoveNext()
|
|
{
|
|
_currentMatch = null;
|
|
|
|
if (_currentState == SearchState.SearchingAliases)
|
|
{
|
|
_currentMatch = SearchForAliases();
|
|
|
|
// Why don't we check IsVisible on other scoped items?
|
|
if (_currentMatch != null && SessionState.IsVisible(_commandOrigin, _currentMatch))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Make sure Current doesn't return an alias that isn't visible
|
|
_currentMatch = null;
|
|
|
|
// Advance the state
|
|
_currentState = SearchState.SearchingFunctions;
|
|
}
|
|
|
|
if (_currentState == SearchState.SearchingFunctions)
|
|
{
|
|
_currentMatch = SearchForFunctions();
|
|
// Return the alias info only if it is visible. If not, then skip to the next
|
|
// stage of command resolution...
|
|
if (_currentMatch != null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Advance the state
|
|
_currentState = SearchState.SearchingCmdlets;
|
|
}
|
|
|
|
if (_currentState == SearchState.SearchingCmdlets)
|
|
{
|
|
_currentMatch = SearchForCmdlets();
|
|
if (_currentMatch != null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Advance the state
|
|
_currentState = SearchState.StartSearchingForExternalCommands;
|
|
}
|
|
|
|
if (_currentState == SearchState.StartSearchingForExternalCommands)
|
|
{
|
|
if ((_commandTypes & (CommandTypes.Application | CommandTypes.ExternalScript)) == 0)
|
|
{
|
|
// Since we are not requiring any path lookup in this search, just return false now
|
|
// because all the remaining searches do path lookup.
|
|
return false;
|
|
}
|
|
|
|
// For security reasons, if the command is coming from outside the runspace and it looks like a path,
|
|
// we want to pre-check that path before doing any probing of the network or drives
|
|
if (_commandOrigin == CommandOrigin.Runspace && _commandName.IndexOfAny(Utils.Separators.DirectoryOrDrive) >= 0)
|
|
{
|
|
bool allowed = false;
|
|
|
|
// Ok - it looks like it might be a path, so we're going to check to see if the command is prefixed
|
|
// by any of the allowed paths. If so, then we allow the search to proceed...
|
|
|
|
// If either the Applications or Script lists contain just '*' the command is allowed
|
|
// at this point.
|
|
if ((_context.EngineSessionState.Applications.Count == 1 &&
|
|
_context.EngineSessionState.Applications[0].Equals("*", StringComparison.OrdinalIgnoreCase)) ||
|
|
(_context.EngineSessionState.Scripts.Count == 1 &&
|
|
_context.EngineSessionState.Scripts[0].Equals("*", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
allowed = true;
|
|
}
|
|
else
|
|
{
|
|
// Ok, see if it's in the applications list
|
|
foreach (string path in _context.EngineSessionState.Applications)
|
|
{
|
|
if (checkPath(path, _commandName))
|
|
{
|
|
allowed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If it wasn't in the applications list, see it's in the script list
|
|
if (!allowed)
|
|
{
|
|
foreach (string path in _context.EngineSessionState.Scripts)
|
|
{
|
|
if (checkPath(path, _commandName))
|
|
{
|
|
allowed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!allowed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Advance the state
|
|
|
|
_currentState = SearchState.PowerShellPathResolution;
|
|
|
|
_currentMatch = ProcessBuiltinScriptState();
|
|
|
|
if (_currentMatch != null)
|
|
{
|
|
// Set the current state to QualifiedFileSystemPath since
|
|
// we want to skip the qualified file system path search
|
|
// in the case where we found a PowerShell qualified path.
|
|
|
|
_currentState = SearchState.QualifiedFileSystemPath;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (_currentState == SearchState.PowerShellPathResolution)
|
|
{
|
|
_currentState = SearchState.QualifiedFileSystemPath;
|
|
|
|
_currentMatch = ProcessPathResolutionState();
|
|
|
|
if (_currentMatch != null)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Search using CommandPathSearch
|
|
|
|
if (_currentState == SearchState.QualifiedFileSystemPath ||
|
|
_currentState == SearchState.PathSearch)
|
|
{
|
|
_currentMatch = ProcessQualifiedFileSystemState();
|
|
|
|
if (_currentMatch != null)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (_currentState == SearchState.PathSearch)
|
|
{
|
|
_currentState = SearchState.PowerShellRelativePath;
|
|
|
|
_currentMatch = ProcessPathSearchState();
|
|
|
|
if (_currentMatch != null)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private CommandInfo? SearchForAliases()
|
|
{
|
|
CommandInfo? currentMatch = null;
|
|
|
|
if (_context.EngineSessionState != null &&
|
|
(_commandTypes & CommandTypes.Alias) != 0)
|
|
{
|
|
currentMatch = GetNextAlias();
|
|
}
|
|
|
|
return currentMatch;
|
|
}
|
|
|
|
private CommandInfo? SearchForFunctions()
|
|
{
|
|
CommandInfo? currentMatch = null;
|
|
|
|
if (_context.EngineSessionState != null &&
|
|
(_commandTypes & (CommandTypes.Function | CommandTypes.Filter | CommandTypes.Configuration)) != 0)
|
|
{
|
|
currentMatch = GetNextFunction();
|
|
}
|
|
|
|
return currentMatch;
|
|
}
|
|
|
|
private CommandInfo? SearchForCmdlets()
|
|
{
|
|
CommandInfo? currentMatch = null;
|
|
|
|
if ((_commandTypes & CommandTypes.Cmdlet) != 0)
|
|
{
|
|
currentMatch = GetNextCmdlet();
|
|
}
|
|
|
|
return currentMatch;
|
|
}
|
|
|
|
private CommandInfo? ProcessBuiltinScriptState()
|
|
{
|
|
CommandInfo? currentMatch = null;
|
|
|
|
// Check to see if the path is qualified
|
|
|
|
if (_context.EngineSessionState != null &&
|
|
_context.EngineSessionState.ProviderCount > 0 &&
|
|
IsQualifiedPSPath(_commandName))
|
|
{
|
|
currentMatch = GetNextFromPath();
|
|
}
|
|
|
|
return currentMatch;
|
|
}
|
|
|
|
private CommandInfo? ProcessPathResolutionState()
|
|
{
|
|
CommandInfo? currentMatch = null;
|
|
|
|
try
|
|
{
|
|
// Check to see if the path is a file system path that
|
|
// is rooted. If so that is the next match
|
|
if (Path.IsPathRooted(_commandName) &&
|
|
File.Exists(_commandName))
|
|
{
|
|
try
|
|
{
|
|
currentMatch = GetInfoFromPath(_commandName);
|
|
}
|
|
catch (FileLoadException)
|
|
{
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
}
|
|
catch (MetadataException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// If the path contains illegal characters that
|
|
// weren't caught by the other APIs, IsPathRooted
|
|
// will throw an exception.
|
|
// For example, looking for a command called
|
|
// `abcdef
|
|
// The `a will be translated into the beep control character
|
|
// which is not a legal file system character, though
|
|
// Path.InvalidPathChars does not contain it as an invalid
|
|
// character.
|
|
}
|
|
|
|
return currentMatch;
|
|
}
|
|
|
|
private CommandInfo? ProcessQualifiedFileSystemState()
|
|
{
|
|
try
|
|
{
|
|
setupPathSearcher();
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
_currentState = SearchState.NoMoreMatches;
|
|
throw;
|
|
}
|
|
catch (PathTooLongException)
|
|
{
|
|
_currentState = SearchState.NoMoreMatches;
|
|
throw;
|
|
}
|
|
|
|
CommandInfo? currentMatch = null;
|
|
_currentState = SearchState.PathSearch;
|
|
if (_canDoPathLookup)
|
|
{
|
|
try
|
|
{
|
|
// the previous call to setupPathSearcher ensures _pathSearcher != null
|
|
while (currentMatch == null && _pathSearcher!.MoveNext())
|
|
{
|
|
currentMatch = GetInfoFromPath(((IEnumerator<string>)_pathSearcher).Current);
|
|
}
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// The enumerator may throw if there are no more matches
|
|
}
|
|
}
|
|
|
|
return currentMatch;
|
|
}
|
|
|
|
private CommandInfo? ProcessPathSearchState()
|
|
{
|
|
CommandInfo? currentMatch = null;
|
|
string? path = DoPowerShellRelativePathLookup();
|
|
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
currentMatch = GetInfoFromPath(path);
|
|
}
|
|
|
|
return currentMatch;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the CommandInfo representing 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>
|
|
CommandInfo IEnumerator<CommandInfo>.Current
|
|
{
|
|
get
|
|
{
|
|
if ((_currentState == SearchState.SearchingAliases && _currentMatch == null) ||
|
|
_currentState == SearchState.NoMoreMatches ||
|
|
_currentMatch == null)
|
|
{
|
|
throw PSTraceSource.NewInvalidOperationException();
|
|
}
|
|
|
|
return _currentMatch;
|
|
}
|
|
}
|
|
|
|
object IEnumerator.Current
|
|
{
|
|
get
|
|
{
|
|
return ((IEnumerator<CommandInfo>)this).Current;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Required by the IEnumerator generic interface.
|
|
/// Resets the searcher.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
if (_pathSearcher != null)
|
|
{
|
|
_pathSearcher.Dispose();
|
|
_pathSearcher = null;
|
|
}
|
|
|
|
Reset();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
#region private members
|
|
|
|
/// <summary>
|
|
/// Gets the next command info using the command name as a path.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A CommandInfo for the next command if it exists as a path, or null otherwise.
|
|
/// </returns>
|
|
private CommandInfo? GetNextFromPath()
|
|
{
|
|
CommandInfo? result = null;
|
|
|
|
do // false loop
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"The name appears to be a qualified path: {0}",
|
|
_commandName);
|
|
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Trying to resolve the path as an PSPath");
|
|
|
|
// Find the match if it is.
|
|
// Try literal path resolution if it is set to run first
|
|
if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveLiteralThenPathPatterns))
|
|
{
|
|
var path = GetNextLiteralPathThatExistsAndHandleExceptions(_commandName, out _);
|
|
|
|
if (path != null)
|
|
{
|
|
return GetInfoFromPath(path);
|
|
}
|
|
}
|
|
|
|
Collection<string> resolvedPaths = new Collection<string>();
|
|
if (WildcardPattern.ContainsWildcardCharacters(_commandName))
|
|
{
|
|
resolvedPaths = GetNextFromPathUsingWildcards(_commandName, out _);
|
|
}
|
|
|
|
// Try literal path resolution if wildcards are enable first and wildcard search failed
|
|
if (!_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveLiteralThenPathPatterns) &&
|
|
resolvedPaths.Count == 0)
|
|
{
|
|
string? path = GetNextLiteralPathThatExistsAndHandleExceptions(_commandName, out _);
|
|
|
|
if (path != null)
|
|
{
|
|
return GetInfoFromPath(path);
|
|
}
|
|
}
|
|
|
|
if (resolvedPaths.Count > 1)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The path resolved to more than one result so this path cannot be used.");
|
|
break;
|
|
}
|
|
|
|
// If the path was resolved, and it exists
|
|
if (resolvedPaths.Count == 1 &&
|
|
File.Exists(resolvedPaths[0]))
|
|
{
|
|
string path = resolvedPaths[0];
|
|
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Path resolved to: {0}",
|
|
path);
|
|
|
|
result = GetInfoFromPath(path);
|
|
}
|
|
} while (false);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next path using WildCards.
|
|
/// </summary>
|
|
/// <param name="command">
|
|
/// The command to search for.
|
|
/// </param>
|
|
/// <param name="provider">The provider that the command was found in.</param>
|
|
/// <returns>
|
|
/// A collection of full paths to the commands which were found.
|
|
/// </returns>
|
|
private Collection<string> GetNextFromPathUsingWildcards(string? command, out ProviderInfo? provider)
|
|
{
|
|
try
|
|
{
|
|
return _context.LocationGlobber.GetGlobbedProviderPathsFromMonadPath(path: command, allowNonexistingPaths: false, provider: out provider, providerInstance: out _);
|
|
}
|
|
catch (ItemNotFoundException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The path could not be found: {0}",
|
|
command);
|
|
}
|
|
catch (DriveNotFoundException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"A drive could not be found for the path: {0}",
|
|
command);
|
|
}
|
|
catch (ProviderNotFoundException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"A provider could not be found for the path: {0}",
|
|
command);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The path specified a home directory, but the provider home directory was not set. {0}",
|
|
command);
|
|
}
|
|
catch (ProviderInvocationException providerException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The provider associated with the path '{0}' encountered an error: {1}",
|
|
command,
|
|
providerException.Message);
|
|
}
|
|
catch (PSNotSupportedException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The provider associated with the path '{0}' does not implement ContainerCmdletProvider",
|
|
command);
|
|
}
|
|
|
|
provider = null;
|
|
return new Collection<string>();
|
|
}
|
|
|
|
private static bool checkPath(string path, string commandName)
|
|
{
|
|
return path.StartsWith(commandName, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the appropriate CommandInfo instance given the specified path.
|
|
/// </summary>
|
|
/// <param name="path">
|
|
/// The path to create the CommandInfo for.
|
|
/// </param>
|
|
/// <returns>
|
|
/// An instance of the appropriate CommandInfo derivative given the specified path.
|
|
/// </returns>
|
|
/// <exception cref="FileLoadException">
|
|
/// The <paramref name="path"/> refers to a cmdlet, or cmdletprovider
|
|
/// and it could not be loaded as an XML document.
|
|
/// </exception>
|
|
/// <exception cref="FormatException">
|
|
/// The <paramref name="path"/> refers to a cmdlet, or cmdletprovider
|
|
/// that does not adhere to the appropriate file format for its extension.
|
|
/// </exception>
|
|
/// <exception cref="MetadataException">
|
|
/// If <paramref name="path"/> refers to a cmdlet file that
|
|
/// contains invalid metadata.
|
|
/// </exception>
|
|
private CommandInfo? GetInfoFromPath(string path)
|
|
{
|
|
CommandInfo? result = null;
|
|
|
|
do // false loop
|
|
{
|
|
if (!File.Exists(path))
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError("The path does not exist: {0}", path);
|
|
break;
|
|
}
|
|
|
|
// Now create the appropriate CommandInfo using the extension
|
|
string? extension = null;
|
|
|
|
try
|
|
{
|
|
extension = Path.GetExtension(path);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// If the path contains illegal characters that
|
|
// weren't caught by the other APIs, GetExtension
|
|
// will throw an exception.
|
|
// For example, looking for a command called
|
|
// `abcdef
|
|
// The `a will be translated into the beep control character
|
|
// which is not a legal file system character.
|
|
}
|
|
|
|
if (extension == null)
|
|
{
|
|
result = null;
|
|
break;
|
|
}
|
|
|
|
if (string.Equals(extension, StringLiterals.PowerShellScriptFileExtension, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if ((_commandTypes & CommandTypes.ExternalScript) != 0)
|
|
{
|
|
string scriptName = Path.GetFileName(path);
|
|
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Command Found: path ({0}) is a script with name: {1}",
|
|
path,
|
|
scriptName);
|
|
|
|
// The path is to a PowerShell script
|
|
|
|
result = new ExternalScriptInfo(scriptName, path, _context);
|
|
break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if ((_commandTypes & CommandTypes.Application) != 0)
|
|
{
|
|
// Anything else is treated like an application
|
|
|
|
string appName = Path.GetFileName(path);
|
|
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Command Found: path ({0}) is an application with name: {1}",
|
|
path,
|
|
appName);
|
|
|
|
result = new ApplicationInfo(appName, path, _context);
|
|
break;
|
|
}
|
|
} while (false);
|
|
|
|
// Verify that this script is not untrusted, if we aren't constrained.
|
|
if (ShouldSkipCommandResolutionForConstrainedLanguage(result, _context))
|
|
{
|
|
result = null;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next matching alias.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A CommandInfo representing the next matching alias if found, otherwise null.
|
|
/// </returns>
|
|
private CommandInfo? GetNextAlias()
|
|
{
|
|
CommandInfo? result = null;
|
|
|
|
if ((_commandResolutionOptions & SearchResolutionOptions.ResolveAliasPatterns) != 0)
|
|
{
|
|
if (_matchingAlias == null)
|
|
{
|
|
// Generate the enumerator of matching alias names
|
|
|
|
Collection<AliasInfo> matchingAliases = new Collection<AliasInfo>();
|
|
|
|
WildcardPattern aliasMatcher =
|
|
WildcardPattern.Get(
|
|
_commandName,
|
|
WildcardOptions.IgnoreCase);
|
|
|
|
foreach (KeyValuePair<string, AliasInfo> aliasEntry in _context.EngineSessionState.GetAliasTable())
|
|
{
|
|
if (aliasMatcher.IsMatch(aliasEntry.Key) ||
|
|
(_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(aliasEntry.Key, _commandName)))
|
|
{
|
|
matchingAliases.Add(aliasEntry.Value);
|
|
}
|
|
}
|
|
|
|
// Process alias from modules
|
|
AliasInfo? c = GetAliasFromModules(_commandName);
|
|
if (c != null)
|
|
{
|
|
matchingAliases.Add(c);
|
|
}
|
|
|
|
_matchingAlias = matchingAliases.GetEnumerator();
|
|
}
|
|
|
|
if (!_matchingAlias.MoveNext())
|
|
{
|
|
// Advance the state
|
|
_currentState = SearchState.SearchingFunctions;
|
|
|
|
_matchingAlias = null;
|
|
}
|
|
else
|
|
{
|
|
result = _matchingAlias.Current;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Advance the state
|
|
_currentState = SearchState.SearchingFunctions;
|
|
|
|
result = _context.EngineSessionState.GetAlias(_commandName) ?? GetAliasFromModules(_commandName);
|
|
}
|
|
|
|
// Verify that this alias was not created by an untrusted constrained language,
|
|
// if we aren't constrained.
|
|
if (ShouldSkipCommandResolutionForConstrainedLanguage(result, _context))
|
|
{
|
|
result = null;
|
|
}
|
|
|
|
if (result != null)
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Alias found: {0} {1}",
|
|
result.Name,
|
|
result.Definition);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next matching function.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A CommandInfo representing the next matching function if found, otherwise null.
|
|
/// </returns>
|
|
private CommandInfo? GetNextFunction()
|
|
{
|
|
CommandInfo? result = null;
|
|
|
|
if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveFunctionPatterns))
|
|
{
|
|
if (_matchingFunctionEnumerator == null)
|
|
{
|
|
Collection<CommandInfo?> matchingFunction = new Collection<CommandInfo?>();
|
|
|
|
// Generate the enumerator of matching function names
|
|
WildcardPattern functionMatcher =
|
|
WildcardPattern.Get(
|
|
_commandName,
|
|
WildcardOptions.IgnoreCase);
|
|
|
|
foreach ((string functionName, FunctionInfo functionInfo) in _context.EngineSessionState.GetFunctionTable())
|
|
{
|
|
if (functionMatcher.IsMatch(functionName) ||
|
|
(_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(functionName, _commandName)))
|
|
{
|
|
matchingFunction.Add(functionInfo);
|
|
}
|
|
else if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion))
|
|
{
|
|
if (_commandName.Equals(ModuleUtils.AbbreviateName(functionName), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
matchingFunction.Add(functionInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process functions from modules
|
|
CommandInfo? cmdInfo = GetFunctionFromModules(_commandName);
|
|
if (cmdInfo != null)
|
|
{
|
|
matchingFunction.Add(cmdInfo);
|
|
}
|
|
|
|
_matchingFunctionEnumerator = matchingFunction.GetEnumerator();
|
|
}
|
|
|
|
if (!_matchingFunctionEnumerator.MoveNext())
|
|
{
|
|
// Advance the state
|
|
_currentState = SearchState.SearchingCmdlets;
|
|
|
|
_matchingFunctionEnumerator = null;
|
|
}
|
|
else
|
|
{
|
|
result = _matchingFunctionEnumerator.Current;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Advance the state
|
|
_currentState = SearchState.SearchingCmdlets;
|
|
|
|
result = GetFunction(_commandName);
|
|
}
|
|
|
|
// Verify that this function was not created by an untrusted constrained language,
|
|
// if we aren't constrained.
|
|
if (ShouldSkipCommandResolutionForConstrainedLanguage(result, _context))
|
|
{
|
|
result = null;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Don't return commands to the user if that might result in:
|
|
// - Trusted commands calling untrusted functions that the user has overridden
|
|
// - Debug prompts calling internal functions that are likely to have code injection
|
|
private static bool ShouldSkipCommandResolutionForConstrainedLanguage(CommandInfo? result, ExecutionContext executionContext)
|
|
{
|
|
if (result == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Don't return untrusted commands to trusted functions
|
|
if ((result.DefiningLanguageMode == PSLanguageMode.ConstrainedLanguage) &&
|
|
(executionContext.LanguageMode == PSLanguageMode.FullLanguage))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Don't allow invocation of trusted functions from debug breakpoints.
|
|
// They were probably defined within a trusted script, and could be
|
|
// susceptible to injection attacks. However, we do allow execution
|
|
// of functions defined in the global scope (i.e.: "more",) as those
|
|
// are intended to be exposed explicitly to users.
|
|
if ((result is FunctionInfo) &&
|
|
(executionContext.LanguageMode == PSLanguageMode.ConstrainedLanguage) &&
|
|
(result.DefiningLanguageMode == PSLanguageMode.FullLanguage) &&
|
|
(executionContext.Debugger != null) &&
|
|
(executionContext.Debugger.InBreakpoint) &&
|
|
(!(executionContext.TopLevelSessionState.GetFunctionTableAtScope("GLOBAL").ContainsKey(result.Name))))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private AliasInfo? GetAliasFromModules(string command)
|
|
{
|
|
AliasInfo? result = null;
|
|
|
|
if (command.IndexOf('\\') > 0)
|
|
{
|
|
// See if it's a module qualified alias...
|
|
PSSnapinQualifiedName? qualifiedName = PSSnapinQualifiedName.GetInstance(command);
|
|
if (qualifiedName != null && !string.IsNullOrEmpty(qualifiedName.PSSnapInName))
|
|
{
|
|
PSModuleInfo? module = GetImportedModuleByName(qualifiedName.PSSnapInName);
|
|
|
|
module?.ExportedAliases.TryGetValue(qualifiedName.ShortName, out result);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private CommandInfo? GetFunctionFromModules(string command)
|
|
{
|
|
FunctionInfo? result = null;
|
|
|
|
if (command.IndexOf('\\') > 0)
|
|
{
|
|
// See if it's a module qualified function call...
|
|
PSSnapinQualifiedName? qualifiedName = PSSnapinQualifiedName.GetInstance(command);
|
|
if (qualifiedName != null && !string.IsNullOrEmpty(qualifiedName.PSSnapInName))
|
|
{
|
|
PSModuleInfo? module = GetImportedModuleByName(qualifiedName.PSSnapInName);
|
|
|
|
module?.ExportedFunctions.TryGetValue(qualifiedName.ShortName, out result);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private PSModuleInfo? GetImportedModuleByName(string moduleName)
|
|
{
|
|
PSModuleInfo? module = null;
|
|
List<PSModuleInfo> modules = _context.Modules.GetModules(new string[] { moduleName }, false);
|
|
|
|
if (modules != null && modules.Count > 0)
|
|
{
|
|
foreach (PSModuleInfo m in modules)
|
|
{
|
|
if (_context.previousModuleImported.ContainsKey(m.Name) && ((string?)_context.previousModuleImported[m.Name] == m.Path))
|
|
{
|
|
module = m;
|
|
break;
|
|
}
|
|
}
|
|
|
|
module ??= modules[0];
|
|
}
|
|
|
|
return module;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the FunctionInfo or FilterInfo for the specified function name.
|
|
/// </summary>
|
|
/// <param name="function">
|
|
/// The name of the function/filter to retrieve.
|
|
/// </param>
|
|
/// <returns>
|
|
/// A FunctionInfo if the function name exists and is a function, a FilterInfo if
|
|
/// the filter name exists and is a filter, or null otherwise.
|
|
/// </returns>
|
|
private CommandInfo? GetFunction(string function)
|
|
{
|
|
CommandInfo? result = _context.EngineSessionState.GetFunction(function);
|
|
|
|
if (result != null)
|
|
{
|
|
if (result is FilterInfo)
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Filter found: {0}",
|
|
function);
|
|
}
|
|
else if (result is ConfigurationInfo)
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Configuration found: {0}",
|
|
function);
|
|
}
|
|
else
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Function found: {0} {1}",
|
|
function);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = GetFunctionFromModules(function);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next cmdlet from the collection of matching cmdlets.
|
|
/// If the collection doesn't exist yet it is created and the
|
|
/// enumerator is moved to the first item in the collection.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// A CmdletInfo for the next matching Cmdlet or null if there are
|
|
/// no more matches.
|
|
/// </returns>
|
|
private CmdletInfo? GetNextCmdlet()
|
|
{
|
|
CmdletInfo? result = null;
|
|
bool useAbbreviationExpansion = _commandResolutionOptions.HasFlag(SearchResolutionOptions.UseAbbreviationExpansion);
|
|
|
|
if (_matchingCmdlet == null)
|
|
{
|
|
if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.CommandNameIsPattern) || useAbbreviationExpansion)
|
|
{
|
|
Collection<CmdletInfo> matchingCmdletInfo = new Collection<CmdletInfo>();
|
|
|
|
PSSnapinQualifiedName? PSSnapinQualifiedCommandName =
|
|
PSSnapinQualifiedName.GetInstance(_commandName);
|
|
|
|
if (!useAbbreviationExpansion && PSSnapinQualifiedCommandName == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string? moduleName = PSSnapinQualifiedCommandName?.PSSnapInName;
|
|
|
|
var cmdletShortName = PSSnapinQualifiedCommandName?.ShortName;
|
|
WildcardPattern? cmdletMatcher = cmdletShortName != null
|
|
? WildcardPattern.Get(cmdletShortName, WildcardOptions.IgnoreCase)
|
|
: null;
|
|
|
|
SessionStateInternal ss = _context.EngineSessionState;
|
|
|
|
foreach (List<CmdletInfo> cmdletList in ss.GetCmdletTable().Values)
|
|
{
|
|
foreach (CmdletInfo cmdlet in cmdletList)
|
|
{
|
|
if ((cmdletMatcher is not null && cmdletMatcher.IsMatch(cmdlet.Name)) ||
|
|
(_fuzzyMatcher is not null && _fuzzyMatcher.IsFuzzyMatch(cmdlet.Name, _commandName)))
|
|
{
|
|
if (string.IsNullOrEmpty(moduleName) || moduleName.Equals(cmdlet.ModuleName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// If PSSnapin is specified, make sure they match
|
|
matchingCmdletInfo.Add(cmdlet);
|
|
}
|
|
}
|
|
else if (useAbbreviationExpansion)
|
|
{
|
|
if (_commandName.Equals(ModuleUtils.AbbreviateName(cmdlet.Name), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
matchingCmdletInfo.Add(cmdlet);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_matchingCmdlet = matchingCmdletInfo.GetEnumerator();
|
|
}
|
|
else
|
|
{
|
|
_matchingCmdlet = _context.CommandDiscovery.GetCmdletInfo(_commandName,
|
|
_commandResolutionOptions.HasFlag(SearchResolutionOptions.SearchAllScopes));
|
|
}
|
|
}
|
|
|
|
if (!_matchingCmdlet.MoveNext())
|
|
{
|
|
// Advance the state
|
|
_currentState = SearchState.StartSearchingForExternalCommands;
|
|
|
|
_matchingCmdlet = null;
|
|
}
|
|
else
|
|
{
|
|
result = _matchingCmdlet.Current;
|
|
}
|
|
|
|
return traceResult(result);
|
|
}
|
|
|
|
private IEnumerator<CmdletInfo>? _matchingCmdlet;
|
|
|
|
[return: NotNullIfNotNull("result")]
|
|
private static CmdletInfo? traceResult(CmdletInfo? result)
|
|
{
|
|
if (result != null)
|
|
{
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"Cmdlet found: {0} {1}",
|
|
result.Name,
|
|
result.ImplementingType);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private string? DoPowerShellRelativePathLookup()
|
|
{
|
|
string? result = null;
|
|
|
|
if (_context.EngineSessionState != null &&
|
|
_context.EngineSessionState.ProviderCount > 0 && _commandName.Length != 0)
|
|
{
|
|
// NTRAID#Windows OS Bugs-1009294-2004/02/04-JeffJon
|
|
// This is really slow. Maybe since we are only allowing FS paths right
|
|
// now we should use the file system APIs to verify the existence of the file.
|
|
|
|
// Since the path to the command was not found using the PATH variable,
|
|
// maybe it is relative to the current location. Try resolving the
|
|
// path.
|
|
// Relative Path: ".\command.exe"
|
|
// Home Path: "~\command.exe"
|
|
// Drive Relative Path: "\Users\User\AppData\Local\Temp\command.exe"
|
|
|
|
char firstChar = _commandName[0];
|
|
if (firstChar == '.' || firstChar == '~' || firstChar == '\\')
|
|
{
|
|
using (CommandDiscovery.discoveryTracer.TraceScope(
|
|
"{0} appears to be a relative path. Trying to resolve relative path",
|
|
_commandName))
|
|
{
|
|
result = ResolvePSPath(_commandName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the given path as an PSPath and ensures that it was resolved
|
|
/// by the FileSystemProvider.
|
|
/// </summary>
|
|
/// <param name="path">
|
|
/// The path to resolve.
|
|
/// </param>
|
|
/// <returns>
|
|
/// The path that was resolved. Null if the path couldn't be resolved or was
|
|
/// not resolved by the FileSystemProvider.
|
|
/// </returns>
|
|
private string? ResolvePSPath(string? path)
|
|
{
|
|
string? result = null;
|
|
|
|
try
|
|
{
|
|
ProviderInfo? provider = null;
|
|
string? resolvedPath = null;
|
|
|
|
// Try literal path resolution if it is set to run first
|
|
if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveLiteralThenPathPatterns))
|
|
{
|
|
// Cannot return early as this code path only expects
|
|
// The file system provider and the final check for that
|
|
// must verify this before we return.
|
|
resolvedPath = GetNextLiteralPathThatExists(path, out provider);
|
|
}
|
|
|
|
if (WildcardPattern.ContainsWildcardCharacters(path) &&
|
|
((resolvedPath == null) || (provider == null)))
|
|
{
|
|
// Let PowerShell resolve relative path with wildcards.
|
|
Collection<string> resolvedPaths = GetNextFromPathUsingWildcards(path, out provider);
|
|
|
|
if (resolvedPaths.Count == 0)
|
|
{
|
|
resolvedPath = null;
|
|
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The relative path with wildcard did not resolve to valid path. {0}",
|
|
path);
|
|
}
|
|
else if (resolvedPaths.Count > 1)
|
|
{
|
|
resolvedPath = null;
|
|
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The relative path with wildcard resolved to multiple paths. {0}",
|
|
path);
|
|
}
|
|
else
|
|
{
|
|
resolvedPath = resolvedPaths[0];
|
|
}
|
|
}
|
|
|
|
// Try literal path resolution if wildcards are enabled first and wildcard search failed
|
|
if (!_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveLiteralThenPathPatterns) &&
|
|
((resolvedPath == null) || (provider == null)))
|
|
{
|
|
resolvedPath = GetNextLiteralPathThatExists(path, out provider);
|
|
}
|
|
|
|
// Verify the path was resolved to a file system path
|
|
if (provider != null && provider.NameEquals(_context.ProviderNames.FileSystem))
|
|
{
|
|
result = resolvedPath;
|
|
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"The relative path was resolved to: {0}",
|
|
result);
|
|
}
|
|
else
|
|
{
|
|
// The path was not to the file system
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The relative path was not a file system path. {0}",
|
|
path);
|
|
}
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The home path was not specified for the provider. {0}",
|
|
path);
|
|
}
|
|
catch (ProviderInvocationException providerInvocationException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"While resolving the path, \"{0}\", an error was encountered by the provider: {1}",
|
|
path,
|
|
providerInvocationException.Message);
|
|
}
|
|
catch (ItemNotFoundException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The path does not exist: {0}",
|
|
path);
|
|
}
|
|
catch (DriveNotFoundException driveNotFound)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The drive does not exist: {0}",
|
|
driveNotFound.ItemName);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
/// <summary>
|
|
/// Gets the next literal path.
|
|
/// Filtering to ones that exist for the filesystem.
|
|
/// Handles Exceptions
|
|
/// </summary>
|
|
/// <param name="command">
|
|
/// The command to search for.
|
|
/// </param>
|
|
/// <param name="provider">The provider that the command was found in.</param>
|
|
/// <returns>
|
|
/// Full path to the command.
|
|
/// </returns>
|
|
private string? GetNextLiteralPathThatExistsAndHandleExceptions(string command, out ProviderInfo? provider)
|
|
{
|
|
try
|
|
{
|
|
return GetNextLiteralPathThatExists(command, out provider);
|
|
}
|
|
catch (ItemNotFoundException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The path could not be found: {0}",
|
|
_commandName);
|
|
}
|
|
catch (DriveNotFoundException)
|
|
{
|
|
// This can be because we think a scope or a url is a drive
|
|
// and need to continue searching.
|
|
// Although, scope does not work through get-command
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"A drive could not be found for the path: {0}",
|
|
_commandName);
|
|
}
|
|
catch (ProviderNotFoundException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"A provider could not be found for the path: {0}",
|
|
_commandName);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The path specified a home directory, but the provider home directory was not set. {0}",
|
|
_commandName);
|
|
}
|
|
catch (ProviderInvocationException providerException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The provider associated with the path '{0}' encountered an error: {1}",
|
|
_commandName,
|
|
providerException.Message);
|
|
}
|
|
catch (PSNotSupportedException)
|
|
{
|
|
CommandDiscovery.discoveryTracer.TraceError(
|
|
"The provider associated with the path '{0}' does not implement ContainerCmdletProvider",
|
|
_commandName);
|
|
}
|
|
|
|
provider = null;
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the next literal path.
|
|
/// Filtering to ones that exist for the filesystem.
|
|
/// </summary>
|
|
/// <param name="command">
|
|
/// The command to search for.
|
|
/// </param>
|
|
/// <param name="provider">The provider that the command was found in.</param>
|
|
/// <returns>
|
|
/// Full path to the command.
|
|
/// </returns>
|
|
private string? GetNextLiteralPathThatExists(string? command, out ProviderInfo? provider)
|
|
{
|
|
string resolvedPath = _context.LocationGlobber.GetProviderPath(command, out provider);
|
|
|
|
if (provider.NameEquals(_context.ProviderNames.FileSystem)
|
|
&& !File.Exists(resolvedPath)
|
|
&& !Directory.Exists(resolvedPath))
|
|
{
|
|
provider = null;
|
|
return null;
|
|
}
|
|
|
|
return resolvedPath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a collection of patterns used to find the command.
|
|
/// </summary>
|
|
/// <param name="name">
|
|
/// The name of the command to search for.
|
|
/// </param>
|
|
/// <param name="commandDiscovery">Get names for command discovery.</param>
|
|
/// <returns>
|
|
/// A collection of the patterns used to find the command.
|
|
/// The patterns are as follows:
|
|
/// 1. [commandName].cmdlet
|
|
/// 2. [commandName].ps1
|
|
/// 3..x
|
|
/// foreach (extension in PATHEXT)
|
|
/// [commandName].[extension]
|
|
/// x+1. [commandName]
|
|
/// </returns>
|
|
/// <exception cref="ArgumentException">
|
|
/// If <paramref name="name"/> contains one or more of the
|
|
/// invalid characters defined in InvalidPathChars.
|
|
/// </exception>
|
|
internal LookupPathCollection ConstructSearchPatternsFromName(string name, bool commandDiscovery = false)
|
|
{
|
|
var result = new LookupPathCollection();
|
|
|
|
// First check to see if the commandName has an extension, if so
|
|
// look for that first
|
|
bool commandNameAddedFirst = Path.HasExtension(name);
|
|
|
|
if (commandNameAddedFirst)
|
|
{
|
|
result.Add(name);
|
|
}
|
|
|
|
// Add the extensions for script, module and data files in that order...
|
|
if (_commandTypes.HasFlag(CommandTypes.ExternalScript))
|
|
{
|
|
result.Add(name + StringLiterals.PowerShellScriptFileExtension);
|
|
if (!commandDiscovery)
|
|
{
|
|
// psd1 and psm1 are not executable, so don't add them
|
|
result.Add(name + StringLiterals.PowerShellModuleFileExtension);
|
|
result.Add(name + StringLiterals.PowerShellDataFileExtension);
|
|
}
|
|
}
|
|
#if !UNIX
|
|
if (_commandTypes.HasFlag(CommandTypes.Application))
|
|
{
|
|
// Now add each extension from the PATHEXT environment variable
|
|
foreach (string extension in CommandDiscovery.PathExtensions)
|
|
{
|
|
result.Add(name + extension);
|
|
}
|
|
}
|
|
#endif
|
|
// Now add the commandName by itself if it wasn't added as the first pattern
|
|
if (!commandNameAddedFirst)
|
|
{
|
|
result.Add(name);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if the given command name is a qualified PowerShell path.
|
|
/// </summary>
|
|
/// <param name="commandName">
|
|
/// The name of the command.
|
|
/// </param>
|
|
/// <returns>
|
|
/// True if the command name is either a provider-qualified or PowerShell drive-qualified
|
|
/// path. False otherwise.
|
|
/// </returns>
|
|
private static bool IsQualifiedPSPath(string commandName)
|
|
{
|
|
Dbg.Assert(
|
|
!string.IsNullOrEmpty(commandName),
|
|
"The caller should have verified the commandName");
|
|
|
|
bool result =
|
|
LocationGlobber.IsAbsolutePath(commandName) ||
|
|
LocationGlobber.IsProviderQualifiedPath(commandName) ||
|
|
LocationGlobber.IsHomePath(commandName) ||
|
|
LocationGlobber.IsProviderDirectPath(commandName);
|
|
|
|
return result;
|
|
}
|
|
|
|
private enum CanDoPathLookupResult
|
|
{
|
|
Yes,
|
|
PathIsRooted,
|
|
WildcardCharacters,
|
|
DirectorySeparator,
|
|
IllegalCharacters
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if the command name has any path special
|
|
/// characters which would require resolution. If so,
|
|
/// path lookup will not succeed.
|
|
/// </summary>
|
|
/// <param name="possiblePath">
|
|
/// The command name (or possible path) to look for the special characters.
|
|
/// </param>
|
|
/// <returns>
|
|
/// True if the command name does not contain any special
|
|
/// characters. False otherwise.
|
|
/// </returns>
|
|
private static CanDoPathLookupResult CanDoPathLookup(string possiblePath)
|
|
{
|
|
CanDoPathLookupResult result = CanDoPathLookupResult.Yes;
|
|
|
|
do // false loop
|
|
{
|
|
// If the command name contains any wildcard characters
|
|
// we can't do the path lookup
|
|
|
|
if (WildcardPattern.ContainsWildcardCharacters(possiblePath))
|
|
{
|
|
result = CanDoPathLookupResult.WildcardCharacters;
|
|
break;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (Path.IsPathRooted(possiblePath))
|
|
{
|
|
result = CanDoPathLookupResult.PathIsRooted;
|
|
break;
|
|
}
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
result = CanDoPathLookupResult.IllegalCharacters;
|
|
break;
|
|
}
|
|
|
|
// If the command contains any path separators, we can't
|
|
// do the path lookup
|
|
if (possiblePath.IndexOfAny(Utils.Separators.Directory) != -1)
|
|
{
|
|
result = CanDoPathLookupResult.DirectorySeparator;
|
|
break;
|
|
}
|
|
|
|
// If the command contains any invalid path characters, we can't
|
|
// do the path lookup
|
|
|
|
if (possiblePath.IndexOfAny(Path.GetInvalidPathChars()) != -1)
|
|
{
|
|
result = CanDoPathLookupResult.IllegalCharacters;
|
|
break;
|
|
}
|
|
} while (false);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The command name to search for.
|
|
/// </summary>
|
|
private string _commandName;
|
|
|
|
/// <summary>
|
|
/// Determines which command types will be globbed.
|
|
/// </summary>
|
|
private readonly SearchResolutionOptions _commandResolutionOptions;
|
|
|
|
/// <summary>
|
|
/// Determines which types of commands to look for.
|
|
/// </summary>
|
|
private CommandTypes _commandTypes = CommandTypes.All;
|
|
|
|
/// <summary>
|
|
/// The enumerator that uses the Path to
|
|
/// search for commands.
|
|
/// </summary>
|
|
private CommandPathSearch? _pathSearcher;
|
|
|
|
/// <summary>
|
|
/// The execution context instance for the current engine...
|
|
/// </summary>
|
|
private readonly ExecutionContext _context;
|
|
|
|
/// <summary>
|
|
/// The fuzzy matcher to use for fuzzy searching.
|
|
/// </summary>
|
|
private readonly FuzzyMatcher? _fuzzyMatcher;
|
|
|
|
/// <summary>
|
|
/// A routine to initialize the path searcher...
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException">
|
|
/// If the commandName used to construct this object
|
|
/// contains one or more of the invalid characters defined
|
|
/// in InvalidPathChars.
|
|
/// </exception>
|
|
private void setupPathSearcher()
|
|
{
|
|
// If it's already set up, just return...
|
|
if (_pathSearcher != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We are never going to look for non-executable commands in CommandSearcher.
|
|
// Even though file types like .DOC, .LOG,.TXT, etc. can be opened / invoked, users think of these as files, not applications.
|
|
// So I don't think we should show applications with the additional extensions at all.
|
|
// Applications should only include files whose extensions are in the PATHEXT list and these would only be returned with the All parameter.
|
|
|
|
if ((_commandResolutionOptions & SearchResolutionOptions.CommandNameIsPattern) != 0)
|
|
{
|
|
_canDoPathLookup = true;
|
|
_canDoPathLookupResult = CanDoPathLookupResult.Yes;
|
|
|
|
_pathSearcher =
|
|
new CommandPathSearch(
|
|
_commandName,
|
|
_context.CommandDiscovery.GetLookupDirectoryPaths(),
|
|
_context,
|
|
acceptableCommandNames: null,
|
|
_fuzzyMatcher);
|
|
}
|
|
else
|
|
{
|
|
_canDoPathLookupResult = CanDoPathLookup(_commandName);
|
|
if (_canDoPathLookupResult == CanDoPathLookupResult.Yes)
|
|
{
|
|
_canDoPathLookup = true;
|
|
_commandName = _commandName.TrimEnd(Utils.Separators.PathSearchTrimEnd);
|
|
|
|
_pathSearcher =
|
|
new CommandPathSearch(
|
|
_commandName,
|
|
_context.CommandDiscovery.GetLookupDirectoryPaths(),
|
|
_context,
|
|
ConstructSearchPatternsFromName(_commandName, commandDiscovery: true),
|
|
fuzzyMatcher: null);
|
|
}
|
|
else if (_canDoPathLookupResult == CanDoPathLookupResult.PathIsRooted)
|
|
{
|
|
_canDoPathLookup = true;
|
|
|
|
string? directory = Path.GetDirectoryName(_commandName);
|
|
var directoryCollection = new LookupPathCollection { directory };
|
|
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"The path is rooted, so only doing the lookup in the specified directory: {0}",
|
|
directory);
|
|
|
|
string fileName = Path.GetFileName(_commandName);
|
|
|
|
if (!string.IsNullOrEmpty(fileName))
|
|
{
|
|
fileName = fileName.TrimEnd(Utils.Separators.PathSearchTrimEnd);
|
|
_pathSearcher =
|
|
new CommandPathSearch(
|
|
fileName,
|
|
directoryCollection,
|
|
_context,
|
|
ConstructSearchPatternsFromName(fileName, commandDiscovery: true),
|
|
fuzzyMatcher: null);
|
|
}
|
|
else
|
|
{
|
|
_canDoPathLookup = false;
|
|
}
|
|
}
|
|
else if (_canDoPathLookupResult == CanDoPathLookupResult.DirectorySeparator)
|
|
{
|
|
_canDoPathLookup = true;
|
|
|
|
// We must try to resolve the path as an PSPath or else we can't do
|
|
// path lookup for relative paths.
|
|
|
|
string? directory = Path.GetDirectoryName(_commandName);
|
|
directory = ResolvePSPath(directory);
|
|
|
|
CommandDiscovery.discoveryTracer.WriteLine(
|
|
"The path is relative, so only doing the lookup in the specified directory: {0}",
|
|
directory);
|
|
|
|
if (directory == null)
|
|
{
|
|
_canDoPathLookup = false;
|
|
}
|
|
else
|
|
{
|
|
var directoryCollection = new LookupPathCollection { directory };
|
|
|
|
string fileName = Path.GetFileName(_commandName);
|
|
|
|
if (!string.IsNullOrEmpty(fileName))
|
|
{
|
|
fileName = fileName.TrimEnd(Utils.Separators.PathSearchTrimEnd);
|
|
_pathSearcher =
|
|
new CommandPathSearch(
|
|
fileName,
|
|
directoryCollection,
|
|
_context,
|
|
ConstructSearchPatternsFromName(fileName, commandDiscovery: true),
|
|
fuzzyMatcher: null);
|
|
}
|
|
else
|
|
{
|
|
_canDoPathLookup = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the enumerator to before the first command match, public for IEnumerable.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
// If this is a command coming from outside the runspace and there are no
|
|
// permitted scripts or applications,
|
|
// remove them from the set of things to search for...
|
|
if (_commandOrigin == CommandOrigin.Runspace)
|
|
{
|
|
if (_context.EngineSessionState.Applications.Count == 0)
|
|
_commandTypes &= ~CommandTypes.Application;
|
|
if (_context.EngineSessionState.Scripts.Count == 0)
|
|
_commandTypes &= ~CommandTypes.ExternalScript;
|
|
}
|
|
|
|
_pathSearcher?.Reset();
|
|
|
|
_currentMatch = null;
|
|
_currentState = SearchState.SearchingAliases;
|
|
_matchingAlias = null;
|
|
_matchingCmdlet = null;
|
|
}
|
|
|
|
internal CommandOrigin CommandOrigin
|
|
{
|
|
get { return _commandOrigin; }
|
|
|
|
set { _commandOrigin = value; }
|
|
}
|
|
|
|
private CommandOrigin _commandOrigin = CommandOrigin.Internal;
|
|
|
|
/// <summary>
|
|
/// An enumerator of the matching aliases.
|
|
/// </summary>
|
|
private IEnumerator<AliasInfo>? _matchingAlias;
|
|
|
|
/// <summary>
|
|
/// An enumerator of the matching functions.
|
|
/// </summary>
|
|
private IEnumerator<CommandInfo?>? _matchingFunctionEnumerator;
|
|
|
|
/// <summary>
|
|
/// The CommandInfo that references the command that matches the pattern.
|
|
/// </summary>
|
|
private CommandInfo? _currentMatch;
|
|
|
|
private bool _canDoPathLookup;
|
|
private CanDoPathLookupResult _canDoPathLookupResult = CanDoPathLookupResult.Yes;
|
|
|
|
/// <summary>
|
|
/// The current state of the enumerator.
|
|
/// </summary>
|
|
private SearchState _currentState = SearchState.SearchingAliases;
|
|
|
|
private enum SearchState
|
|
{
|
|
// the searcher has been reset or has not been advanced since being created.
|
|
SearchingAliases,
|
|
|
|
// the searcher has finished alias resolution and is now searching for functions.
|
|
SearchingFunctions,
|
|
|
|
// the searcher has finished function resolution and is now searching for cmdlets
|
|
SearchingCmdlets,
|
|
|
|
// the search has finished builtin script resolution and is now searching for external commands
|
|
StartSearchingForExternalCommands,
|
|
|
|
// the searcher has moved to
|
|
PowerShellPathResolution,
|
|
|
|
// the searcher has moved to a qualified file system path
|
|
QualifiedFileSystemPath,
|
|
|
|
// the searcher has moved to using a CommandPathSearch object
|
|
// for resolution
|
|
PathSearch,
|
|
|
|
// the searcher has moved to using a CommandPathSearch object
|
|
// with get prepended to the command name for resolution
|
|
GetPathSearch,
|
|
|
|
// the searcher has moved to resolving the command as a
|
|
// relative PowerShell path
|
|
PowerShellRelativePath,
|
|
|
|
// No more matches can be found
|
|
NoMoreMatches,
|
|
}
|
|
|
|
#endregion private members
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines which types of commands should be globbed using the specified
|
|
/// pattern. Any flag that is not specified will only match if exact.
|
|
/// </summary>
|
|
[Flags]
|
|
internal enum SearchResolutionOptions
|
|
{
|
|
None = 0x0,
|
|
ResolveAliasPatterns = 0x01,
|
|
ResolveFunctionPatterns = 0x02,
|
|
CommandNameIsPattern = 0x04,
|
|
SearchAllScopes = 0x08,
|
|
|
|
/// <summary>
|
|
/// Enable searching for cmdlets/functions by abbreviation expansion.
|
|
/// </summary>
|
|
UseAbbreviationExpansion = 0x10,
|
|
|
|
/// <summary>
|
|
/// Enable resolving wildcard in paths.
|
|
/// </summary>
|
|
ResolveLiteralThenPathPatterns = 0x20
|
|
}
|
|
}
|