diff --git a/src/System.Management.Automation/engine/GetCommandCommand.cs b/src/System.Management.Automation/engine/GetCommandCommand.cs index 06bdf643a5b..a709fed7943 100644 --- a/src/System.Management.Automation/engine/GetCommandCommand.cs +++ b/src/System.Management.Automation/engine/GetCommandCommand.cs @@ -531,9 +531,7 @@ private void OutputResultsHelper(IEnumerable results) { if (!string.IsNullOrEmpty(result.Syntax)) { - PSObject syntax = PSObject.AsPSObject(result.Syntax); - - syntax.IsHelpObject = true; + PSObject syntax = GetSyntaxObject(result); WriteObject(syntax); } @@ -571,6 +569,81 @@ private void OutputResultsHelper(IEnumerable results) #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. /// @@ -1229,33 +1302,37 @@ private bool IsCommandMatch(ref CommandInfo current, out bool isDuplicate) if (isCommandMatch) { - if (ArgumentList != null) + if (Syntax.IsPresent && current is AliasInfo ai) { - AliasInfo ai = current as AliasInfo; - if (ai != null) + // 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) { - // If the matching command was an alias, then use the resolved command - // instead of the alias... - current = ai.ResolvedCommand; - if (current == null) - { - return false; - } - } - else if (!(current is CmdletInfo || current is 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)); + return false; } } + if (ArgumentList != null && !(current is CmdletInfo || current is 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 diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 index 9847d40b8ac..7707d425737 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Command.Tests.ps1 @@ -103,3 +103,80 @@ Describe "Get-Command Feature tests" -Tag Feature { } } } + +Describe "Get-Command" -Tag CI { + BeforeAll { + Import-Module Microsoft.PowerShell.Management + } + Context "-Syntax tests" { + It "Should return a string object when -Name is an alias and -Syntax is specified" { + $Result = Get-Command -Name del -Syntax + + $Result | Should -BeOfType [String] + $Result | Should -Match 'del \[-Path\]' + } + + It "Should replace commands with aliases in matching commands when using a wildcard search" { + $Result = Get-Command -Name sp* -Syntax + + $Result | Should -BeOfType [String] + $Result -join '' | Should -Match 'sp \(alias\) -> Set-ItemProperty' + $Result -join '' | Should -Match 'sp \[-Path\]' + } + + It "Should not add the alias (alias) -> command decorator for non-alias commands" { + $Result = Get-Command -Name sp* -Syntax + + $Result -join '' | Should -Not -Match 'Split-Path \(alias\) -> Split-Path' + $Result -join '' | Should -Match 'Split-Path \[-Path\]' + } + + It "Should only replace aliases when given multiple entries including a command and an alias" { + $Result = Get-Command -Name get-help, del -Syntax + + $Result -join '' | Should -Match 'del \(alias\) -> Remove-Item' + $Result -join '' | Should -Match 'del \[-Path\]' + $Result -join '' | Should -Match 'Get-Help \[\[-Name\]' + $Result -join '' | Should -Not -Match 'del \(alias\) -> Get-Help' + } + + It "Should return the path to an aliased script when -Syntax is specified" { + # First, create a script file + $TestGcmSyntax = @' + [CmdletBinding()] + param( + [Parameter(Position=0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [ValidateNotNullOrEmpty()] + [string[]] + $Name + ) + process { + "Processing ${Name}" + } +'@ + Set-Content -Path TestDrive:\Test-GcmSyntax.ps1 -Value $TestGcmSyntax + + # Now set up an alias for that file + New-Alias -Name tgs -Value TestDrive:\Test-GcmSyntax.ps1 + + $Result = Get-Command -Name tgs -Syntax + + $Result | Should -Match "tgs \(alias\) -> $([Regex]::Escape((Get-Item TestDrive:\\Test-GcmSyntax.ps1).FullName))" + } + } + + Context "-Name tests" { + It "Should return a AliasInfo object when -Name is an alias" { + $Result = Get-Command -Name del + + $Result | Should -BeOfType [System.Management.Automation.AliasInfo] + $Result.DisplayName | Should -Be 'del -> Remove-Item' + } + + It "Should return a CommandInfo object when -Name is a command" { + $Result = Get-Command -Name Remove-Item + + $Result | Should -BeOfType [System.Management.Automation.CommandInfo] + } + } +}