Add -RepeatHeader to Format-Table to enable repeating header for each screen full (#8481)

When using a screen reader or just getting the output of a table with lots of rows, the header is no longer on the screen and the columns may no longer make sense without the context.  This change adds a `-RepeatHeader` switch to `Format-Table` to enable re-outputting the header after every screen full (minus 1 row).  Expectation is that the user is piping the output to a pager (e.g. less) which uses the bottom row for pager information.  I followed the `AutoSize` parameter as the way to get the parameter from the cmdlet into the deep formatting object where it's needed.
This commit is contained in:
Steve Lee
2018-12-20 11:05:34 -08:00
committed by Dongbo Wang
parent 64bbdbe61c
commit 110f78b818
11 changed files with 163 additions and 47 deletions
@@ -758,6 +758,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
}
private Nullable<bool> _autosize = null;
/// <summary>
/// Gets or sets if header is repeated per screen.
/// </summary>
[Parameter]
public SwitchParameter RepeatHeader { get; set; }
/// <summary>
/// optional, non positional parameter
/// </summary>
@@ -809,6 +815,11 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
if (_autosize.HasValue)
parameters.autosize = _autosize.Value;
if (RepeatHeader)
{
parameters.repeatHeader = true;
}
parameters.groupByParameter = this.ProcessGroupByParameter();
TableSpecificParameters tableParameters = new TableSpecificParameters();
@@ -45,6 +45,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
/// </summary>
internal Nullable<bool> autosize = null;
/// <summary>
/// If true, the header for a table is repeated after each screen full
/// of content.
/// </summary>
internal bool repeatHeader = false;
/// <summary>
/// errors are shown as out of band messages
/// </summary>
@@ -250,6 +250,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
private enum PreprocessingState { raw, processed, error }
private const int DefaultConsoleWidth = 120;
private const int DefaultConsoleHeight = int.MaxValue;
internal const int StackAllocThreshold = 120;
/// <summary>
@@ -743,12 +744,6 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
_formattingHint = hint;
}
/// <summary>
/// In cases like implicit remoting, there is no console so reading the console width results in an exception.
/// Instead of handling exception every time we cache this value to increase performance.
/// </summary>
static private bool _noConsole = false;
/// <summary>
/// Tables and Wides need to use spaces for padding to maintain table look even if console window is resized.
/// For all other output, we use int.MaxValue if the user didn't explicitly specify a width.
@@ -756,7 +751,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
/// However, if we can't read that (for example, implicit remoting has no console window), we default
/// to something reasonable: 120 columns.
/// </summary>
static private int GetConsoleWindowWidth(int columnNumber)
private static int GetConsoleWindowWidth(int columnNumber)
{
if (InternalTestHooks.SetConsoleWidthToZero)
{
@@ -765,10 +760,6 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
if (columnNumber == int.MaxValue)
{
if (_noConsole)
{
return DefaultConsoleWidth;
}
try
{
// if Console width is set to 0, the default width is returned so that the output string is not null.
@@ -777,13 +768,40 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
}
catch
{
_noConsole = true;
return DefaultConsoleWidth;
}
}
return columnNumber;
}
/// <summary>
/// Return the console height.null If not available (like when remoting), treat as Int.MaxValue.
/// </summary>
private static int GetConsoleWindowHeight(int rowNumber)
{
if (InternalTestHooks.SetConsoleHeightToZero)
{
return DefaultConsoleHeight;
}
if (rowNumber <= 0)
{
try
{
// if Console height is set to 0, the default height is returned.
// This can happen in environments where TERM is not set.
return (Console.WindowHeight > 0) ? Console.WindowHeight : DefaultConsoleHeight;
}
catch
{
return DefaultConsoleHeight;
}
}
return rowNumber;
}
/// <summary>
/// base class for all the formatting hints
/// </summary>
@@ -925,6 +943,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
private sealed class TableOutputContext : TableOutputContextBase
{
private int _rowCount = 0;
private int _consoleHeight = -1;
private int _consoleWidth = -1;
private const int WhitespaceAndPagerLineCount = 2;
private bool _repeatHeader = false;
/// <summary>
/// construct a context to push on the stack
/// </summary>
@@ -936,6 +960,13 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
GroupStartData formatData)
: base(cmd, parentContext, formatData)
{
if (parentContext is FormatOutputContext foc)
{
if (foc.Data.shapeInfo is TableHeaderInfo thi)
{
_repeatHeader = thi.repeatHeader;
}
}
}
/// <summary>
@@ -952,7 +983,8 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
columnWidthsHint = tableHint.columnWidths;
}
int columnsOnTheScreen = GetConsoleWindowWidth(this.InnerCommand._lo.ColumnNumber);
_consoleHeight = GetConsoleWindowHeight(this.InnerCommand._lo.RowNumber);
_consoleWidth = GetConsoleWindowWidth(this.InnerCommand._lo.ColumnNumber);
int columns = this.CurrentTableHeaderInfo.tableColumnInfoList.Count;
if (columns == 0)
@@ -971,7 +1003,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
alignment[k] = tci.alignment;
k++;
}
this.Writer.Initialize(0, columnsOnTheScreen, columnWidths, alignment, this.CurrentTableHeaderInfo.hideHeader);
this.Writer.Initialize(0, _consoleWidth, columnWidths, alignment, this.CurrentTableHeaderInfo.hideHeader);
}
/// <summary>
@@ -992,7 +1024,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
{
properties[k++] = tci.label ?? tci.propertyName;
}
this.Writer.GenerateHeader(properties, this.InnerCommand._lo);
_rowCount += this.Writer.GenerateHeader(properties, this.InnerCommand._lo);
}
/// <summary>
@@ -1008,6 +1040,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
return;
}
if (_repeatHeader && _rowCount >= _consoleHeight - WhitespaceAndPagerLineCount)
{
this.InnerCommand._lo.WriteLine(string.Empty);
_rowCount = this.Writer.GenerateHeader(null, this.InnerCommand._lo);
}
TableRowEntry tre = fed.formatEntryInfo as TableRowEntry;
// need to make sure we have matching counts: the header count will have to prevail
@@ -1029,7 +1067,8 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
alignment[k] = TextAlignment.Left; // hard coded default
}
}
this.Writer.GenerateRow(values, this.InnerCommand._lo, tre.multiLine, alignment, InnerCommand._lo.DisplayCells);
this.Writer.GenerateRow(values, this.InnerCommand._lo, tre.multiLine, alignment, InnerCommand._lo.DisplayCells, generatedRows: null);
_rowCount++;
}
private TableHeaderInfo CurrentTableHeaderInfo
@@ -1182,7 +1221,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
alignment[k] = TextAlignment.Left;
}
this.Writer.Initialize(0, columnsOnTheScreen, columnWidths, alignment, false);
this.Writer.Initialize(0, columnsOnTheScreen, columnWidths, alignment, false, GetConsoleWindowHeight(this.InnerCommand._lo.RowNumber));
}
/// <summary>
@@ -1239,7 +1278,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
else
values[k] = string.Empty;
}
this.Writer.GenerateRow(values, this.InnerCommand._lo, false, null, InnerCommand._lo.DisplayCells);
this.Writer.GenerateRow(values, this.InnerCommand._lo, false, null, InnerCommand._lo.DisplayCells, generatedRows: null);
_buffer.Reset();
}
@@ -424,6 +424,11 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
/// RULE: valid only for table and wide only
/// </summary>
internal bool? autosize = null;
/// <summary>
/// RULE: only valid for table
/// </summary>
internal bool repeatHeader = false;
}
/// <summary>
@@ -63,6 +63,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
InitializeFormatErrorManager();
InitializeGroupBy();
InitializeAutoSize();
InitializeRepeatHeader();
}
private void InitializeFormatErrorManager()
@@ -147,6 +148,14 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
}
}
private void InitializeRepeatHeader()
{
if (parameters != null)
{
_repeatHeader = parameters.repeatHeader;
}
}
internal virtual FormatStartData GenerateStartData(PSObject so)
{
FormatStartData startFormat = new FormatStartData();
@@ -312,6 +321,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
}
private bool _autosize = false;
protected bool RepeatHeader
{
get { return _repeatHeader; }
}
private bool _repeatHeader = false;
protected class DataBaseInfo
{
internal TypeInfoDataBase db = null;
@@ -156,6 +156,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
bool dummy;
List<TableRowItemDefinition> activeRowItemDefinitionList = GetActiveTableRowDefinition(_tableBody, so, out dummy);
thi.hideHeader = this.HideHeaders;
thi.repeatHeader = this.RepeatHeader;
int col = 0;
foreach (TableRowItemDefinition rowItem in activeRowItemDefinitionList)
@@ -293,6 +294,19 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
}
}
private bool RepeatHeaders
{
get
{
if (this.parameters != null)
{
return this.parameters.repeatHeader;
}
return false;
}
}
private static int ComputeDefaultAlignment(PSObject so, PSPropertyExpression ex)
{
List<PSPropertyExpressionResult> rList = ex.GetValues(so);
@@ -220,6 +220,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
public override string ClassId2e4f51ef21dd47e99d3c952918aff9cd { get { return CLSID; } }
public bool hideHeader;
public bool repeatHeader;
public List<TableColumnInfo> tableColumnInfoList;
}
@@ -575,6 +575,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
{
base.Deserialize(so, deserializer);
this.hideHeader = deserializer.DeserializeBoolMemberVariable(so, "hideHeader");
this.hideHeader = deserializer.DeserializeBoolMemberVariable(so, "repeatHeader");
FormatInfoDataListDeserializer<TableColumnInfo>.ReadList(so, "tableColumnInfoList", this.tableColumnInfoList, deserializer);
}
}
@@ -27,6 +27,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
private class ScreenInfo
{
internal int screenColumns = 0;
internal int screenRows = 0;
internal const int separatorCharacterCount = 1;
@@ -40,6 +41,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
private ScreenInfo _si;
private const char ESC = '\u001b';
private const string ResetConsoleVt100Code = "\u001b[m";
private List<string> _header;
internal static int ComputeWideViewBestItemsPerRowFit(int stringLen, int screenColumns)
{
@@ -73,35 +75,35 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
}
/// <summary>
/// Initialize the table specifying the width of each column
/// Initialize the table specifying the width of each column.
/// </summary>
/// <param name="leftMarginIndent">left margin indentation</param>
/// <param name="screenColumns">number of character columns on the screen</param>
/// <param name="columnWidths">array of specified column widths</param>
/// <param name="alignment">array of alignment flags</param>
/// <param name="suppressHeader">if true, suppress header printing</param>
internal void Initialize(int leftMarginIndent, int screenColumns, Span<int> columnWidths, ReadOnlySpan<int> alignment, bool suppressHeader)
/// <param name="leftMarginIndent">Left margin indentation</param>
/// <param name="screenColumns">Number of character columns on the screen</param>
/// <param name="columnWidths">Array of specified column widths</param>
/// <param name="alignment">Array of alignment flags</param>
/// <param name="suppressHeader">If true, suppress header printing</param>
/// <param name="screenRows">Number of rows on the screen</param>
internal void Initialize(int leftMarginIndent, int screenColumns, Span<int> columnWidths, ReadOnlySpan<int> alignment, bool suppressHeader, int screenRows = int.MaxValue)
{
//Console.WriteLine(" 1 2 3 4 5 6 7");
//Console.WriteLine("01234567890123456789012345678901234567890123456789012345678901234567890123456789");
if (leftMarginIndent < 0)
{
leftMarginIndent = 0;
}
if (screenColumns - leftMarginIndent < ScreenInfo.minimumScreenColumns)
{
_disabled = true;
return;
}
_startColumn = leftMarginIndent;
_startColumn = leftMarginIndent;
_hideHeader = suppressHeader;
// make sure the column widths are correct; if not, take appropriate action
ColumnWidthManager manager = new ColumnWidthManager(screenColumns - leftMarginIndent,
ScreenInfo.minimumColumnWidth,
ScreenInfo.separatorCharacterCount);
ColumnWidthManager manager = new ColumnWidthManager(
screenColumns - leftMarginIndent,
ScreenInfo.minimumColumnWidth,
ScreenInfo.separatorCharacterCount);
manager.CalculateColumnWidths(columnWidths);
@@ -126,6 +128,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
// now set the run time data structures
_si = new ScreenInfo();
_si.screenColumns = screenColumns;
_si.screenRows = screenRows;
_si.columnInfo = new ColumnInfo[columnWidths.Length];
int startCol = _startColumn;
@@ -136,20 +139,29 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
_si.columnInfo[k].width = columnWidths[k];
_si.columnInfo[k].alignment = alignment[k];
startCol += columnWidths[k] + ScreenInfo.separatorCharacterCount;
//Console.WriteLine("start = {0} width = {1}", si.columnInfo[k].startCol, si.columnInfo[k].width);
}
}
internal void GenerateHeader(string[] values, LineOutput lo)
internal int GenerateHeader(string[] values, LineOutput lo)
{
if (_disabled)
return;
if (_disabled || _hideHeader)
{
return 0;
}
else if (_header != null)
{
foreach (string line in _header)
{
lo.WriteLine(line);
}
if (_hideHeader)
return;
return _header.Count;
}
_header = new List<string>();
// generate the row with the header labels
GenerateRow(values, lo, true, null, lo.DisplayCells);
GenerateRow(values, lo, true, null, lo.DisplayCells, _header);
// generate an array of "--" as header markers below
// the column header labels
@@ -175,10 +187,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
// would be invalidated
breakLine[k] = StringUtil.DashPadding(count);
}
GenerateRow(breakLine, lo, false, null, lo.DisplayCells);
GenerateRow(breakLine, lo, false, null, lo.DisplayCells, _header);
return _header.Count;
}
internal void GenerateRow(string[] values, LineOutput lo, bool multiLine, ReadOnlySpan<int> alignment, DisplayCells dc)
internal void GenerateRow(string[] values, LineOutput lo, bool multiLine, ReadOnlySpan<int> alignment, DisplayCells dc, List<string> generatedRows)
{
if (_disabled)
return;
@@ -207,16 +221,17 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
if (multiLine)
{
string[] lines = GenerateTableRow(values, currentAlignment, lo.DisplayCells);
for (int k = 0; k < lines.Length; k++)
foreach (string line in GenerateTableRow(values, currentAlignment, lo.DisplayCells))
{
lo.WriteLine(lines[k]);
generatedRows?.Add(line);
lo.WriteLine(line);
}
}
else
{
lo.WriteLine(GenerateRow(values, currentAlignment, dc));
string line = GenerateRow(values, currentAlignment, dc);
generatedRows?.Add(line);
lo.WriteLine(line);
}
}
@@ -1556,7 +1556,7 @@ namespace System.Management.Automation
WriteVerbose(ps, string.Format(CultureInfo.CurrentCulture, ParserStrings.ImplicitRemotingPipelineBatchingException, ex.Message));
}
}
return false;
}
@@ -1806,6 +1806,7 @@ namespace System.Management.Automation.Internal
internal static bool StopwatchIsNotHighResolution;
internal static bool DisableGACLoading;
internal static bool SetConsoleWidthToZero;
internal static bool SetConsoleHeightToZero;
// A location to test PSEdition compatibility functionality for Windows PowerShell modules with
// since we can't manipulate the System32 directory in a test
@@ -791,4 +791,12 @@ A Name B
[system.management.automation.internal.internaltesthooks]::SetTestHook('SetConsoleWidthToZero', $false)
}
}
It "-RepeatHeader should output the header at every screen full" -Skip:([Console]::WindowHeight -eq 0) {
$numHeaders = 4
$numObjects = [Console]::WindowHeight * $numHeaders
$out = 1..$numObjects | ForEach-Object { @{foo=$_} } | Format-Table -RepeatHeader | Out-String
$lines = $out.Split([System.Environment]::NewLine)
($lines | Select-String "Name\s*Value").Count | Should -Be ($numHeaders + 1)
}
}