PowerShell/src/System.Management.Automation/FormatAndOutput/common/BaseOutputtingCommand.cs
xtqqczze 883ca98dd7
Seal private classes (#15725)
* Seal private classes

* Fix CS0509

* Fix CS0628
2021-07-19 14:09:12 +05:00

1402 lines
50 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Management.Automation;
using System.Management.Automation.Internal;
namespace Microsoft.PowerShell.Commands.Internal.Format
{
/// <summary>
/// OutCommand base implementation
/// it manages the formatting protocol and it writes to a generic
/// screen host.
/// </summary>
internal class OutCommandInner : ImplementationCommandBase
{
#region tracer
[TraceSource("format_out_OutCommandInner", "OutCommandInner")]
internal static readonly PSTraceSource tracer = PSTraceSource.GetTracer("format_out_OutCommandInner", "OutCommandInner");
#endregion tracer
internal override void BeginProcessing()
{
base.BeginProcessing();
_formatObjectDeserializer = new FormatObjectDeserializer(this.TerminatingErrorContext);
// hook up the event handlers for the context manager object
_ctxManager.contextCreation = new FormatMessagesContextManager.FormatContextCreationCallback(this.CreateOutputContext);
_ctxManager.fs = new FormatMessagesContextManager.FormatStartCallback(this.ProcessFormatStart);
_ctxManager.fe = new FormatMessagesContextManager.FormatEndCallback(this.ProcessFormatEnd);
_ctxManager.gs = new FormatMessagesContextManager.GroupStartCallback(this.ProcessGroupStart);
_ctxManager.ge = new FormatMessagesContextManager.GroupEndCallback(this.ProcessGroupEnd);
_ctxManager.payload = new FormatMessagesContextManager.PayloadCallback(this.ProcessPayload);
}
/// <summary>
/// Execution entry point override
/// we assume that a LineOutput interface instance already has been acquired
///
/// IMPORTANT: it assumes the presence of a pre-processing formatting command.
/// </summary>
internal override void ProcessRecord()
{
PSObject so = this.ReadObject();
if (so == null || so == AutomationNull.Value)
return;
// try to process the object
if (ProcessObject(so))
return;
// send to the formatter for preprocessing
Array results = ApplyFormatting(so);
if (results != null)
{
foreach (object r in results)
{
PSObject obj = PSObjectHelper.AsPSObject(r);
obj.IsHelpObject = so.IsHelpObject;
ProcessObject(obj);
}
}
}
internal override void EndProcessing()
{
base.EndProcessing();
if (_command != null)
{
// shut down the command processor, if we ever used it
Array results = _command.ShutDown();
if (results != null)
{
foreach (object o in results)
{
ProcessObject(PSObjectHelper.AsPSObject(o));
}
}
}
if (this.LineOutput.RequiresBuffering)
{
// we need to notify the interface implementor that
// we are about to do the playback
LineOutput.DoPlayBackCall playBackCall = new LineOutput.DoPlayBackCall(this.DrainCache);
this.LineOutput.ExecuteBufferPlayBack(playBackCall);
}
else
{
// we drain the cache ourselves
DrainCache();
}
}
private void DrainCache()
{
if (_cache != null)
{
// drain the cache, since we are shutting down
List<PacketInfoData> unprocessedObjects = _cache.Drain();
if (unprocessedObjects != null)
{
foreach (object obj in unprocessedObjects)
{
_ctxManager.Process(obj);
}
}
}
}
private bool ProcessObject(PSObject so)
{
object o = _formatObjectDeserializer.Deserialize(so);
// Console.WriteLine("OutCommandInner.Execute() retrieved object {0}, of type {1}", o.ToString(), o.GetType());
if (NeedsPreprocessing(o))
{
return false;
}
// instantiate the cache if not done yet
if (_cache == null)
{
_cache = new FormattedObjectsCache(this.LineOutput.RequiresBuffering);
}
// no need for formatting, just process the object
FormatStartData formatStart = o as FormatStartData;
if (formatStart != null)
{
// get autosize flag from object
// turn on group caching
if (formatStart.autosizeInfo != null)
{
FormattedObjectsCache.ProcessCachedGroupNotification callBack = new FormattedObjectsCache.ProcessCachedGroupNotification(ProcessCachedGroup);
_cache.EnableGroupCaching(callBack, formatStart.autosizeInfo.objectCount);
}
else
{
// If the format info doesn't define column widths, then auto-size based on the first ten elements
TableHeaderInfo headerInfo = formatStart.shapeInfo as TableHeaderInfo;
if ((headerInfo != null) &&
(headerInfo.tableColumnInfoList.Count > 0) &&
(headerInfo.tableColumnInfoList[0].width == 0))
{
FormattedObjectsCache.ProcessCachedGroupNotification callBack = new FormattedObjectsCache.ProcessCachedGroupNotification(ProcessCachedGroup);
_cache.EnableGroupCaching(callBack, TimeSpan.FromMilliseconds(300));
}
}
}
// Console.WriteLine("OutCommandInner.Execute() calling ctxManager.Process({0})",o.ToString());
List<PacketInfoData> info = _cache.Add((PacketInfoData)o);
if (info != null)
{
for (int k = 0; k < info.Count; k++)
_ctxManager.Process(info[k]);
}
return true;
}
/// <summary>
/// Helper to return what shape we have to use to format the output.
/// </summary>
private FormatShape ActiveFormattingShape
{
get
{
// we assume that the format context
// contains the information
const FormatShape shape = FormatShape.Table; // default
FormatOutputContext foc = this.FormatContext;
if (foc == null || foc.Data.shapeInfo == null)
return shape;
if (foc.Data.shapeInfo is TableHeaderInfo)
return FormatShape.Table;
if (foc.Data.shapeInfo is ListViewHeaderInfo)
return FormatShape.List;
if (foc.Data.shapeInfo is WideViewHeaderInfo)
return FormatShape.Wide;
if (foc.Data.shapeInfo is ComplexViewHeaderInfo)
return FormatShape.Complex;
return shape;
}
}
protected override void InternalDispose()
{
base.InternalDispose();
if (_command != null)
{
_command.Dispose();
_command = null;
}
}
/// <summary>
/// Enum describing the state for the output finite state machine.
/// </summary>
private enum FormattingState
{
/// <summary>
/// We are in the clear state: no formatting in process.
/// </summary>
Reset,
/// <summary>
/// We received a Format Start message, but we are not inside a group.
/// </summary>
Formatting,
/// <summary>
/// We are inside a group because we received a Group Start.
/// </summary>
InsideGroup
}
/// <summary>
/// Toggle to signal if we are in a formatting sequence.
/// </summary>
private FormattingState _currentFormattingState = FormattingState.Reset;
/// <summary>
/// Instance of a command wrapper to execute the
/// default formatter when needed.
/// </summary>
private CommandWrapper _command;
/// <summary>
/// Enumeration to drive the preprocessing stage.
/// </summary>
private enum PreprocessingState { raw, processed, error }
private const int DefaultConsoleWidth = 120;
private const int DefaultConsoleHeight = int.MaxValue;
internal const int StackAllocThreshold = 120;
/// <summary>
/// Test if an object coming from the pipeline needs to be
/// preprocessed by the default formatter.
/// </summary>
/// <param name="o">Object to examine for formatting.</param>
/// <returns>Whether the object needs to be shunted to preprocessing.</returns>
private bool NeedsPreprocessing(object o)
{
FormatEntryData fed = o as FormatEntryData;
if (fed != null)
{
// we got an already pre-processed object
if (!fed.outOfBand)
{
// we allow out of band data in any state
ValidateCurrentFormattingState(FormattingState.InsideGroup, o);
}
return false;
}
else if (o is FormatStartData)
{
// when encountering FormatStartDate out of sequence,
// pretend that the previous formatting directives were properly closed
if (_currentFormattingState == FormattingState.InsideGroup)
{
this.EndProcessing();
this.BeginProcessing();
}
// we got a Fs message, we enter a sequence
ValidateCurrentFormattingState(FormattingState.Reset, o);
_currentFormattingState = FormattingState.Formatting;
return false;
}
else if (o is FormatEndData)
{
// we got a Fe message, we exit a sequence
ValidateCurrentFormattingState(FormattingState.Formatting, o);
_currentFormattingState = FormattingState.Reset;
return false;
}
else if (o is GroupStartData)
{
ValidateCurrentFormattingState(FormattingState.Formatting, o);
_currentFormattingState = FormattingState.InsideGroup;
return false;
}
else if (o is GroupEndData)
{
ValidateCurrentFormattingState(FormattingState.InsideGroup, o);
_currentFormattingState = FormattingState.Formatting;
return false;
}
// this is a raw object
return true;
}
private void ValidateCurrentFormattingState(FormattingState expectedFormattingState, object obj)
{
// check if we are in the expected formatting state
if (_currentFormattingState != expectedFormattingState)
{
// we are not in the expected state, some message is out of sequence,
// need to abort the command
string violatingCommand = "format-*";
StartData sdObj = obj as StartData;
if (sdObj != null)
{
if (sdObj.shapeInfo is WideViewHeaderInfo)
{
violatingCommand = "format-wide";
}
else if (sdObj.shapeInfo is TableHeaderInfo)
{
violatingCommand = "format-table";
}
else if (sdObj.shapeInfo is ListViewHeaderInfo)
{
violatingCommand = "format-list";
}
else if (sdObj.shapeInfo is ComplexViewHeaderInfo)
{
violatingCommand = "format-complex";
}
}
string msg = StringUtil.Format(FormatAndOut_out_xxx.OutLineOutput_OutOfSequencePacket,
obj.GetType().FullName, violatingCommand);
ErrorRecord errorRecord = new ErrorRecord(
new InvalidOperationException(),
"ConsoleLineOutputOutOfSequencePacket",
ErrorCategory.InvalidData,
null);
errorRecord.ErrorDetails = new ErrorDetails(msg);
this.TerminatingErrorContext.ThrowTerminatingError(errorRecord);
}
}
/// <summary>
/// Shunt object to the formatting pipeline for preprocessing.
/// </summary>
/// <param name="o">Object to be preprocessed.</param>
/// <returns>Array of objects returned by the preprocessing step.</returns>
private Array ApplyFormatting(object o)
{
if (_command == null)
{
_command = new CommandWrapper();
_command.Initialize(this.OuterCmdlet().Context, "format-default", typeof(FormatDefaultCommand));
}
return _command.Process(o);
}
/// <summary>
/// Class factory for output context.
/// </summary>
/// <param name="parentContext">Parent context in the stack.</param>
/// <param name="formatInfoData">Fromat info data received from the pipeline.</param>
/// <returns></returns>
private FormatMessagesContextManager.OutputContext CreateOutputContext(
FormatMessagesContextManager.OutputContext parentContext,
FormatInfoData formatInfoData)
{
FormatStartData formatStartData = formatInfoData as FormatStartData;
// initialize the format context
if (formatStartData != null)
{
FormatOutputContext foc = new FormatOutputContext(parentContext, formatStartData);
return foc;
}
GroupStartData gsd = formatInfoData as GroupStartData;
// we are starting a group, initialize the group context
if (gsd != null)
{
GroupOutputContext goc = null;
switch (ActiveFormattingShape)
{
case FormatShape.Table:
{
goc = new TableOutputContext(this, parentContext, gsd);
break;
}
case FormatShape.List:
{
goc = new ListOutputContext(this, parentContext, gsd);
break;
}
case FormatShape.Wide:
{
goc = new WideOutputContext(this, parentContext, gsd);
break;
}
case FormatShape.Complex:
{
goc = new ComplexOutputContext(this, parentContext, gsd);
break;
}
default:
{
Diagnostics.Assert(false, "Invalid shape. This should never happen");
}
break;
}
goc.Initialize();
return goc;
}
return null;
}
/// <summary>
/// Callback for Fs processing.
/// </summary>
/// <param name="c">The context containing the Fs entry.</param>
private void ProcessFormatStart(FormatMessagesContextManager.OutputContext c)
{
// we just add an empty line to the display
this.LineOutput.WriteLine(string.Empty);
}
/// <summary>
/// Callback for Fe processing.
/// </summary>
/// <param name="fe">Fe notification message.</param>
/// <param name="c">Current context, with Fs in it.</param>
private void ProcessFormatEnd(FormatEndData fe, FormatMessagesContextManager.OutputContext c)
{
// Console.WriteLine("ProcessFormatEnd");
// we just add an empty line to the display
this.LineOutput.WriteLine(string.Empty);
}
/// <summary>
/// Callback for Gs processing.
/// </summary>
/// <param name="c">The context containing the Gs entry.</param>
private void ProcessGroupStart(FormatMessagesContextManager.OutputContext c)
{
// Console.WriteLine("ProcessGroupStart");
GroupOutputContext goc = (GroupOutputContext)c;
if (goc.Data.groupingEntry != null)
{
ComplexWriter writer = new ComplexWriter();
writer.Initialize(_lo, _lo.ColumnNumber);
writer.WriteObject(goc.Data.groupingEntry.formatValueList);
_lo.WriteLine(string.Empty);
}
goc.GroupStart();
}
/// <summary>
/// Callback for Ge processing.
/// </summary>
/// <param name="ge">Ge notification message.</param>
/// <param name="c">Current context, with Gs in it.</param>
private void ProcessGroupEnd(GroupEndData ge, FormatMessagesContextManager.OutputContext c)
{
// Console.WriteLine("ProcessGroupEnd");
GroupOutputContext goc = (GroupOutputContext)c;
goc.GroupEnd();
}
/// <summary>
/// Process the current payload object.
/// </summary>
/// <param name="fed">FormatEntryData to process.</param>
/// <param name="c">Currently active context.</param>
private void ProcessPayload(FormatEntryData fed, FormatMessagesContextManager.OutputContext c)
{
// we assume FormatEntryData as a standard wrapper
if (fed == null)
{
PSTraceSource.NewArgumentNullException(nameof(fed));
}
if (fed.formatEntryInfo == null)
{
PSTraceSource.NewArgumentNullException("fed.formatEntryInfo");
}
WriteStreamType oldWSState = _lo.WriteStream;
try
{
_lo.WriteStream = fed.writeStream;
if (c == null)
{
ProcessOutOfBandPayload(fed);
}
else
{
GroupOutputContext goc = (GroupOutputContext)c;
goc.ProcessPayload(fed);
}
}
finally
{
_lo.WriteStream = oldWSState;
}
}
private void ProcessOutOfBandPayload(FormatEntryData fed)
{
// try if it is raw text
RawTextFormatEntry rte = fed.formatEntryInfo as RawTextFormatEntry;
if (rte != null)
{
if (fed.isHelpObject)
{
ComplexWriter complexWriter = new ComplexWriter();
complexWriter.Initialize(_lo, _lo.ColumnNumber);
complexWriter.WriteString(rte.text);
}
else
{
_lo.WriteLine(rte.text);
}
return;
}
// try if it is a complex entry
ComplexViewEntry cve = fed.formatEntryInfo as ComplexViewEntry;
if (cve != null && cve.formatValueList != null)
{
ComplexWriter complexWriter = new ComplexWriter();
complexWriter.Initialize(_lo, int.MaxValue);
complexWriter.WriteObject(cve.formatValueList);
return;
}
// try if it is a list view
ListViewEntry lve = fed.formatEntryInfo as ListViewEntry;
if (lve != null && lve.listViewFieldList != null)
{
ListWriter listWriter = new ListWriter();
_lo.WriteLine(string.Empty);
string[] properties = ListOutputContext.GetProperties(lve);
listWriter.Initialize(properties, _lo.ColumnNumber, _lo.DisplayCells);
string[] values = ListOutputContext.GetValues(lve);
listWriter.WriteProperties(values, _lo);
_lo.WriteLine(string.Empty);
return;
}
}
/// <summary>
/// The screen host associated with this outputter.
/// </summary>
private LineOutput _lo = null;
internal LineOutput LineOutput
{
get { return _lo; }
set { _lo = value; }
}
private ShapeInfo ShapeInfoOnFormatContext
{
get
{
FormatOutputContext foc = this.FormatContext;
if (foc == null)
return null;
return foc.Data.shapeInfo;
}
}
/// <summary>
/// Retrieve the active FormatOutputContext on the stack
/// by walking up to the top of the stack.
/// </summary>
private FormatOutputContext FormatContext
{
get
{
for (FormatMessagesContextManager.OutputContext oc = _ctxManager.ActiveOutputContext; oc != null; oc = oc.ParentContext)
{
FormatOutputContext foc = oc as FormatOutputContext;
if (foc != null)
return foc;
}
return null;
}
}
/// <summary>
/// Context manager instance to guide the message traversal.
/// </summary>
private readonly FormatMessagesContextManager _ctxManager = new FormatMessagesContextManager();
private FormattedObjectsCache _cache = null;
/// <summary>
/// Handler for processing the caching notification and responsible for
/// setting the value of the formatting hint.
/// </summary>
/// <param name="formatStartData"></param>
/// <param name="objects"></param>
private void ProcessCachedGroup(FormatStartData formatStartData, List<PacketInfoData> objects)
{
_formattingHint = null;
TableHeaderInfo thi = formatStartData.shapeInfo as TableHeaderInfo;
if (thi != null)
{
ProcessCachedGroupOnTable(thi, objects);
return;
}
WideViewHeaderInfo wvhi = formatStartData.shapeInfo as WideViewHeaderInfo;
if (wvhi != null)
{
ProcessCachedGroupOnWide(wvhi, objects);
return;
}
}
private void ProcessCachedGroupOnTable(TableHeaderInfo thi, List<PacketInfoData> objects)
{
if (thi.tableColumnInfoList.Count == 0)
return;
int[] widths = new int[thi.tableColumnInfoList.Count];
for (int k = 0; k < thi.tableColumnInfoList.Count; k++)
{
string label = thi.tableColumnInfoList[k].label;
if (string.IsNullOrEmpty(label))
label = thi.tableColumnInfoList[k].propertyName;
if (string.IsNullOrEmpty(label))
widths[k] = 0;
else
widths[k] = _lo.DisplayCells.Length(label);
}
int cellCount; // scratch variable
foreach (PacketInfoData o in objects)
{
if (o is FormatEntryData fed)
{
TableRowEntry tre = fed.formatEntryInfo as TableRowEntry;
int kk = 0;
foreach (FormatPropertyField fpf in tre.formatPropertyFieldList)
{
cellCount = _lo.DisplayCells.Length(fpf.propertyValue);
if (widths[kk] < cellCount)
widths[kk] = cellCount;
kk++;
}
}
}
TableFormattingHint hint = new TableFormattingHint();
hint.columnWidths = widths;
_formattingHint = hint;
}
private void ProcessCachedGroupOnWide(WideViewHeaderInfo wvhi, List<PacketInfoData> objects)
{
if (wvhi.columns != 0)
{
// columns forced on the client
return;
}
int maxLen = 0;
int cellCount; // scratch variable
foreach (PacketInfoData o in objects)
{
if (o is FormatEntryData fed)
{
WideViewEntry wve = fed.formatEntryInfo as WideViewEntry;
FormatPropertyField fpf = wve.formatPropertyField as FormatPropertyField;
if (!string.IsNullOrEmpty(fpf.propertyValue))
{
cellCount = _lo.DisplayCells.Length(fpf.propertyValue);
if (cellCount > maxLen)
maxLen = cellCount;
}
}
}
WideFormattingHint hint = new WideFormattingHint();
hint.maxWidth = maxLen;
_formattingHint = hint;
}
/// <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.
/// If we detect that int.MaxValue is used, first we try to get the current console window width.
/// However, if we can't read that (for example, implicit remoting has no console window), we default
/// to something reasonable: 120 columns.
/// </summary>
private static int GetConsoleWindowWidth(int columnNumber)
{
if (InternalTestHooks.SetConsoleWidthToZero)
{
return DefaultConsoleWidth;
}
if (columnNumber == int.MaxValue)
{
try
{
// if Console width is set to 0, the default width is returned so that the output string is not null.
// This can happen in environments where TERM is not set.
return (Console.WindowWidth != 0) ? Console.WindowWidth : DefaultConsoleWidth;
}
catch
{
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>
private abstract class FormattingHint
{
}
/// <summary>
/// Hint for format-table.
/// </summary>
private sealed class TableFormattingHint : FormattingHint
{
internal int[] columnWidths = null;
}
/// <summary>
/// Hint for format-wide.
/// </summary>
private sealed class WideFormattingHint : FormattingHint
{
internal int maxWidth = 0;
}
/// <summary>
/// Variable holding the autosize hint (set by the caching code and reset by the hint consumer.
/// </summary>
private FormattingHint _formattingHint = null;
/// <summary>
/// Helper for consuming the formatting hint.
/// </summary>
/// <returns></returns>
private FormattingHint RetrieveFormattingHint()
{
FormattingHint fh = _formattingHint;
_formattingHint = null;
return fh;
}
private FormatObjectDeserializer _formatObjectDeserializer;
/// <summary>
/// Context for the outer scope of the format sequence.
/// </summary>
private sealed class FormatOutputContext : FormatMessagesContextManager.OutputContext
{
/// <summary>
/// Construct a context to push on the stack.
/// </summary>
/// <param name="parentContext">Parent context in the stack.</param>
/// <param name="formatData">Format data to put in the context.</param>
internal FormatOutputContext(FormatMessagesContextManager.OutputContext parentContext, FormatStartData formatData)
: base(parentContext)
{
Data = formatData;
}
/// <summary>
/// Retrieve the format data in the context.
/// </summary>
internal FormatStartData Data { get; } = null;
}
/// <summary>
/// Context for the currently active group.
/// </summary>
private abstract class GroupOutputContext : FormatMessagesContextManager.OutputContext
{
/// <summary>
/// Construct a context to push on the stack.
/// </summary>
internal GroupOutputContext(OutCommandInner cmd,
FormatMessagesContextManager.OutputContext parentContext,
GroupStartData formatData)
: base(parentContext)
{
InnerCommand = cmd;
Data = formatData;
}
/// <summary>
/// Called at creation time, overrides will initialize here, e.g.
/// column widths, etc.
/// </summary>
internal virtual void Initialize() { }
/// <summary>
/// Called when a group of data is started, overridden will do
/// things such as headers, etc...
/// </summary>
internal virtual void GroupStart() { }
/// <summary>
/// Called when the end of a group is reached, overrides will do
/// things such as group footers.
/// </summary>
internal virtual void GroupEnd() { }
/// <summary>
/// Called when there is an entry to process, overrides will do
/// things such as writing a row in a table.
/// </summary>
/// <param name="fed">FormatEntryData to process.</param>
internal virtual void ProcessPayload(FormatEntryData fed) { }
/// <summary>
/// Retrieve the format data in the context.
/// </summary>
internal GroupStartData Data { get; } = null;
protected OutCommandInner InnerCommand { get; }
}
private class TableOutputContextBase : GroupOutputContext
{
/// <summary>
/// Construct a context to push on the stack.
/// </summary>
/// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>
/// <param name="parentContext">Parent context in the stack.</param>
/// <param name="formatData">Format data to put in the context.</param>
internal TableOutputContextBase(OutCommandInner cmd,
FormatMessagesContextManager.OutputContext parentContext,
GroupStartData formatData)
: base(cmd, parentContext, formatData)
{
}
/// <summary>
/// Get the table writer for this context.
/// </summary>
protected TableWriter Writer { get { return _tableWriter; } }
/// <summary>
/// Helper class to properly write a table using text output.
/// </summary>
private readonly TableWriter _tableWriter = new TableWriter();
}
private sealed class TableOutputContext : TableOutputContextBase
{
private int _rowCount = 0;
private int _consoleHeight = -1;
private int _consoleWidth = -1;
private const int WhitespaceAndPagerLineCount = 2;
private readonly bool _repeatHeader = false;
/// <summary>
/// Construct a context to push on the stack.
/// </summary>
/// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>
/// <param name="parentContext">Parent context in the stack.</param>
/// <param name="formatData">Format data to put in the context.</param>
internal TableOutputContext(OutCommandInner cmd,
FormatMessagesContextManager.OutputContext parentContext,
GroupStartData formatData)
: base(cmd, parentContext, formatData)
{
if (parentContext is FormatOutputContext foc)
{
if (foc.Data.shapeInfo is TableHeaderInfo thi)
{
_repeatHeader = thi.repeatHeader;
}
}
}
/// <summary>
/// Initialize column widths.
/// </summary>
internal override void Initialize()
{
TableFormattingHint tableHint = this.InnerCommand.RetrieveFormattingHint() as TableFormattingHint;
int[] columnWidthsHint = null;
// We expect that console width is less then 120.
if (tableHint != null)
{
columnWidthsHint = tableHint.columnWidths;
}
_consoleHeight = GetConsoleWindowHeight(this.InnerCommand._lo.RowNumber);
_consoleWidth = GetConsoleWindowWidth(this.InnerCommand._lo.ColumnNumber);
int columns = this.CurrentTableHeaderInfo.tableColumnInfoList.Count;
if (columns == 0)
{
return;
}
// create arrays for widths and alignment
Span<int> columnWidths = columns <= StackAllocThreshold ? stackalloc int[columns] : new int[columns];
Span<int> alignment = columns <= StackAllocThreshold ? stackalloc int[columns] : new int[columns];
int k = 0;
foreach (TableColumnInfo tci in this.CurrentTableHeaderInfo.tableColumnInfoList)
{
columnWidths[k] = (columnWidthsHint != null) ? columnWidthsHint[k] : tci.width;
alignment[k] = tci.alignment;
k++;
}
this.Writer.Initialize(0, _consoleWidth, columnWidths, alignment, this.CurrentTableHeaderInfo.hideHeader);
}
/// <summary>
/// Write the headers.
/// </summary>
internal override void GroupStart()
{
int columns = this.CurrentTableHeaderInfo.tableColumnInfoList.Count;
if (columns == 0)
{
return;
}
string[] properties = new string[columns];
int k = 0;
foreach (TableColumnInfo tci in this.CurrentTableHeaderInfo.tableColumnInfoList)
{
properties[k++] = tci.label ?? tci.propertyName;
}
_rowCount += this.Writer.GenerateHeader(properties, this.InnerCommand._lo);
}
/// <summary>
/// Write a row into the table.
/// </summary>
/// <param name="fed">FormatEntryData to process.</param>
internal override void ProcessPayload(FormatEntryData fed)
{
int headerColumns = this.CurrentTableHeaderInfo.tableColumnInfoList.Count;
if (headerColumns == 0)
{
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
string[] values = new string[headerColumns];
Span<int> alignment = headerColumns <= StackAllocThreshold ? stackalloc int[headerColumns] : new int[headerColumns];
int fieldCount = tre.formatPropertyFieldList.Count;
for (int k = 0; k < headerColumns; k++)
{
if (k < fieldCount)
{
values[k] = tre.formatPropertyFieldList[k].propertyValue;
alignment[k] = tre.formatPropertyFieldList[k].alignment;
}
else
{
values[k] = string.Empty;
alignment[k] = TextAlignment.Left; // hard coded default
}
}
this.Writer.GenerateRow(values, this.InnerCommand._lo, tre.multiLine, alignment, InnerCommand._lo.DisplayCells, generatedRows: null);
_rowCount++;
}
private TableHeaderInfo CurrentTableHeaderInfo
{
get
{
return (TableHeaderInfo)this.InnerCommand.ShapeInfoOnFormatContext;
}
}
}
private sealed class ListOutputContext : GroupOutputContext
{
/// <summary>
/// Construct a context to push on the stack.
/// </summary>
/// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>
/// <param name="parentContext">Parent context in the stack.</param>
/// <param name="formatData">Format data to put in the context.</param>
internal ListOutputContext(OutCommandInner cmd,
FormatMessagesContextManager.OutputContext parentContext,
GroupStartData formatData)
: base(cmd, parentContext, formatData)
{
}
/// <summary>
/// Initialize column widths.
/// </summary>
internal override void Initialize()
{
}
private void InternalInitialize(ListViewEntry lve)
{
_properties = GetProperties(lve);
_listWriter.Initialize(_properties, this.InnerCommand._lo.ColumnNumber, InnerCommand._lo.DisplayCells);
}
internal static string[] GetProperties(ListViewEntry lve)
{
StringCollection props = new StringCollection();
foreach (ListViewField lvf in lve.listViewFieldList)
{
props.Add(lvf.label ?? lvf.propertyName);
}
if (props.Count == 0)
return null;
string[] retVal = new string[props.Count];
props.CopyTo(retVal, 0);
return retVal;
}
internal static string[] GetValues(ListViewEntry lve)
{
StringCollection vals = new StringCollection();
foreach (ListViewField lvf in lve.listViewFieldList)
{
vals.Add(lvf.formatPropertyField.propertyValue);
}
if (vals.Count == 0)
return null;
string[] retVal = new string[vals.Count];
vals.CopyTo(retVal, 0);
return retVal;
}
/// <summary>
/// Write the headers.
/// </summary>
internal override void GroupStart()
{
}
/// <summary>
/// Write a row into the list.
/// </summary>
/// <param name="fed">FormatEntryData to process.</param>
internal override void ProcessPayload(FormatEntryData fed)
{
ListViewEntry lve = fed.formatEntryInfo as ListViewEntry;
InternalInitialize(lve);
string[] values = GetValues(lve);
_listWriter.WriteProperties(values, this.InnerCommand._lo);
this.InnerCommand._lo.WriteLine(string.Empty);
}
/// <summary>
/// Property list currently active.
/// </summary>
private string[] _properties = null;
/// <summary>
/// Writer to do the actual formatting.
/// </summary>
private readonly ListWriter _listWriter = new ListWriter();
}
private sealed class WideOutputContext : TableOutputContextBase
{
/// <summary>
/// Construct a context to push on the stack.
/// </summary>
/// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>
/// <param name="parentContext">Parent context in the stack.</param>
/// <param name="formatData">Format data to put in the context.</param>
internal WideOutputContext(OutCommandInner cmd,
FormatMessagesContextManager.OutputContext parentContext,
GroupStartData formatData)
: base(cmd, parentContext, formatData)
{
}
private StringValuesBuffer _buffer = null;
/// <summary>
/// Initialize column widths.
/// </summary>
internal override void Initialize()
{
// set the hard wider default, to be used if no other info is available
int itemsPerRow = 2;
// get the header info and the view hint
WideFormattingHint hint = this.InnerCommand.RetrieveFormattingHint() as WideFormattingHint;
int columnsOnTheScreen = GetConsoleWindowWidth(this.InnerCommand._lo.ColumnNumber);
// give a preference to the hint, if there
if (hint != null && hint.maxWidth > 0)
{
itemsPerRow = TableWriter.ComputeWideViewBestItemsPerRowFit(hint.maxWidth, columnsOnTheScreen);
}
else if (this.CurrentWideHeaderInfo.columns > 0)
{
itemsPerRow = this.CurrentWideHeaderInfo.columns;
}
// create a buffer object to hold partial rows
_buffer = new StringValuesBuffer(itemsPerRow);
// initialize the writer
Span<int> columnWidths = itemsPerRow <= StackAllocThreshold ? stackalloc int[itemsPerRow] : new int[itemsPerRow];
Span<int> alignment = itemsPerRow <= StackAllocThreshold ? stackalloc int[itemsPerRow] : new int[itemsPerRow];
for (int k = 0; k < itemsPerRow; k++)
{
columnWidths[k] = 0; // autosize
alignment[k] = TextAlignment.Left;
}
this.Writer.Initialize(0, columnsOnTheScreen, columnWidths, alignment, false, GetConsoleWindowHeight(this.InnerCommand._lo.RowNumber));
}
/// <summary>
/// Write the headers.
/// </summary>
internal override void GroupStart()
{
}
/// <summary>
/// Called when the end of a group is reached, flush the
/// write buffer.
/// </summary>
internal override void GroupEnd()
{
WriteStringBuffer();
}
/// <summary>
/// Write a row into the table.
/// </summary>
/// <param name="fed">FormatEntryData to process.</param>
internal override void ProcessPayload(FormatEntryData fed)
{
WideViewEntry wve = fed.formatEntryInfo as WideViewEntry;
FormatPropertyField fpf = wve.formatPropertyField as FormatPropertyField;
_buffer.Add(fpf.propertyValue);
if (_buffer.IsFull)
{
WriteStringBuffer();
}
}
private WideViewHeaderInfo CurrentWideHeaderInfo
{
get
{
return (WideViewHeaderInfo)this.InnerCommand.ShapeInfoOnFormatContext;
}
}
private void WriteStringBuffer()
{
if (_buffer.IsEmpty)
{
return;
}
string[] values = new string[_buffer.Length];
for (int k = 0; k < values.Length; k++)
{
if (k < _buffer.CurrentCount)
values[k] = _buffer[k];
else
values[k] = string.Empty;
}
this.Writer.GenerateRow(values, this.InnerCommand._lo, false, null, InnerCommand._lo.DisplayCells, generatedRows: null);
_buffer.Reset();
}
/// <summary>
/// Helper class to accumulate the display values so that when the end
/// of a line is reached, a full line can be composed.
/// </summary>
private sealed class StringValuesBuffer
{
/// <summary>
/// Construct the buffer.
/// </summary>
/// <param name="size">Number of entries to cache.</param>
internal StringValuesBuffer(int size)
{
_arr = new string[size];
Reset();
}
/// <summary>
/// Get the size of the buffer.
/// </summary>
internal int Length { get { return _arr.Length; } }
/// <summary>
/// Get the current number of entries in the buffer.
/// </summary>
internal int CurrentCount { get { return _lastEmptySpot; } }
/// <summary>
/// Check if the buffer is full.
/// </summary>
internal bool IsFull
{
get { return _lastEmptySpot == _arr.Length; }
}
/// <summary>
/// Check if the buffer is empty.
/// </summary>
internal bool IsEmpty
{
get { return _lastEmptySpot == 0; }
}
/// <summary>
/// Indexer to access the k-th item in the buffer.
/// </summary>
internal string this[int k] { get { return _arr[k]; } }
/// <summary>
/// Add an item to the buffer.
/// </summary>
/// <param name="s">String to add.</param>
internal void Add(string s)
{
_arr[_lastEmptySpot++] = s;
}
/// <summary>
/// Reset the buffer.
/// </summary>
internal void Reset()
{
_lastEmptySpot = 0;
for (int k = 0; k < _arr.Length; k++)
_arr[k] = null;
}
private readonly string[] _arr;
private int _lastEmptySpot;
}
}
private sealed class ComplexOutputContext : GroupOutputContext
{
/// <summary>
/// Construct a context to push on the stack.
/// </summary>
/// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>
/// <param name="parentContext">Parent context in the stack.</param>
/// <param name="formatData">Format data to put in the context.</param>
internal ComplexOutputContext(OutCommandInner cmd,
FormatMessagesContextManager.OutputContext parentContext,
GroupStartData formatData)
: base(cmd, parentContext, formatData)
{
}
internal override void Initialize()
{
_writer.Initialize(this.InnerCommand._lo,
this.InnerCommand._lo.ColumnNumber);
}
/// <summary>
/// Write a row into the list.
/// </summary>
/// <param name="fed">FormatEntryData to process.</param>
internal override void ProcessPayload(FormatEntryData fed)
{
ComplexViewEntry cve = fed.formatEntryInfo as ComplexViewEntry;
if (cve == null || cve.formatValueList == null)
return;
_writer.WriteObject(cve.formatValueList);
}
private readonly ComplexWriter _writer = new ComplexWriter();
}
}
}