MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System;5using System.Collections.Generic;6using System.Management.Automation;7using System.Management.Automation.Internal;8 9namespace Microsoft.PowerShell.Commands.Internal.Format10{11 /// <summary>12 /// OutCommand base implementation13 /// it manages the formatting protocol and it writes to a generic14 /// screen host.15 /// </summary>16 internal class OutCommandInner : ImplementationCommandBase17 {18 #region tracer19 [TraceSource("format_out_OutCommandInner", "OutCommandInner")]20 internal static readonly PSTraceSource tracer = PSTraceSource.GetTracer("format_out_OutCommandInner", "OutCommandInner");21 #endregion tracer22 23 internal override void BeginProcessing()24 {25 base.BeginProcessing();26 27 _formatObjectDeserializer = new FormatObjectDeserializer(this.TerminatingErrorContext);28 29 // hook up the event handlers for the context manager object30 _ctxManager.contextCreation = new FormatMessagesContextManager.FormatContextCreationCallback(this.CreateOutputContext);31 _ctxManager.fs = new FormatMessagesContextManager.FormatStartCallback(this.ProcessFormatStart);32 _ctxManager.fe = new FormatMessagesContextManager.FormatEndCallback(this.ProcessFormatEnd);33 _ctxManager.gs = new FormatMessagesContextManager.GroupStartCallback(this.ProcessGroupStart);34 _ctxManager.ge = new FormatMessagesContextManager.GroupEndCallback(this.ProcessGroupEnd);35 _ctxManager.payload = new FormatMessagesContextManager.PayloadCallback(this.ProcessPayload);36 }37 38 /// <summary>39 /// Execution entry point override40 /// we assume that a LineOutput interface instance already has been acquired41 ///42 /// IMPORTANT: it assumes the presence of a pre-processing formatting command.43 /// </summary>44 internal override void ProcessRecord()45 {46 PSObject so = this.ReadObject();47 48 if (so == null || so == AutomationNull.Value)49 return;50 51 // try to process the object52 if (ProcessObject(so))53 return;54 55 // send to the formatter for preprocessing56 Array results = ApplyFormatting(so);57 58 if (results != null)59 {60 foreach (object r in results)61 {62 PSObject obj = PSObjectHelper.AsPSObject(r);63 64 obj.IsHelpObject = so.IsHelpObject;65 66 ProcessObject(obj);67 }68 }69 }70 71 internal override void EndProcessing()72 {73 base.EndProcessing();74 if (_command != null)75 {76 // shut down the command processor, if we ever used it77 Array results = _command.ShutDown();78 79 if (results != null)80 {81 foreach (object o in results)82 {83 ProcessObject(PSObjectHelper.AsPSObject(o));84 }85 }86 }87 88 if (this.LineOutput.RequiresBuffering)89 {90 // we need to notify the interface implementor that91 // we are about to do the playback92 LineOutput.DoPlayBackCall playBackCall = new LineOutput.DoPlayBackCall(this.DrainCache);93 94 this.LineOutput.ExecuteBufferPlayBack(playBackCall);95 }96 else97 {98 // we drain the cache ourselves99 DrainCache();100 }101 }102 103 private void DrainCache()104 {105 if (_cache != null)106 {107 // drain the cache, since we are shutting down108 List<PacketInfoData> unprocessedObjects = _cache.Drain();109 110 if (unprocessedObjects != null)111 {112 foreach (object obj in unprocessedObjects)113 {114 _ctxManager.Process(obj);115 }116 }117 }118 }119 120 private bool ProcessObject(PSObject so)121 {122 object o = _formatObjectDeserializer.Deserialize(so);123 124 // Console.WriteLine("OutCommandInner.Execute() retrieved object {0}, of type {1}", o.ToString(), o.GetType());125 if (NeedsPreprocessing(o))126 {127 return false;128 }129 130 // instantiate the cache if not done yet131 _cache ??= new FormattedObjectsCache(this.LineOutput.RequiresBuffering);132 133 // no need for formatting, just process the object134 if (o is FormatStartData formatStart)135 {136 // get autosize flag from object137 // turn on group caching138 if (formatStart.autosizeInfo != null)139 {140 FormattedObjectsCache.ProcessCachedGroupNotification callBack = new FormattedObjectsCache.ProcessCachedGroupNotification(ProcessCachedGroup);141 _cache.EnableGroupCaching(callBack, formatStart.autosizeInfo.objectCount);142 }143 else144 {145 // If the format info doesn't define column widths, then auto-size based on the first ten elements146 if ((formatStart.shapeInfo is TableHeaderInfo headerInfo) &&147 (headerInfo.tableColumnInfoList.Count > 0) &&148 (headerInfo.tableColumnInfoList[0].width == 0))149 {150 FormattedObjectsCache.ProcessCachedGroupNotification callBack = new FormattedObjectsCache.ProcessCachedGroupNotification(ProcessCachedGroup);151 _cache.EnableGroupCaching(callBack, TimeSpan.FromMilliseconds(300));152 }153 }154 }155 156 // Console.WriteLine("OutCommandInner.Execute() calling ctxManager.Process({0})",o.ToString());157 List<PacketInfoData> info = _cache.Add((PacketInfoData)o);158 159 if (info != null)160 {161 for (int k = 0; k < info.Count; k++)162 _ctxManager.Process(info[k]);163 }164 165 return true;166 }167 168 /// <summary>169 /// Helper to return what shape we have to use to format the output.170 /// </summary>171 private FormatShape ActiveFormattingShape172 {173 get174 {175 // we assume that the format context176 // contains the information177 const FormatShape shape = FormatShape.Table; // default178 FormatOutputContext foc = this.FormatContext;179 180 if (foc == null || foc.Data.shapeInfo == null)181 return shape;182 183 if (foc.Data.shapeInfo is TableHeaderInfo)184 return FormatShape.Table;185 186 if (foc.Data.shapeInfo is ListViewHeaderInfo)187 return FormatShape.List;188 189 if (foc.Data.shapeInfo is WideViewHeaderInfo)190 return FormatShape.Wide;191 192 if (foc.Data.shapeInfo is ComplexViewHeaderInfo)193 return FormatShape.Complex;194 195 return shape;196 }197 }198 199 protected override void InternalDispose()200 {201 base.InternalDispose();202 if (_command != null)203 {204 _command.Dispose();205 _command = null;206 }207 }208 209 /// <summary>210 /// Enum describing the state for the output finite state machine.211 /// </summary>212 private enum FormattingState213 {214 /// <summary>215 /// We are in the clear state: no formatting in process.216 /// </summary>217 Reset,218 219 /// <summary>220 /// We received a Format Start message, but we are not inside a group.221 /// </summary>222 Formatting,223 224 /// <summary>225 /// We are inside a group because we received a Group Start.226 /// </summary>227 InsideGroup228 }229 230 /// <summary>231 /// Toggle to signal if we are in a formatting sequence.232 /// </summary>233 private FormattingState _currentFormattingState = FormattingState.Reset;234 235 /// <summary>236 /// Instance of a command wrapper to execute the237 /// default formatter when needed.238 /// </summary>239 private CommandWrapper _command;240 241 /// <summary>242 /// Enumeration to drive the preprocessing stage.243 /// </summary>244 private enum PreprocessingState { raw, processed, error }245 246 private const int DefaultConsoleWidth = 120;247 private const int DefaultConsoleHeight = int.MaxValue;248 internal const int StackAllocThreshold = 120;249 250 /// <summary>251 /// Test if an object coming from the pipeline needs to be252 /// preprocessed by the default formatter.253 /// </summary>254 /// <param name="o">Object to examine for formatting.</param>255 /// <returns>Whether the object needs to be shunted to preprocessing.</returns>256 private bool NeedsPreprocessing(object o)257 {258 if (o is FormatEntryData fed)259 {260 // we got an already pre-processed object261 if (!fed.outOfBand)262 {263 // we allow out of band data in any state264 ValidateCurrentFormattingState(FormattingState.InsideGroup, o);265 }266 267 return false;268 }269 else if (o is FormatStartData)270 {271 // when encountering FormatStartDate out of sequence,272 // pretend that the previous formatting directives were properly closed273 if (_currentFormattingState == FormattingState.InsideGroup)274 {275 this.EndProcessing();276 this.BeginProcessing();277 }278 279 // we got a Fs message, we enter a sequence280 ValidateCurrentFormattingState(FormattingState.Reset, o);281 _currentFormattingState = FormattingState.Formatting;282 283 return false;284 }285 else if (o is FormatEndData)286 {287 // we got a Fe message, we exit a sequence288 ValidateCurrentFormattingState(FormattingState.Formatting, o);289 _currentFormattingState = FormattingState.Reset;290 291 return false;292 }293 else if (o is GroupStartData)294 {295 ValidateCurrentFormattingState(FormattingState.Formatting, o);296 _currentFormattingState = FormattingState.InsideGroup;297 298 return false;299 }300 else if (o is GroupEndData)301 {302 ValidateCurrentFormattingState(FormattingState.InsideGroup, o);303 _currentFormattingState = FormattingState.Formatting;304 305 return false;306 }307 308 // this is a raw object309 return true;310 }311 312 private void ValidateCurrentFormattingState(FormattingState expectedFormattingState, object obj)313 {314 // check if we are in the expected formatting state315 if (_currentFormattingState != expectedFormattingState)316 {317 // we are not in the expected state, some message is out of sequence,318 // need to abort the command319 320 string violatingCommand = "format-*";321 if (obj is StartData sdObj)322 {323 if (sdObj.shapeInfo is WideViewHeaderInfo)324 {325 violatingCommand = "format-wide";326 }327 else if (sdObj.shapeInfo is TableHeaderInfo)328 {329 violatingCommand = "format-table";330 }331 else if (sdObj.shapeInfo is ListViewHeaderInfo)332 {333 violatingCommand = "format-list";334 }335 else if (sdObj.shapeInfo is ComplexViewHeaderInfo)336 {337 violatingCommand = "format-complex";338 }339 }340 341 string msg = StringUtil.Format(FormatAndOut_out_xxx.OutLineOutput_OutOfSequencePacket,342 obj.GetType().FullName, violatingCommand);343 344 ErrorRecord errorRecord = new ErrorRecord(345 new InvalidOperationException(),346 "ConsoleLineOutputOutOfSequencePacket",347 ErrorCategory.InvalidData,348 null);349 350 errorRecord.ErrorDetails = new ErrorDetails(msg);351 this.TerminatingErrorContext.ThrowTerminatingError(errorRecord);352 }353 }354 355 /// <summary>356 /// Shunt object to the formatting pipeline for preprocessing.357 /// </summary>358 /// <param name="o">Object to be preprocessed.</param>359 /// <returns>Array of objects returned by the preprocessing step.</returns>360 private Array ApplyFormatting(object o)361 {362 if (_command == null)363 {364 _command = new CommandWrapper();365 _command.Initialize(this.OuterCmdlet().Context, "format-default", typeof(FormatDefaultCommand));366 }367 368 return _command.Process(o);369 }370 371 /// <summary>372 /// Class factory for output context.373 /// </summary>374 /// <param name="parentContext">Parent context in the stack.</param>375 /// <param name="formatInfoData">Fromat info data received from the pipeline.</param>376 /// <returns></returns>377 private FormatMessagesContextManager.OutputContext CreateOutputContext(378 FormatMessagesContextManager.OutputContext parentContext,379 FormatInfoData formatInfoData)380 {381 // initialize the format context382 if (formatInfoData is FormatStartData formatStartData)383 {384 FormatOutputContext foc = new FormatOutputContext(parentContext, formatStartData);385 386 return foc;387 }388 389 // we are starting a group, initialize the group context390 if (formatInfoData is GroupStartData gsd)391 {392 GroupOutputContext goc = null;393 394 switch (ActiveFormattingShape)395 {396 case FormatShape.Table:397 {398 goc = new TableOutputContext(this, parentContext, gsd);399 break;400 }401 402 case FormatShape.List:403 {404 goc = new ListOutputContext(this, parentContext, gsd);405 break;406 }407 408 case FormatShape.Wide:409 {410 goc = new WideOutputContext(this, parentContext, gsd);411 break;412 }413 414 case FormatShape.Complex:415 {416 goc = new ComplexOutputContext(this, parentContext, gsd);417 break;418 }419 420 default:421 {422 Diagnostics.Assert(false, "Invalid shape. This should never happen");423 }424 425 break;426 }427 428 goc.Initialize();429 return goc;430 }431 432 return null;433 }434 435 /// <summary>436 /// Callback for Fs processing.437 /// </summary>438 /// <param name="c">The context containing the Fs entry.</param>439 private void ProcessFormatStart(FormatMessagesContextManager.OutputContext c)440 {441 // we just add an empty line to the display442 this.LineOutput.WriteLine(string.Empty);443 }444 445 /// <summary>446 /// Callback for Fe processing.447 /// </summary>448 /// <param name="fe">Fe notification message.</param>449 /// <param name="c">Current context, with Fs in it.</param>450 private void ProcessFormatEnd(FormatEndData fe, FormatMessagesContextManager.OutputContext c)451 {452 if (c is FormatOutputContext foContext453 && foContext.Data.shapeInfo is ListViewHeaderInfo)454 {455 // Skip writing out a new line for List view, because we already wrote out456 // an extra new line after displaying the last list entry.457 return;458 }459 460 // We just add an empty line to the display.461 LineOutput.WriteLine(string.Empty);462 }463 464 /// <summary>465 /// Callback for Gs processing.466 /// </summary>467 /// <param name="c">The context containing the Gs entry.</param>468 private void ProcessGroupStart(FormatMessagesContextManager.OutputContext c)469 {470 // Console.WriteLine("ProcessGroupStart");471 GroupOutputContext goc = (GroupOutputContext)c;472 473 if (goc.Data.groupingEntry != null)474 {475 ComplexWriter writer = new ComplexWriter();476 writer.Initialize(_lo, _lo.ColumnNumber);477 writer.WriteObject(goc.Data.groupingEntry.formatValueList);478 _lo.WriteLine(string.Empty);479 }480 481 goc.GroupStart();482 }483 484 /// <summary>485 /// Callback for Ge processing.486 /// </summary>487 /// <param name="ge">Ge notification message.</param>488 /// <param name="c">Current context, with Gs in it.</param>489 private void ProcessGroupEnd(GroupEndData ge, FormatMessagesContextManager.OutputContext c)490 {491 // Console.WriteLine("ProcessGroupEnd");492 GroupOutputContext goc = (GroupOutputContext)c;493 494 goc.GroupEnd();495 }496 497 /// <summary>498 /// Process the current payload object.499 /// </summary>500 /// <param name="fed">FormatEntryData to process.</param>501 /// <param name="c">Currently active context.</param>502 private void ProcessPayload(FormatEntryData fed, FormatMessagesContextManager.OutputContext c)503 {504 // we assume FormatEntryData as a standard wrapper505 if (fed == null)506 {507 PSTraceSource.NewArgumentNullException(nameof(fed));508 }509 510 if (fed.formatEntryInfo == null)511 {512 PSTraceSource.NewArgumentNullException("fed.formatEntryInfo");513 }514 515 WriteStreamType oldWSState = _lo.WriteStream;516 try517 {518 _lo.WriteStream = fed.writeStream;519 520 if (c == null)521 {522 ProcessOutOfBandPayload(fed);523 }524 else525 {526 GroupOutputContext goc = (GroupOutputContext)c;527 528 goc.ProcessPayload(fed);529 }530 }531 finally532 {533 _lo.WriteStream = oldWSState;534 }535 }536 537 private void ProcessOutOfBandPayload(FormatEntryData fed)538 {539 // try if it is raw text540 if (fed.formatEntryInfo is RawTextFormatEntry rte)541 {542 if (fed.isHelpObject)543 {544 ComplexWriter complexWriter = new ComplexWriter();545 546 complexWriter.Initialize(_lo, _lo.ColumnNumber);547 complexWriter.WriteString(rte.text);548 }549 else550 {551 // Write out raw text without any changes to it.552 _lo.WriteRawText(rte.text);553 }554 555 return;556 }557 558 // try if it is a complex entry559 if (fed.formatEntryInfo is ComplexViewEntry cve && cve.formatValueList != null)560 {561 ComplexWriter complexWriter = new ComplexWriter();562 563 complexWriter.Initialize(_lo, int.MaxValue);564 complexWriter.WriteObject(cve.formatValueList);565 566 return;567 }568 // try if it is a list view569 if (fed.formatEntryInfo is ListViewEntry lve && lve.listViewFieldList != null)570 {571 ListWriter listWriter = new ListWriter();572 573 _lo.WriteLine(string.Empty);574 575 string[] properties = ListOutputContext.GetProperties(lve);576 listWriter.Initialize(properties, _lo.ColumnNumber, _lo.DisplayCells);577 string[] values = ListOutputContext.GetValues(lve);578 listWriter.WriteProperties(values, _lo);579 580 _lo.WriteLine(string.Empty);581 582 return;583 }584 }585 586 /// <summary>587 /// The screen host associated with this outputter.588 /// </summary>589 private LineOutput _lo = null;590 591 internal LineOutput LineOutput592 {593 get { return _lo; }594 595 set { _lo = value; }596 }597 598 private ShapeInfo ShapeInfoOnFormatContext599 {600 get601 {602 FormatOutputContext foc = this.FormatContext;603 604 if (foc == null)605 return null;606 607 return foc.Data.shapeInfo;608 }609 }610 611 /// <summary>612 /// Retrieve the active FormatOutputContext on the stack613 /// by walking up to the top of the stack.614 /// </summary>615 private FormatOutputContext FormatContext616 {617 get618 {619 for (FormatMessagesContextManager.OutputContext oc = _ctxManager.ActiveOutputContext; oc != null; oc = oc.ParentContext)620 {621 if (oc is FormatOutputContext foc)622 return foc;623 }624 625 return null;626 }627 }628 629 /// <summary>630 /// Context manager instance to guide the message traversal.631 /// </summary>632 private readonly FormatMessagesContextManager _ctxManager = new FormatMessagesContextManager();633 634 private FormattedObjectsCache _cache = null;635 636 /// <summary>637 /// Handler for processing the caching notification and responsible for638 /// setting the value of the formatting hint.639 /// </summary>640 /// <param name="formatStartData"></param>641 /// <param name="objects"></param>642 private void ProcessCachedGroup(FormatStartData formatStartData, List<PacketInfoData> objects)643 {644 _formattingHint = null;645 646 if (formatStartData.shapeInfo is TableHeaderInfo thi)647 {648 ProcessCachedGroupOnTable(thi, objects);649 return;650 }651 652 if (formatStartData.shapeInfo is WideViewHeaderInfo wvhi)653 {654 ProcessCachedGroupOnWide(wvhi, objects);655 return;656 }657 }658 659 private void ProcessCachedGroupOnTable(TableHeaderInfo thi, List<PacketInfoData> objects)660 {661 if (thi.tableColumnInfoList.Count == 0)662 return;663 664 int[] widths = new int[thi.tableColumnInfoList.Count];665 666 for (int k = 0; k < thi.tableColumnInfoList.Count; k++)667 {668 string label = thi.tableColumnInfoList[k].label;669 670 if (string.IsNullOrEmpty(label))671 label = thi.tableColumnInfoList[k].propertyName;672 673 if (string.IsNullOrEmpty(label))674 widths[k] = 0;675 else676 widths[k] = _lo.DisplayCells.Length(label);677 }678 679 int cellCount; // scratch variable680 foreach (PacketInfoData o in objects)681 {682 if (o is FormatEntryData fed)683 {684 TableRowEntry tre = fed.formatEntryInfo as TableRowEntry;685 int kk = 0;686 687 foreach (FormatPropertyField fpf in tre.formatPropertyFieldList)688 {689 cellCount = _lo.DisplayCells.Length(fpf.propertyValue);690 if (widths[kk] < cellCount)691 widths[kk] = cellCount;692 693 kk++;694 }695 }696 }697 698 TableFormattingHint hint = new TableFormattingHint();699 700 hint.columnWidths = widths;701 _formattingHint = hint;702 }703 704 private void ProcessCachedGroupOnWide(WideViewHeaderInfo wvhi, List<PacketInfoData> objects)705 {706 if (wvhi.columns != 0)707 {708 // columns forced on the client709 return;710 }711 712 int maxLen = 0;713 int cellCount; // scratch variable714 715 foreach (PacketInfoData o in objects)716 {717 if (o is FormatEntryData fed)718 {719 WideViewEntry wve = fed.formatEntryInfo as WideViewEntry;720 FormatPropertyField fpf = wve.formatPropertyField as FormatPropertyField;721 722 if (!string.IsNullOrEmpty(fpf.propertyValue))723 {724 cellCount = _lo.DisplayCells.Length(fpf.propertyValue);725 if (cellCount > maxLen)726 maxLen = cellCount;727 }728 }729 }730 731 WideFormattingHint hint = new WideFormattingHint();732 733 hint.maxWidth = maxLen;734 _formattingHint = hint;735 }736 737 /// <summary>738 /// Tables and Wides need to use spaces for padding to maintain table look even if console window is resized.739 /// For all other output, we use int.MaxValue if the user didn't explicitly specify a width.740 /// If we detect that int.MaxValue is used, first we try to get the current console window width.741 /// However, if we can't read that (for example, implicit remoting has no console window), we default742 /// to something reasonable: 120 columns.743 /// </summary>744 private static int GetConsoleWindowWidth(int columnNumber)745 {746 if (InternalTestHooks.SetConsoleWidthToZero)747 {748 return DefaultConsoleWidth;749 }750 751 if (columnNumber == int.MaxValue)752 {753 try754 {755 // if Console width is set to 0, the default width is returned so that the output string is not null.756 // This can happen in environments where TERM is not set.757 return (Console.WindowWidth != 0) ? Console.WindowWidth : DefaultConsoleWidth;758 }759 catch760 {761 return DefaultConsoleWidth;762 }763 }764 765 return columnNumber;766 }767 768 /// <summary>769 /// Return the console height.null If not available (like when remoting), treat as Int.MaxValue.770 /// </summary>771 private static int GetConsoleWindowHeight(int rowNumber)772 {773 if (InternalTestHooks.SetConsoleHeightToZero)774 {775 return DefaultConsoleHeight;776 }777 778 if (rowNumber <= 0)779 {780 try781 {782 // if Console height is set to 0, the default height is returned.783 // This can happen in environments where TERM is not set.784 return (Console.WindowHeight > 0) ? Console.WindowHeight : DefaultConsoleHeight;785 }786 catch787 {788 return DefaultConsoleHeight;789 }790 }791 792 return rowNumber;793 }794 795 /// <summary>796 /// Base class for all the formatting hints.797 /// </summary>798 private abstract class FormattingHint799 {800 }801 802 /// <summary>803 /// Hint for format-table.804 /// </summary>805 private sealed class TableFormattingHint : FormattingHint806 {807 internal int[] columnWidths = null;808 }809 810 /// <summary>811 /// Hint for format-wide.812 /// </summary>813 private sealed class WideFormattingHint : FormattingHint814 {815 internal int maxWidth = 0;816 }817 818 /// <summary>819 /// Variable holding the autosize hint (set by the caching code and reset by the hint consumer.820 /// </summary>821 private FormattingHint _formattingHint = null;822 823 /// <summary>824 /// Helper for consuming the formatting hint.825 /// </summary>826 /// <returns></returns>827 private FormattingHint RetrieveFormattingHint()828 {829 FormattingHint fh = _formattingHint;830 _formattingHint = null;831 return fh;832 }833 834 private FormatObjectDeserializer _formatObjectDeserializer;835 836 /// <summary>837 /// Context for the outer scope of the format sequence.838 /// </summary>839 private sealed class FormatOutputContext : FormatMessagesContextManager.OutputContext840 {841 /// <summary>842 /// Construct a context to push on the stack.843 /// </summary>844 /// <param name="parentContext">Parent context in the stack.</param>845 /// <param name="formatData">Format data to put in the context.</param>846 internal FormatOutputContext(FormatMessagesContextManager.OutputContext parentContext, FormatStartData formatData)847 : base(parentContext)848 {849 Data = formatData;850 }851 852 /// <summary>853 /// Retrieve the format data in the context.854 /// </summary>855 internal FormatStartData Data { get; } = null;856 }857 858 /// <summary>859 /// Context for the currently active group.860 /// </summary>861 private abstract class GroupOutputContext : FormatMessagesContextManager.OutputContext862 {863 /// <summary>864 /// Construct a context to push on the stack.865 /// </summary>866 internal GroupOutputContext(OutCommandInner cmd,867 FormatMessagesContextManager.OutputContext parentContext,868 GroupStartData formatData)869 : base(parentContext)870 {871 InnerCommand = cmd;872 Data = formatData;873 }874 875 /// <summary>876 /// Called at creation time, overrides will initialize here, e.g.877 /// column widths, etc.878 /// </summary>879 internal virtual void Initialize() { }880 881 /// <summary>882 /// Called when a group of data is started, overridden will do883 /// things such as headers, etc...884 /// </summary>885 internal virtual void GroupStart() { }886 887 /// <summary>888 /// Called when the end of a group is reached, overrides will do889 /// things such as group footers.890 /// </summary>891 internal virtual void GroupEnd() { }892 893 /// <summary>894 /// Called when there is an entry to process, overrides will do895 /// things such as writing a row in a table.896 /// </summary>897 /// <param name="fed">FormatEntryData to process.</param>898 internal virtual void ProcessPayload(FormatEntryData fed) { }899 900 /// <summary>901 /// Retrieve the format data in the context.902 /// </summary>903 internal GroupStartData Data { get; } = null;904 905 protected OutCommandInner InnerCommand { get; }906 }907 908 private class TableOutputContextBase : GroupOutputContext909 {910 /// <summary>911 /// Construct a context to push on the stack.912 /// </summary>913 /// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>914 /// <param name="parentContext">Parent context in the stack.</param>915 /// <param name="formatData">Format data to put in the context.</param>916 internal TableOutputContextBase(OutCommandInner cmd,917 FormatMessagesContextManager.OutputContext parentContext,918 GroupStartData formatData)919 : base(cmd, parentContext, formatData)920 {921 }922 923 /// <summary>924 /// Get the table writer for this context.925 /// </summary>926 protected TableWriter Writer { get { return _tableWriter; } }927 928 /// <summary>929 /// Helper class to properly write a table using text output.930 /// </summary>931 private readonly TableWriter _tableWriter = new TableWriter();932 }933 934 private sealed class TableOutputContext : TableOutputContextBase935 {936 private int _rowCount = 0;937 private int _consoleHeight = -1;938 private int _consoleWidth = -1;939 940 private const int WhitespaceAndPagerLineCount = 2;941 942 private readonly bool _repeatHeader = false;943 944 /// <summary>945 /// Construct a context to push on the stack.946 /// </summary>947 /// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>948 /// <param name="parentContext">Parent context in the stack.</param>949 /// <param name="formatData">Format data to put in the context.</param>950 internal TableOutputContext(OutCommandInner cmd,951 FormatMessagesContextManager.OutputContext parentContext,952 GroupStartData formatData)953 : base(cmd, parentContext, formatData)954 {955 if (parentContext is FormatOutputContext foc)956 {957 if (foc.Data.shapeInfo is TableHeaderInfo thi)958 {959 _repeatHeader = thi.repeatHeader;960 }961 }962 }963 964 /// <summary>965 /// Initialize column widths.966 /// </summary>967 internal override void Initialize()968 {969 int[] columnWidthsHint = null;970 // We expect that console width is less than 120.971 972 if (this.InnerCommand.RetrieveFormattingHint() is TableFormattingHint tableHint)973 {974 columnWidthsHint = tableHint.columnWidths;975 }976 977 _consoleHeight = GetConsoleWindowHeight(this.InnerCommand._lo.RowNumber);978 _consoleWidth = GetConsoleWindowWidth(this.InnerCommand._lo.ColumnNumber);979 980 int columns = this.CurrentTableHeaderInfo.tableColumnInfoList.Count;981 if (columns == 0)982 {983 return;984 }985 986 // create arrays for widths and alignment987 Span<int> columnWidths = columns <= StackAllocThreshold ? stackalloc int[columns] : new int[columns];988 Span<int> alignment = columns <= StackAllocThreshold ? stackalloc int[columns] : new int[columns];989 Span<bool> headerMatchesProperty = columns <= StackAllocThreshold ? stackalloc bool[columns] : new bool[columns];990 991 int k = 0;992 foreach (TableColumnInfo tci in this.CurrentTableHeaderInfo.tableColumnInfoList)993 {994 columnWidths[k] = (columnWidthsHint != null) ? columnWidthsHint[k] : tci.width;995 alignment[k] = tci.alignment;996 headerMatchesProperty[k] = tci.HeaderMatchesProperty;997 k++;998 }999 1000 this.Writer.Initialize(0, _consoleWidth, columnWidths, alignment, headerMatchesProperty, this.CurrentTableHeaderInfo.hideHeader);1001 }1002 1003 /// <summary>1004 /// Write the headers.1005 /// </summary>1006 internal override void GroupStart()1007 {1008 int columns = this.CurrentTableHeaderInfo.tableColumnInfoList.Count;1009 1010 if (columns == 0)1011 {1012 return;1013 }1014 1015 string[] properties = new string[columns];1016 int k = 0;1017 foreach (TableColumnInfo tci in this.CurrentTableHeaderInfo.tableColumnInfoList)1018 {1019 properties[k++] = tci.label ?? tci.propertyName;1020 }1021 1022 _rowCount += this.Writer.GenerateHeader(properties, this.InnerCommand._lo);1023 }1024 1025 /// <summary>1026 /// Write a row into the table.1027 /// </summary>1028 /// <param name="fed">FormatEntryData to process.</param>1029 internal override void ProcessPayload(FormatEntryData fed)1030 {1031 int headerColumns = this.CurrentTableHeaderInfo.tableColumnInfoList.Count;1032 1033 if (headerColumns == 0)1034 {1035 return;1036 }1037 1038 if (_repeatHeader && _rowCount >= _consoleHeight - WhitespaceAndPagerLineCount)1039 {1040 this.InnerCommand._lo.WriteLine(string.Empty);1041 _rowCount = this.Writer.GenerateHeader(null, this.InnerCommand._lo);1042 }1043 1044 TableRowEntry tre = fed.formatEntryInfo as TableRowEntry;1045 1046 // need to make sure we have matching counts: the header count will have to prevail1047 string[] values = new string[headerColumns];1048 Span<int> alignment = headerColumns <= StackAllocThreshold ? stackalloc int[headerColumns] : new int[headerColumns];1049 1050 int fieldCount = tre.formatPropertyFieldList.Count;1051 1052 for (int k = 0; k < headerColumns; k++)1053 {1054 if (k < fieldCount)1055 {1056 values[k] = tre.formatPropertyFieldList[k].propertyValue;1057 alignment[k] = tre.formatPropertyFieldList[k].alignment;1058 }1059 else1060 {1061 values[k] = string.Empty;1062 alignment[k] = TextAlignment.Left; // hard coded default1063 }1064 }1065 1066 this.Writer.GenerateRow(values, this.InnerCommand._lo, tre.multiLine, alignment, InnerCommand._lo.DisplayCells, generatedRows: null);1067 _rowCount++;1068 }1069 1070 private TableHeaderInfo CurrentTableHeaderInfo1071 {1072 get1073 {1074 return (TableHeaderInfo)this.InnerCommand.ShapeInfoOnFormatContext;1075 }1076 }1077 }1078 1079 private sealed class ListOutputContext : GroupOutputContext1080 {1081 /// <summary>1082 /// Construct a context to push on the stack.1083 /// </summary>1084 /// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>1085 /// <param name="parentContext">Parent context in the stack.</param>1086 /// <param name="formatData">Format data to put in the context.</param>1087 internal ListOutputContext(OutCommandInner cmd,1088 FormatMessagesContextManager.OutputContext parentContext,1089 GroupStartData formatData)1090 : base(cmd, parentContext, formatData)1091 {1092 }1093 1094 /// <summary>1095 /// Initialize column widths.1096 /// </summary>1097 internal override void Initialize()1098 {1099 }1100 1101 private void InternalInitialize(ListViewEntry lve)1102 {1103 _properties = GetProperties(lve);1104 _listWriter.Initialize(_properties, this.InnerCommand._lo.ColumnNumber, InnerCommand._lo.DisplayCells);1105 }1106 1107 internal static string[] GetProperties(ListViewEntry lve)1108 {1109 int count = lve.listViewFieldList.Count;1110 1111 if (count == 0)1112 {1113 return null;1114 }1115 1116 string[] result = new string[count];1117 for (int index = 0; index < result.Length; ++index)1118 {1119 ListViewField lvf = lve.listViewFieldList[index];1120 result[index] = lvf.label ?? lvf.propertyName;1121 }1122 1123 return result;1124 }1125 1126 internal static string[] GetValues(ListViewEntry lve)1127 {1128 int count = lve.listViewFieldList.Count;1129 1130 if (count == 0)1131 {1132 return null;1133 }1134 1135 string[] result = new string[count];1136 for (int index = 0; index < result.Length; ++index)1137 {1138 ListViewField lvf = lve.listViewFieldList[index];1139 result[index] = lvf.formatPropertyField.propertyValue;1140 }1141 1142 return result;1143 }1144 1145 /// <summary>1146 /// Write the headers.1147 /// </summary>1148 internal override void GroupStart()1149 {1150 }1151 1152 /// <summary>1153 /// Write a row into the list.1154 /// </summary>1155 /// <param name="fed">FormatEntryData to process.</param>1156 internal override void ProcessPayload(FormatEntryData fed)1157 {1158 ListViewEntry lve = fed.formatEntryInfo as ListViewEntry;1159 InternalInitialize(lve);1160 string[] values = GetValues(lve);1161 _listWriter.WriteProperties(values, this.InnerCommand._lo);1162 this.InnerCommand._lo.WriteLine(string.Empty);1163 }1164 1165 /// <summary>1166 /// Property list currently active.1167 /// </summary>1168 private string[] _properties = null;1169 1170 /// <summary>1171 /// Writer to do the actual formatting.1172 /// </summary>1173 private readonly ListWriter _listWriter = new ListWriter();1174 }1175 1176 private sealed class WideOutputContext : TableOutputContextBase1177 {1178 /// <summary>1179 /// Construct a context to push on the stack.1180 /// </summary>1181 /// <param name="cmd">Reference to the OutCommandInner instance who owns this instance.</param>1182 /// <param name="parentContext">Parent context in the stack.</param>1183 /// <param name="formatData">Format data to put in the context.</param>1184 internal WideOutputContext(OutCommandInner cmd,1185 FormatMessagesContextManager.OutputContext parentContext,1186 GroupStartData formatData)1187 : base(cmd, parentContext, formatData)1188 {1189 }1190 1191 private StringValuesBuffer _buffer = null;1192 1193 /// <summary>1194 /// Initialize column widths.1195 /// </summary>1196 internal override void Initialize()1197 {1198 // set the hard wider default, to be used if no other info is available1199 int itemsPerRow = 2;1200 