// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Internal; using System.Management.Automation.Language; using Dbg = System.Management.Automation.Diagnostics; namespace Microsoft.PowerShell.Commands { /// /// The get-command cmdlet. It uses the command discovery APIs to find one or more /// commands of the given name. It returns an instance of CommandInfo for each /// command that is found. /// [Cmdlet(VerbsCommon.Get, "Command", DefaultParameterSetName = "CmdletSet", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096579")] [OutputType(typeof(AliasInfo), typeof(ApplicationInfo), typeof(FunctionInfo), typeof(CmdletInfo), typeof(ExternalScriptInfo), typeof(FilterInfo), typeof(string), typeof(PSObject))] public sealed class GetCommandCommand : PSCmdlet { #region Definitions of cmdlet parameters /// /// Gets or sets the path(s) or name(s) of the commands to retrieve. /// [Parameter( Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "AllCommandSet")] [ValidateNotNullOrEmpty] public string[] Name { get { return _names; } set { _nameContainsWildcard = false; _names = value; if (value != null) { foreach (string commandName in value) { if (WildcardPattern.ContainsWildcardCharacters(commandName)) { _nameContainsWildcard = true; break; } } } } } private string[] _names; private bool _nameContainsWildcard; /// /// Gets or sets the verb parameter to the cmdlet. /// [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "CmdletSet")] public string[] Verb { get { return _verbs; } set { if (value == null) { value = Array.Empty(); } _verbs = value; _verbPatterns = null; } } private string[] _verbs = Array.Empty(); /// /// Gets or sets the noun parameter to the cmdlet. /// [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "CmdletSet")] [ArgumentCompleter(typeof(NounArgumentCompleter))] public string[] Noun { get { return _nouns; } set { if (value == null) { value = Array.Empty(); } _nouns = value; _nounPatterns = null; } } private string[] _nouns = Array.Empty(); /// /// Gets or sets the PSSnapin/Module parameter to the cmdlet. /// [Parameter(ValueFromPipelineByPropertyName = true)] [Alias("PSSnapin")] public string[] Module { get { return _modules; } set { if (value == null) { value = Array.Empty(); } _modules = value; _modulePatterns = null; _isModuleSpecified = true; } } private string[] _modules = Array.Empty(); private bool _isModuleSpecified = false; /// /// Gets or sets the FullyQualifiedModule parameter to the cmdlet. /// [Parameter(ValueFromPipelineByPropertyName = true)] public ModuleSpecification[] FullyQualifiedModule { get { return _moduleSpecifications; } set { if (value != null) { _moduleSpecifications = value; } _isFullyQualifiedModuleSpecified = true; } } private ModuleSpecification[] _moduleSpecifications = Array.Empty(); private bool _isFullyQualifiedModuleSpecified = false; /// /// Gets or sets the type of the command to get. /// [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "AllCommandSet")] [Alias("Type")] public CommandTypes CommandType { get { return _commandType; } set { _commandType = value; _isCommandTypeSpecified = true; } } private CommandTypes _commandType = CommandTypes.All; private bool _isCommandTypeSpecified = false; /// /// The parameter representing the total number of commands that will /// be returned. If negative, all matching commands that are found will /// be returned. /// [Parameter(ValueFromPipelineByPropertyName = true)] public int TotalCount { get; set; } = -1; /// /// The parameter that determines if the CommandInfo or the string /// definition of the command is output. /// [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter Syntax { get { return _usage; } set { _usage = value; } } private bool _usage; /// /// This parameter causes the output to be packaged into ShowCommandInfo PSObject types /// needed to display GUI command information. /// [Parameter()] public SwitchParameter ShowCommandInfo { get; set; } /// /// The parameter that all additional arguments get bound to. These arguments are used /// when retrieving dynamic parameters from cmdlets that support them. /// [Parameter(Position = 1, ValueFromRemainingArguments = true)] [AllowNull] [AllowEmptyCollection] [Alias("Args")] public object[] ArgumentList { get; set; } /// /// The parameter that determines if additional matching commands should be returned. /// (Additional matching functions and aliases are returned from module tables) /// [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter All { get { return _all; } set { _all = value; } } private bool _all; /// /// The parameter that determines if additional matching commands from available modules should be returned. /// If set to true, only those commands currently in the session are returned. /// [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter ListImported { get { return _listImported; } set { _listImported = value; } } private bool _listImported; /// /// The parameter that filters commands returned to only include commands that have a parameter with a name that matches one of the ParameterName's arguments. /// [Parameter] [ValidateNotNullOrEmpty] public string[] ParameterName { get { return _parameterNames; } set { _parameterNames = value ?? throw new ArgumentNullException(nameof(value)); _parameterNameWildcards = SessionStateUtilities.CreateWildcardsFromStrings( _parameterNames, WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase); } } private Collection _parameterNameWildcards; private string[] _parameterNames; private HashSet _matchedParameterNames; /// /// The parameter that filters commands returned to only include commands that have a parameter of a type that matches one of the ParameterType's arguments. /// [Parameter] [ValidateNotNullOrEmpty] public PSTypeName[] ParameterType { get { return _parameterTypes; } set { if (value == null) { throw new ArgumentNullException(nameof(value)); } // if '...CimInstance#Win32_Process' is specified, then exclude '...CimInstance' List filteredParameterTypes = new List(value.Length); for (int i = 0; i < value.Length; i++) { PSTypeName ptn = value[i]; if (value.Any(otherPtn => otherPtn.Name.StartsWith(ptn.Name + "#", StringComparison.OrdinalIgnoreCase))) { continue; } if ((i != 0) && (ptn.Type != null) && (ptn.Type.Equals(typeof(object)))) { continue; } filteredParameterTypes.Add(ptn); } _parameterTypes = filteredParameterTypes.ToArray(); } } private PSTypeName[] _parameterTypes; /// /// Gets or sets the parameter that enables using fuzzy matching. /// [Parameter(ParameterSetName = "AllCommandSet")] public SwitchParameter UseFuzzyMatching { get; set; } private readonly List _commandScores = new List(); /// /// Gets or sets the parameter that determines if return cmdlets based on abbreviation expansion. /// This means it matches cmdlets where the uppercase characters for the noun match /// the given characters. i.e., g-sgc would match Get-SomeGreatCmdlet. /// [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "AllCommandSet")] public SwitchParameter UseAbbreviationExpansion { get; set; } #endregion Definitions of cmdlet parameters #region Overrides /// /// Begin Processing. /// protected override void BeginProcessing() { #if LEGACYTELEMETRY _timer.Start(); #endif base.BeginProcessing(); if (ShowCommandInfo.IsPresent && Syntax.IsPresent) { ThrowTerminatingError( new ErrorRecord( new PSArgumentException(DiscoveryExceptions.GetCommandShowCommandInfoParamError), "GetCommandCannotSpecifySyntaxAndShowCommandInfoTogether", ErrorCategory.InvalidArgument, null)); } } /// /// Method that implements get-command. /// protected override void ProcessRecord() { _commandsWritten.Clear(); // Module and FullyQualifiedModule should not be specified at the same time. // Throw out terminating error if this is the case. if (_isModuleSpecified && _isFullyQualifiedModuleSpecified) { string errMsg = string.Format(CultureInfo.InvariantCulture, SessionStateStrings.GetContent_TailAndHeadCannotCoexist, "Module", "FullyQualifiedModule"); ErrorRecord error = new ErrorRecord(new InvalidOperationException(errMsg), "ModuleAndFullyQualifiedModuleCannotBeSpecifiedTogether", ErrorCategory.InvalidOperation, null); ThrowTerminatingError(error); } // Initialize the module patterns if (_modulePatterns == null) { _modulePatterns = SessionStateUtilities.CreateWildcardsFromStrings(Module, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); } switch (ParameterSetName) { case "CmdletSet": AccumulateMatchingCmdlets(); break; case "AllCommandSet": AccumulateMatchingCommands(); break; default: Dbg.Assert( false, "Only the valid parameter set names should be used"); break; } } /// /// Writes out the accumulated matching commands. /// protected override void EndProcessing() { // We do not show the pithy aliases (not of the format Verb-Noun) and applications by default. // We will show them only if the Name, All and totalCount are not specified. if ((this.Name == null) && (!_all) && TotalCount == -1 && !UseFuzzyMatching) { CommandTypes commandTypesToIgnore = 0; if (((this.CommandType & CommandTypes.Alias) != CommandTypes.Alias) || (!_isCommandTypeSpecified)) { commandTypesToIgnore |= CommandTypes.Alias; } if (((_commandType & CommandTypes.Application) != CommandTypes.Application) || (!_isCommandTypeSpecified)) { commandTypesToIgnore |= CommandTypes.Application; } _accumulatedResults = _accumulatedResults.Where( commandInfo => (((commandInfo.CommandType & commandTypesToIgnore) == 0) || (commandInfo.Name.IndexOf('-') > 0))).ToList(); } // report not-found errors for ParameterName and ParameterType if needed if ((_matchedParameterNames != null) && (ParameterName != null)) { foreach (string requestedParameterName in ParameterName) { if (WildcardPattern.ContainsWildcardCharacters(requestedParameterName)) { continue; } if (_matchedParameterNames.Contains(requestedParameterName)) { continue; } string errorMessage = string.Format( CultureInfo.InvariantCulture, DiscoveryExceptions.CommandParameterNotFound, requestedParameterName); var exception = new ArgumentException(errorMessage, requestedParameterName); var errorRecord = new ErrorRecord(exception, "CommandParameterNotFound", ErrorCategory.ObjectNotFound, requestedParameterName); WriteError(errorRecord); } } // Only sort if they didn't fully specify a name) if ((_names == null) || (_nameContainsWildcard)) { // Use the stable sorting to sort the result list _accumulatedResults = _accumulatedResults.OrderBy(static a => a, new CommandInfoComparer()).ToList(); } OutputResultsHelper(_accumulatedResults); object pssenderInfo = Context.GetVariableValue(SpecialVariables.PSSenderInfoVarPath); if ((pssenderInfo != null) && (pssenderInfo is System.Management.Automation.Remoting.PSSenderInfo)) { // Win8: 593295. Exchange has around 1000 cmdlets. During Import-PSSession, // Get-Command | select-object ..,HelpURI,... is run. HelpURI is a script property // which in turn runs Get-Help. Get-Help loads the help content and caches it in the process. // This caching is using around 190 MB. During V3, we have implemented HelpURI attribute // and this should solve it. In V2, we dont have this attribute and hence 3rd parties // run into the same issue. The fix here is to reset help cache whenever get-command is run on // a remote endpoint. In the worst case, this will affect get-help to run a little longer // after get-command is run..but that should be OK because get-help is used mainly for // document reading purposes and not in production. Context.HelpSystem.ResetHelpProviders(); } } #endregion #region Private Methods private void OutputResultsHelper(IEnumerable results) { CommandOrigin origin = this.MyInvocation.CommandOrigin; if (UseFuzzyMatching) { results = _commandScores.OrderBy(static x => x.Score).Select(static x => x.Command).ToList(); } int count = 0; foreach (CommandInfo result in results) { count += 1; // Only write the command if it is visible to the requestor if (SessionState.IsVisible(origin, result)) { // If the -syntax flag was specified, write the definition as a string // otherwise just return the object... if (Syntax) { if (!string.IsNullOrEmpty(result.Syntax)) { PSObject syntax = GetSyntaxObject(result); WriteObject(syntax); } } else { if (ShowCommandInfo.IsPresent) { // Write output as ShowCommandCommandInfo object. WriteObject( ConvertToShowCommandInfo(result)); } else { // Write output as normal command info object. WriteObject(result); } } } } #if LEGACYTELEMETRY _timer.Stop(); // No telemetry here - capturing the name of a command which we are not familiar with // may be confidential customer information // We want telementry on commands people look for but don't exist - this should give us an idea // what sort of commands people expect but either don't exist, or maybe should be installed by default. // The StartsWith is to avoid logging telemetry when suggestion mode checks the // current directory for scripts/exes in the current directory and '.' is not in the path. if (count == 0 && Name != null && Name.Length > 0 && !Name[0].StartsWith(".\\", StringComparison.OrdinalIgnoreCase)) { Telemetry.Internal.TelemetryAPI.ReportGetCommandFailed(Name, _timer.ElapsedMilliseconds); } #endif } /// /// Creates the syntax output based on if the command is an alias, script, application or command. /// /// /// CommandInfo object containing the syntax to be output. /// /// /// Syntax string cast as a PSObject for outputting. /// private PSObject GetSyntaxObject(CommandInfo command) { PSObject syntax = PSObject.AsPSObject(command.Syntax); // This is checking if the command name that's been passed in is one that was specified by a user, // if not then we have to assume they specified an alias or a wildcard and do some extra formatting for those, // if it is then just go with the default formatting. // So if a user runs Get-Command -Name del -Syntax the code will find del and the command it resolves to as Remove-Item // and attempt to return that, but as the user specified del we want to fiddle with the output a bit to make it clear // that's an alias but still give the Remove-Item syntax. if (this.Name != null && !Array.Exists(this.Name, name => name.Equals(command.Name, StringComparison.InvariantCultureIgnoreCase))) { string aliasName = _nameContainsWildcard ? command.Name : this.Name[0]; IDictionary aliasTable = SessionState.Internal.GetAliasTable(); foreach (KeyValuePair tableEntry in aliasTable) { if ((Array.Exists(this.Name, name => name.Equals(tableEntry.Key, StringComparison.InvariantCultureIgnoreCase)) && tableEntry.Value.Definition == command.Name) || (_nameContainsWildcard && tableEntry.Value.Definition == command.Name)) { aliasName = tableEntry.Key; break; } } string replacedSyntax = string.Empty; switch (command) { case ExternalScriptInfo externalScript: replacedSyntax = string.Format( "{0} (alias) -> {1}{2}{3}", aliasName, string.Format("{0}{1}", externalScript.Path, Environment.NewLine), Environment.NewLine, command.Syntax.Replace(command.Name, aliasName)); break; case ApplicationInfo app: replacedSyntax = app.Path; break; default: if (aliasName.Equals(command.Name)) { replacedSyntax = command.Syntax; } else { replacedSyntax = string.Format( "{0} (alias) -> {1}{2}{3}", aliasName, command.Name, Environment.NewLine, command.Syntax.Replace(command.Name, aliasName)); } break; } syntax = PSObject.AsPSObject(replacedSyntax); } syntax.IsHelpObject = true; return syntax; } /// /// The comparer to sort CommandInfo objects in the result list. /// private sealed class CommandInfoComparer : IComparer { /// /// Compare two CommandInfo objects first by their command types, and if they /// are with the same command type, then we compare their names. /// /// /// /// public int Compare(CommandInfo x, CommandInfo y) { if ((int)x.CommandType < (int)y.CommandType) { return -1; } else if ((int)x.CommandType > (int)y.CommandType) { return 1; } else { return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); } } } private void AccumulateMatchingCmdlets() { _commandType = CommandTypes.Cmdlet | CommandTypes.Function | CommandTypes.Filter | CommandTypes.Alias | CommandTypes.Configuration; Collection commandNames = new Collection(); commandNames.Add("*"); AccumulateMatchingCommands(commandNames); } private bool IsNounVerbMatch(CommandInfo command) { bool result = false; do // false loop { if (_verbPatterns == null) { _verbPatterns = SessionStateUtilities.CreateWildcardsFromStrings(Verb, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); } if (_nounPatterns == null) { _nounPatterns = SessionStateUtilities.CreateWildcardsFromStrings(Noun, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); } if (!string.IsNullOrEmpty(command.ModuleName)) { if (_isFullyQualifiedModuleSpecified) { if (!_moduleSpecifications.Any( moduleSpecification => ModuleIntrinsics.IsModuleMatchingModuleSpec(command.Module, moduleSpecification))) { break; } } else if (!SessionStateUtilities.MatchesAnyWildcardPattern(command.ModuleName, _modulePatterns, true)) { break; } } else { if (_modulePatterns.Count > 0 || _moduleSpecifications.Length > 0) { // Its not a match if we are filtering on a PSSnapin/Module name but the cmdlet doesn't have one. break; } } // Get the noun and verb to check... string verb; string noun; CmdletInfo cmdlet = command as CmdletInfo; if (cmdlet != null) { verb = cmdlet.Verb; noun = cmdlet.Noun; } else { if (!CmdletInfo.SplitCmdletName(command.Name, out verb, out noun)) break; } if (!SessionStateUtilities.MatchesAnyWildcardPattern(verb, _verbPatterns, true)) { break; } if (!SessionStateUtilities.MatchesAnyWildcardPattern(noun, _nounPatterns, true)) { break; } result = true; } while (false); return result; } /// /// Writes out the commands for the AllCommandSet using the specified CommandType. /// private void AccumulateMatchingCommands() { Collection commandNames = SessionStateUtilities.ConvertArrayToCollection(this.Name); if (commandNames.Count == 0) { commandNames.Add("*"); } AccumulateMatchingCommands(commandNames); } private void AccumulateMatchingCommands(IEnumerable commandNames) { // First set the search options SearchResolutionOptions options = SearchResolutionOptions.None; if (All) { options = SearchResolutionOptions.SearchAllScopes; } if (UseAbbreviationExpansion) { options |= SearchResolutionOptions.UseAbbreviationExpansion; } if (UseFuzzyMatching) { options |= SearchResolutionOptions.FuzzyMatch; } if ((this.CommandType & CommandTypes.Alias) != 0) { options |= SearchResolutionOptions.ResolveAliasPatterns; } if ((this.CommandType & (CommandTypes.Function | CommandTypes.Filter | CommandTypes.Configuration)) != 0) { options |= SearchResolutionOptions.ResolveFunctionPatterns; } foreach (string commandName in commandNames) { try { // Determine if the command name is module-qualified, and search // available modules for the command. string moduleName; string plainCommandName = Utils.ParseCommandName(commandName, out moduleName); bool isModuleQualified = (moduleName != null); // If they've specified a module name, we can do some smarter filtering. // Otherwise, we have to filter everything. if ((this.Module.Length == 1) && (!WildcardPattern.ContainsWildcardCharacters(this.Module[0]))) { moduleName = this.Module[0]; } bool isPattern = WildcardPattern.ContainsWildcardCharacters(plainCommandName) || UseAbbreviationExpansion || UseFuzzyMatching; if (isPattern) { options |= SearchResolutionOptions.CommandNameIsPattern; } // Try to initially find the command in the available commands int count = 0; bool isDuplicate; bool resultFound = FindCommandForName(options, commandName, isPattern, true, ref count, out isDuplicate); // If we didn't find the command, or if it had a wildcard, also see if it // is in an available module if (!resultFound || isPattern) { // If the command name had no wildcards or was module-qualified, // import the module so that we can return the fully structured data. // This uses the same code path as module auto-loading. if ((!isPattern) || (!string.IsNullOrEmpty(moduleName))) { string tempCommandName = commandName; if ((!isModuleQualified) && (!string.IsNullOrEmpty(moduleName))) { tempCommandName = moduleName + "\\" + commandName; } try { CommandDiscovery.LookupCommandInfo(tempCommandName, this.MyInvocation.CommandOrigin, this.Context); } catch (CommandNotFoundException) { // Ignore, LookupCommandInfo doesn't handle wildcards. } resultFound = FindCommandForName(options, commandName, isPattern, false, ref count, out isDuplicate); } // Show additional commands from available modules only if ListImported is not specified else if (!ListImported) { if (TotalCount < 0 || count < TotalCount) { IEnumerable commands; if (UseFuzzyMatching) { foreach (var commandScore in System.Management.Automation.Internal.ModuleUtils.GetFuzzyMatchingCommands( plainCommandName, this.Context, this.MyInvocation.CommandOrigin, rediscoverImportedModules: true, moduleVersionRequired: _isFullyQualifiedModuleSpecified)) { _commandScores.Add(commandScore); } commands = _commandScores.Select(static x => x.Command).ToList(); } else { commands = System.Management.Automation.Internal.ModuleUtils.GetMatchingCommands( plainCommandName, this.Context, this.MyInvocation.CommandOrigin, rediscoverImportedModules: true, moduleVersionRequired: _isFullyQualifiedModuleSpecified, useAbbreviationExpansion: UseAbbreviationExpansion); } foreach (CommandInfo command in commands) { // Cannot pass in "command" by ref (foreach iteration variable) CommandInfo current = command; if (IsCommandMatch(ref current, out isDuplicate) && (!IsCommandInResult(current)) && IsParameterMatch(current)) { _accumulatedResults.Add(current); // Make sure we don't exceed the TotalCount parameter ++count; if (TotalCount >= 0 && count >= TotalCount) { break; } } } } } } // If we are trying to match a single specific command name (no glob characters) // then we need to write an error if we didn't find it. if (!isDuplicate) { if (!resultFound && !isPattern) { CommandNotFoundException e = new CommandNotFoundException( commandName, null, "CommandNotFoundException", DiscoveryExceptions.CommandNotFoundException); WriteError( new ErrorRecord( e.ErrorRecord, e)); continue; } } } catch (CommandNotFoundException exception) { WriteError( new ErrorRecord( exception.ErrorRecord, exception)); } } } private bool FindCommandForName(SearchResolutionOptions options, string commandName, bool isPattern, bool emitErrors, ref int currentCount, out bool isDuplicate) { CommandSearcher searcher = new CommandSearcher( commandName, options, this.CommandType, this.Context); bool resultFound = false; isDuplicate = false; while (true) { try { if (!searcher.MoveNext()) { break; } } catch (ArgumentException argumentException) { if (emitErrors) { WriteError(new ErrorRecord(argumentException, "GetCommandInvalidArgument", ErrorCategory.SyntaxError, null)); } continue; } catch (PathTooLongException pathTooLong) { if (emitErrors) { WriteError(new ErrorRecord(pathTooLong, "GetCommandInvalidArgument", ErrorCategory.SyntaxError, null)); } continue; } catch (FileLoadException fileLoadException) { if (emitErrors) { WriteError(new ErrorRecord(fileLoadException, "GetCommandFileLoadError", ErrorCategory.ReadError, null)); } continue; } catch (MetadataException metadataException) { if (emitErrors) { WriteError(new ErrorRecord(metadataException, "GetCommandMetadataError", ErrorCategory.MetadataError, null)); } continue; } catch (FormatException formatException) { if (emitErrors) { WriteError(new ErrorRecord(formatException, "GetCommandBadFileFormat", ErrorCategory.InvalidData, null)); } continue; } CommandInfo current = ((IEnumerator)searcher).Current; // skip private commands as early as possible // (i.e. before setting "result found" flag and before trying to use ArgumentList parameter) // see bugs Windows 7: #520498 and #520470 CommandOrigin origin = this.MyInvocation.CommandOrigin; if (!SessionState.IsVisible(origin, current)) { continue; } bool tempResultFound = IsCommandMatch(ref current, out isDuplicate); if (tempResultFound && (!IsCommandInResult(current))) { resultFound = true; if (IsParameterMatch(current)) { // Make sure we don't exceed the TotalCount parameter ++currentCount; if (TotalCount >= 0 && currentCount > TotalCount) { break; } if (UseFuzzyMatching) { int score = FuzzyMatcher.GetDamerauLevenshteinDistance(current.Name, commandName); _commandScores.Add(new CommandScore(current, score)); } _accumulatedResults.Add(current); if (ArgumentList != null) { // Don't iterate the enumerator any more. If -arguments was specified, then we stop at the first match break; } } // Only for this case, the loop should exit // Get-Command Foo if (isPattern || All || TotalCount != -1 || _isCommandTypeSpecified || _isModuleSpecified || _isFullyQualifiedModuleSpecified) { continue; } else { break; } } } if (All) { // Get additional matching commands from module tables. foreach (CommandInfo command in GetMatchingCommandsFromModules(commandName)) { CommandInfo c = command; bool tempResultFound = IsCommandMatch(ref c, out isDuplicate); if (tempResultFound) { resultFound = true; if (!IsCommandInResult(command) && IsParameterMatch(c)) { ++currentCount; if (TotalCount >= 0 && currentCount > TotalCount) { break; } _accumulatedResults.Add(c); } // Make sure we don't exceed the TotalCount parameter } } } return resultFound; } /// /// Determines if the specific command information has already been /// written out based on the path or definition. /// /// /// The command information to check for duplication. /// /// /// true if the command has already been written out. /// private bool IsDuplicate(CommandInfo info) { bool result = false; string key = null; do // false loop { ApplicationInfo appInfo = info as ApplicationInfo; if (appInfo != null) { key = appInfo.Path; break; } CmdletInfo cmdletInfo = info as CmdletInfo; if (cmdletInfo != null) { key = cmdletInfo.FullName; break; } ScriptInfo scriptInfo = info as ScriptInfo; if (scriptInfo != null) { key = scriptInfo.Definition; break; } ExternalScriptInfo externalScriptInfo = info as ExternalScriptInfo; if (externalScriptInfo != null) { key = externalScriptInfo.Path; break; } } while (false); if (key != null) { if (_commandsWritten.ContainsKey(key)) { result = true; } else { _commandsWritten.Add(key, info); } } return result; } private bool IsParameterMatch(CommandInfo commandInfo) { if ((this.ParameterName == null) && (this.ParameterType == null)) { return true; } if (_matchedParameterNames == null) { _matchedParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); } IEnumerable commandParameters = null; try { IDictionary tmp = commandInfo.Parameters; if (tmp != null) { commandParameters = tmp.Values; } } catch (Exception) { // ignore all exceptions when getting parameter metadata (i.e. parse exceptions, dangling alias exceptions) // and proceed as if there was no parameter metadata } if (commandParameters == null) { // do not match commands which have not been imported yet / for which we don't have parameter metadata yet return false; } else { bool foundMatchingParameter = false; foreach (ParameterMetadata parameterMetadata in commandParameters) { if (IsParameterMatch(parameterMetadata)) { foundMatchingParameter = true; // not breaking out of the loop early, to ensure that _matchedParameterNames gets populated for all command parameters } } return foundMatchingParameter; } } private bool IsParameterMatch(ParameterMetadata parameterMetadata) { // // ParameterName matching // bool nameIsDirectlyMatching = SessionStateUtilities.MatchesAnyWildcardPattern(parameterMetadata.Name, _parameterNameWildcards, true); bool oneOfAliasesIsMatching = false; foreach (string alias in parameterMetadata.Aliases ?? Enumerable.Empty()) { if (SessionStateUtilities.MatchesAnyWildcardPattern(alias, _parameterNameWildcards, true)) { _matchedParameterNames.Add(alias); oneOfAliasesIsMatching = true; // don't want to break out of the loop early (need to fully populate _matchedParameterNames hashset) } } bool nameIsMatching = nameIsDirectlyMatching || oneOfAliasesIsMatching; if (nameIsMatching) { _matchedParameterNames.Add(parameterMetadata.Name); } // // ParameterType matching // bool typeIsMatching; if ((_parameterTypes == null) || (_parameterTypes.Length == 0)) { typeIsMatching = true; } else { typeIsMatching = false; if (_parameterTypes != null && _parameterTypes.Length > 0) { typeIsMatching |= _parameterTypes.Any(parameterMetadata.IsMatchingType); } } return nameIsMatching && typeIsMatching; } private bool IsCommandMatch(ref CommandInfo current, out bool isDuplicate) { bool isCommandMatch = false; isDuplicate = false; // Be sure we haven't already found this command before if (!IsDuplicate(current)) { if ((current.CommandType & this.CommandType) != 0) { isCommandMatch = true; } // If the command in question is a cmdlet or (a function/filter/configuration/alias and we are filtering on nouns or verbs), // then do the verb/noun check if (current.CommandType == CommandTypes.Cmdlet || ((_verbs.Length > 0 || _nouns.Length > 0) && (current.CommandType == CommandTypes.Function || current.CommandType == CommandTypes.Filter || current.CommandType == CommandTypes.Configuration || current.CommandType == CommandTypes.Alias))) { if (!IsNounVerbMatch(current)) { isCommandMatch = false; } } else { if (_isFullyQualifiedModuleSpecified) { bool foundModuleMatch = false; foreach (var moduleSpecification in _moduleSpecifications) { if (ModuleIntrinsics.IsModuleMatchingModuleSpec(current.Module, moduleSpecification)) { foundModuleMatch = true; break; } } if (!foundModuleMatch) { isCommandMatch = false; } } else if (_modulePatterns != null && _modulePatterns.Count > 0) { if (!SessionStateUtilities.MatchesAnyWildcardPattern(current.ModuleName, _modulePatterns, true)) { isCommandMatch = false; } } } if (isCommandMatch) { if (Syntax.IsPresent && current is AliasInfo ai) { // If the matching command was an alias, then use the resolved command // instead of the alias... current = ai.ResolvedCommand ?? CommandDiscovery.LookupCommandInfo( ai.UnresolvedCommandName, this.MyInvocation.CommandOrigin, this.Context); // there are situations where both ResolvedCommand and UnresolvedCommandName // are both null (often due to multiple versions of modules with aliases) // therefore we need to exit early. if (current == null) { return false; } } if (ArgumentList != null && current is not CmdletInfo && current is not IScriptCommandInfo) { // If current is not a cmdlet or script, we need to throw a terminating error. ThrowTerminatingError( new ErrorRecord( PSTraceSource.NewArgumentException( "ArgumentList", DiscoveryExceptions.CommandArgsOnlyForSingleCmdlet), "CommandArgsOnlyForSingleCmdlet", ErrorCategory.InvalidArgument, current)); } // If the command implements dynamic parameters // then we must make a copy of the CommandInfo which merges the // dynamic parameter metadata with the statically defined parameter // metadata bool needCopy = false; try { // We can ignore some errors that occur when checking if // the command implements dynamic parameters. needCopy = current.ImplementsDynamicParameters; } catch (PSSecurityException) { // Ignore execution policies in get-command, those will get // raised when trying to run the real command } catch (RuntimeException) { // Ignore parse/runtime exceptions. Again, they will get // raised again if the script is actually run. } if (needCopy) { try { CommandInfo newCurrent = current.CreateGetCommandCopy(ArgumentList); if (ArgumentList != null) { // We need to prepopulate the parameter metadata in the CmdletInfo to // ensure there are no errors. Getting the ParameterSets property // triggers the parameter metadata to be generated ReadOnlyCollection parameterSets = newCurrent.ParameterSets; } current = newCurrent; } catch (MetadataException metadataException) { // A metadata exception can be thrown if the dynamic parameters duplicates a parameter // of the cmdlet. WriteError(new ErrorRecord(metadataException, "GetCommandMetadataError", ErrorCategory.MetadataError, current)); } catch (ParameterBindingException parameterBindingException) { // if the exception is thrown when retrieving dynamic parameters, ignore it and // the static parameter info will be used. if (!parameterBindingException.ErrorRecord.FullyQualifiedErrorId.StartsWith( "GetDynamicParametersException", StringComparison.Ordinal)) { throw; } } } } } else { isDuplicate = true; } return isCommandMatch; } /// /// Gets matching commands from the module tables. /// /// /// The commandname to look for /// /// /// IEnumerable of CommandInfo objects /// private IEnumerable GetMatchingCommandsFromModules(string commandName) { WildcardPattern matcher = WildcardPattern.Get( commandName, WildcardOptions.IgnoreCase); // Use ModuleTableKeys list in reverse order for (int i = Context.EngineSessionState.ModuleTableKeys.Count - 1; i >= 0; i--) { PSModuleInfo module = null; if (!Context.EngineSessionState.ModuleTable.TryGetValue(Context.EngineSessionState.ModuleTableKeys[i], out module)) { Dbg.Assert(false, "ModuleTableKeys should be in sync with ModuleTable"); } else { bool isModuleMatch = false; if (!_isFullyQualifiedModuleSpecified) { isModuleMatch = SessionStateUtilities.MatchesAnyWildcardPattern(module.Name, _modulePatterns, true); } else if (_moduleSpecifications.Any(moduleSpecification => ModuleIntrinsics.IsModuleMatchingModuleSpec(module, moduleSpecification))) { isModuleMatch = true; } if (isModuleMatch) { if (module.SessionState != null) { // Look in function table if ((this.CommandType & (CommandTypes.Function | CommandTypes.Filter | CommandTypes.Configuration)) != 0) { foreach ((string functionName, FunctionInfo functionInfo) in module.SessionState.Internal.GetFunctionTable()) { if (matcher.IsMatch(functionName) && functionInfo.IsImported) { // make sure function doesn't come from the current module's nested module if (functionInfo.Module.Path.Equals(module.Path, StringComparison.OrdinalIgnoreCase)) yield return functionInfo; } } } // Look in alias table if ((this.CommandType & CommandTypes.Alias) != 0) { foreach (var alias in module.SessionState.Internal.GetAliasTable()) { if (matcher.IsMatch(alias.Key) && alias.Value.IsImported) { // make sure alias doesn't come from the current module's nested module if (alias.Value.Module.Path.Equals(module.Path, StringComparison.OrdinalIgnoreCase)) yield return alias.Value; } } } } } } } } /// /// Determines if the specific command information has already been /// added to the result from CommandSearcher. /// /// /// The command information to check for duplication. /// /// /// true if the command is present in the result. /// private bool IsCommandInResult(CommandInfo command) { bool isPresent = false; if (command.Module is not null) { foreach (CommandInfo commandInfo in _accumulatedResults) { if (commandInfo.Module is null || commandInfo.CommandType != command.CommandType) { continue; } // We do reference equal comparison if both command are imported. If either one is not imported, we compare the module path if ((!commandInfo.IsImported || !command.IsImported || !commandInfo.Module.Equals(command.Module)) && ((commandInfo.IsImported && command.IsImported) || !commandInfo.Module.Path.Equals(command.Module.Path, StringComparison.OrdinalIgnoreCase))) { continue; } // If the command has been imported with a prefix, then just checking the names for duplication will not be enough. // Hence, an additional check is done with the prefix information if (commandInfo.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase) || ModuleCmdletBase.RemovePrefixFromCommandName(commandInfo.Name, commandInfo.Prefix).Equals(command.Name, StringComparison.OrdinalIgnoreCase)) { isPresent = true; break; } } } return isPresent; } #endregion #region Members private readonly Dictionary _commandsWritten = new Dictionary(StringComparer.OrdinalIgnoreCase); private List _accumulatedResults = new List(); // These members are the collection of wildcard patterns for the "CmdletSet" private Collection _verbPatterns; private Collection _nounPatterns; private Collection _modulePatterns; #if LEGACYTELEMETRY private Stopwatch _timer = new Stopwatch(); #endif #endregion #region ShowCommandInfo support // Converts to PSObject containing ShowCommand information. private static PSObject ConvertToShowCommandInfo(CommandInfo cmdInfo) { PSObject showCommandInfo = new PSObject(); showCommandInfo.Properties.Add(new PSNoteProperty("Name", cmdInfo.Name)); showCommandInfo.Properties.Add(new PSNoteProperty("ModuleName", cmdInfo.ModuleName)); showCommandInfo.Properties.Add(new PSNoteProperty("Module", GetModuleInfo(cmdInfo))); showCommandInfo.Properties.Add(new PSNoteProperty("CommandType", cmdInfo.CommandType)); showCommandInfo.Properties.Add(new PSNoteProperty("Definition", cmdInfo.Definition)); showCommandInfo.Properties.Add(new PSNoteProperty("ParameterSets", GetParameterSets(cmdInfo))); return showCommandInfo; } private static PSObject GetModuleInfo(CommandInfo cmdInfo) { PSObject moduleInfo = new PSObject(); string moduleName = (cmdInfo.Module != null) ? cmdInfo.Module.Name : string.Empty; moduleInfo.Properties.Add(new PSNoteProperty("Name", moduleName)); return moduleInfo; } private static PSObject[] GetParameterSets(CommandInfo cmdInfo) { ReadOnlyCollection parameterSets = null; try { if (cmdInfo.ParameterSets != null) { parameterSets = cmdInfo.ParameterSets; } } catch (InvalidOperationException) { } catch (PSNotSupportedException) { } catch (PSNotImplementedException) { } if (parameterSets == null) { return Array.Empty(); } List returnParameterSets = new List(cmdInfo.ParameterSets.Count); foreach (CommandParameterSetInfo parameterSetInfo in parameterSets) { PSObject parameterSetObj = new PSObject(); parameterSetObj.Properties.Add(new PSNoteProperty("Name", parameterSetInfo.Name)); parameterSetObj.Properties.Add(new PSNoteProperty("IsDefault", parameterSetInfo.IsDefault)); parameterSetObj.Properties.Add(new PSNoteProperty("Parameters", GetParameterInfo(parameterSetInfo.Parameters))); returnParameterSets.Add(parameterSetObj); } return returnParameterSets.ToArray(); } private static PSObject[] GetParameterInfo(ReadOnlyCollection parameters) { List parameterObjs = new List(parameters.Count); foreach (CommandParameterInfo parameter in parameters) { PSObject parameterObj = new PSObject(); parameterObj.Properties.Add(new PSNoteProperty("Name", parameter.Name)); parameterObj.Properties.Add(new PSNoteProperty("IsMandatory", parameter.IsMandatory)); parameterObj.Properties.Add(new PSNoteProperty("ValueFromPipeline", parameter.ValueFromPipeline)); parameterObj.Properties.Add(new PSNoteProperty("Position", parameter.Position)); parameterObj.Properties.Add(new PSNoteProperty("ParameterType", GetParameterType(parameter.ParameterType))); bool hasParameterSet = false; IList validValues = new List(); var validateSetAttribute = parameter.Attributes.OfType().LastOrDefault(); if (validateSetAttribute != null) { hasParameterSet = true; validValues = validateSetAttribute.ValidValues; } parameterObj.Properties.Add(new PSNoteProperty("HasParameterSet", hasParameterSet)); parameterObj.Properties.Add(new PSNoteProperty("ValidParamSetValues", validValues)); parameterObjs.Add(parameterObj); } return parameterObjs.ToArray(); } private static PSObject GetParameterType(Type parameterType) { PSObject returnParameterType = new PSObject(); bool isEnum = parameterType.IsEnum; bool isArray = parameterType.IsArray; returnParameterType.Properties.Add(new PSNoteProperty("FullName", parameterType.FullName)); returnParameterType.Properties.Add(new PSNoteProperty("IsEnum", isEnum)); returnParameterType.Properties.Add(new PSNoteProperty("IsArray", isArray)); ArrayList enumValues = (isEnum) ? new ArrayList(Enum.GetValues(parameterType)) : new ArrayList(); returnParameterType.Properties.Add(new PSNoteProperty("EnumValues", enumValues)); bool hasFlagAttribute = (isArray) && ((parameterType.GetCustomAttributes(typeof(FlagsAttribute), true)).Length > 0); returnParameterType.Properties.Add(new PSNoteProperty("HasFlagAttribute", hasFlagAttribute)); // Recurse into array elements. object elementType = (isArray) ? GetParameterType(parameterType.GetElementType()) : null; returnParameterType.Properties.Add(new PSNoteProperty("ElementType", elementType)); bool implementsDictionary = (!isEnum && !isArray && (parameterType is IDictionary)); returnParameterType.Properties.Add(new PSNoteProperty("ImplementsDictionary", implementsDictionary)); return returnParameterType; } #endregion } /// /// public class NounArgumentCompleter : IArgumentCompleter { /// /// public IEnumerable CompleteArgument(string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) { if (fakeBoundParameters == null) { throw PSTraceSource.NewArgumentNullException(nameof(fakeBoundParameters)); } var commandInfo = new CmdletInfo("Get-Command", typeof(GetCommandCommand)); var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace) .AddCommand(commandInfo) .AddParameter("Noun", wordToComplete + "*"); if (fakeBoundParameters.Contains("Module")) { ps.AddParameter("Module", fakeBoundParameters["Module"]); } HashSet nouns = new HashSet(); var results = ps.Invoke(); foreach (var result in results) { var dash = result.Name.IndexOf('-'); if (dash != -1) { nouns.Add(result.Name.Substring(dash + 1)); } } return nouns.OrderBy(static noun => noun).Select(static noun => new CompletionResult(noun, noun, CompletionResultType.Text, noun)); } } }