Skip to content

Commit 4c6c4f3

Browse files
authored
Fix formatting truncation to handle strings with VT sequences (PowerShell#17251)
1 parent c000fc5 commit 4c6c4f3

File tree

9 files changed

+445
-200
lines changed

9 files changed

+445
-200
lines changed

src/System.Management.Automation/FormatAndOutput/common/ComplexWriter.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displa
451451
{
452452
// we are not at the end of the string, select a sub string
453453
// that would fit in the remaining display length
454-
int charactersToAdd = displayCells.GetHeadSplitLength(currentLine, offset, currentDisplayLen);
454+
int charactersToAdd = displayCells.TruncateTail(currentLine, offset, currentDisplayLen);
455455

456456
if (charactersToAdd <= 0)
457457
{
@@ -465,7 +465,7 @@ private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displa
465465
else
466466
{
467467
// of the given length, add it to the accumulator
468-
accumulator.AddLine(currentLine.Substring(offset, charactersToAdd));
468+
accumulator.AddLine(currentLine.VtSubstring(offset, charactersToAdd));
469469
}
470470

471471
// increase the offset by the # of characters added
@@ -474,7 +474,7 @@ private static StringCollection GenerateLinesWithoutWordWrap(DisplayCells displa
474474
else
475475
{
476476
// we reached the last (partial) line, we add it all
477-
accumulator.AddLine(currentLine.Substring(offset));
477+
accumulator.AddLine(currentLine.VtSubstring(offset));
478478
break;
479479
}
480480
}
@@ -553,7 +553,7 @@ private static StringCollection GenerateLinesWithWordWrap(DisplayCells displayCe
553553
// Handle soft hyphen
554554
if (word.Delim == s_softHyphen.ToString())
555555
{
556-
int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(s_softHyphen.ToString());
556+
int wordWidthWithHyphen = displayCells.Length(wordToAdd) + displayCells.Length(s_softHyphen);
557557

558558
// Add hyphen only if necessary
559559
if (wordWidthWithHyphen == spacesLeft)

src/System.Management.Automation/FormatAndOutput/common/ILineOutput.cs

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,60 +14,112 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
1414
{
1515
/// <summary>
1616
/// Base class providing support for string manipulation.
17-
/// This class is a tear off class provided by the LineOutput class
18-
///
19-
/// Assumptions (in addition to the assumptions made for LineOutput):
20-
/// - characters map to one or more character cells
21-
///
22-
/// NOTE: we provide a base class that is valid for devices that have a
23-
/// 1:1 mapping between a UNICODE character and a display cell.
17+
/// This class is a tear off class provided by the LineOutput class.
2418
/// </summary>
2519
internal class DisplayCells
2620
{
27-
internal virtual int Length(string str)
21+
/// <summary>
22+
/// Calculate the buffer cell length of the given string.
23+
/// </summary>
24+
/// <param name="str">String that may contain VT escape sequences.</param>
25+
/// <returns>Number of buffer cells the string needs to take.</returns>
26+
internal int Length(string str)
2827
{
2928
return Length(str, 0);
3029
}
3130

31+
/// <summary>
32+
/// Calculate the buffer cell length of the given string.
33+
/// </summary>
34+
/// <param name="str">String that may contain VT escape sequences.</param>
35+
/// <param name="offset">
36+
/// When the string doesn't contain VT sequences, it's the starting index.
37+
/// When the string contains VT sequences, it means starting from the 'n-th' char that doesn't belong to a escape sequence.</param>
38+
/// <returns>Number of buffer cells the string needs to take.</returns>
3239
internal virtual int Length(string str, int offset)
3340
{
34-
int length = 0;
41+
if (string.IsNullOrEmpty(str))
42+
{
43+
return 0;
44+
}
3545

36-
foreach (char c in str)
46+
var valueStrDec = new ValueStringDecorated(str);
47+
if (valueStrDec.IsDecorated)
3748
{
38-
length += LengthInBufferCells(c);
49+
str = valueStrDec.ToString(OutputRendering.PlainText);
3950
}
4051

41-
return length - offset;
42-
}
52+
int length = 0;
53+
for (; offset < str.Length; offset++)
54+
{
55+
length += CharLengthInBufferCells(str[offset]);
56+
}
4357

44-
internal virtual int Length(char character) { return 1; }
58+
return length;
59+
}
4560

46-
internal virtual int GetHeadSplitLength(string str, int displayCells)
61+
/// <summary>
62+
/// Calculate the buffer cell length of the given character.
63+
/// </summary>
64+
/// <param name="character"></param>
65+
/// <returns>Number of buffer cells the character needs to take.</returns>
66+
internal virtual int Length(char character)
4767
{
48-
return GetHeadSplitLength(str, 0, displayCells);
68+
return CharLengthInBufferCells(character);
4969
}
5070

51-
internal virtual int GetHeadSplitLength(string str, int offset, int displayCells)
71+
/// <summary>
72+
/// Truncate from the tail of the string.
73+
/// </summary>
74+
/// <param name="str">String that may contain VT escape sequences.</param>
75+
/// <param name="displayCells">Number of buffer cells to fit in.</param>
76+
/// <returns>Number of non-escape-sequence characters from head of the string that can fit in the space.</returns>
77+
internal int TruncateTail(string str, int displayCells)
5278
{
53-
int len = str.Length - offset;
54-
return (len < displayCells) ? len : displayCells;
79+
return TruncateTail(str, offset: 0, displayCells);
5580
}
5681

57-
internal virtual int GetTailSplitLength(string str, int displayCells)
82+
/// <summary>
83+
/// Truncate from the tail of the string.
84+
/// </summary>
85+
/// <param name="str">String that may contain VT escape sequences.</param>
86+
/// <param name="offset">
87+
/// When the string doesn't contain VT sequences, it's the starting index.
88+
/// When the string contains VT sequences, it means starting from the 'n-th' char that doesn't belong to a escape sequence.</param>
89+
/// <param name="displayCells">Number of buffer cells to fit in.</param>
90+
/// <returns>Number of non-escape-sequence characters from head of the string that can fit in the space.</returns>
91+
internal int TruncateTail(string str, int offset, int displayCells)
5892
{
59-
return GetTailSplitLength(str, 0, displayCells);
93+
var valueStrDec = new ValueStringDecorated(str);
94+
if (valueStrDec.IsDecorated)
95+
{
96+
str = valueStrDec.ToString(OutputRendering.PlainText);
97+
}
98+
99+
return GetFitLength(str, offset, displayCells, startFromHead: true);
60100
}
61101

62-
internal virtual int GetTailSplitLength(string str, int offset, int displayCells)
102+
/// <summary>
103+
/// Truncate from the head of the string.
104+
/// </summary>
105+
/// <param name="str">String that may contain VT escape sequences.</param>
106+
/// <param name="displayCells">Number of buffer cells to fit in.</param>
107+
/// <returns>Number of non-escape-sequence characters from head of the string that should be skipped.</returns>
108+
internal int TruncateHead(string str, int displayCells)
63109
{
64-
int len = str.Length - offset;
65-
return (len < displayCells) ? len : displayCells;
110+
var valueStrDec = new ValueStringDecorated(str);
111+
if (valueStrDec.IsDecorated)
112+
{
113+
str = valueStrDec.ToString(OutputRendering.PlainText);
114+
}
115+
116+
int tailCount = GetFitLength(str, offset: 0, displayCells, startFromHead: false);
117+
return str.Length - tailCount;
66118
}
67119

68120
#region Helpers
69121

70-
protected static int LengthInBufferCells(char c)
122+
protected static int CharLengthInBufferCells(char c)
71123
{
72124
// The following is based on http://www.cl.cam.ac.uk/~mgk25/c/wcwidth.c
73125
// which is derived from https://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt
@@ -94,25 +146,26 @@ protected static int LengthInBufferCells(char c)
94146
/// Given a string and a number of display cells, it computes how many
95147
/// characters would fit starting from the beginning or end of the string.
96148
/// </summary>
97-
/// <param name="str">String to be displayed.</param>
149+
/// <param name="str">String to be displayed, which doesn't contain any VT sequences.</param>
98150
/// <param name="offset">Offset inside the string.</param>
99151
/// <param name="displayCells">Number of display cells.</param>
100-
/// <param name="head">If true compute from the head (i.e. k++) else from the tail (i.e. k--).</param>
152+
/// <param name="startFromHead">If true compute from the head (i.e. k++) else from the tail (i.e. k--).</param>
101153
/// <returns>Number of characters that would fit.</returns>
102-
protected int GetSplitLengthInternalHelper(string str, int offset, int displayCells, bool head)
154+
protected int GetFitLength(string str, int offset, int displayCells, bool startFromHead)
103155
{
104156
int filledDisplayCellsCount = 0; // number of cells that are filled in
105157
int charactersAdded = 0; // number of characters that fit
106158
int currCharDisplayLen; // scratch variable
107159

108-
int k = (head) ? offset : str.Length - 1;
109-
int kFinal = (head) ? str.Length - 1 : offset;
160+
int k = startFromHead ? offset : str.Length - 1;
161+
int kFinal = startFromHead ? str.Length - 1 : offset;
110162
while (true)
111163
{
112-
if ((head && (k > kFinal)) || ((!head) && (k < kFinal)))
164+
if ((startFromHead && k > kFinal) || (!startFromHead && k < kFinal))
113165
{
114166
break;
115167
}
168+
116169
// compute the cell number for the current character
117170
currCharDisplayLen = this.Length(str[k]);
118171

@@ -121,6 +174,7 @@ protected int GetSplitLengthInternalHelper(string str, int offset, int displayCe
121174
// if we added this character it would not fit, we cannot continue
122175
break;
123176
}
177+
124178
// keep adding, we fit
125179
filledDisplayCellsCount += currCharDisplayLen;
126180
charactersAdded++;
@@ -132,13 +186,13 @@ protected int GetSplitLengthInternalHelper(string str, int offset, int displayCe
132186
break;
133187
}
134188

135-
k = (head) ? (k + 1) : (k - 1);
189+
k = startFromHead ? (k + 1) : (k - 1);
136190
}
137191

138192
return charactersAdded;
139193
}
140-
#endregion
141194

195+
#endregion
142196
}
143197

144198
/// <summary>
@@ -354,11 +408,11 @@ private void WriteLineInternal(string val, int cols)
354408
{
355409
// the string is still too long to fit, write the first cols characters
356410
// and go back for more wraparound
357-
int splitLen = _displayCells.GetHeadSplitLength(s, cols);
358-
WriteLineInternal(s.Substring(0, splitLen), cols);
411+
int headCount = _displayCells.TruncateTail(s, cols);
412+
WriteLineInternal(s.VtSubstring(0, headCount), cols);
359413

360414
// chop off the first fieldWidth characters, already printed
361-
s = s.Substring(splitLen);
415+
s = s.VtSubstring(headCount);
362416
if (_displayCells.Length(s) <= cols)
363417
{
364418
// if we fit, print the tail of the string and we are done

src/System.Management.Automation/FormatAndOutput/common/ListWriter.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,20 @@ internal void Initialize(string[] propertyNames, int screenColumnWidth, DisplayC
8888

8989
for (int k = 0; k < propertyNames.Length; k++)
9090
{
91+
string propertyName = propertyNames[k];
9192
if (propertyNameCellCounts[k] < _propertyLabelsDisplayLength)
9293
{
9394
// shorter than the max, add padding
94-
_propertyLabels[k] = propertyNames[k] + StringUtil.Padding(_propertyLabelsDisplayLength - propertyNameCellCounts[k]);
95+
_propertyLabels[k] = propertyName + StringUtil.Padding(_propertyLabelsDisplayLength - propertyNameCellCounts[k]);
9596
}
9697
else if (propertyNameCellCounts[k] > _propertyLabelsDisplayLength)
9798
{
9899
// longer than the max, clip
99-
_propertyLabels[k] = propertyNames[k].Substring(0, dc.GetHeadSplitLength(propertyNames[k], _propertyLabelsDisplayLength));
100+
_propertyLabels[k] = propertyName.VtSubstring(0, dc.TruncateTail(propertyName, _propertyLabelsDisplayLength));
100101
}
101102
else
102103
{
103-
_propertyLabels[k] = propertyNames[k];
104+
_propertyLabels[k] = propertyName;
104105
}
105106

106107
_propertyLabels[k] += Separator;

src/System.Management.Automation/FormatAndOutput/common/TableWriter.cs

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ private sealed class ScreenInfo
4242

4343
private ScreenInfo _si;
4444

45-
private const char ESC = '\u001b';
46-
4745
private List<string> _header;
4846

4947
internal static int ComputeWideViewBestItemsPerRowFit(int stringLen, int screenColumns)
@@ -457,8 +455,10 @@ private string GenerateRow(string[] values, ReadOnlySpan<int> alignment, Display
457455
}
458456
}
459457

460-
sb.Append(GenerateRowField(values[k], _si.columnInfo[k].width, alignment[k], dc, addPadding));
461-
if (values[k] is not null && values[k].Contains(ESC))
458+
string rowField = GenerateRowField(values[k], _si.columnInfo[k].width, alignment[k], dc, addPadding);
459+
sb.Append(rowField);
460+
461+
if (rowField is not null && rowField.Contains(ValueStringDecorated.ESC) && !rowField.AsSpan().TrimEnd().EndsWith(PSStyle.Instance.Reset))
462462
{
463463
// Reset the console output if the content of this column contains ESC
464464
sb.Append(PSStyle.Instance.Reset);
@@ -472,9 +472,7 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
472472
{
473473
// make sure the string does not have any embedded <CR> in it
474474
string s = StringManipulationHelper.TruncateAtNewLine(val);
475-
476-
string currentValue = s;
477-
int currentValueDisplayLength = dc.Length(currentValue);
475+
int currentValueDisplayLength = dc.Length(s);
478476

479477
if (currentValueDisplayLength < width)
480478
{
@@ -531,18 +529,10 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
531529
case TextAlignment.Right:
532530
{
533531
// get from "abcdef" to "...f"
534-
int tailCount = dc.GetTailSplitLength(s, truncationDisplayLength);
535-
s = s.Substring(s.Length - tailCount);
536-
s = PSObjectHelper.Ellipsis + s;
537-
}
538-
539-
break;
540-
541-
case TextAlignment.Center:
542-
{
543-
// get from "abcdef" to "a..."
544-
s = s.Substring(0, dc.GetHeadSplitLength(s, truncationDisplayLength));
545-
s += PSObjectHelper.Ellipsis;
532+
s = s.VtSubstring(
533+
startOffset: dc.TruncateHead(s, truncationDisplayLength),
534+
prependStr: PSObjectHelper.EllipsisStr,
535+
appendStr: null);
546536
}
547537

548538
break;
@@ -551,8 +541,11 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
551541
{
552542
// left align is the default
553543
// get from "abcdef" to "a..."
554-
s = s.Substring(0, dc.GetHeadSplitLength(s, truncationDisplayLength));
555-
s += PSObjectHelper.Ellipsis;
544+
s = s.VtSubstring(
545+
startOffset: 0,
546+
length: dc.TruncateTail(s, truncationDisplayLength),
547+
prependStr: null,
548+
appendStr: PSObjectHelper.EllipsisStr);
556549
}
557550

558551
break;
@@ -561,23 +554,12 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
561554
else
562555
{
563556
// not enough space for the ellipsis, just truncate at the width
564-
int len = width;
565-
566557
switch (alignment)
567558
{
568559
case TextAlignment.Right:
569560
{
570561
// get from "abcdef" to "f"
571-
int tailCount = dc.GetTailSplitLength(s, len);
572-
s = s.Substring(s.Length - tailCount, tailCount);
573-
}
574-
575-
break;
576-
577-
case TextAlignment.Center:
578-
{
579-
// get from "abcdef" to "a"
580-
s = s.Substring(0, dc.GetHeadSplitLength(s, len));
562+
s = s.VtSubstring(startOffset: dc.TruncateHead(s, width));
581563
}
582564

583565
break;
@@ -586,7 +568,7 @@ private static string GenerateRowField(string val, int width, int alignment, Dis
586568
{
587569
// left align is the default
588570
// get from "abcdef" to "a"
589-
s = s.Substring(0, dc.GetHeadSplitLength(s, len));
571+
s = s.VtSubstring(startOffset: 0, length: dc.TruncateTail(s, width));
590572
}
591573

592574
break;

src/System.Management.Automation/FormatAndOutput/common/Utilities/MshObjectUtil.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal static class PSObjectHelper
2626
#endregion tracer
2727

2828
internal const char Ellipsis = '\u2026';
29+
internal const string EllipsisStr = "\u2026";
2930

3031
internal static string PSObjectIsOfExactType(Collection<string> typeNames)
3132
{

0 commit comments

Comments
 (0)