diff --git a/build.psm1 b/build.psm1 index c61750c9ec6..5aedbfd33f1 100644 --- a/build.psm1 +++ b/build.psm1 @@ -907,7 +907,10 @@ function Start-PSPester { [switch]$IncludeCommonTests, [string]$ExperimentalFeatureName, [Parameter(HelpMessage='Title to publish the results as.')] - [string]$Title = 'PowerShell Core Tests' + [string]$Title = 'PowerShell Core Tests', + [Parameter(ParameterSetName='Wait', Mandatory=$true, + HelpMessage='Wait for the debugger to attach to PowerShell before Pester starts. Debug builds only!')] + [switch]$Wait ) if (-not (Get-Module -ListAvailable -Name $Pester -ErrorAction SilentlyContinue | Where-Object { $_.Version -ge "4.2" } )) @@ -1101,6 +1104,13 @@ function Start-PSPester { $PSFlags = @("-settings", $configFile, "-noprofile") } + # -Wait is only available on Debug builds + # It is used to allow the debugger to attach before PowerShell + # runs pester in this case + if($Wait.IsPresent){ + $PSFlags += '-wait' + } + # To ensure proper testing, the module path must not be inherited by the spawned process try { $originalModulePath = $env:PSModulePath diff --git a/src/System.Management.Automation/engine/CommandDiscovery.cs b/src/System.Management.Automation/engine/CommandDiscovery.cs index 5ad7717f084..2d557c46901 100644 --- a/src/System.Management.Automation/engine/CommandDiscovery.cs +++ b/src/System.Management.Automation/engine/CommandDiscovery.cs @@ -735,7 +735,7 @@ internal CommandInfo LookupCommandInfo(string commandName, CommandOrigin command internal static CommandInfo LookupCommandInfo(string commandName, CommandOrigin commandOrigin, ExecutionContext context) { - return LookupCommandInfo(commandName, CommandTypes.All, SearchResolutionOptions.None, commandOrigin, context); + return LookupCommandInfo(commandName, CommandTypes.All, SearchResolutionOptions.ResolveLiteralThenPathPatterns, commandOrigin, context); } internal static CommandInfo LookupCommandInfo( diff --git a/src/System.Management.Automation/engine/CommandSearcher.cs b/src/System.Management.Automation/engine/CommandSearcher.cs index f3f29a514c0..2a4346a5f4d 100644 --- a/src/System.Management.Automation/engine/CommandSearcher.cs +++ b/src/System.Management.Automation/engine/CommandSearcher.cs @@ -456,52 +456,33 @@ private CommandInfo GetNextFromPath() "Trying to resolve the path as an PSPath"); // Find the match if it is. - - Collection resolvedPaths = new Collection(); - - try - { - Provider.CmdletProvider providerInstance; - ProviderInfo provider; - resolvedPaths = - _context.LocationGlobber.GetGlobbedProviderPathsFromMonadPath(_commandName, false, out provider, out providerInstance); - } - catch (ItemNotFoundException) + // Try literal path resolution if it is set to run first + if (_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveLiteralThenPathPatterns)) { - CommandDiscovery.discoveryTracer.TraceError( - "The path could not be found: {0}", - _commandName); - } - catch (DriveNotFoundException) - { - 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); + var path = GetNextLiteralPathThatExists(_commandName, out _); + + if (path != null) + { + return GetInfoFromPath(path); + } } - catch (ProviderInvocationException providerException) + + Collection resolvedPaths = new Collection(); + if (WildcardPattern.ContainsWildcardCharacters(_commandName)) { - CommandDiscovery.discoveryTracer.TraceError( - "The provider associated with the path '{0}' encountered an error: {1}", - _commandName, - providerException.Message); + resolvedPaths = GetNextFromPathUsingWildcards(_commandName, out _); } - catch (PSNotSupportedException) + + // Try literal path resolution if wildcards are enable first and wildcard search failed + if (!_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveLiteralThenPathPatterns) && + resolvedPaths.Count == 0) { - CommandDiscovery.discoveryTracer.TraceError( - "The provider associated with the path '{0}' does not implement ContainerCmdletProvider", - _commandName); + string path = GetNextLiteralPathThatExists(_commandName, out _); + + if (path != null) + { + return GetInfoFromPath(path); + } } if (resolvedPaths.Count > 1) @@ -528,6 +509,64 @@ private CommandInfo GetNextFromPath() return result; } + /// + /// Gets the next path using WildCards. + /// + /// + /// The command to search for. + /// + /// The provider that the command was found in. + /// + /// A collection of full paths to the commands which were found. + /// + private Collection 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 null; + } + private static bool checkPath(string path, string commandName) { return path.StartsWith(commandName, StringComparison.OrdinalIgnoreCase); @@ -1092,15 +1131,21 @@ private string ResolvePSPath(string path) { ProviderInfo provider = null; string resolvedPath = null; - if (WildcardPattern.ContainsWildcardCharacters(path)) + + // 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. - Provider.CmdletProvider providerInstance; - Collection resolvedPaths = _context.LocationGlobber.GetGlobbedProviderPathsFromMonadPath( - path, - false, - out provider, - out providerInstance); + Collection resolvedPaths = GetNextFromPathUsingWildcards(path, out provider); if (resolvedPaths.Count == 0) { @@ -1124,14 +1169,15 @@ private string ResolvePSPath(string path) } } - // Revert to previous path resolver if wildcards produces no results. - if ((resolvedPath == null) || (provider == null)) + // Try literal path resolution if wildcards are enabled first and wildcard search failed + if (!_commandResolutionOptions.HasFlag(SearchResolutionOptions.ResolveLiteralThenPathPatterns) && + ((resolvedPath == null) || (provider == null))) { - resolvedPath = _context.LocationGlobber.GetProviderPath(path, out provider); + resolvedPath = GetNextLiteralPathThatExists(path, out provider); } // Verify the path was resolved to a file system path - if (provider.NameEquals(_context.ProviderNames.FileSystem)) + if (provider != null && provider.NameEquals(_context.ProviderNames.FileSystem)) { result = resolvedPath; @@ -1176,6 +1222,32 @@ private string ResolvePSPath(string path) return result; } + /// + /// Gets the next literal path. + /// Filtering to ones that exist for the filesystem. + /// + /// + /// The command to search for. + /// + /// The provider that the command was found in. + /// + /// Full path to the command. + /// + 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; + } + /// /// Creates a collection of patterns used to find the command. /// @@ -1609,5 +1681,10 @@ internal enum SearchResolutionOptions /// Enable searching for cmdlets/functions by abbreviation expansion. /// UseAbbreviationExpansion = 0x20, + + /// + /// Enable resolving wildcard in paths. + /// + ResolveLiteralThenPathPatterns = 0x40 } } diff --git a/test/powershell/engine/Basic/CommandDiscovery.Tests.ps1 b/test/powershell/engine/Basic/CommandDiscovery.Tests.ps1 index 4b9be7a736b..030301477c2 100644 --- a/test/powershell/engine/Basic/CommandDiscovery.Tests.ps1 +++ b/test/powershell/engine/Basic/CommandDiscovery.Tests.ps1 @@ -85,18 +85,107 @@ Describe "Command Discovery tests" -Tags "CI" { (& 'location').Path | Should -Be (get-location).Path } - Context "Get-Command should use globbing for scripts" { + Context "Use literal path first when executing scripts" { + BeforeAll { + $firstFileName = '[test1].ps1' + $secondFileName = '1.ps1' + $thirdFileName = '2.ps1' + $firstResult = "executing $firstFileName in root" + $secondResult = "executing $secondFileName in root" + $thirdResult = "executing $thirdFileName in root" + setup -f $firstFileName -content "'$firstResult'" + setup -f $secondFileName -content "'$secondResult'" + setup -f $thirdFileName -content "'$thirdResult'" + + $subFolder = 'subFolder' + $firstFileInSubFolder = Join-Path $subFolder -ChildPath $firstFileName + $secondFileInSubFolder = Join-Path $subFolder -ChildPath $secondFileName + $thirdFileInSubFolder = Join-Path $subFolder -ChildPath $thirdFileName + setup -f $firstFileInSubFolder -content "'$firstResult'" + setup -f $secondFileInSubFolder -content "'$secondResult'" + setup -f $thirdFileInSubFolder -content "'$thirdResult'" + + $secondFileSearchInSubfolder = (Join-Path -Path $subFolder -ChildPath '[t1].ps1') + + $executionWithWildcardCases = @( + #Region relative paths with './' + @{command = '.\[test1].ps1' ; expectedResult = $firstResult; name = '.\[test1].ps1'} + @{command = '.\[t1].ps1' ; expectedResult = $secondResult; name = '.\[t1].ps1'} + #endregion + + #Region relative Subfolder paths without './' + @{command = $secondFileInSubFolder ; expectedResult = $secondResult; name = $secondFileInSubFolder} + + # Wildcard search is not being performed in this scenario before this change. + # I noted the issue in the pending message + @{command = $firstFileInSubFolder ; expectedResult = $firstResult; name = $firstFileInSubFolder; Pending="See note about wildcard in https://github.com/PowerShell/PowerShell/issues/9256"} + @{command = $secondFileSearchInSubfolder ; expectedResult = $secondResult; name = $secondFileSearchInSubfolder; Pending="See note about wildcard in https://github.com/PowerShell/PowerShell/issues/9256"} + #endregion + #Region relative Subfolder paths with '.\' + @{command = '.\' + $secondFileInSubFolder ; expectedResult = $secondResult; name = $secondFileInSubFolder} + @{command = '.\subFolder\[test1].ps1' ; expectedResult = $firstResult; name = '.\subFolder\[test1].ps1'} + @{command = '.\subFolder\[t1].ps1' ; expectedResult = $secondResult; name = '.\' + $secondFileSearchInSubfolder} + @{command = '.\' + $firstFileInSubFolder ; expectedResult = $firstResult; name = '.\' + $firstFileInSubFolder} + @{command = '.\' + $secondFileSearchInSubfolder ; expectedResult = $secondResult; name = '.\' + $secondFileSearchInSubfolder} + #endregion + + #region rooted paths + @{command = (Join-Path ${TestDrive} -ChildPath '[test1].ps1') ; expectedResult = $firstResult; name = '.\[test1].ps1 by fully qualified path'} + @{command = (Join-Path ${TestDrive} -ChildPath '[t1].ps1') ; expectedResult = $secondResult; name = '.\1.ps1 by fully qualified path with wildcard'} + #endregion + ) + + $shouldNotExecuteCases = @( + @{command = 'subFolder\[test1].ps1' ; testName = 'Relative path that where module qualified syntax overlaps'; ExpectedErrorId = 'CouldNotAutoLoadModule'} + @{command = '.\[12].ps1' ; testName = 'relative path with bracket wildcard matctching multiple files'} + @{command = (Join-Path ${TestDrive} -ChildPath '[12].ps1') ; testName = 'fully qualified path with bracket wildcard matching multiple files'} + ) + + Push-Location ${TestDrive}\ + } + + AfterAll { + Pop-Location + } + + It "Invoking should return ''" -TestCases $executionWithWildcardCases { + param($command, $expectedResult, [String]$Pending) + + if($Pending) + { + Set-TestInconclusive -Message $Pending + } + + & $command | Should -BeExactly $expectedResult + } + + It "'' should not execute" -TestCases $shouldNotExecuteCases { + param( + [string] + $command, + [string] + $ExpectedErrorId = 'CommandNotFoundException' + ) + { & $command } | Should -Throw -ErrorId $ExpectedErrorId + } + } + + Context "Get-Command should use globbing first for scripts" { BeforeAll { $firstResult = '[first script]' $secondResult = 'alt script' + $thirdResult = 'bad script' setup -f '[test1].ps1' -content "'$firstResult'" setup -f '1.ps1' -content "'$secondResult'" + setup -f '2.ps1' -content "'$thirdResult'" $gcmWithWildcardCases = @( @{command = '.\?[tb]est1?.ps1'; expectedCommand = '[test1].ps1'; expectedCommandCount =1; name = '''.\?[tb]est1?.ps1'''} @{command = (Join-Path ${TestDrive} -ChildPath '?[tb]est1?.ps1'); expectedCommand = '[test1].ps1'; expectedCommandCount =1 ; name = '''.\?[tb]est1?.ps1'' by fully qualified path'} @{command = '.\[test1].ps1'; expectedCommand = '1.ps1'; expectedCommandCount =1; name = '''.\[test1].ps1'''} @{command = (Join-Path ${TestDrive} -ChildPath '[test1].ps1'); expectedCommand = '1.ps1'; expectedCommandCount =1 ; name = '''.\[test1].ps1'' by fully qualified path'} + @{command = '.\[12].ps1'; expectedCommand = '1.ps1'; expectedCommandCount =0; name = 'relative path with bracket wildcard matctching multiple files'} + @{command = (Join-Path ${TestDrive} -ChildPath '[12].ps1'); expectedCommand = '1.ps1'; expectedCommandCount =0 ; name = 'fully qualified path with bracket wildcard matctching multiple files'} ) Push-Location ${TestDrive}\ @@ -108,9 +197,12 @@ Describe "Command Discovery tests" -Tags "CI" { It "Get-Command should return command named ''" -TestCases $gcmWithWildcardCases { param($command, $expectedCommand, $expectedCommandCount) - $commands = Get-Command -Name $command + $commands = @(Get-Command -Name $command) $commands.Count | Should -Be $expectedCommandCount - $commands.Name | Should -BeExactly $expectedCommand + if($expectedCommandCount -gt 0) + { + $commands.Name | Should -BeExactly $expectedCommand + } } } }