mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
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.
483 lines
20 KiB
C#
483 lines
20 KiB
C#
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Management.Automation;
|
|
using System.Management.Automation.Internal;
|
|
|
|
namespace Microsoft.PowerShell.Commands.Internal.Format
|
|
{
|
|
internal sealed class TableViewGenerator : ViewGenerator
|
|
{
|
|
// tableBody to use for this instance of the ViewGenerator;
|
|
private TableControlBody _tableBody;
|
|
|
|
internal override void Initialize(TerminatingErrorContext terminatingErrorContext, PSPropertyExpressionFactory mshExpressionFactory, TypeInfoDataBase db, ViewDefinition view, FormattingCommandLineParameters formatParameters)
|
|
{
|
|
base.Initialize(terminatingErrorContext, mshExpressionFactory, db, view, formatParameters);
|
|
if ((this.dataBaseInfo != null) && (this.dataBaseInfo.view != null))
|
|
{
|
|
_tableBody = (TableControlBody)this.dataBaseInfo.view.mainControl;
|
|
}
|
|
}
|
|
|
|
internal override void Initialize(TerminatingErrorContext errorContext, PSPropertyExpressionFactory expressionFactory,
|
|
PSObject so, TypeInfoDataBase db,
|
|
FormattingCommandLineParameters parameters)
|
|
{
|
|
base.Initialize(errorContext, expressionFactory, so, db, parameters);
|
|
|
|
if ((this.dataBaseInfo != null) && (this.dataBaseInfo.view != null))
|
|
{
|
|
_tableBody = (TableControlBody)this.dataBaseInfo.view.mainControl;
|
|
}
|
|
|
|
List<MshParameter> rawMshParameterList = null;
|
|
|
|
if (parameters != null)
|
|
rawMshParameterList = parameters.mshParameterList;
|
|
|
|
// check if we received properties from the command line
|
|
if (rawMshParameterList != null && rawMshParameterList.Count > 0)
|
|
{
|
|
this.activeAssociationList = AssociationManager.ExpandTableParameters(rawMshParameterList, so);
|
|
return;
|
|
}
|
|
|
|
// we did not get any properties:
|
|
//try to get properties from the default property set of the object
|
|
this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory);
|
|
if (this.activeAssociationList.Count > 0)
|
|
{
|
|
// we got a valid set of properties from the default property set..add computername for
|
|
// remoteobjects (if available)
|
|
if (PSObjectHelper.ShouldShowComputerNameProperty(so))
|
|
{
|
|
activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null,
|
|
new PSPropertyExpression(RemotingConstants.ComputerNameNoteProperty)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// we failed to get anything from the default property set
|
|
this.activeAssociationList = AssociationManager.ExpandAll(so);
|
|
if (this.activeAssociationList.Count > 0)
|
|
{
|
|
// Remove PSComputerName and PSShowComputerName from the display as needed.
|
|
AssociationManager.HandleComputerNameProperties(so, activeAssociationList);
|
|
FilterActiveAssociationList();
|
|
return;
|
|
}
|
|
|
|
// we were unable to retrieve any properties, so we leave an empty list
|
|
this.activeAssociationList = new List<MshResolvedExpressionParameterAssociation>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Let the view prepare itself for RemoteObjects. This will add "ComputerName" to the
|
|
/// table columns.
|
|
/// </summary>
|
|
/// <param name="so"></param>
|
|
internal override void PrepareForRemoteObjects(PSObject so)
|
|
{
|
|
Diagnostics.Assert(so != null, "so cannot be null");
|
|
|
|
// make sure computername property exists.
|
|
Diagnostics.Assert(so.Properties[RemotingConstants.ComputerNameNoteProperty] != null,
|
|
"PrepareForRemoteObjects cannot be called when the object does not contain ComputerName property.");
|
|
|
|
if ((dataBaseInfo != null) && (dataBaseInfo.view != null) && (dataBaseInfo.view.mainControl != null))
|
|
{
|
|
// dont change the original format definition in the database..just make a copy and work
|
|
// with the copy
|
|
_tableBody = (TableControlBody)this.dataBaseInfo.view.mainControl.Copy();
|
|
|
|
TableRowItemDefinition cnRowDefinition = new TableRowItemDefinition();
|
|
PropertyTokenBase propToken = new FieldPropertyToken();
|
|
propToken.expression = new ExpressionToken(RemotingConstants.ComputerNameNoteProperty, false);
|
|
cnRowDefinition.formatTokenList.Add(propToken);
|
|
_tableBody.defaultDefinition.rowItemDefinitionList.Add(cnRowDefinition);
|
|
|
|
// add header only if there are other header definitions
|
|
if (_tableBody.header.columnHeaderDefinitionList.Count > 0)
|
|
{
|
|
TableColumnHeaderDefinition cnHeaderDefinition = new TableColumnHeaderDefinition();
|
|
cnHeaderDefinition.label = new TextToken();
|
|
cnHeaderDefinition.label.text = RemotingConstants.ComputerNameNoteProperty;
|
|
_tableBody.header.columnHeaderDefinitionList.Add(cnHeaderDefinition);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal override FormatStartData GenerateStartData(PSObject so)
|
|
{
|
|
FormatStartData startFormat = base.GenerateStartData(so);
|
|
|
|
if (this.dataBaseInfo.view != null)
|
|
startFormat.shapeInfo = GenerateTableHeaderInfoFromDataBaseInfo(so);
|
|
else
|
|
startFormat.shapeInfo = GenerateTableHeaderInfoFromProperties(so);
|
|
return startFormat;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method to filter resolved expressions as per table view needs.
|
|
/// For v1.0, table view supports only 10 properties.
|
|
///
|
|
/// This method filters and updates "activeAssociationList" instance property.
|
|
/// </summary>
|
|
/// <returns>None.</returns>
|
|
/// <remarks>This method updates "activeAssociationList" instance property.</remarks>
|
|
private void FilterActiveAssociationList()
|
|
{
|
|
// we got a valid set of properties from the default property set
|
|
// make sure we do not have too many properties
|
|
|
|
// NOTE: this is an arbitrary number, chosen to be a sensitive default
|
|
int nMax = 10;
|
|
|
|
if (activeAssociationList.Count > nMax)
|
|
{
|
|
List<MshResolvedExpressionParameterAssociation> tmp = this.activeAssociationList;
|
|
this.activeAssociationList = new List<MshResolvedExpressionParameterAssociation>();
|
|
for (int k = 0; k < nMax; k++)
|
|
this.activeAssociationList.Add(tmp[k]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
private TableHeaderInfo GenerateTableHeaderInfoFromDataBaseInfo(PSObject so)
|
|
{
|
|
TableHeaderInfo thi = new TableHeaderInfo();
|
|
|
|
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)
|
|
{
|
|
TableColumnInfo ci = new TableColumnInfo();
|
|
TableColumnHeaderDefinition colHeader = null;
|
|
if (_tableBody.header.columnHeaderDefinitionList.Count > 0)
|
|
colHeader = _tableBody.header.columnHeaderDefinitionList[col];
|
|
|
|
if (colHeader != null)
|
|
{
|
|
ci.width = colHeader.width;
|
|
ci.alignment = colHeader.alignment;
|
|
if (colHeader.label != null)
|
|
ci.label = this.dataBaseInfo.db.displayResourceManagerCache.GetTextTokenString(colHeader.label);
|
|
}
|
|
|
|
if (ci.alignment == TextAlignment.Undefined)
|
|
{
|
|
ci.alignment = rowItem.alignment;
|
|
}
|
|
|
|
if (ci.label == null)
|
|
{
|
|
FormatToken token = null;
|
|
if (rowItem.formatTokenList.Count > 0)
|
|
token = rowItem.formatTokenList[0];
|
|
if (token != null)
|
|
{
|
|
FieldPropertyToken fpt = token as FieldPropertyToken;
|
|
if (fpt != null)
|
|
{
|
|
ci.label = fpt.expression.expressionValue;
|
|
}
|
|
else
|
|
{
|
|
TextToken tt = token as TextToken;
|
|
if (tt != null)
|
|
{
|
|
ci.label = this.dataBaseInfo.db.displayResourceManagerCache.GetTextTokenString(tt);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ci.label = string.Empty;
|
|
}
|
|
}
|
|
|
|
thi.tableColumnInfoList.Add(ci);
|
|
col++;
|
|
}
|
|
return thi;
|
|
}
|
|
|
|
private TableHeaderInfo GenerateTableHeaderInfoFromProperties(PSObject so)
|
|
{
|
|
TableHeaderInfo thi = new TableHeaderInfo();
|
|
|
|
thi.hideHeader = this.HideHeaders;
|
|
|
|
for (int k = 0; k < this.activeAssociationList.Count; k++)
|
|
{
|
|
MshResolvedExpressionParameterAssociation a = this.activeAssociationList[k];
|
|
TableColumnInfo ci = new TableColumnInfo();
|
|
|
|
// set the label of the column
|
|
if (a.OriginatingParameter != null)
|
|
{
|
|
object key = a.OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.LabelEntryKey);
|
|
if (key != AutomationNull.Value)
|
|
ci.propertyName = (string)key;
|
|
}
|
|
if (ci.propertyName == null)
|
|
{
|
|
ci.propertyName = this.activeAssociationList[k].ResolvedExpression.ToString();
|
|
}
|
|
|
|
// set the width of the table
|
|
if (a.OriginatingParameter != null)
|
|
{
|
|
object key = a.OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.WidthEntryKey);
|
|
|
|
if (key != AutomationNull.Value)
|
|
ci.width = (int)key;
|
|
else
|
|
{
|
|
ci.width = 0; // let Column Width Manager decide the width
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ci.width = 0; // let Column Width Manager decide the width
|
|
}
|
|
|
|
// set the alignment
|
|
if (a.OriginatingParameter != null)
|
|
{
|
|
object key = a.OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.AlignmentEntryKey);
|
|
|
|
if (key != AutomationNull.Value)
|
|
ci.alignment = (int)key;
|
|
else
|
|
ci.alignment = ComputeDefaultAlignment(so, a.ResolvedExpression);
|
|
}
|
|
else
|
|
{
|
|
ci.alignment = ComputeDefaultAlignment(so, a.ResolvedExpression);
|
|
}
|
|
|
|
thi.tableColumnInfoList.Add(ci);
|
|
}
|
|
return thi;
|
|
}
|
|
|
|
private bool HideHeaders
|
|
{
|
|
get
|
|
{
|
|
// first check command line, it takes the precedence
|
|
if (this.parameters != null && this.parameters.shapeParameters != null)
|
|
{
|
|
TableSpecificParameters tableSpecific = (TableSpecificParameters)this.parameters.shapeParameters;
|
|
if (tableSpecific != null && tableSpecific.hideHeaders.HasValue)
|
|
{
|
|
return tableSpecific.hideHeaders.Value;
|
|
}
|
|
}
|
|
// if we have a view, get the value out of it
|
|
if (this.dataBaseInfo.view != null)
|
|
{
|
|
return _tableBody.header.hideHeader;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
if ((rList.Count == 0) || (rList[0].Exception != null))
|
|
return TextAlignment.Left;
|
|
|
|
object val = rList[0].Result;
|
|
if (val == null)
|
|
return TextAlignment.Left;
|
|
|
|
PSObject soVal = PSObject.AsPSObject(val);
|
|
var typeNames = soVal.InternalTypeNames;
|
|
if (string.Equals(PSObjectHelper.PSObjectIsOfExactType(typeNames),
|
|
"System.String", StringComparison.OrdinalIgnoreCase))
|
|
return TextAlignment.Left;
|
|
|
|
if (DefaultScalarTypes.IsTypeInList(typeNames))
|
|
return TextAlignment.Right;
|
|
|
|
return TextAlignment.Left;
|
|
}
|
|
|
|
internal override FormatEntryData GeneratePayload(PSObject so, int enumerationLimit)
|
|
{
|
|
FormatEntryData fed = new FormatEntryData();
|
|
|
|
TableRowEntry tre;
|
|
if (this.dataBaseInfo.view != null)
|
|
{
|
|
tre = GenerateTableRowEntryFromDataBaseInfo(so, enumerationLimit);
|
|
}
|
|
else
|
|
{
|
|
tre = GenerateTableRowEntryFromFromProperties(so, enumerationLimit);
|
|
// get the global setting for multiline
|
|
tre.multiLine = this.dataBaseInfo.db.defaultSettingsSection.MultilineTables;
|
|
}
|
|
fed.formatEntryInfo = tre;
|
|
|
|
// override from command line, if there
|
|
if (this.parameters != null && this.parameters.shapeParameters != null)
|
|
{
|
|
TableSpecificParameters tableSpecific = (TableSpecificParameters)this.parameters.shapeParameters;
|
|
if (tableSpecific != null && tableSpecific.multiLine.HasValue)
|
|
{
|
|
tre.multiLine = tableSpecific.multiLine.Value;
|
|
}
|
|
}
|
|
return fed;
|
|
}
|
|
|
|
private List<TableRowItemDefinition> GetActiveTableRowDefinition(TableControlBody tableBody, PSObject so,
|
|
out bool multiLine)
|
|
{
|
|
multiLine = tableBody.defaultDefinition.multiLine;
|
|
if (tableBody.optionalDefinitionList.Count == 0)
|
|
{
|
|
// we do not have any override, use default
|
|
return tableBody.defaultDefinition.rowItemDefinitionList;
|
|
}
|
|
|
|
// see if we have an override that matches
|
|
TableRowDefinition matchingRowDefinition = null;
|
|
|
|
var typeNames = so.InternalTypeNames;
|
|
TypeMatch match = new TypeMatch(expressionFactory, this.dataBaseInfo.db, typeNames);
|
|
|
|
foreach (TableRowDefinition x in tableBody.optionalDefinitionList)
|
|
{
|
|
if (match.PerfectMatch(new TypeMatchItem(x, x.appliesTo)))
|
|
{
|
|
matchingRowDefinition = x;
|
|
break;
|
|
}
|
|
}
|
|
if (matchingRowDefinition == null)
|
|
{
|
|
matchingRowDefinition = match.BestMatch as TableRowDefinition;
|
|
}
|
|
|
|
if (matchingRowDefinition == null)
|
|
{
|
|
Collection<string> typesWithoutPrefix = Deserializer.MaskDeserializationPrefix(typeNames);
|
|
if (typesWithoutPrefix != null)
|
|
{
|
|
match = new TypeMatch(expressionFactory, this.dataBaseInfo.db, typesWithoutPrefix);
|
|
|
|
foreach (TableRowDefinition x in tableBody.optionalDefinitionList)
|
|
{
|
|
if (match.PerfectMatch(new TypeMatchItem(x, x.appliesTo)))
|
|
{
|
|
matchingRowDefinition = x;
|
|
break;
|
|
}
|
|
}
|
|
if (matchingRowDefinition == null)
|
|
{
|
|
matchingRowDefinition = match.BestMatch as TableRowDefinition;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (matchingRowDefinition == null)
|
|
{
|
|
// no matching override, use default
|
|
return tableBody.defaultDefinition.rowItemDefinitionList;
|
|
}
|
|
|
|
// the overriding row definition takes the precedence
|
|
if (matchingRowDefinition.multiLine)
|
|
multiLine = matchingRowDefinition.multiLine;
|
|
|
|
// we have an override, we need to compute the merge of the active cells
|
|
List<TableRowItemDefinition> activeRowItemDefinitionList = new List<TableRowItemDefinition>();
|
|
int col = 0;
|
|
foreach (TableRowItemDefinition rowItem in matchingRowDefinition.rowItemDefinitionList)
|
|
{
|
|
// check if the row is an override or not
|
|
if (rowItem.formatTokenList.Count == 0)
|
|
{
|
|
// it's a place holder, use the default
|
|
activeRowItemDefinitionList.Add(tableBody.defaultDefinition.rowItemDefinitionList[col]);
|
|
}
|
|
else
|
|
{
|
|
// use the override
|
|
activeRowItemDefinitionList.Add(rowItem);
|
|
}
|
|
col++;
|
|
}
|
|
|
|
return activeRowItemDefinitionList;
|
|
}
|
|
|
|
private TableRowEntry GenerateTableRowEntryFromDataBaseInfo(PSObject so, int enumerationLimit)
|
|
{
|
|
TableRowEntry tre = new TableRowEntry();
|
|
|
|
List<TableRowItemDefinition> activeRowItemDefinitionList = GetActiveTableRowDefinition(_tableBody, so, out tre.multiLine);
|
|
foreach (TableRowItemDefinition rowItem in activeRowItemDefinitionList)
|
|
{
|
|
FormatPropertyField fpf = GenerateFormatPropertyField(rowItem.formatTokenList, so, enumerationLimit);
|
|
|
|
// get the alignment from the row entry
|
|
// NOTE: if it's not set, the alignment sent with the header will prevail
|
|
fpf.alignment = rowItem.alignment;
|
|
|
|
tre.formatPropertyFieldList.Add(fpf);
|
|
}
|
|
|
|
return tre;
|
|
}
|
|
|
|
private TableRowEntry GenerateTableRowEntryFromFromProperties(PSObject so, int enumerationLimit)
|
|
{
|
|
TableRowEntry tre = new TableRowEntry();
|
|
for (int k = 0; k < this.activeAssociationList.Count; k++)
|
|
{
|
|
FormatPropertyField fpf = new FormatPropertyField();
|
|
FieldFormattingDirective directive = null;
|
|
if (activeAssociationList[k].OriginatingParameter != null)
|
|
{
|
|
directive = activeAssociationList[k].OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.FormatStringEntryKey) as FieldFormattingDirective;
|
|
}
|
|
fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, this.activeAssociationList[k].ResolvedExpression, directive);
|
|
tre.formatPropertyFieldList.Add(fpf);
|
|
}
|
|
return tre;
|
|
}
|
|
}
|
|
}
|
|
|