Push locals of automatic variables to 'DottedScopes' when dotting script cmdlets (#4709)

When dotting a script cmdlet, the locals of automatic variables from the `PSScriptCmdlet` is not set up in the current scope before parameter binding. The fix is to push the locals in `CommandProcessor.OnSetCurrentScope` and pop them in `CommandProcessor.OnRestorePreviousScope`, which will be called from `SetCurrentScopeToExecutionScope` and `RestorePreviousScope` respectively.

Summary of changes:
1. When a new local scope is used, currently we set the locals for `CommandProcessor` right before parameter binding (in `BindCommandLineParametersNoValidation`); we set the locals for `ScriptCommandProcessor` in `Prepare`. I moved both to the constructor, right after the new scope is created so that the code is more consistent.

2. In `CmdletParameterBinderController.cs`, we set up the `PSBoundParameters` and `MyInvocation` variables in `HandleCommandLineDynamicParameters` again, which I think is unnecessary because this method is only called from `BindCommandLineParametersNoValidation`, where the setup is done for the first time.

3. Currently, the locals will be set for dotted script cmdlet in `EnterScope()` and `ExitScope()`. Now, that logic is moved to `OnSetCurrentScope` and `OnRestorePreviousScope` of `CommandProcessor`. This not only makes sure that locals are set before parameter binding, but also is consistent with the `ScriptCommandProcessor`.
This commit is contained in:
Dongbo Wang
2017-09-06 10:25:33 -07:00
committed by GitHub
parent 955e01dc65
commit 41f12b1d2c
5 changed files with 116 additions and 36 deletions
@@ -215,9 +215,7 @@ namespace System.Management.Automation
var psCompiledScriptCmdlet = this.Command as PSScriptCmdlet;
if (psCompiledScriptCmdlet != null)
{
psCompiledScriptCmdlet.PrepareForBinding(
((ScriptParameterBinder)this.DefaultParameterBinder).LocalScope,
this.CommandLineParameters);
psCompiledScriptCmdlet.PrepareForBinding(this.CommandLineParameters);
}
// Add the passed in arguments to the unboundArguments collection
@@ -1759,14 +1757,6 @@ namespace System.Management.Automation
{
s_tracer.WriteLine("Getting the bindable object from the Cmdlet");
var psCompiledScriptCmdlet = this.Command as PSScriptCmdlet;
if (psCompiledScriptCmdlet != null)
{
psCompiledScriptCmdlet.PrepareForBinding(
((ScriptParameterBinder)this.DefaultParameterBinder).LocalScope,
this.CommandLineParameters);
}
// Now get the dynamic parameter bindable object.
object dynamicParamBindableObject;
@@ -225,6 +225,28 @@ namespace System.Management.Automation
BindCommandLineParameters();
}
protected override void OnSetCurrentScope()
{
// When dotting a script cmdlet, push the locals of automatic variables to
// the 'DottedScopes' of the current scope.
PSScriptCmdlet scriptCmdlet = this.Command as PSScriptCmdlet;
if (scriptCmdlet != null && !UseLocalScope)
{
scriptCmdlet.PushDottedScope(CommandSessionState.CurrentScope);
}
}
protected override void OnRestorePreviousScope()
{
// When dotting a script cmdlet, pop the locals of automatic variables from
// the 'DottedScopes' of the current scope.
PSScriptCmdlet scriptCmdlet = this.Command as PSScriptCmdlet;
if (scriptCmdlet != null && !UseLocalScope)
{
scriptCmdlet.PopDottedScope(CommandSessionState.CurrentScope);
}
}
/// <summary>
/// Execute BeginProcessing part of command
/// </summary>
@@ -731,14 +753,18 @@ namespace System.Management.Automation
private void Init(IScriptCommandInfo scriptCommandInfo)
{
InternalCommand scriptCmdlet =
new PSScriptCmdlet(scriptCommandInfo.ScriptBlock, _useLocalScope, FromScriptFile, _context);
var scriptCmdlet = new PSScriptCmdlet(scriptCommandInfo.ScriptBlock, UseLocalScope, FromScriptFile, _context);
this.Command = scriptCmdlet;
this.CommandScope = _useLocalScope
this.CommandScope = UseLocalScope
? this.CommandSessionState.NewScope(_fromScriptFile)
: this.CommandSessionState.CurrentScope;
if (UseLocalScope)
{
// Set the 'LocalsTuple' of the new scope to that of the scriptCmdlet
scriptCmdlet.SetLocalsTupleForNewScope(CommandScope);
}
InitCommon();
// If the script has been dotted, throw an error if it's from a different language mode.
@@ -269,6 +269,12 @@ namespace System.Management.Automation
_obsoleteAttribute = _scriptBlock.ObsoleteAttribute;
_runOptimizedCode = _scriptBlock.Compile(optimized: _context._debuggingMode > 0 ? false : UseLocalScope);
_localsTuple = _scriptBlock.MakeLocalsTuple(_runOptimizedCode);
if (UseLocalScope)
{
Diagnostics.Assert(CommandScope.LocalsTuple == null, "a newly created scope shouldn't have it's tuple set.");
CommandScope.LocalsTuple = _localsTuple;
}
}
/// <summary>
@@ -282,12 +288,6 @@ namespace System.Management.Automation
internal override void Prepare(IDictionary psDefaultParameterValues)
{
if (UseLocalScope)
{
Diagnostics.Assert(CommandScope.LocalsTuple == null, "a newly created scope shouldn't have it's tuple set.");
CommandScope.LocalsTuple = _localsTuple;
}
_localsTuple.SetAutomaticVariable(AutomaticVariable.MyInvocation, this.Command.MyInvocation, _context);
_scriptBlock.SetPSScriptRootAndPSCommandPath(_localsTuple, _context);
_functionContext = new FunctionContext
@@ -585,6 +585,8 @@ namespace System.Management.Automation
protected override void OnSetCurrentScope()
{
// When dotting a script, push the locals of automatic variables to
// the 'DottedScopes' of the current scope.
if (!UseLocalScope)
{
CommandSessionState.CurrentScope.DottedScopes.Push(_localsTuple);
@@ -593,6 +595,8 @@ namespace System.Management.Automation
protected override void OnRestorePreviousScope()
{
// When dotting a script, pop the locals of automatic variables from
// the 'DottedScopes' of the current scope.
if (!UseLocalScope)
{
CommandSessionState.CurrentScope.DottedScopes.Pop();
@@ -1864,21 +1864,11 @@ namespace System.Management.Automation
private void EnterScope()
{
_commandRuntime.SetVariableListsInPipe();
if (!_useLocalScope)
{
this.Context.SessionState.Internal.CurrentScope.DottedScopes.Push(_localsTuple);
}
}
private void ExitScope()
{
_commandRuntime.RemoveVariableListsInPipe();
if (!_useLocalScope)
{
this.Context.SessionState.Internal.CurrentScope.DottedScopes.Pop();
}
}
private void RunClause(Action<FunctionContext> clause, object dollarUnderbar, object inputToProcess)
@@ -1986,12 +1976,33 @@ namespace System.Management.Automation
return null;
}
public void PrepareForBinding(SessionStateScope scope, CommandLineParameters commandLineParameters)
/// <summary>
/// If the script cmdlet will run in a new local scope, this method is used to set the locals to the newly created scope.
/// </summary>
internal void SetLocalsTupleForNewScope(SessionStateScope scope)
{
Diagnostics.Assert(scope.LocalsTuple == null, "a newly created scope shouldn't have it's tuple set.");
scope.LocalsTuple = _localsTuple;
}
/// <summary>
/// If the script cmdlet is dotted, this method is used to push the locals to the 'DottedScopes' of the current scope.
/// </summary>
internal void PushDottedScope(SessionStateScope scope)
{
scope.DottedScopes.Push(_localsTuple);
}
/// <summary>
/// If the script cmdlet is dotted, this method is used to pop the locals from the 'DottedScopes' of the current scope.
/// </summary>
internal void PopDottedScope(SessionStateScope scope)
{
scope.DottedScopes.Pop();
}
internal void PrepareForBinding(CommandLineParameters commandLineParameters)
{
if (_useLocalScope && scope.LocalsTuple == null)
{
scope.LocalsTuple = _localsTuple;
}
_localsTuple.SetAutomaticVariable(AutomaticVariable.PSBoundParameters,
commandLineParameters.GetValueToBindToPSBoundParameters(), this.Context);
_localsTuple.SetAutomaticVariable(AutomaticVariable.MyInvocation, MyInvocation, this.Context);
@@ -280,4 +280,53 @@
$_.Exception.Message | should match "Parameter2"
}
}
Context "Use automatic variables as default value for parameters" {
BeforeAll {
## Explicit use of 'CmdletBinding' make it a script cmdlet
$test1 = @'
[CmdletBinding()]
param ($Root = $PSScriptRoot)
"[$Root]"
'@
## Use of 'Parameter' implicitly make it a script cmdlet
$test2 = @'
param (
[Parameter()]
$Root = $PSScriptRoot
)
"[$Root]"
'@
$tempDir = Join-Path -Path $TestDrive -ChildPath "DefaultValueTest"
$test1File = Join-Path -Path $tempDir -ChildPath "test1.ps1"
$test2File = Join-Path -Path $tempDir -ChildPath "test2.ps1"
$expected = "[$tempDir]"
$psPath = "$PSHOME\powershell"
$null = New-Item -Path $tempDir -ItemType Directory -Force
Set-Content -Path $test1File -Value $test1 -Force
Set-Content -Path $test2File -Value $test2 -Force
}
AfterAll {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
It "Test dot-source should evaluate '`$PSScriptRoot' for parameter default value" {
$result = . $test1File
$result | Should Be $expected
$result = . $test2File
$result | Should Be $expected
}
It "Test 'powershell -File' should evaluate '`$PSScriptRoot' for parameter default value" {
$result = & $psPath -NoProfile -File $test1File
$result | Should Be $expected
$result = & $psPath -NoProfile -File $test2File
$result | Should Be $expected
}
}
}