diff --git a/assets/files.wxs b/assets/files.wxs index 4c03b334d20..8e8d60b9799 100644 --- a/assets/files.wxs +++ b/assets/files.wxs @@ -1,4 +1,4 @@ - + @@ -7,6 +7,12 @@ + + + + + + @@ -1822,6 +1828,8 @@ + + diff --git a/src/System.Management.Automation/FormatAndOutput/common/TableWriter.cs b/src/System.Management.Automation/FormatAndOutput/common/TableWriter.cs index ad0998ac4c9..37374ea07ef 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/TableWriter.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/TableWriter.cs @@ -261,7 +261,7 @@ private string[] GenerateTableRow(string[] values, int[] alignment, DisplayCells // pad with a blank (or any character that ALWAYS maps to a single screen cell if (k > 0) { - // skipping the first ones, add a separator for catenation + // skipping the first ones, add a separator for concatenation for (int j = 0; j < scArray[k].Count; j++) { scArray[k][j] = StringUtil.Padding(ScreenInfo.separatorCharacterCount) + scArray[k][j]; @@ -289,22 +289,64 @@ private string[] GenerateTableRow(string[] values, int[] alignment, DisplayCells screenRows = scArray[k].Count; } + // column headers can span multiple rows if the width of the column is shorter than the header text like: + // + // Long Header2 Head + // Head er3 + // er + // ---- ------- ---- + // 1 2 3 + // + // To ensure we don't add whitespace to the end, we need to determine the last column in each row with content + + System.Span lastColWithContent = stackalloc int[screenRows]; + for (int row = 0; row < screenRows; row++) + { + for (int col = scArray.Length - 1; col > 0; col--) + { + int colWidth = _si.columnInfo[validColumnArray[col]].width; + int headerLength = values[col].Length; + if (headerLength / colWidth >= row && headerLength % colWidth > 0) + { + lastColWithContent[row] = col; + break; + } + } + } + // add padding for the columns that are shorter - for (int col = 0; col < scArray.Length-1; col++) + for (int col = 0; col < scArray.Length; col++) { - int paddingBlanks = _si.columnInfo[validColumnArray[col]].width; - if (col > 0) - paddingBlanks += ScreenInfo.separatorCharacterCount; - else + int paddingBlanks = 0; + + // don't pad if last column + if (col < scArray.Length - 1) { - paddingBlanks += _startColumn; + paddingBlanks = _si.columnInfo[validColumnArray[col]].width; + if (col > 0) + { + paddingBlanks += ScreenInfo.separatorCharacterCount; + } + else + { + paddingBlanks += _startColumn; + } } + int paddingEntries = screenRows - scArray[col].Count; if (paddingEntries > 0) { - for (int j = 0; j < paddingEntries; j++) + for (int row = screenRows - paddingEntries; row < screenRows; row++) { - scArray[col].Add(StringUtil.Padding(paddingBlanks)); + // if the column is beyond the last column with content, just use empty string + if (col > lastColWithContent[row]) + { + scArray[col].Add(""); + } + else + { + scArray[col].Add(StringUtil.Padding(paddingBlanks)); + } } } } @@ -314,10 +356,18 @@ private string[] GenerateTableRow(string[] values, int[] alignment, DisplayCells for (int row = 0; row < rows.Length; row++) { StringBuilder sb = new StringBuilder(); - // for a give row, walk the columns + // for a given row, walk the columns for (int col = 0; col < scArray.Length; col++) { - sb.Append(scArray[col][row]); + // if the column is the last column with content, we need to trim trailing whitespace + if (col == lastColWithContent[row]) + { + sb.Append(scArray[col][row].TrimEnd()); + } + else + { + sb.Append(scArray[col][row]); + } } rows[row] = sb.ToString(); } diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index 6b23a7f377c..b2167fe7ade 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -4,6 +4,7 @@ PowerShell Core's System.Management.Automation project $(NoWarn);CS1570;CS1734 System.Management.Automation + 7.2 @@ -13,6 +14,7 @@ + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 index e5359e67e01..c9d1283ec43 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 @@ -339,4 +339,141 @@ Left Center Right $output = $ps.Invoke() $output.Replace("`n","").Replace("`r","") | Should -BeExactly $expected } + + It "Format-Table should correctly render headers that span multiple rows: " -TestCases @( + @{ view = "Default"; widths = 4,7,4; variation = "4 row, 1 row, 2 row"; expectedTable = @" + +Long Header2 Head +Long er3 +Head +er +---- ------- ---- +1 2 3 + + + +"@ }, + @{ view = "Default"; widths = 4,4,7; variation = "4 row, 2 row, 1 row"; expectedTable = @" + +Long Head Header3 +Long er2 +Head +er +---- ---- ------- +1 2 3 + + + +"@ }, + @{ view = "Default"; widths = 14,7,3; variation = "1 row, 1 row, 3 row"; expectedTable = @" + +LongLongHeader Header2 Hea + der + 3 +-------------- ------- --- +1 2 3 + + + +"@ }, + @{ view = "One"; widths = 4,1,1; variation = "1 column"; expectedTable = @" + +Long +Long +Head +er +---- +1 + + + +"@ } + ) { + param($view, $widths, $expectedTable) + $ps1xml = @" + + + + Default + + Test.Format + + + + + + {0} + + + + {1} + + + + {2} + + + + + + + First + + + Second + + + Third + + + + + + + + One + + Test.Format + + + + + + {0} + + + + + + + First + + + + + + + + +"@ + $ps1xml = $ps1xml.Replace("{0}", $widths[0]).Replace("{1}", $widths[1]).Replace("{2}", $widths[2]) + $ps1xmlPath = Join-Path -Path $TestDrive -ChildPath "test.format.ps1xml" + Set-Content -Path $ps1xmlPath -Value $ps1xml + # run in own runspace so not affect global sessionstate + $ps = [powershell]::Create() + $ps.AddScript( { + param($ps1xmlPath, $view) + Update-FormatData -AppendPath $ps1xmlPath + $a = [PSCustomObject]@{First=1;Second=2;Third=3} + $a.PSObject.TypeNames.Insert(0,"Test.Format") + $a | Format-Table -View $view | Out-String + } ).AddArgument($ps1xmlPath).AddArgument($view) | Out-Null + $output = $ps.Invoke() + foreach ($e in $ps.Streams.Error) + { + Write-Verbose $e.ToString() -Verbose + } + $ps.HadErrors | Should -BeFalse + $output.Replace("`r","").Replace(" ",".").Replace("`n","``") | Should -BeExactly $expectedTable.Replace("`r","").Replace(" ",".").Replace("`n","``") + } }