mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
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:
@@ -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();
|
||||
|
||||
+6
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user