diff --git a/src/System.Management.Automation/FormatAndOutput/common/BaseOutputtingCommand.cs b/src/System.Management.Automation/FormatAndOutput/common/BaseOutputtingCommand.cs index 136b7a26ef5..17cbae65894 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/BaseOutputtingCommand.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/BaseOutputtingCommand.cs @@ -552,7 +552,8 @@ private void ProcessOutOfBandPayload(FormatEntryData fed) } else { - _lo.WriteLine(rte.text); + // Write out raw text without any changes to it. + _lo.WriteRawText(rte.text); } return; diff --git a/src/System.Management.Automation/FormatAndOutput/common/ILineOutput.cs b/src/System.Management.Automation/FormatAndOutput/common/ILineOutput.cs index a1f373a5dd5..3c200664636 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/ILineOutput.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/ILineOutput.cs @@ -250,6 +250,13 @@ internal virtual void ExecuteBufferPlayBack(DoPlayBackCall playback) { } /// internal abstract void WriteLine(string s); + /// + /// Write a line of string as raw text to the output device, with no change to the string. + /// For example, keeping VT escape sequences intact in it. + /// + /// The raw text to be written to the device. + internal virtual void WriteRawText(string s) => WriteLine(s); + internal WriteStreamType WriteStream { get; @@ -431,7 +438,7 @@ private void WriteLineInternal(string val, int cols) /// Implementation of the ILineOutput interface accepting an instance of a /// TextWriter abstract class. /// - internal class TextWriterLineOutput : LineOutput + internal sealed class TextWriterLineOutput : LineOutput { #region ILineOutput methods @@ -467,9 +474,17 @@ internal override int RowNumber /// internal override void WriteLine(string s) { - CheckStopProcessing(); + WriteRawText(PSHostUserInterface.GetOutputString(s, isHost: false)); + } - s = PSHostUserInterface.GetOutputString(s, isHost: false); + /// + /// Write a raw text by delegating to the writer underneath, with no change to the text. + /// For example, keeping VT escape sequences intact in it. + /// + /// The raw text to be written to the device. + internal override void WriteRawText(string s) + { + CheckStopProcessing(); if (_suppressNewline) { @@ -480,6 +495,7 @@ internal override void WriteLine(string s) _writer.WriteLine(s); } } + #endregion /// diff --git a/test/powershell/engine/Formatting/ErrorView.Tests.ps1 b/test/powershell/engine/Formatting/ErrorView.Tests.ps1 index 7de4f9d431d..18173354a65 100644 --- a/test/powershell/engine/Formatting/ErrorView.Tests.ps1 +++ b/test/powershell/engine/Formatting/ErrorView.Tests.ps1 @@ -115,7 +115,8 @@ Describe 'Tests for $ErrorView' -Tag CI { It "Error shows for advanced function" { # need to have it virtually interactive so that InvocationInfo.MyCommand is empty - $e = '[cmdletbinding()]param()$pscmdlet.writeerror([System.Management.Automation.ErrorRecord]::new(([System.NotImplementedException]::new("myTest")),"stub","notimplemented","command"))' | pwsh -noprofile -file - 2>&1 | Out-String + $e = '[cmdletbinding()]param()$pscmdlet.writeerror([System.Management.Automation.ErrorRecord]::new(([System.NotImplementedException]::new("myTest")),"stub","notimplemented","command"))' | pwsh -noprofile -file - 2>&1 + $e = $e | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } | Out-String $e | Should -Not -BeNullOrEmpty # need to see if ANSI escape sequences are in the output as ANSI is disabled for CI diff --git a/test/powershell/engine/Formatting/OutputRendering.Tests.ps1 b/test/powershell/engine/Formatting/OutputRendering.Tests.ps1 index 53732a2bd6a..7b67f71233c 100644 --- a/test/powershell/engine/Formatting/OutputRendering.Tests.ps1 +++ b/test/powershell/engine/Formatting/OutputRendering.Tests.ps1 @@ -39,7 +39,7 @@ Describe 'OutputRendering tests' { param($outputRendering, $ansi) $PSStyle.OutputRendering = $outputRendering - $out = "$($PSStyle.Foreground.Green)hello" | Out-String + $out = [pscustomobject] @{ key = "$($PSStyle.Foreground.Green)hello" } | Out-String if ($ansi) { $out | Should -BeLike "*`e*" -Because ($out | Format-Hex | Out-String) diff --git a/test/powershell/engine/Formatting/PSStyle.Tests.ps1 b/test/powershell/engine/Formatting/PSStyle.Tests.ps1 index 4fc8087a904..e232ebe6d57 100644 --- a/test/powershell/engine/Formatting/PSStyle.Tests.ps1 +++ b/test/powershell/engine/Formatting/PSStyle.Tests.ps1 @@ -286,6 +286,77 @@ Describe 'Tests for $PSStyle automatic variable' { $strDec.ContentLength | Should -Be $word.Length $strDec.ToString("PlainText") | Should -Be $word } + + It "String intput to Out-String should be intact with OutputRendering=''" -TestCases @( + @{ OutputRendering = 'Ansi'; ContainsAnsi = $true } + @{ OutputRendering = 'Host'; ContainsAnsi = $false } + @{ OutputRendering = 'PlainText'; ContainsAnsi = $false } + ) { + param($OutputRendering, $ContainsAnsi) + + $oldRender = $PSStyle.OutputRendering + $testStr = "`e[31mABC`e[0m" + + try { + $PSStyle.OutputRendering = $OutputRendering + ## For input that actually goes through formatting, Out-String should remove VT sequences + ## from the formatting output based on the output rendering option that is in effect. + (Get-Verb -Verb Get | Out-String).Contains("`e[") | Should -Be $ContainsAnsi + ## For string input, since no formatting is applied, Out-String should keep the string intact. + ($testStr | Out-String).Trim() | Should -BeExactly $testStr + } + finally { + $PSStyle.OutputRendering = $oldRender + } + } + + It "String input to Out-File should be intact with OutputRendering=''" -TestCases @( + @{ OutputRendering = 'Ansi'; } + @{ OutputRendering = 'Host'; } + @{ OutputRendering = 'PlainText'; } + ) { + param($OutputRendering) + + $oldRender = $PSStyle.OutputRendering + $content = "Read-Host -Prompt '`e[33mEnter your device code`e[0m'" + Set-Content -Path $TestDrive\test.ps1 -Value $content -Encoding utf8NoBOM + + try { + $PSStyle.OutputRendering = $OutputRendering + Get-Content $TestDrive\test.ps1 > $TestDrive\copy.ps1 + (Get-Content $TestDrive\copy.ps1 -Raw).Trim() | Should -BeExactly $content + } + finally { + $PSStyle.OutputRendering = $oldRender + Remove-Item $TestDrive\test.ps1 -Force + Remove-Item $TestDrive\copy.ps1 -Force + } + } + + It "Comment based help works with `$PSStyle when OutputRendering=''" -TestCases @( + @{ OutputRendering = 'Ansi'; ContainsAnsi = $true } + @{ OutputRendering = 'Host'; ContainsAnsi = $false } + @{ OutputRendering = 'PlainText'; ContainsAnsi = $false } + ) { + param($OutputRendering, $ContainsAnsi) + + $oldRender = $PSStyle.OutputRendering + + function Test-PSStyle { + <# + .Description + Get-Function displays the name and syntax of all functions in the session. + #> + } + + try { + $PSStyle.OutputRendering = $OutputRendering + (Get-Help Test-PSStyle | Out-String).Contains("`e[31mdisplays`e[0m") | Should -Be $ContainsAnsi + } + finally { + $PSStyle.OutputRendering = $oldRender + } + } } Describe 'Handle strings with escape sequences in formatting' {