diff --git a/src/System.Management.Automation/engine/CmdletParameterBinderController.cs b/src/System.Management.Automation/engine/CmdletParameterBinderController.cs index 88fadc3342d..fa3361002b8 100644 --- a/src/System.Management.Automation/engine/CmdletParameterBinderController.cs +++ b/src/System.Management.Automation/engine/CmdletParameterBinderController.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Internal; using System.Management.Automation.Language; @@ -202,7 +203,7 @@ internal void BindCommandLineParameters(Collection arg internal void BindCommandLineParametersNoValidation(Collection arguments) { var psCompiledScriptCmdlet = this.Command as PSScriptCmdlet; - psCompiledScriptCmdlet?.PrepareForBinding(this.CommandLineParameters); + psCompiledScriptCmdlet?.PrepareForBinding(this); InitUnboundArguments(arguments); CommandMetadata cmdletMetadata = _commandMetadata; @@ -1596,139 +1597,145 @@ private void HandleCommandLineDynamicParameters(out ParameterBindingException ou { outgoingBindingException = null; - if (_commandMetadata.ImplementsDynamicParameters) + if (_commandMetadata.ImplementsDynamicParameters && this.Command is IDynamicParameters dynamicParameterCmdlet) { using (ParameterBinderBase.bindingTracer.TraceScope( "BIND cmd line args to DYNAMIC parameters.")) - { s_tracer.WriteLine("The Cmdlet supports the dynamic parameter interface"); + if (_dynamicParameterBinder == null) + { + s_tracer.WriteLine("Getting the bindable object from the Cmdlet"); - IDynamicParameters dynamicParameterCmdlet = this.Command as IDynamicParameters; + // Now get the dynamic parameter bindable object. + object dynamicParamBindableObject = null; - if (dynamicParameterCmdlet != null) + try { - if (_dynamicParameterBinder == null) + dynamicParamBindableObject = dynamicParameterCmdlet.GetDynamicParameters(); + } + catch (ParameterBindingException e) + { + outgoingBindingException = e; + } + catch (Exception e) // Catch-all OK, this is a third-party callout + { + if (e is ProviderInvocationException) { - s_tracer.WriteLine("Getting the bindable object from the Cmdlet"); - - // Now get the dynamic parameter bindable object. - object dynamicParamBindableObject; - - try - { - dynamicParamBindableObject = dynamicParameterCmdlet.GetDynamicParameters(); - } - catch (Exception e) // Catch-all OK, this is a third-party callout - { - if (e is ProviderInvocationException) - { - throw; - } - - ParameterBindingException bindingException = - new ParameterBindingException( - e, - ErrorCategory.InvalidArgument, - this.Command.MyInvocation, - null, - null, - null, - null, - ParameterBinderStrings.GetDynamicParametersException, - "GetDynamicParametersException", - e.Message); - - // This exception is caused because failure happens when retrieving the dynamic parameters, - // this is not caused by introducing the default parameter binding. - throw bindingException; - } - - if (dynamicParamBindableObject != null) - { - ParameterBinderBase.bindingTracer.WriteLine( - "DYNAMIC parameter object: [{0}]", - dynamicParamBindableObject.GetType()); - - s_tracer.WriteLine("Creating a new parameter binder for the dynamic parameter object"); + throw; + } - InternalParameterMetadata dynamicParameterMetadata; + ParameterBindingException bindingException = + new ParameterBindingException( + e, + ErrorCategory.InvalidArgument, + this.Command.MyInvocation, + null, + null, + null, + null, + ParameterBinderStrings.GetDynamicParametersException, + "GetDynamicParametersException", + e.Message); - RuntimeDefinedParameterDictionary runtimeParamDictionary = dynamicParamBindableObject as RuntimeDefinedParameterDictionary; - if (runtimeParamDictionary != null) - { - // Generate the type metadata for the runtime-defined parameters - dynamicParameterMetadata = - InternalParameterMetadata.Get(runtimeParamDictionary, true, true); - - _dynamicParameterBinder = - new RuntimeDefinedParameterBinder( - runtimeParamDictionary, - this.Command, - this.CommandLineParameters); - } - else - { - // Generate the type metadata or retrieve it from the cache - dynamicParameterMetadata = - InternalParameterMetadata.Get(dynamicParamBindableObject.GetType(), Context, true); + // This exception is caused because failure happens when retrieving the dynamic parameters, + // this is not caused by introducing the default parameter binding. + throw bindingException; + } - // Create the parameter binder for the dynamic parameter object + if (dynamicParamBindableObject == null) + { + if (_dynamicParameterBinder == null) + { + s_tracer.WriteLine("No dynamic parameter object or RuntimeDefinedParameters were returned from the Cmdlet"); + return; + }else + { + s_tracer.WriteLine("RuntimeDefinedParameters were just-in-time bound from the output pipeline of the dynamicparam block"); + } + } + else + { + ParameterBinderBase.bindingTracer.WriteLine( + "DYNAMIC parameter object: [{0}]", + dynamicParamBindableObject.GetType()); - _dynamicParameterBinder = - new ReflectionParameterBinder( - dynamicParamBindableObject, - this.Command, - this.CommandLineParameters); - } + // Now merge the metadata with other metadata for the command - // Now merge the metadata with other metadata for the command + MergeStaticAndDynamicParameterMetadata(dynamicParamBindableObject); + } + } + BindDynamicParameters(out outgoingBindingException); + } + } - var dynamicParams = - BindableParameters.AddMetadataForBinder( - dynamicParameterMetadata, - ParameterBinderAssociation.DynamicParameters); - foreach (var param in dynamicParams) - { - UnboundParameters.Add(param); - } + internal void MergeStaticAndDynamicParameterMetadata(object dynamicParamBindableObject) + { + InternalParameterMetadata dynamicParameterMetadata; + if (dynamicParamBindableObject is RuntimeDefinedParameterDictionary runtimeParamDictionary) + { + // Generate the type metadata for the runtime-defined parameters + dynamicParameterMetadata = + InternalParameterMetadata.Get(runtimeParamDictionary, true, true); + s_tracer.WriteLine("Creating a new {0} for the returned RuntimeDefinedParameterDictionary", nameof(RuntimeDefinedParameterBinder)); + _dynamicParameterBinder = + new RuntimeDefinedParameterBinder( + runtimeParamDictionary, + this.Command, + this.CommandLineParameters); + } + else + { + // Generate the type metadata or retrieve it from the cache + Type objectType = dynamicParamBindableObject.GetType(); + dynamicParameterMetadata = + InternalParameterMetadata.Get(objectType, Context, true); - // Now set the parameter set flags for the new type metadata. - _commandMetadata.DefaultParameterSetFlag = - this.BindableParameters.GenerateParameterSetMappingFromMetadata(_commandMetadata.DefaultParameterSetName); - } - } + // Create the parameter binder for the dynamic parameter object + s_tracer.WriteLine("Creating a new {0} for the returned object type [{1}]", nameof(ReflectionParameterBinder), objectType.FullName); - if (_dynamicParameterBinder == null) - { - s_tracer.WriteLine("No dynamic parameter object was returned from the Cmdlet"); - return; - } + _dynamicParameterBinder = + new ReflectionParameterBinder( + dynamicParamBindableObject, + this.Command, + this.CommandLineParameters); + } + var dynamicParams = BindableParameters.AddMetadataForBinder( + dynamicParameterMetadata, + ParameterBinderAssociation.DynamicParameters); + foreach (var param in dynamicParams) + { + UnboundParameters.Add(param); + } + // Now set the parameter set flags for the new type metadata. + _commandMetadata.DefaultParameterSetFlag = + this.BindableParameters.GenerateParameterSetMappingFromMetadata(_commandMetadata.DefaultParameterSetName); + } - if (UnboundArguments.Count > 0) - { - using (ParameterBinderBase.bindingTracer.TraceScope( - "BIND NAMED args to DYNAMIC parameters")) - { - // Try to bind the unbound arguments as static parameters to the - // dynamic parameter object. + internal void BindDynamicParameters(out ParameterBindingException outgoingBindingException) + { + outgoingBindingException = null; + if (UnboundArguments.Count > 0) + { + using (ParameterBinderBase.bindingTracer.TraceScope( + "BIND NAMED args to DYNAMIC parameters")) + { + // Try to bind the unbound arguments as static parameters to the + // dynamic parameter object. - ReparseUnboundArguments(); + ReparseUnboundArguments(); - UnboundArguments = BindNamedParameters(_currentParameterSetFlag, UnboundArguments); - } + UnboundArguments = BindNamedParameters(_currentParameterSetFlag, UnboundArguments); + } - using (ParameterBinderBase.bindingTracer.TraceScope( - "BIND POSITIONAL args to DYNAMIC parameters")) - { - UnboundArguments = - BindPositionalParameters( - UnboundArguments, - _currentParameterSetFlag, - _commandMetadata.DefaultParameterSetFlag, - out outgoingBindingException); - } - } - } + using (ParameterBinderBase.bindingTracer.TraceScope( + "BIND POSITIONAL args to DYNAMIC parameters")) + { + UnboundArguments = + BindPositionalParameters( + UnboundArguments, + _currentParameterSetFlag, + _commandMetadata.DefaultParameterSetFlag, + out outgoingBindingException); } } } diff --git a/src/System.Management.Automation/engine/CommandBase.cs b/src/System.Management.Automation/engine/CommandBase.cs index 4d90a7c2490..27e424c98c1 100644 --- a/src/System.Management.Automation/engine/CommandBase.cs +++ b/src/System.Management.Automation/engine/CommandBase.cs @@ -69,6 +69,11 @@ internal InternalCommand() /// internal IScriptExtent InvocationExtent { get; set; } + /// + /// Allows you to access the AST for this command invocation... + /// + internal CommandAst InvocationAst { get; set; } + private InvocationInfo _myInvocation = null; /// /// Return the invocation data object for this command. diff --git a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs index 2569dd3a492..a1624be958b 100644 --- a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs +++ b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs @@ -2404,30 +2404,89 @@ private void RunClause(Action clause, object dollarUnderbar, ob } } + /// + /// Gets a ScriptBlock's Dynamic Parameters + /// + /// A dictionary of dynamic parameters. public object GetDynamicParameters() { - _commandRuntime = (MshCommandRuntime)commandRuntime; - - if (_scriptBlock.HasDynamicParameters) - { - var resultList = new List(); - Diagnostics.Assert(_functionContext._outputPipe == null, "Output pipe should not be set yet."); - _functionContext._outputPipe = new Pipe(resultList); - RunClause( - clause: _runOptimized ? _scriptBlock.DynamicParamBlock : _scriptBlock.UnoptimizedDynamicParamBlock, - dollarUnderbar: AutomationNull.Value, - inputToProcess: AutomationNull.Value); - if (resultList.Count > 1) + // If there is not a dynamicParam block, + if (!_scriptBlock.HasDynamicParameters) { + return null; // return nothing, since it cannot have dynamic parameters. + } + + // In order to bind dynamic parameters as soon as possible, we want to capture each output of the dynamic param block. + _commandRuntime = (MshCommandRuntime)commandRuntime; + var resultList = new List(); + // this works because normally the dynamic parameter block does not output to the pipeline + // (hence, no output pipe should already exist). + Diagnostics.Assert(_functionContext._outputPipe == null, "Output pipe should not be set yet."); + + // So let's create one. + _functionContext._outputPipe = new Pipe(); + var objectStream = new ObjectStream(); + var objectWriter = new ObjectWriter(objectStream); + _functionContext._outputPipe.ExternalWriter = objectWriter; + + // and also create a dictionary to accumulate dynamic parameters. + RuntimeDefinedParameterDictionary accumulatedDynamicParameters = new RuntimeDefinedParameterDictionary(); + + // As data comes thru the output pipe + objectStream.DataReady += (sender, args) => + { + var objectStream = sender as ObjectStream; + while (objectStream.Count > 0) { - throw PSTraceSource.NewInvalidOperationException( - AutomationExceptions.DynamicParametersWrongType, - PSObject.ToStringParser(this.Context, resultList)); + // read each output + var obj = objectStream.Read(); + // If it could be a RuntimeDefinedParameter + RuntimeDefinedParameter runtimeDefinedParameter = obj as RuntimeDefinedParameter; + // get a reference to the base object (since almost everything in PowerShell is wrapped in a PSObject) + runtimeDefinedParameter ??= (obj as PSObject)?.BaseObject as RuntimeDefinedParameter; + // If we could not cast to a runtime parameter + if (!(runtimeDefinedParameter is RuntimeDefinedParameter)) + { + // pass the result back thru + resultList.Add(obj); + } else + { + // otherwise, accumulate that dynamic parameter. + accumulatedDynamicParameters.Add(runtimeDefinedParameter.Name, runtimeDefinedParameter); + } } + }; - return resultList.Count == 0 ? null : PSObject.Base(resultList[0]); + // We want to provide dynamicParams with as much context as we can. + CommandAst commandAst = this.InvocationAst; + + RunClause( + // Now we run the dynamic parameter block (which will trigger ObjectStream.DataReady). + clause: _runOptimized ? _scriptBlock.DynamicParamBlock : _scriptBlock.UnoptimizedDynamicParamBlock, + // $_ should be the command with dynamic parameters + dollarUnderbar: this.MyInvocation.MyCommand, + // $Input will be the CommandAST's elements (thus giving dynamicParam enough context to be conditional) + inputToProcess: commandAst != null ? commandAst.CommandElements : AutomationNull.Value + ); + + // If more than one result is being outputted + if (resultList.Count > 1) + { + // then they have provided two properties, and we will throw + throw PSTraceSource.NewInvalidOperationException( + AutomationExceptions.DynamicParametersWrongType, + PSObject.ToStringParser(this.Context, resultList)); + } + + // If only one result was provided + if (resultList.Count == 1) { + return resultList[0]; // return that + } else if (accumulatedDynamicParameters.Count >= 1) { + // If one or more parameter was accumulated, return them. + return accumulatedDynamicParameters; + } else { + // Otherwise, return nothing. + return null; } - - return null; } /// @@ -2449,11 +2508,12 @@ internal void SetLocalsTupleForNewScope(SessionStateScope scope) /// internal void PopDottedScope(SessionStateScope scope) => scope.DottedScopes.Pop(); - internal void PrepareForBinding(CommandLineParameters commandLineParameters) + internal void PrepareForBinding(CmdletParameterBinderController parameterBinderController) { + _parameterBinderController = parameterBinderController; _localsTuple.SetAutomaticVariable( AutomaticVariable.PSBoundParameters, - value: commandLineParameters.GetValueToBindToPSBoundParameters(), + value: _parameterBinderController.CommandLineParameters.GetValueToBindToPSBoundParameters(), this.Context); _localsTuple.SetAutomaticVariable(AutomaticVariable.MyInvocation, value: MyInvocation, this.Context); } @@ -2522,6 +2582,7 @@ protected override void StopProcessing() #region IDispose private bool _disposed; + private CmdletParameterBinderController _parameterBinderController; internal event EventHandler DisposingEvent; diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index ddc70fabd50..dbed0994604 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -28,6 +28,16 @@ namespace System.Management.Automation { internal static class PipelineOps { + /// + /// This method adds commands to be processed by PowerShell. + /// It is called while evaluating PowerShell and is responsible for command resolution and argument assignment. + /// + /// The pipeline processor. + /// The command elements. + /// The current command's base AST. + /// Any command redirections. + /// The context used to execute the command. + /// private static CommandProcessorBase AddCommand(PipelineProcessor pipe, CommandParameterInternal[] commandElements, CommandBaseAst commandBaseAst, @@ -207,8 +217,13 @@ private static CommandProcessorBase AddCommand(PipelineProcessor pipe, { commandProcessor = CommandProcessorBase.CreateGetHelpCommandProcessor(context, helpTarget, helpCategory); } - + + // This is roughly where scripts are wired up to their current invocation + // we were already tracking the extent of the AST. commandProcessor.Command.InvocationExtent = commandBaseAst.Extent; + // in order to enable additional context in dynamic parameters, we will attach the ast to the InternalCommand. + // This will likely be useful for other scenarios as well, since it saves the time and effort of performing a callstack peek. + commandProcessor.Command.InvocationAst = commandAst; commandProcessor.Command.MyInvocation.ScriptPosition = commandBaseAst.Extent; commandProcessor.Command.MyInvocation.InvocationName = invocationName; diff --git a/test/powershell/Language/Scripting/Dynamicparameters.Tests.ps1 b/test/powershell/Language/Scripting/Dynamicparameters.Tests.ps1 index b266732cba7..e6da00a651c 100644 --- a/test/powershell/Language/Scripting/Dynamicparameters.Tests.ps1 +++ b/test/powershell/Language/Scripting/Dynamicparameters.Tests.ps1 @@ -1,81 +1,182 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Describe "Dynamic parameter support in script cmdlets." -Tags "CI" { - BeforeAll { - Class MyTestParameter { - [parameter(ParameterSetName = 'pset1', position=0, mandatory=1)] - [string] $name - } +Describe "Dynamic parameters" -Tags "CI" { + context "Dynamic Parameter Basics" { + BeforeAll { + function Test-DynamicParameterBasics { + [CmdletBinding()] + param() - function foo-bar - { - [CmdletBinding()] - param($path) + dynamicParam { + $dynamicParams = [Management.Automation.RuntimeDefinedParameterDictionary]::new() + $dynamicParams.Add( + "Foo", [Management.Automation.RuntimeDefinedParameter]::new( + "Foo", + [int], + @( + $parameter = [Parameter]::new() + $parameter.Mandatory = $true + $parameter.Position = 0 + $parameter + ) + ) + ) + $dynamicParams.Add( + "Bar", [Management.Automation.RuntimeDefinedParameter]::new( + "Bar", + [string], + @( + $parameter = [Parameter]::new() + $parameter.Position = 1 + $parameter + ) + ) + ) + $dynamicParams + } - dynamicparam { - if ($PSBoundParameters["path"] -contains "abc") { - $attributes = [System.Management.Automation.ParameterAttribute]::New() - $attributes.ParameterSetName = 'pset1' - $attributes.Mandatory = $false + process { + [PSCustomObject]([Ordered]@{} + $PSBoundParameters) + } + } + } + it "Will bind to dynamic parameters" { + $output = Test-DynamicParameterBasics -Foo 1 -bar 2 + $output.Foo | Should -be 1 + $output.bar | Should -be 2 + } - $attributeCollection = [System.Collections.ObjectModel.Collection``1[System.Attribute]]::new() - $attributeCollection.Add($attributes) + it "Will allow dynamic parameters to be accessed from .Parameters" { + (Get-Command Test-DynamicParameterBasics).Parameters["Foo"].Attributes | + Where-Object { $_.Mandatory -is [bool] } | + Select-Object -ExpandProperty Mandatory | + Should -Be $true + (Get-Command Test-DynamicParameterBasics).Parameters["Bar"] | Should -not -be $null + } + } - $dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new("dp1", [Int32], $attributeCollection) + context "Dynamic Parameters Using InvocationName" { + BeforeAll { + function Test-DynamicParameterWithSmartAlias { + [CmdletBinding()] + param() - $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() - $paramDictionary.Add("dp1", $dynParam1) + dynamicParam { + if ($MyInvocation.InvocationName -and + ($MyInvocation.InvocationName -ne $MyInvocation.MyCommand.Name)) { + $dynamicParams = [Management.Automation.RuntimeDefinedParameterDictionary]::new() + $dynamicParams.Add( + "$($MyInvocation.InvocationName)", [Management.Automation.RuntimeDefinedParameter]::new( + "$($MyInvocation.InvocationName)", + [switch], + @( + [Parameter]::new() + ) + ) + ) + $dynamicParams + } - return $paramDictionary - } - elseif($PSBoundParameters["path"] -contains "class") { - $paramDictionary = [MyTestParameter]::new() - return $paramDictionary } - $paramDictionary = $null - return $null + process { + [Ordered]@{} + $PSBoundParameters + } } - begin { - if(($null -ne $paramDictionary) -and ($paramDictionary -is [MyTestParameter]) ) { - $paramDictionary.name + Set-Alias AliasingWithDynamicParameters Test-DynamicParameterWithSmartAlias + Set-Alias ItIsPossibleToHaveMultipleAliasesToTheSameCommand Test-DynamicParameterWithSmartAlias + Set-Alias YouCanUseCommandNamesToInfluenceDynamicParameters Test-DynamicParameterWithSmartAlias + } + + it "Can have different parameters depending on what it is called" { + AliasingWithDynamicParameters -AliasingWithDynamicParameters + ItIsPossibleToHaveMultipleAliasesToTheSameCommand -ItIsPossibleToHaveMultipleAliasesToTheSameCommand + YouCanUseCommandNamesToInfluenceDynamicParameters -YouCanUseCommandNamesToInfluenceDynamicParameters + } + } + + context "Emitting Dynamic Parameters" { + BeforeAll { + + function Test-DynamicEmit { + [CmdletBinding()] + param($path) + + dynamicparam { + # $Input will contain the command elements of the current invocation. + # $_ will be the current command. + + # Simply emit a few dynamic parameters. + # All dynamic emitted dynamic parameters will be joined into a dictionary. + [Management.Automation.RuntimeDefinedParameter]::new( + "Foo", + [string], + @([Parameter]::new()) + ) + + [Management.Automation.RuntimeDefinedParameter]::new( + "Bar", + [string], + @([Parameter]::new()) + ) + + [Management.Automation.RuntimeDefinedParameter]::new( + "Baz", + [string], + @([Parameter]::new()) + ) + } + + process { + @($PSBoundParameters.Values) } - elseif ($null -ne $paramDictionary) { - if ($null -ne $paramDictionary.dp1.Value) { - $paramDictionary.dp1.Value + } + + function Test-DynamicConditionalEmit { + [CmdletBinding()] + param($path) + + dynamicparam { + # $Input will contain the command elements of the current invocation. + # $_ will be the current command. + $pathToBe = @($input)[1] + + # This is a simple and easily testable example, and not anywhere near complete parsing + # (this check will only work if $Path is assigned positionally) + if ($pathToBe -is [Management.Automation.Language.StringConstantExpressionAst]) { + $pathToBe = $pathToBe.Value } - else { - "dynamic parameters not passed" + + # If the path is not explicitly "badPath" + if ($pathToBe -ne "BadPath") { + # create a dynamic parameter + [Management.Automation.RuntimeDefinedParameter]::new( + "foo", + [int], + @([Parameter]::new()) + ) } } - else { - "no dynamic parameters" + + process { + @($PSBoundParameters.Values) } } - process {} - end {} - } - } - - It "The dynamic parameter is enabled and bound" { - foo-bar -path abc -dp1 42 | Should -Be 42 - } - It "When the dynamic parameter is not available, and raises an error when specified" { - { foo-bar -path def -dp1 42 } | Should -Throw -ErrorId "NamedParameterNotFound,foo-bar" - } + } - It "No dynamic parameter shouldn't cause an errr " { - foo-bar -path def | Should -BeExactly 'no dynamic parameters' - } + it "Can emit dynamic parameters, rather than returning a RuntimeParameterDictionary" { + Test-DynamicEmit -Foo 1 -Bar 2 -Baz 3 | Should -Be @(1,2,3) + } - It "Not specifying dynamic parameter shouldn't cause an error" { - foo-bar -path abc | Should -BeExactly 'dynamic parameters not passed' - } + it "Can conditionally emit a parameter" { + Test-DynamicConditionalEmit -Foo 1 | Should -Be @(1) + } - It "Parameter is defined in Class" { - foo-bar -path class -Name "myName" | Should -BeExactly 'myName' + it "Can conditionally _not_ emit a parameter" { + { Test-DynamicConditionalEmit badpath -Foo 1 -ErrorAction stop } | Should -Throw + } } }