diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index 985d90ab3ec..a707748deeb 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -4161,13 +4161,18 @@ internal void Trace(string messageId, string resourceString, params object[] arg internal void TraceLine(IScriptExtent extent) { - string msg = PositionUtilities.BriefMessage(extent.StartScriptPosition); + string msg = PositionUtilities.BriefMessage(extent); InternalHostUserInterface ui = (InternalHostUserInterface)_context.EngineHostInterface.UI; ActionPreference pref = _context.PSDebugTraceStep ? ActionPreference.Inquire : ActionPreference.Continue; - ui.WriteDebugLine(msg, ref pref); + // Write each line separately so each gets the DEBUG: prefix + string[] lines = msg.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + foreach (string line in lines) + { + ui.WriteDebugLine(line, ref pref); + } if (pref == ActionPreference.Continue) _context.PSDebugTraceStep = false; diff --git a/src/System.Management.Automation/engine/parser/Position.cs b/src/System.Management.Automation/engine/parser/Position.cs index 9e1363e4a53..f39d25ec89d 100644 --- a/src/System.Management.Automation/engine/parser/Position.cs +++ b/src/System.Management.Automation/engine/parser/Position.cs @@ -281,6 +281,65 @@ internal static string BriefMessage(IScriptPosition position) return StringUtil.Format(ParserStrings.TraceScriptLineMessage, position.LineNumber, message.ToString()); } + /// + /// Return a message for an extent that may span multiple lines. + /// For single-line extents, the format is: + /// 12+ >>>> $x + $b. + /// For multi-line extents (e.g., line continuation with backtick), the format is: + /// 12+ >>>> Write-Output "foo ` + /// 13+ >>>> bar" + /// + internal static string BriefMessage(IScriptExtent extent) + { + // For single-line extents, delegate to the existing single-position method + if (extent.StartLineNumber == extent.EndLineNumber) + { + return BriefMessage(extent.StartScriptPosition); + } + + // For multi-line extents, include all lines + string[] lines = extent.Text.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.None); + StringBuilder result = new StringBuilder(); + int lineNumber = extent.StartLineNumber; + + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + StringBuilder message = new StringBuilder(line); + + // Insert the marker at the appropriate position + if (i == 0) + { + // For the first line, insert at the start column + if (extent.StartColumnNumber > message.Length + 1) + { + message.Append(" <<<< "); + } + else + { + message.Insert(extent.StartColumnNumber - 1, " >>>> "); + } + } + else + { + // For continuation lines, insert the marker at the beginning + message.Insert(0, " >>>> "); + } + + string formattedLine = StringUtil.Format(ParserStrings.TraceScriptLineMessage, lineNumber, message.ToString()); + + if (i > 0) + { + result.AppendLine(); + } + + result.Append(formattedLine); + lineNumber++; + } + + return result.ToString(); + } + internal static IScriptExtent NewScriptExtent(IScriptExtent start, IScriptExtent end) { if (start == end) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Set-PSDebug.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Set-PSDebug.Tests.ps1 index 297e494d4d2..0f8af2eb59e 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Set-PSDebug.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Set-PSDebug.Tests.ps1 @@ -26,5 +26,51 @@ Describe "Set-PSDebug" -Tags "CI" { [ClassWithDefaultCtor]::new() } | Should -Not -Throw } + + It "Should trace all lines of a multiline command" { + $tempScript = Join-Path $TestDrive "multiline-trace.ps1" + $scriptContent = "Set-PSDebug -Trace 1`nWrite-Output `"foo ```nbar`"" + Set-Content -Path $tempScript -Value $scriptContent -NoNewline + + # Run in a separate process to capture trace output + $pinfo = [System.Diagnostics.ProcessStartInfo]::new() + $pinfo.FileName = (Get-Process -Id $PID).Path + $pinfo.Arguments = "-NoProfile -File `"$tempScript`"" + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $pinfo + $process.Start() | Should -BeTrue + $output = $process.StandardOutput.ReadToEnd() + $process.WaitForExit() + + # The debug trace for multiline commands should include all lines with DEBUG: prefix + $output | Should -Match 'DEBUG:.*Write-Output' -Because "debug output should contain the command with DEBUG: prefix" + $output | Should -Match 'DEBUG:.*bar"' -Because "debug output should contain the continuation line with DEBUG: prefix" + } + + It "Should trace all lines of a multiline command with -Trace 2" { + $tempScript = Join-Path $TestDrive "multiline-trace2.ps1" + $scriptContent = "Set-PSDebug -Trace 2`nWrite-Output `"foo ```nbar`"" + Set-Content -Path $tempScript -Value $scriptContent -NoNewline + + # Run in a separate process to capture trace output + $pinfo = [System.Diagnostics.ProcessStartInfo]::new() + $pinfo.FileName = (Get-Process -Id $PID).Path + $pinfo.Arguments = "-NoProfile -File `"$tempScript`"" + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $pinfo + $process.Start() | Should -BeTrue + $output = $process.StandardOutput.ReadToEnd() + $process.WaitForExit() + + # The debug trace for multiline commands should include all lines with DEBUG: prefix + $output | Should -Match 'DEBUG:.*Write-Output' -Because "debug output should contain the command with DEBUG: prefix" + $output | Should -Match 'DEBUG:.*bar"' -Because "debug output should contain the continuation line with DEBUG: prefix" + } } }