diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 14057f05f6..bcddbee0b2 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -3279,9 +3279,9 @@ namespace System.Management.Automation.Language /// True is the compiler should add the success setting, false otherwise. private bool ShouldSetExecutionStatusToSuccess(PipelineAst pipelineAst) { - ExpressionAst expressionAst = pipelineAst.GetPureExpression(); + ExpressionAst expressionAst = GetSingleExpressionFromPipeline(pipelineAst); - // If the pipeline is not a simple expression, it will set $? + // If the pipeline is not a single expression, it will set $? if (expressionAst == null) { return false; @@ -3291,6 +3291,22 @@ namespace System.Management.Automation.Language return ShouldSetExecutionStatusToSuccess(expressionAst); } + /// + /// If the pipeline contains a single expression, the expression is returned, otherwise null is returned. + /// This method is different from in that it allows the single + /// expression to have redirections. + /// + private static ExpressionAst GetSingleExpressionFromPipeline(PipelineAst pipelineAst) + { + var pipelineElements = pipelineAst.PipelineElements; + if (pipelineElements.Count == 1 && pipelineElements[0] is CommandExpressionAst expr) + { + return expr.Expression; + } + + return null; + } + /// /// Determines whether an assignment statement must have an explicit setting /// for $? = $true after it by the compiler. diff --git a/test/powershell/Language/Scripting/DollarHook.Tests.ps1 b/test/powershell/Language/Scripting/DollarHook.Tests.ps1 index 86fb70306d..198b93a1de 100644 --- a/test/powershell/Language/Scripting/DollarHook.Tests.ps1 +++ b/test/powershell/Language/Scripting/DollarHook.Tests.ps1 @@ -85,4 +85,84 @@ Describe 'Tests for setting $? for execution success' -Tag 'CI' { $script:hookValues | Should -Be $HookResults $output | Should -Be $PipelineResults } + + It 'Sets $? correctly for single expression with redirection ''''' -TestCases @( + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" > $null; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" >> $null; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" > TESTDRIVE:\out.txt; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" >> TESTDRIVE:\out.txt; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" 2> $null; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" 2>> $null; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" 2> TESTDRIVE:\out.txt; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" 2>> TESTDRIVE:\out.txt; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" 2>&1; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" 2>&1; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" 2>&1 > $null; Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; "b" 2>&1 > TESTDRIVE:\out.txt; Hook $?'; HookResults = $($false, $true); } + + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" > $null); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" >> $null); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" > TESTDRIVE:\out.txt); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" >> TESTDRIVE:\out.txt); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" 2> $null); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" 2>> $null); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" 2> TESTDRIVE:\out.txt); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" 2>> TESTDRIVE:\out.txt); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" 2>&1); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" 2>&1 > $null); Hook $?'; HookResults = $($false, $true); } + @{ Expression = 'Write-Error "Bad"; Hook $?; ("b" 2>&1 > TESTDRIVE:\out.txt); Hook $?'; HookResults = $($false, $true); } + ) { + param([string]$Expression, [object[]]$HookResults) + + Invoke-Expression $Expression 2>&1 >$null + + $script:hookValues | Should -Be $HookResults + } + + Context 'Validate $? with potential terminating error' { + + ## Script execution directly in Pester tests will be enclosed in try/catch by the Pester, + ## and therefore, general exceptions thrown from an expression like "1/0" will be turned + ## into a terminating exception, which will stop the execution of remaining scripts. + ## + ## For those test cases, we have to use a PowerShell instance, so as to keep the default + ## error handling behavior for the general exceptions. + + BeforeAll { + $pwsh = [powershell]::Create() + + function Invoke([string] $script) + { + $pwsh.Commands.Clear() + $pwsh.Streams.ClearStreams() + $pwsh.AddScript($script).Invoke() + } + + $root = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString()) + $null = Invoke "New-PSDrive -Name TESTDRIVE -PSProvider FileSystem -Root $root" + } + + Afterall { + $null = Invoke "Remove-PSDrive -Name TESTDRIVE -PSProvider FileSystem -Force" + $pwsh.Dispose() + } + + It 'Sets $? correctly for single expression with redirection ''''' -TestCases @( + @{ Expression = '1/0 > $null; $?'; Result = $false; } + @{ Expression = '1/0 >> $null; $?'; Result = $false; } + @{ Expression = '1/0 > TESTDRIVE:\out.txt; $?'; Result = $false; } + @{ Expression = '1/0 >> TESTDRIVE:\out.txt; $?'; Result = $false; } + @{ Expression = '1/0 2>&1; $?'; Result = $false; } + @{ Expression = '1/0 2>&1 > $null; $?'; Result = $false; } + @{ Expression = '1/0 2>&1 > TESTDRIVE:\out.txt; $?'; Result = $false; } + + @{ Expression = '"b" > NonExistDrive:\nowhere.txt; $?'; Result = $false; } + @{ Expression = '"b" >> NonExistDrive:\nowhere.txt; $?'; Result = $false; } + @{ Expression = '"b" 2>&1 > NonExistDrive:\nowhere.txt; $?'; Result = $false; } + ) { + param([string]$Expression, $Result) + + Invoke $Expression | Should -Be $Result + } + } }