diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/group-object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/group-object.cs
index 971ce1300e..3b9a558663 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/group-object.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/group-object.cs
@@ -6,8 +6,10 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Text;
@@ -22,28 +24,27 @@ namespace Microsoft.PowerShell.Commands
///
/// ArrayToTuple is a helper method used to create a tuple for the supplied input array.
///
+ /// The first generic type parameter.
/// Input objects used to create a tuple.
/// Tuple object.
- internal static object ArrayToTuple(object[] inputObjects)
+ internal static object ArrayToTuple(IList inputObjects)
{
- Diagnostics.Assert(inputObjects != null, "inputObjects is null");
- Diagnostics.Assert(inputObjects.Length > 0, "inputObjects is empty");
-
return ArrayToTuple(inputObjects, 0);
}
///
/// ArrayToTuple is a helper method used to create a tuple for the supplied input array.
///
- /// Input objects used to create a tuple
+ /// The first generic type parameter.
+ /// Input objects used to create a tuple.
/// Start index of the array from which the objects have to considered for the tuple creation.
/// Tuple object.
- internal static object ArrayToTuple(object[] inputObjects, int startIndex)
+ private static object ArrayToTuple(IList inputObjects, int startIndex)
{
Diagnostics.Assert(inputObjects != null, "inputObjects is null");
- Diagnostics.Assert(inputObjects.Length > 0, "inputObjects is empty");
+ Diagnostics.Assert(inputObjects.Count > 0, "inputObjects is empty");
- switch (inputObjects.Length - startIndex)
+ switch (inputObjects.Count - startIndex)
{
case 0:
return null;
@@ -56,19 +57,49 @@ namespace Microsoft.PowerShell.Commands
case 4:
return Tuple.Create(inputObjects[startIndex], inputObjects[startIndex + 1], inputObjects[startIndex + 2], inputObjects[startIndex + 3]);
case 5:
- return Tuple.Create(inputObjects[startIndex], inputObjects[startIndex + 1], inputObjects[startIndex + 2], inputObjects[startIndex + 3], inputObjects[startIndex + 4]);
+ return Tuple.Create(
+ inputObjects[startIndex],
+ inputObjects[startIndex + 1],
+ inputObjects[startIndex + 2],
+ inputObjects[startIndex + 3],
+ inputObjects[startIndex + 4]);
case 6:
- return Tuple.Create(inputObjects[startIndex], inputObjects[startIndex + 1], inputObjects[startIndex + 2], inputObjects[startIndex + 3], inputObjects[startIndex + 4],
+ return Tuple.Create(
+ inputObjects[startIndex],
+ inputObjects[startIndex + 1],
+ inputObjects[startIndex + 2],
+ inputObjects[startIndex + 3],
+ inputObjects[startIndex + 4],
inputObjects[startIndex + 5]);
case 7:
- return Tuple.Create(inputObjects[startIndex], inputObjects[startIndex + 1], inputObjects[startIndex + 2], inputObjects[startIndex + 3], inputObjects[startIndex + 4],
- inputObjects[startIndex + 5], inputObjects[startIndex + 6]);
+ return Tuple.Create(
+ inputObjects[startIndex],
+ inputObjects[startIndex + 1],
+ inputObjects[startIndex + 2],
+ inputObjects[startIndex + 3],
+ inputObjects[startIndex + 4],
+ inputObjects[startIndex + 5],
+ inputObjects[startIndex + 6]);
case 8:
- return Tuple.Create(inputObjects[startIndex], inputObjects[startIndex + 1], inputObjects[startIndex + 2], inputObjects[startIndex + 3], inputObjects[startIndex + 4],
- inputObjects[startIndex + 5], inputObjects[startIndex + 6], inputObjects[startIndex + 7]);
+ return Tuple.Create(
+ inputObjects[startIndex],
+ inputObjects[startIndex + 1],
+ inputObjects[startIndex + 2],
+ inputObjects[startIndex + 3],
+ inputObjects[startIndex + 4],
+ inputObjects[startIndex + 5],
+ inputObjects[startIndex + 6],
+ inputObjects[startIndex + 7]);
default:
- return Tuple.Create(inputObjects[startIndex], inputObjects[startIndex + 1], inputObjects[startIndex + 2], inputObjects[startIndex + 3], inputObjects[startIndex + 4],
- inputObjects[startIndex + 5], inputObjects[startIndex + 6], ArrayToTuple(inputObjects, startIndex + 7));
+ return Tuple.Create(
+ inputObjects[startIndex],
+ inputObjects[startIndex + 1],
+ inputObjects[startIndex + 2],
+ inputObjects[startIndex + 3],
+ inputObjects[startIndex + 4],
+ inputObjects[startIndex + 5],
+ inputObjects[startIndex + 6],
+ ArrayToTuple(inputObjects, startIndex + 7));
}
}
}
@@ -78,8 +109,7 @@ namespace Microsoft.PowerShell.Commands
///
public sealed class GroupInfoNoElement : GroupInfo
{
- internal GroupInfoNoElement(OrderByPropertyEntry groupValue)
- : base(groupValue)
+ internal GroupInfoNoElement(OrderByPropertyEntry groupValue) : base(groupValue)
{
}
@@ -92,6 +122,7 @@ namespace Microsoft.PowerShell.Commands
///
/// Emitted by Group-Object.
///
+ [DebuggerDisplay("{Name} ({Count})")]
public class GroupInfo
{
internal GroupInfo(OrderByPropertyEntry groupValue)
@@ -113,17 +144,17 @@ namespace Microsoft.PowerShell.Commands
StringBuilder sb = new StringBuilder();
foreach (ObjectCommandPropertyValue propValue in propValues)
{
- if (propValue != null && propValue.PropertyValue != null)
+ var propValuePropertyValue = propValue?.PropertyValue;
+ if (propValuePropertyValue != null)
{
- var propertyValueItems = propValue.PropertyValue as ICollection;
- if (propertyValueItems != null)
+ if (propValuePropertyValue is ICollection propertyValueItems)
{
sb.Append("{");
var length = sb.Length;
foreach (object item in propertyValueItems)
{
- sb.Append(string.Format(CultureInfo.InvariantCulture, "{0}, ", item.ToString()));
+ sb.AppendFormat(CultureInfo.InvariantCulture, "{0}, ", item.ToString());
}
sb = sb.Length > length ? sb.Remove(sb.Length - 2, 2) : sb;
@@ -131,15 +162,16 @@ namespace Microsoft.PowerShell.Commands
}
else
{
- sb.Append(string.Format(CultureInfo.InvariantCulture, "{0}, ", propValue.PropertyValue.ToString()));
+ sb.AppendFormat(CultureInfo.InvariantCulture, "{0}, ", propValuePropertyValue.ToString());
}
}
}
+
return sb.Length >= 2 ? sb.Remove(sb.Length - 2, 2).ToString() : string.Empty;
}
///
- /// Values of the group.
+ /// Gets the values of the group.
///
public ArrayList Values
{
@@ -150,29 +182,30 @@ namespace Microsoft.PowerShell.Commands
{
values.Add(propValue.PropertyValue);
}
+
return values;
}
}
///
- /// Number of objects in the group.
+ /// Gets the number of objects in the group.
///
public int Count { get; internal set; }
///
- /// The list of objects in this group.
+ /// Gets the list of objects in this group.
///
- public Collection Group { get; } = null;
+ public Collection Group { get; }
///
- /// The name of the group.
+ /// Gets the name of the group.
///
- public string Name { get; } = null;
+ public string Name { get; }
///
- /// The OrderByPropertyEntry used to build this group object.
+ /// Gets the OrderByPropertyEntry used to build this group object.
///
- internal OrderByPropertyEntry GroupValue { get; } = null;
+ internal OrderByPropertyEntry GroupValue { get; }
}
///
@@ -187,30 +220,22 @@ namespace Microsoft.PowerShell.Commands
///
/// An instance of the PSTraceSource class used for trace output.
///
- [TraceSourceAttribute(
- "GroupObjectCommand",
- "Class that has group base implementation")]
- private static PSTraceSource s_tracer =
- PSTraceSource.GetTracer("GroupObjectCommand",
- "Class that has group base implementation");
+ [TraceSource("GroupObjectCommand", "Class that has group base implementation")]
+ private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer("GroupObjectCommand", "Class that has group base implementation");
#endregion tracer
#region Command Line Switches
///
- /// Flatten the groups.
+ /// Gets or sets the NoElement parameter indicating of the groups should be flattened.
///
///
[Parameter]
- public SwitchParameter NoElement
- {
- get { return _noElement; }
- set { _noElement = value; }
- }
- private bool _noElement;
+ public SwitchParameter NoElement { get; set; }
+
///
- /// the AsHashTable parameter.
+ /// Gets or sets the AsHashTable parameter.
///
///
[Parameter(ParameterSetName = "HashTable")]
@@ -219,16 +244,19 @@ namespace Microsoft.PowerShell.Commands
public SwitchParameter AsHashTable { get; set; }
///
+ /// Gets or sets the AsString parameter.
///
- ///
[Parameter(ParameterSetName = "HashTable")]
public SwitchParameter AsString { get; set; }
- private List _groups = new List();
- private OrderByProperty _orderByProperty = new OrderByProperty();
+ private readonly List _groups = new List();
+ private readonly OrderByProperty _orderByProperty = new OrderByProperty();
+ private readonly Dictionary
/// Input object that needs to be grouped.
- /// true if we are not accumulating objects
+ /// True if we are not accumulating objects.
/// List containing Groups.
/// Dictionary used to keep track of the groups with hash of the property values being the key.
/// The Comparer to be used while comparing to check if new group has to be created.
- internal static void DoGrouping(OrderByPropertyEntry currentObjectEntry, bool noElement, List groups, Dictionary groupInfoDictionary,
+ private static void DoGrouping(
+ OrderByPropertyEntry currentObjectEntry,
+ bool noElement,
+ List groups,
+ Dictionary groupInfoDictionary,
OrderByPropertyComparer orderByPropertyComparer)
{
- if (currentObjectEntry != null && currentObjectEntry.orderValues != null && currentObjectEntry.orderValues.Count > 0)
+ var currentObjectOrderValues = currentObjectEntry.orderValues;
+ if (currentObjectOrderValues != null && currentObjectOrderValues.Count > 0)
{
- object currentTupleObject = PSTuple.ArrayToTuple(currentObjectEntry.orderValues.ToArray());
+ object currentTupleObject = PSTuple.ArrayToTuple(currentObjectOrderValues);
- GroupInfo currentGroupInfo = null;
- if (groupInfoDictionary.TryGetValue(currentTupleObject, out currentGroupInfo))
+ if (groupInfoDictionary.TryGetValue(currentTupleObject, out var currentGroupInfo))
{
- if (currentGroupInfo != null)
- {
- //add this inputObject to an existing group
- currentGroupInfo.Add(currentObjectEntry.inputObject);
- }
+ // add this inputObject to an existing group
+ currentGroupInfo.Add(currentObjectEntry.inputObject);
}
else
{
@@ -277,7 +306,7 @@ namespace Microsoft.PowerShell.Commands
if (!isCurrentItemGrouped)
{
// create a new group
- s_tracer.WriteLine("Create a new group: {0}", currentObjectEntry.orderValues);
+ s_tracer.WriteLine("Create a new group: {0}", currentObjectOrderValues);
GroupInfo newObjGrp = noElement ? new GroupInfoNoElement(currentObjectEntry) : new GroupInfo(currentObjectEntry);
groups.Add(newObjGrp);
@@ -286,12 +315,71 @@ namespace Microsoft.PowerShell.Commands
}
}
}
- private void WriteNonTerminatingError(Exception exception, string resourceIdAndErrorId,
- ErrorCategory category)
+
+ ///
+ /// Utility function called by Group-Object to create Groups.
+ ///
+ /// Input object that needs to be grouped.
+ /// True if we are not accumulating objects.
+ /// List containing Groups.
+ /// Dictionary used to keep track of the groups with hash of the property values being the key.
+ /// The Comparer to be used while comparing to check if new group has to be created.
+ private static void DoOrderedGrouping(
+ OrderByPropertyEntry currentObjectEntry,
+ bool noElement,
+ List groups,
+ Dictionary groupInfoDictionary,
+ OrderByPropertyComparer orderByPropertyComparer)
+ {
+ var currentObjectOrderValues = currentObjectEntry.orderValues;
+ if (currentObjectOrderValues != null && currentObjectOrderValues.Count > 0)
+ {
+ object currentTupleObject = PSTuple.ArrayToTuple(currentObjectOrderValues);
+
+ if (groupInfoDictionary.TryGetValue(currentTupleObject, out var currentGroupInfo))
+ {
+ // add this inputObject to an existing group
+ currentGroupInfo.Add(currentObjectEntry.inputObject);
+ }
+ else
+ {
+ bool isCurrentItemGrouped = false;
+
+ if (groups.Count > 0)
+ {
+ var lastGroup = groups[groups.Count - 1];
+
+ // Check if the current input object can be converted to one of the already known types
+ // by looking up in the type to GroupInfo mapping.
+ if (orderByPropertyComparer.Compare(lastGroup.GroupValue, currentObjectEntry) == 0)
+ {
+ lastGroup.Add(currentObjectEntry.inputObject);
+ isCurrentItemGrouped = true;
+ }
+ }
+
+ if (!isCurrentItemGrouped)
+ {
+ // create a new group
+ s_tracer.WriteLine("Create a new group: {0}", currentObjectOrderValues);
+ GroupInfo newObjGrp = noElement
+ ? new GroupInfoNoElement(currentObjectEntry)
+ : new GroupInfo(currentObjectEntry);
+
+ groups.Add(newObjGrp);
+
+ groupInfoDictionary.Add(currentTupleObject, newObjGrp);
+ }
+ }
+ }
+ }
+
+ private void WriteNonTerminatingError(Exception exception, string resourceIdAndErrorId, ErrorCategory category)
{
Exception ex = new Exception(StringUtil.Format(resourceIdAndErrorId), exception);
WriteError(new ErrorRecord(ex, resourceIdAndErrorId, category, null));
}
+
#endregion utils
///
@@ -301,7 +389,7 @@ namespace Microsoft.PowerShell.Commands
{
if (InputObject != null && InputObject != AutomationNull.Value)
{
- OrderByPropertyEntry currentEntry = null;
+ OrderByPropertyEntry currentEntry;
if (!_hasProcessedFirstInputObject)
{
@@ -309,14 +397,30 @@ namespace Microsoft.PowerShell.Commands
{
Property = OrderByProperty.GetDefaultKeyPropertySet(InputObject);
}
+
_orderByProperty.ProcessExpressionParameter(this, Property);
+ if (AsString && !AsHashTable)
+ {
+ ArgumentException ex = new ArgumentException(UtilityCommonStrings.GroupObjectWithHashTable);
+ ErrorRecord er = new ErrorRecord(ex, "ArgumentException", ErrorCategory.InvalidArgument, AsString);
+ ThrowTerminatingError(er);
+ }
+
+ if (AsHashTable && !AsString && (Property != null && (Property.Length > 1 || _orderByProperty.MshParameterList.Count > 1)))
+ {
+ ArgumentException ex = new ArgumentException(UtilityCommonStrings.GroupObjectSingleProperty);
+ ErrorRecord er = new ErrorRecord(ex, "ArgumentException", ErrorCategory.InvalidArgument, Property);
+ ThrowTerminatingError(er);
+ }
+
currentEntry = _orderByProperty.CreateOrderByPropertyEntry(this, InputObject, CaseSensitive, _cultureInfo);
bool[] ascending = new bool[currentEntry.orderValues.Count];
for (int index = 0; index < currentEntry.orderValues.Count; index++)
{
ascending[index] = true;
}
+
_orderByPropertyComparer = new OrderByPropertyComparer(ascending, _cultureInfo, CaseSensitive);
_hasProcessedFirstInputObject = true;
@@ -326,40 +430,109 @@ namespace Microsoft.PowerShell.Commands
currentEntry = _orderByProperty.CreateOrderByPropertyEntry(this, InputObject, CaseSensitive, _cultureInfo);
}
- DoGrouping(currentEntry, this.NoElement, _groups, _tupleToGroupInfoMappingDictionary, _orderByPropertyComparer);
+ _entriesToOrder.Add(currentEntry);
+
+ var currentEntryOrderValues = currentEntry.orderValues;
+ if (!_hasDifferentValueTypes)
+ {
+ UpdateOrderPropertyTypeInfo(currentEntryOrderValues);
+ }
+ }
+ }
+
+ private void UpdateOrderPropertyTypeInfo(List currentEntryOrderValues)
+ {
+ if (_propertyTypesCandidate == null)
+ {
+ _propertyTypesCandidate = currentEntryOrderValues.Select(c => PSObject.Base(c.PropertyValue)?.GetType()).ToArray();
+ return;
+ }
+
+ if (_propertyTypesCandidate.Length != currentEntryOrderValues.Count)
+ {
+ _hasDifferentValueTypes = true;
+ return;
+ }
+
+ // check all the types we group on.
+ // if we find more than one set of types, _hasDifferentValueTypes is set to true,
+ // and we are forced to take a slower code path when we group our objects
+ for (int i = 0; i < _propertyTypesCandidate.Length; i++)
+ {
+ var candidateType = _propertyTypesCandidate[i];
+ var propertyType = PSObject.Base(currentEntryOrderValues[i].PropertyValue)?.GetType();
+ if (propertyType == null)
+ {
+ // we ignore properties without values. We can always compare against null.
+ continue;
+ }
+
+ // if we haven't gotten a type for a property yet, update it when we do get a value
+ if (propertyType != candidateType)
+ {
+ if (candidateType == null)
+ {
+ _propertyTypesCandidate[i] = propertyType;
+ }
+ else
+ {
+ _hasDifferentValueTypes = true;
+ break;
+ }
+ }
}
}
///
+ /// Completes the processing of the gathered group objects.
///
protected override void EndProcessing()
{
+ if (!_hasDifferentValueTypes)
+ {
+ // using OrderBy to get stable sort.
+ // fast path when we only have the same object types to group
+ foreach (var entry in _entriesToOrder.OrderBy(e => e, _orderByPropertyComparer))
+ {
+ DoOrderedGrouping(entry, NoElement, _groups, _tupleToGroupInfoMappingDictionary, _orderByPropertyComparer);
+ if (Stopping)
+ {
+ return;
+ }
+ }
+ }
+ else
+ {
+ foreach (var entry in _entriesToOrder)
+ {
+ DoGrouping(entry, NoElement, _groups, _tupleToGroupInfoMappingDictionary, _orderByPropertyComparer);
+ if (Stopping)
+ {
+ return;
+ }
+ }
+ }
+
s_tracer.WriteLine(_groups.Count);
if (_groups.Count > 0)
{
if (AsHashTable)
{
- Hashtable _table = CollectionsUtil.CreateCaseInsensitiveHashtable();
+ Hashtable hashtable = CollectionsUtil.CreateCaseInsensitiveHashtable();
try
{
- foreach (GroupInfo _grp in _groups)
+ if (AsString)
{
- if (AsString)
+ foreach (GroupInfo grp in _groups)
{
- _table.Add(_grp.Name, _grp.Group);
+ hashtable.Add(grp.Name, grp.Group);
}
- else
+ }
+ else
+ {
+ foreach (GroupInfo grp in _groups)
{
- if (_grp.Values.Count == 1)
- {
- _table.Add(PSObject.Base(_grp.Values[0]), _grp.Group);
- }
- else
- {
- ArgumentException ex = new ArgumentException(UtilityCommonStrings.GroupObjectSingleProperty);
- ErrorRecord er = new ErrorRecord(ex, "ArgumentException", ErrorCategory.InvalidArgument, Property);
- ThrowTerminatingError(er);
- }
+ hashtable.Add(PSObject.Base(grp.Values[0]), grp.Group);
}
}
}
@@ -368,20 +541,12 @@ namespace Microsoft.PowerShell.Commands
WriteNonTerminatingError(e, UtilityCommonStrings.InvalidOperation, ErrorCategory.InvalidArgument);
return;
}
- WriteObject(_table);
+
+ WriteObject(hashtable);
}
else
{
- if (AsString)
- {
- ArgumentException ex = new ArgumentException(UtilityCommonStrings.GroupObjectWithHashTable);
- ErrorRecord er = new ErrorRecord(ex, "ArgumentException", ErrorCategory.InvalidArgument, AsString);
- ThrowTerminatingError(er);
- }
- else
- {
- WriteObject(_groups, true);
- }
+ WriteObject(_groups, true);
}
}
}
diff --git a/src/System.Management.Automation/engine/CommandBase.cs b/src/System.Management.Automation/engine/CommandBase.cs
index b3a1416c4f..433ca142e8 100644
--- a/src/System.Management.Automation/engine/CommandBase.cs
+++ b/src/System.Management.Automation/engine/CommandBase.cs
@@ -35,7 +35,7 @@ namespace System.Management.Automation.Internal
/// Ideally this would be an internal class, but C# does not support
/// public classes deriving from internal classes.
/// -->
- [DebuggerDisplay("Command = {commandInfo}")]
+ [DebuggerDisplay("Command = {_commandInfo}")]
public abstract class InternalCommand
{
#region private_members