mirror of
https://github.com/aphrody-labs/PowerShell.git
synced 2026-10-17 12:45:47 +00:00
Make Select-Object/ForEach-Object/Where-Object see dynamic properties (#6898)
Dynamic (DLR) objects work in some places today, but not others. This change expands that support to ForEach-Object, Where-Object and the family of cmdlets that use 'MshExpression' (Select-Object, etc.).
This change addresses both wildcard and non-wildcard cases. In wildcard cases, it uses the existing support of generating PSDynamicMember objects for names returned by GetDynamicMemberNames.
In non-wildcard cases, a dynamic property access is attempted whether or not the name shows up in GetDynamicMemberNames, but a truly "blind" access is only attempted if we see that the base object is an IDMOP. If the dynamic access fails, you'll get the same or a similar error experience as before ("The property 'Blarg' cannot be found", or no error at all, depending on the cmdlet and the strict mode setting).
The included test coverage includes a stub for the.ForEach operator--once people are happy with this change, I can continue by adding support there.
This change should allegedly also have positive perf impact, though in actual perf testing, although it does seem ever-so-slightly faster, I found it difficult to measure much difference at all.
This commit is contained in:
1 parent
3a5a3e4c6e
commit
ce3cb68551
7 files changed
+415
-89
No files matched your search
@@ -4,9 +4,11 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using System.Management.Automation.Internal;
|
||||
using System.Management.Automation.Language;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
{
|
||||
@@ -123,13 +125,20 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
{
|
||||
// get the members first: this will expand the globbing on each parameter
|
||||
members = target.Members.Match(_stringValue,
|
||||
PSMemberTypes.Properties | PSMemberTypes.PropertySet);
|
||||
PSMemberTypes.Properties | PSMemberTypes.PropertySet | PSMemberTypes.Dynamic);
|
||||
}
|
||||
else
|
||||
{
|
||||
// we have no globbing: try an exact match, because this is quicker.
|
||||
PSMemberInfo x = target.Members[_stringValue];
|
||||
|
||||
if ((x == null) && (target.BaseObject is System.Dynamic.IDynamicMetaObjectProvider))
|
||||
{
|
||||
// We could check if GetDynamicMemberNames includes the name... but
|
||||
// GetDynamicMemberNames is only a hint, not a contract, so we'd want
|
||||
// to attempt the binding whether it's in there or not.
|
||||
x = new PSDynamicMember(_stringValue);
|
||||
}
|
||||
List<PSMemberInfo> temp = new List<PSMemberInfo>();
|
||||
if (x != null)
|
||||
{
|
||||
@@ -165,10 +174,14 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// it can be a property
|
||||
if (member is PSPropertyInfo)
|
||||
else if (member is PSPropertyInfo)
|
||||
{
|
||||
temporaryMemberList.Add(member);
|
||||
}
|
||||
// it can be a dynamic member
|
||||
else if (member is PSDynamicMember)
|
||||
{
|
||||
temporaryMemberList.Add(member);
|
||||
}
|
||||
@@ -225,11 +238,13 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
|
||||
#region Private Members
|
||||
|
||||
private CallSite<Func<CallSite, object, object>> _getValueDynamicSite;
|
||||
|
||||
private MshExpressionResult GetValue(PSObject target, bool eatExceptions)
|
||||
{
|
||||
try
|
||||
{
|
||||
object result;
|
||||
object result = null;
|
||||
|
||||
if (Script != null)
|
||||
{
|
||||
@@ -243,12 +258,16 @@ namespace Microsoft.PowerShell.Commands.Internal.Format
|
||||
}
|
||||
else
|
||||
{
|
||||
PSMemberInfo member = target.Properties[_stringValue];
|
||||
if (member == null)
|
||||
if (_getValueDynamicSite == null)
|
||||
{
|
||||
return new MshExpressionResult(null, this, null);
|
||||
_getValueDynamicSite =
|
||||
CallSite<Func<CallSite, object, object>>.Create(
|
||||
PSGetMemberBinder.Get(
|
||||
_stringValue,
|
||||
classScope: (Type) null,
|
||||
@static: false));
|
||||
}
|
||||
result = member.Value;
|
||||
result = _getValueDynamicSite.Target.Invoke(_getValueDynamicSite, target);
|
||||
}
|
||||
|
||||
return new MshExpressionResult(result, this, null);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
@@ -16,6 +18,38 @@ using Dbg = System.Management.Automation.Diagnostics;
|
||||
|
||||
namespace Microsoft.PowerShell.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// A thin wrapper over a property-getting Callsite, to allow reuse when possible.
|
||||
/// </summary>
|
||||
struct DynamicPropertyGetter
|
||||
{
|
||||
private CallSite<Func<CallSite, object, object>> _getValueDynamicSite;
|
||||
|
||||
// For the wildcard case, lets us know if we can reuse the callsite:
|
||||
private string _lastUsedPropertyName;
|
||||
|
||||
public object GetValue(PSObject inputObject, string propertyName)
|
||||
{
|
||||
Dbg.Assert(!WildcardPattern.ContainsWildcardCharacters(propertyName), "propertyName should be pre-resolved by caller");
|
||||
|
||||
// If wildcards are involved, the resolved property name could potentially
|
||||
// be different on every object... but probably not, so we'll attempt to
|
||||
// reuse the callsite if possible.
|
||||
|
||||
if (!propertyName.Equals(_lastUsedPropertyName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_lastUsedPropertyName = propertyName;
|
||||
_getValueDynamicSite = CallSite<Func<CallSite, object, object>>.Create(
|
||||
PSGetMemberBinder.Get(
|
||||
propertyName,
|
||||
classScope: (Type) null,
|
||||
@static: false));
|
||||
}
|
||||
|
||||
return _getValueDynamicSite.Target.Invoke(_getValueDynamicSite, inputObject);
|
||||
}
|
||||
}
|
||||
|
||||
#region Built-in cmdlets that are used by or require direct access to the engine.
|
||||
|
||||
/// <summary>
|
||||
@@ -136,6 +170,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
private string _propertyOrMethodName;
|
||||
private string _targetString;
|
||||
private DynamicPropertyGetter _propGetter;
|
||||
|
||||
/// <summary>
|
||||
/// The arguments passed to a method invocation
|
||||
@@ -351,80 +386,107 @@ namespace Microsoft.PowerShell.Commands
|
||||
member = _inputObject.Members[_propertyOrMethodName];
|
||||
}
|
||||
|
||||
if (member == null)
|
||||
// member is a method
|
||||
if (member is PSMethodInfo)
|
||||
{
|
||||
errorRecord = GenerateNameParameterError("Name", InternalCommandStrings.PropertyOrMethodNotFound,
|
||||
"PropertyOrMethodNotFound", _inputObject,
|
||||
_propertyOrMethodName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// member is a method
|
||||
if (member is PSMethodInfo)
|
||||
{
|
||||
// first we check if the member is a ParameterizedProperty
|
||||
PSParameterizedProperty targetParameterizedProperty = member as PSParameterizedProperty;
|
||||
if (targetParameterizedProperty != null)
|
||||
{
|
||||
// should process
|
||||
string propertyAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectPropertyAction, targetParameterizedProperty.Name);
|
||||
|
||||
// ParameterizedProperty always take parameters, so we output the member.Value directly
|
||||
if (ShouldProcess(_targetString, propertyAction))
|
||||
{
|
||||
WriteObject(member.Value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PSMethodInfo targetMethod = member as PSMethodInfo;
|
||||
Dbg.Assert(targetMethod != null, "targetMethod should not be null here.");
|
||||
try
|
||||
{
|
||||
// should process
|
||||
string methodAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectMethodActionWithoutArguments, targetMethod.Name);
|
||||
|
||||
if (ShouldProcess(_targetString, methodAction))
|
||||
{
|
||||
if (!BlockMethodInLanguageMode(InputObject))
|
||||
{
|
||||
object result = targetMethod.Invoke(Utils.EmptyArray<object>());
|
||||
WriteToPipelineWithUnrolling(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (PipelineStoppedException)
|
||||
{
|
||||
// PipelineStoppedException can be caused by select-object
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MethodException mex = ex as MethodException;
|
||||
if (mex != null && mex.ErrorRecord != null && mex.ErrorRecord.FullyQualifiedErrorId == "MethodCountCouldNotFindBest")
|
||||
{
|
||||
WriteObject(targetMethod.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteError(new ErrorRecord(ex, "MethodInvocationError", ErrorCategory.InvalidOperation, _inputObject));
|
||||
}
|
||||
}
|
||||
}
|
||||
// member is a property
|
||||
else
|
||||
// first we check if the member is a ParameterizedProperty
|
||||
PSParameterizedProperty targetParameterizedProperty = member as PSParameterizedProperty;
|
||||
if (targetParameterizedProperty != null)
|
||||
{
|
||||
// should process
|
||||
string propertyAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectPropertyAction, member.Name);
|
||||
InternalCommandStrings.ForEachObjectPropertyAction, targetParameterizedProperty.Name);
|
||||
|
||||
// ParameterizedProperty always take parameters, so we output the member.Value directly
|
||||
if (ShouldProcess(_targetString, propertyAction))
|
||||
{
|
||||
WriteObject(member.Value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PSMethodInfo targetMethod = member as PSMethodInfo;
|
||||
Dbg.Assert(targetMethod != null, "targetMethod should not be null here.");
|
||||
try
|
||||
{
|
||||
// should process
|
||||
string methodAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectMethodActionWithoutArguments, targetMethod.Name);
|
||||
|
||||
if (ShouldProcess(_targetString, methodAction))
|
||||
{
|
||||
if (!BlockMethodInLanguageMode(InputObject))
|
||||
{
|
||||
object result = targetMethod.Invoke(Utils.EmptyArray<object>());
|
||||
WriteToPipelineWithUnrolling(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (PipelineStoppedException)
|
||||
{
|
||||
// PipelineStoppedException can be caused by select-object
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MethodException mex = ex as MethodException;
|
||||
if (mex != null && mex.ErrorRecord != null && mex.ErrorRecord.FullyQualifiedErrorId == "MethodCountCouldNotFindBest")
|
||||
{
|
||||
WriteObject(targetMethod.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteError(new ErrorRecord(ex, "MethodInvocationError", ErrorCategory.InvalidOperation, _inputObject));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string resolvedPropertyName = null;
|
||||
bool isBlindDynamicAccess = false;
|
||||
if (member == null)
|
||||
{
|
||||
if ((_inputObject.BaseObject is IDynamicMetaObjectProvider) &&
|
||||
!WildcardPattern.ContainsWildcardCharacters(_propertyOrMethodName))
|
||||
{
|
||||
// Let's just try a dynamic property access. Note that if it
|
||||
// comes to depending on dynamic access, we are assuming it is a
|
||||
// property; we don't have ETS info to tell us up front if it
|
||||
// even exists or not, let alone if it is a method or something
|
||||
// else.
|
||||
//
|
||||
// Note that this is "truly blind"--the name did not show up in
|
||||
// GetDynamicMemberNames(), else it would show up as a dynamic
|
||||
// member.
|
||||
|
||||
resolvedPropertyName = _propertyOrMethodName;
|
||||
isBlindDynamicAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorRecord = GenerateNameParameterError("Name", InternalCommandStrings.PropertyOrMethodNotFound,
|
||||
"PropertyOrMethodNotFound", _inputObject,
|
||||
_propertyOrMethodName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// member is [presumably] a property (note that it could be a
|
||||
// dynamic property, if it shows up in GetDynamicMemberNames())
|
||||
resolvedPropertyName = member.Name;
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(resolvedPropertyName))
|
||||
{
|
||||
// should process
|
||||
string propertyAction = String.Format(CultureInfo.InvariantCulture,
|
||||
InternalCommandStrings.ForEachObjectPropertyAction, resolvedPropertyName);
|
||||
|
||||
if (ShouldProcess(_targetString, propertyAction))
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteToPipelineWithUnrolling(member.Value);
|
||||
WriteToPipelineWithUnrolling(_propGetter.GetValue(InputObject, resolvedPropertyName));
|
||||
}
|
||||
catch (TerminateException) // The debugger is terminating the execution
|
||||
{
|
||||
@@ -439,17 +501,54 @@ namespace Microsoft.PowerShell.Commands
|
||||
// PipelineStoppedException can be caused by select-object
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// When the property is not gettable or it throws an exception.
|
||||
// e.g. when trying to access an assembly's location property, since dynamic assemblies are not backed up by a file,
|
||||
// an exception will be thrown when accessing its location property. In this case, return null.
|
||||
WriteObject(null);
|
||||
// For normal property accesses, we do not generate an error
|
||||
// here. The problem for truly blind dynamic accesses (the
|
||||
// member did not show up in GetDynamicMemberNames) is that
|
||||
// we can't tell the difference between "it failed because
|
||||
// the property does not exist" (let's call this case 1) and
|
||||
// "it failed because accessing it actually threw some
|
||||
// exception" (let's call that case 2).
|
||||
//
|
||||
// PowerShell behavior for normal (non-dynamic) properties
|
||||
// is different for these two cases: case 1 gets an error
|
||||
// (which is possible because the ETS tells us up front if
|
||||
// the property exists or not), and case 2 does not. (For
|
||||
// normal properties, this catch block /is/ case 2.)
|
||||
//
|
||||
// For IDMOPs, we have the chance to attempt a "blind"
|
||||
// access, but the cost is that we must have the same
|
||||
// response to both cases (because we cannot distinguish
|
||||
// between the two). So we have to make a choice: we can
|
||||
// either swallow ALL errors (including "The property
|
||||
// 'Blarg' does not exist"), or expose them all.
|
||||
//
|
||||
// Here, for truly blind dynamic access, we choose to
|
||||
// preserve the behavior of showing "The property 'Blarg'
|
||||
// does not exist" (case 1) errors than to suppress
|
||||
// "FooException thrown when accessing Bloop property" (case
|
||||
// 2) errors.
|
||||
|
||||
if (isBlindDynamicAccess)
|
||||
{
|
||||
errorRecord = new ErrorRecord(ex,
|
||||
"DynamicPropertyAccessFailed_" + _propertyOrMethodName,
|
||||
ErrorCategory.InvalidOperation,
|
||||
InputObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
// When the property is not gettable or it throws an exception.
|
||||
// e.g. when trying to access an assembly's location property, since dynamic assemblies are not backed up by a file,
|
||||
// an exception will be thrown when accessing its location property. In this case, return null.
|
||||
WriteObject(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end of member is a property
|
||||
} // member is not null
|
||||
} // no args provided
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorRecord != null)
|
||||
{
|
||||
@@ -1452,6 +1551,8 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
}
|
||||
|
||||
private DynamicPropertyGetter _propGetter;
|
||||
|
||||
/// <summary>
|
||||
/// Execute the script block passing in the current pipeline object as
|
||||
/// it's only parameter.
|
||||
@@ -1579,6 +1680,9 @@ namespace Microsoft.PowerShell.Commands
|
||||
// has keys that can't be compared to property.
|
||||
}
|
||||
|
||||
string resolvedPropertyName = null;
|
||||
bool isBlindDynamicAccess = false;
|
||||
|
||||
ReadOnlyPSMemberInfoCollection<PSMemberInfo> members = GetMatchMembers();
|
||||
if (members.Count > 1)
|
||||
{
|
||||
@@ -1598,7 +1702,21 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
else if (members.Count == 0)
|
||||
{
|
||||
if (Context.IsStrictVersion(2))
|
||||
if ((InputObject.BaseObject is IDynamicMetaObjectProvider) &&
|
||||
!WildcardPattern.ContainsWildcardCharacters(_property))
|
||||
{
|
||||
// Let's just try a dynamic property access. Note that if it comes to
|
||||
// depending on dynamic access, we are assuming it is a property; we
|
||||
// don't have ETS info to tell us up front if it even exists or not,
|
||||
// let alone if it is a method or something else.
|
||||
//
|
||||
// Note that this is "truly blind"--the name did not show up in
|
||||
// GetDynamicMemberNames(), else it would show up as a dynamic member.
|
||||
|
||||
resolvedPropertyName = _property;
|
||||
isBlindDynamicAccess = true;
|
||||
}
|
||||
else if (Context.IsStrictVersion(2))
|
||||
{
|
||||
WriteError(ForEachObjectCommand.GenerateNameParameterError("Property",
|
||||
InternalCommandStrings.PropertyNotFound,
|
||||
@@ -1607,10 +1725,15 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPropertyName = members[0].Name;
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(resolvedPropertyName))
|
||||
{
|
||||
try
|
||||
{
|
||||
return members[0].Value;
|
||||
return _propGetter.GetValue(_inputObject, resolvedPropertyName);
|
||||
}
|
||||
catch (TerminateException)
|
||||
{
|
||||
@@ -1620,10 +1743,45 @@ namespace Microsoft.PowerShell.Commands
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// When the property is not gettable or it throws an exception
|
||||
return null;
|
||||
// For normal property accesses, we do not generate an error here. The problem
|
||||
// for truly blind dynamic accesses (the member did not show up in
|
||||
// GetDynamicMemberNames) is that we can't tell the difference between "it
|
||||
// failed because the property does not exist" (let's call this case
|
||||
// 1) and "it failed because accessing it actually threw some exception" (let's
|
||||
// call that case 2).
|
||||
//
|
||||
// PowerShell behavior for normal (non-dynamic) properties is different for
|
||||
// these two cases: case 1 gets an error (if strict mode is on) (which is
|
||||
// possible because the ETS tells us up front if the property exists or not),
|
||||
// and case 2 does not. (For normal properties, this catch block /is/ case 2.)
|
||||
//
|
||||
// For IDMOPs, we have the chance to attempt a "blind" access, but the cost is
|
||||
// that we must have the same response to both cases (because we cannot
|
||||
// distinguish between the two). So we have to make a choice: we can either
|
||||
// swallow ALL errors (including "The property 'Blarg' does not exist"), or
|
||||
// expose them all.
|
||||
//
|
||||
// Here, for truly blind dynamic access, we choose to preserve the behavior of
|
||||
// showing "The property 'Blarg' does not exist" (case 1) errors than to
|
||||
// suppress "FooException thrown when accessing Bloop property" (case
|
||||
// 2) errors.
|
||||
|
||||
if (isBlindDynamicAccess && Context.IsStrictVersion(2))
|
||||
{
|
||||
WriteError(new ErrorRecord(ex,
|
||||
"DynamicPropertyAccessFailed_" + _property,
|
||||
ErrorCategory.InvalidOperation,
|
||||
_inputObject));
|
||||
|
||||
error = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// When the property is not gettable or it throws an exception
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user