Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4161,13 +4161,16 @@ internal void Trace(string messageId, string resourceString, params object[] arg

internal void TraceLine(IScriptExtent extent)
{
string msg = PositionUtilities.BriefMessage(extent.StartScriptPosition);
string[] lines = PositionUtilities.BriefMessage(extent);
InternalHostUserInterface ui = (InternalHostUserInterface)_context.EngineHostInterface.UI;

ActionPreference pref = _context.PSDebugTraceStep ?
ActionPreference.Inquire : ActionPreference.Continue;

ui.WriteDebugLine(msg, ref pref);
foreach (string line in lines)
{
ui.WriteDebugLine(line, ref pref);
}

if (pref == ActionPreference.Continue)
_context.PSDebugTraceStep = false;
Expand Down
52 changes: 52 additions & 0 deletions src/System.Management.Automation/engine/parser/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,58 @@ internal static string BriefMessage(IScriptPosition position)
return StringUtil.Format(ParserStrings.TraceScriptLineMessage, position.LineNumber, message.ToString());
}

/// <summary>
/// Return messages for an extent that may span multiple lines.
/// For single-line extents, returns an array with one element:
/// 12+ >>>> $x + $b.
/// For multi-line extents (e.g., line continuation with backtick), returns an array with one element per line:
/// 12+ >>>> Write-Output "foo `
/// 13+ >>>> bar"
/// </summary>
internal static string[] BriefMessage(IScriptExtent extent)
{
// For single-line extents, delegate to the existing single-position method
if (extent.StartLineNumber == extent.EndLineNumber)
{
return new[] { BriefMessage(extent.StartScriptPosition) };
}

// For multi-line extents, include all lines
string[] lines = extent.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
string[] result = new string[lines.Length];
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, " >>>> ");
}

result[i] = StringUtil.Format(ParserStrings.TraceScriptLineMessage, lineNumber, message.ToString());
lineNumber++;
}

return result;
}

internal static IScriptExtent NewScriptExtent(IScriptExtent start, IScriptExtent end)
{
if (start == end)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,59 @@ 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()
$exited = $process.WaitForExit(5000)
if (-not $exited) {
$process.Kill()
}
$exited | Should -BeTrue -Because "process should exit within timeout"

# 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()
$exited = $process.WaitForExit(5000)
if (-not $exited) {
$process.Kill()
}
$exited | Should -BeTrue -Because "process should exit within timeout"

# 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"
}
}
}