Allow explicitly specified named parameter to supersede the same one from hashtable splatting (#13162)

Allow explicitly specified named parameter to supersede the same one from hashtable splatting.
The work is done in parameter binder, so that parameters can be resolved to cover a parameter's official name, alias name, and unambiguous partial prefix name.

The changes covers covers Hashtable splatting in 3 scenarios:

- Cmdlet or advanced script invocation;
- Simple function invocation;
- ScriptBlock.GetPowerShell(...), where the script block contains command invocation only and uses Hashtable splatting.

Some code refactoring is done to ParameterBinderController to avoid redundant code being duplicated in CmdletParameterBinderController and ScriptParameterBinderController.
This commit is contained in:
Dongbo Wang
2020-08-05 10:59:53 +05:00
committed by GitHub
parent ada1b36ab6
commit 63cf0c330c
12 changed files with 646 additions and 291 deletions
@@ -207,13 +207,7 @@ namespace System.Management.Automation
psCompiledScriptCmdlet.PrepareForBinding(this.CommandLineParameters);
}
// Add the passed in arguments to the unboundArguments collection
foreach (CommandParameterInternal argument in arguments)
{
UnboundArguments.Add(argument);
}
InitUnboundArguments(arguments);
CommandMetadata cmdletMetadata = _commandMetadata;
// Clear the warningSet at the beginning.
_warningSet.Clear();
@@ -232,7 +226,7 @@ namespace System.Management.Automation
_commandMetadata.Name))
{
// Bind the actual arguments
UnboundArguments = BindParameters(_currentParameterSetFlag, this.UnboundArguments);
UnboundArguments = BindNamedParameters(_currentParameterSetFlag, this.UnboundArguments);
}
ParameterBindingException reportedBindingException;
@@ -1064,134 +1058,56 @@ namespace System.Management.Automation
}
/// <summary>
/// Binds the actual arguments to only the formal parameters
/// for only the parameters in the specified parameter set.
/// Validate the given named parameter against the specified parameter set,
/// and then bind the argument to the parameter.
/// </summary>
/// <param name="parameterSets">
/// The parameter set used to bind the arguments.
/// </param>
/// <param name="arguments">
/// The arguments that should be attempted to bind to the parameters of the specified
/// parameter binder.
/// </param>
/// <exception cref="ParameterBindingException">
/// if multiple parameters are found matching the name.
/// or
/// if no match could be found.
/// or
/// If argument transformation fails.
/// or
/// The argument could not be coerced to the appropriate type for the parameter.
/// or
/// The parameter argument transformation, prerequisite, or validation failed.
/// or
/// If the binding to the parameter fails.
/// </exception>
private Collection<CommandParameterInternal> BindParameters(uint parameterSets, Collection<CommandParameterInternal> arguments)
protected override void BindNamedParameter(
uint parameterSets,
CommandParameterInternal argument,
MergedCompiledCommandParameter parameter)
{
Collection<CommandParameterInternal> result = new Collection<CommandParameterInternal>();
foreach (CommandParameterInternal argument in arguments)
if ((parameter.Parameter.ParameterSetFlags & parameterSets) == 0 &&
!parameter.Parameter.IsInAllSets)
{
if (!argument.ParameterNameSpecified)
{
result.Add(argument);
continue;
}
string parameterSetName = BindableParameters.GetParameterSetName(parameterSets);
// We don't want to throw an exception yet because
// the parameter might be a positional argument or it
// might match up to a dynamic parameter
MergedCompiledCommandParameter parameter =
BindableParameters.GetMatchingParameter(
ParameterBindingException bindingException =
new ParameterBindingException(
ErrorCategory.InvalidArgument,
this.Command.MyInvocation,
errorPosition: null,
argument.ParameterName,
false, true,
new InvocationInfo(this.InvocationInfo.MyCommand, argument.ParameterExtent));
parameterType: null,
typeSpecified: null,
ParameterBinderStrings.ParameterNotInParameterSet,
"ParameterNotInParameterSet",
parameterSetName);
// If the parameter is not in the specified parameter set,
// throw a binding exception
if (parameter != null)
// Might be caused by default parameter binding
if (!DefaultParameterBindingInUse)
{
// Now check to make sure it hasn't already been
// bound by looking in the boundParameters collection
if (BoundParameters.ContainsKey(parameter.Parameter.Name))
{
ParameterBindingException bindingException =
new ParameterBindingException(
ErrorCategory.InvalidArgument,
this.InvocationInfo,
GetParameterErrorExtent(argument),
argument.ParameterName,
null,
null,
ParameterBinderStrings.ParameterAlreadyBound,
nameof(ParameterBinderStrings.ParameterAlreadyBound));
// Multiple values assigned to the same parameter.
// Not caused by default parameter binding
throw bindingException;
}
if ((parameter.Parameter.ParameterSetFlags & parameterSets) == 0 &&
!parameter.Parameter.IsInAllSets)
{
string parameterSetName = BindableParameters.GetParameterSetName(parameterSets);
ParameterBindingException bindingException =
new ParameterBindingException(
ErrorCategory.InvalidArgument,
this.Command.MyInvocation,
null,
argument.ParameterName,
null,
null,
ParameterBinderStrings.ParameterNotInParameterSet,
"ParameterNotInParameterSet",
parameterSetName);
// Might be caused by default parameter binding
if (!DefaultParameterBindingInUse)
{
throw bindingException;
}
else
{
ThrowElaboratedBindingException(bindingException);
}
}
try
{
BindParameter(parameterSets, argument, parameter,
ParameterBindingFlags.ShouldCoerceType | ParameterBindingFlags.DelayBindScriptBlock);
}
catch (ParameterBindingException pbex)
{
if (!DefaultParameterBindingInUse)
{
throw;
}
ThrowElaboratedBindingException(pbex);
}
}
else if (argument.ParameterName.Equals(Parser.VERBATIM_PARAMETERNAME, StringComparison.Ordinal))
{
// We sometimes send a magic parameter from a remote machine with the values referenced via
// a using expression ($using:x). We then access these values via PSBoundParameters, so
// "bind" them here.
DefaultParameterBinder.CommandLineParameters.SetImplicitUsingParameters(argument.ArgumentValue);
throw bindingException;
}
else
{
result.Add(argument);
ThrowElaboratedBindingException(bindingException);
}
}
return result;
try
{
BindParameter(parameterSets, argument, parameter,
ParameterBindingFlags.ShouldCoerceType | ParameterBindingFlags.DelayBindScriptBlock);
}
catch (ParameterBindingException pbex)
{
if (!DefaultParameterBindingInUse)
{
throw;
}
ThrowElaboratedBindingException(pbex);
}
}
/// <summary>
@@ -1259,17 +1175,6 @@ namespace System.Management.Automation
return result;
}
/// <summary>
/// Binds the specified parameters to the cmdlet.
/// </summary>
/// <param name="parameters">
/// The parameters to bind.
/// </param>
internal override Collection<CommandParameterInternal> BindParameters(Collection<CommandParameterInternal> parameters)
{
return BindParameters(uint.MaxValue, parameters);
}
/// <summary>
/// Binds the specified argument to the specified parameter using the appropriate
/// parameter binder. If the argument is of type ScriptBlock and the parameter takes
@@ -1815,7 +1720,7 @@ namespace System.Management.Automation
ReparseUnboundArguments();
UnboundArguments = BindParameters(_currentParameterSetFlag, UnboundArguments);
UnboundArguments = BindNamedParameters(_currentParameterSetFlag, UnboundArguments);
}
using (ParameterBinderBase.bindingTracer.TraceScope(
@@ -29,14 +29,17 @@ namespace System.Management.Automation
private Parameter _parameter;
private Argument _argument;
private bool _spaceAfterParameter;
private bool _fromHashtableSplatting;
internal bool SpaceAfterParameter { get { return _spaceAfterParameter; } }
internal bool SpaceAfterParameter => _spaceAfterParameter;
internal bool ParameterNameSpecified { get { return _parameter != null; } }
internal bool ParameterNameSpecified => _parameter != null;
internal bool ArgumentSpecified { get { return _argument != null; } }
internal bool ArgumentSpecified => _argument != null;
internal bool ParameterAndArgumentSpecified { get { return ParameterNameSpecified && ArgumentSpecified; } }
internal bool ParameterAndArgumentSpecified => ParameterNameSpecified && ArgumentSpecified;
internal bool FromHashtableSplatting => _fromHashtableSplatting;
/// <summary>
/// Gets and sets the string that represents parameter name, which does not include the '-' (dash).
@@ -111,7 +114,7 @@ namespace System.Management.Automation
/// <summary>
/// If an argument was specified and is to be splatted, returns true, otherwise false.
/// </summary>
internal bool ArgumentSplatted
internal bool ArgumentToBeSplatted
{
get { return _argument != null ? _argument.splatted : false; }
}
@@ -201,19 +204,22 @@ namespace System.Management.Automation
/// <param name="argumentAst">The ast of the argument value in the script.</param>
/// <param name="value">The argument value.</param>
/// <param name="spaceAfterParameter">Used in native commands to correctly handle -foo:bar vs. -foo: bar.</param>
/// <param name="fromSplatting">Indicate if this parameter-argument pair comes from splatting.</param>
internal static CommandParameterInternal CreateParameterWithArgument(
Ast parameterAst,
string parameterName,
string parameterText,
Ast argumentAst,
object value,
bool spaceAfterParameter)
bool spaceAfterParameter,
bool fromSplatting = false)
{
return new CommandParameterInternal
{
_parameter = new Parameter { ast = parameterAst, parameterName = parameterName, parameterText = parameterText },
_argument = new Argument { ast = argumentAst, value = value },
_spaceAfterParameter = spaceAfterParameter
_spaceAfterParameter = spaceAfterParameter,
_fromHashtableSplatting = fromSplatting,
};
}
@@ -37,24 +37,6 @@ namespace System.Management.Automation
#endregion ctor
/// <summary>
/// Override of parent class which should not be used.
/// </summary>
/// <param name="parameters">
/// The parameters to bind.
/// </param>
/// <remarks>
/// For any parameters that do not have a name, they are added to the command
/// line arguments for the command
/// </remarks>
internal override
Collection<CommandParameterInternal>
BindParameters(Collection<CommandParameterInternal> parameters)
{
Dbg.Assert(false, "this method should be used");
return null;
}
/// <summary>
/// Value of input format. This property should be read after binding of parameters.
/// </summary>
@@ -130,7 +130,7 @@ namespace System.Management.Automation
/// Or
/// The name of the argument matches more than one parameter.
/// </exception>
internal void ReparseUnboundArguments()
protected void ReparseUnboundArguments()
{
Collection<CommandParameterInternal> result = new Collection<CommandParameterInternal>();
@@ -260,6 +260,34 @@ namespace System.Management.Automation
UnboundArguments = result;
}
protected void InitUnboundArguments(Collection<CommandParameterInternal> arguments)
{
// Add the passed in arguments to the unboundArguments collection
Collection<CommandParameterInternal> paramsFromSplatting = null;
foreach (CommandParameterInternal argument in arguments)
{
if (argument.FromHashtableSplatting)
{
paramsFromSplatting ??= new Collection<CommandParameterInternal>();
paramsFromSplatting.Add(argument);
}
else
{
UnboundArguments.Add(argument);
}
}
// Move the arguments from hashtable splatting to the end of the unbound args list, so that
// the explicitly specified named arguments can supersede those from a hashtable splatting.
if (paramsFromSplatting != null)
{
foreach (CommandParameterInternal argument in paramsFromSplatting)
{
UnboundArguments.Add(argument);
}
}
}
private static bool IsSwitchAndSetValue(
string argumentName,
CommandParameterInternal argument,
@@ -447,7 +475,10 @@ namespace System.Management.Automation
/// <returns>
/// The arguments which are still not bound.
/// </returns>
internal abstract Collection<CommandParameterInternal> BindParameters(Collection<CommandParameterInternal> parameters);
internal virtual Collection<CommandParameterInternal> BindParameters(Collection<CommandParameterInternal> parameters)
{
throw new NotImplementedException();
}
/// <summary>
/// Bind the argument to the specified parameter.
@@ -514,6 +545,121 @@ namespace System.Management.Automation
return result;
}
/// <summary>
/// This is used by <see cref="BindNamedParameters"/> to validate and bind a given named parameter.
/// </summary>
protected virtual void BindNamedParameter(
uint parameterSets,
CommandParameterInternal argument,
MergedCompiledCommandParameter parameter)
{
BindParameter(parameterSets, argument, parameter, ParameterBindingFlags.ShouldCoerceType);
}
/// <summary>
/// Bind the named parameters from the specified argument collection,
/// for only the parameters in the specified parameter set.
/// </summary>
/// <param name="parameterSets">
/// The parameter set used to bind the arguments.
/// </param>
/// <param name="arguments">
/// The arguments that should be attempted to bind to the parameters of the specified parameter binder.
/// </param>
/// <exception cref="ParameterBindingException">
/// if multiple parameters are found matching the name.
/// or
/// if no match could be found.
/// or
/// If argument transformation fails.
/// or
/// The argument could not be coerced to the appropriate type for the parameter.
/// or
/// The parameter argument transformation, prerequisite, or validation failed.
/// or
/// If the binding to the parameter fails.
/// </exception>
protected Collection<CommandParameterInternal> BindNamedParameters(uint parameterSets, Collection<CommandParameterInternal> arguments)
{
Collection<CommandParameterInternal> result = new Collection<CommandParameterInternal>();
HashSet<string> boundExplicitNamedParams = null;
foreach (CommandParameterInternal argument in arguments)
{
if (!argument.ParameterNameSpecified)
{
result.Add(argument);
continue;
}
// We don't want to throw an exception yet because the parameter might be a positional argument,
// or in case of a cmdlet or an advanced function, it might match up to a dynamic parameter.
MergedCompiledCommandParameter parameter =
BindableParameters.GetMatchingParameter(
name: argument.ParameterName,
throwOnParameterNotFound: false,
tryExactMatching: true,
invocationInfo: new InvocationInfo(this.InvocationInfo.MyCommand, argument.ParameterExtent));
// If the parameter is not in the specified parameter set, throw a binding exception
if (parameter != null)
{
string formalParamName = parameter.Parameter.Name;
if (argument.FromHashtableSplatting)
{
boundExplicitNamedParams ??= new HashSet<string>(
BoundParameters.Keys,
StringComparer.OrdinalIgnoreCase);
if (boundExplicitNamedParams.Contains(formalParamName))
{
// This named parameter from splatting is also explicitly specified by the user,
// which was successfully bound, so we ignore the one from splatting because it
// is superceded by the explicit one. For example:
// $splat = @{ Path = $path1 }
// dir @splat -Path $path2
continue;
}
}
// Now check to make sure it hasn't already been
// bound by looking in the boundParameters collection
if (BoundParameters.ContainsKey(formalParamName))
{
ParameterBindingException bindingException =
new ParameterBindingException(
ErrorCategory.InvalidArgument,
this.InvocationInfo,
GetParameterErrorExtent(argument),
argument.ParameterName,
null,
null,
ParameterBinderStrings.ParameterAlreadyBound,
nameof(ParameterBinderStrings.ParameterAlreadyBound));
throw bindingException;
}
BindNamedParameter(parameterSets, argument, parameter);
}
else if (argument.ParameterName.Equals(Parser.VERBATIM_PARAMETERNAME, StringComparison.Ordinal))
{
// We sometimes send a magic parameter from a remote machine with the values referenced via
// a using expression ($using:x). We then access these values via PSBoundParameters, so
// "bind" them here.
DefaultParameterBinder.CommandLineParameters.SetImplicitUsingParameters(argument.ArgumentValue);
}
else
{
result.Add(argument);
}
}
return result;
}
/// <summary>
/// Binds the unbound arguments to positional parameters.
/// </summary>
@@ -359,6 +359,26 @@ namespace System.Management.Automation
return this;
}
/// <summary>
/// Adds a <see cref="CommandParameter"/> instance to the last added command.
/// </summary>
internal PSCommand AddParameter(CommandParameter parameter)
{
if (_currentCommand == null)
{
throw PSTraceSource.NewInvalidOperationException(PSCommandStrings.ParameterRequiresCommand,
new object[] { "PSCommand" });
}
if (_owner != null)
{
_owner.AssertChangesAreAccepted();
}
_currentCommand.Parameters.Add(parameter);
return this;
}
/// <summary>
/// Adds an argument to the last added command.
/// For example, to construct a command string "get-process | select-object name"
@@ -80,9 +80,10 @@ namespace System.Management.Automation.Runspaces
#endregion Public properties
#region Private Fields
#endregion Private Fields
/// <summary>
/// Gets whether the parameter was from splatting a Hashtable.
/// </summary>
private bool FromHashtableSplatting { get; set; }
#region Conversion from and to CommandParameterInternal
@@ -108,17 +109,14 @@ namespace System.Management.Automation.Runspaces
Diagnostics.Assert(name.Trim().Length != 1, "Parameter name has to have some non-whitespace characters in it");
}
if (internalParameter.ParameterAndArgumentSpecified)
{
return new CommandParameter(name, internalParameter.ArgumentValue);
}
CommandParameter result = internalParameter.ParameterAndArgumentSpecified
? new CommandParameter(name, internalParameter.ArgumentValue)
: name != null
? new CommandParameter(name)
: new CommandParameter(name: null, internalParameter.ArgumentValue);
if (name != null) // either a switch parameter or first part of parameter+argument
{
return new CommandParameter(name);
}
// either a positional argument or second part of parameter+argument
return new CommandParameter(null, internalParameter.ArgumentValue);
result.FromHashtableSplatting = internalParameter.FromHashtableSplatting;
return result;
}
internal static CommandParameterInternal ToCommandParameterInternal(CommandParameter publicParameter, bool forNativeCommand)
@@ -143,9 +141,12 @@ namespace System.Management.Automation.Runspaces
{
parameterText = forNativeCommand ? name : "-" + name;
return CommandParameterInternal.CreateParameterWithArgument(
/*parameterAst*/null, name, parameterText,
/*argumentAst*/null, value,
true);
parameterAst: null,
parameterName: name,
parameterText: parameterText,
argumentAst: null,
value: value,
spaceAfterParameter: true);
}
// if first character of name is '-', then we try to fake the original token
@@ -184,9 +185,13 @@ namespace System.Management.Automation.Runspaces
// name+value pair
return CommandParameterInternal.CreateParameterWithArgument(
/*parameterAst*/null, parameterName, parameterText,
/*argumentAst*/null, value,
spaceAfterParameter);
parameterAst: null,
parameterName,
parameterText,
argumentAst: null,
value,
spaceAfterParameter,
publicParameter.FromHashtableSplatting);
}
#endregion
@@ -233,34 +238,6 @@ namespace System.Management.Automation.Runspaces
}
#endregion
#region Win Blue Extensions
#if !CORECLR // PSMI Not Supported On CSS
internal CimInstance ToCimInstance()
{
CimInstance c = InternalMISerializer.CreateCimInstance("PS_Parameter");
CimProperty nameProperty = InternalMISerializer.CreateCimProperty("Name", this.Name,
Microsoft.Management.Infrastructure.CimType.String);
c.CimInstanceProperties.Add(nameProperty);
Microsoft.Management.Infrastructure.CimType cimType = CimConverter.GetCimType(this.Value.GetType());
CimProperty valueProperty;
if (cimType == Microsoft.Management.Infrastructure.CimType.Unknown)
{
valueProperty = InternalMISerializer.CreateCimProperty("Value", (object)PSMISerializer.Serialize(this.Value),
Microsoft.Management.Infrastructure.CimType.Instance);
}
else
{
valueProperty = InternalMISerializer.CreateCimProperty("Value", this.Value, cimType);
}
c.CimInstanceProperties.Add(valueProperty);
return c;
}
#endif
#endregion Win Blue Extensions
}
/// <summary>
@@ -1266,6 +1266,24 @@ namespace System.Management.Automation
}
}
/// <summary>
/// Adds a <see cref="CommandParameter"/> instance to the last added command.
/// </summary>
internal PowerShell AddParameter(CommandParameter parameter)
{
lock (_syncObject)
{
if (_psCommand.Commands.Count == 0)
{
throw PSTraceSource.NewInvalidOperationException(PowerShellStrings.ParameterRequiresCommand);
}
AssertChangesAreAccepted();
_psCommand.AddParameter(parameter);
return this;
}
}
/// <summary>
/// Adds a set of parameters to the last added command.
/// </summary>
@@ -4241,7 +4241,8 @@ namespace System.Management.Automation.Language
Expression.Constant(errorPos.Text),
Expression.Constant(arg),
Expression.Convert(GetCommandArgumentExpression(arg), typeof(object)),
ExpressionCache.Constant(spaceAfterParameter));
ExpressionCache.Constant(spaceAfterParameter),
ExpressionCache.Constant(false));
}
return Expression.Call(
@@ -174,7 +174,7 @@ namespace System.Management.Automation
}
}
if (cpi.ArgumentSplatted)
if (cpi.ArgumentToBeSplatted)
{
foreach (var splattedCpi in Splat(cpi.ArgumentValue, cpi.ArgumentAst))
{
@@ -338,8 +338,13 @@ namespace System.Management.Automation
}
yield return CommandParameterInternal.CreateParameterWithArgument(
splatAst, parameterName, parameterText,
splatAst, parameterValue, false);
parameterAst: splatAst,
parameterName: parameterName,
parameterText: parameterText,
argumentAst: splatAst,
value: parameterValue,
spaceAfterParameter: false,
fromSplatting: true);
}
}
else
@@ -748,7 +748,7 @@ namespace System.Management.Automation
foreach (var splattedParameter in PipelineOps.Splat(splattedValue, variableAst))
{
CommandParameter publicParameter = CommandParameter.FromCommandParameterInternal(splattedParameter);
_powershell.AddParameter(publicParameter.Name, publicParameter.Value);
_powershell.AddParameter(publicParameter);
}
}
@@ -77,16 +77,10 @@ namespace System.Management.Automation
internal void BindCommandLineParameters(Collection<CommandParameterInternal> arguments)
{
// Add the passed in arguments to the unboundArguments collection
foreach (CommandParameterInternal argument in arguments)
{
UnboundArguments.Add(argument);
}
InitUnboundArguments(arguments);
ReparseUnboundArguments();
// To support named parameters you just have un-comment the following line
UnboundArguments = BindParameters(UnboundArguments);
UnboundArguments = BindNamedParameters(uint.MaxValue, UnboundArguments);
ParameterBindingException parameterBindingError;
UnboundArguments =
@@ -136,75 +130,6 @@ namespace System.Management.Automation
return true;
}
/// <summary>
/// Binds the specified parameters to the shell function.
/// </summary>
/// <param name="arguments">
/// The arguments to bind.
/// </param>
internal override Collection<CommandParameterInternal> BindParameters(Collection<CommandParameterInternal> arguments)
{
Collection<CommandParameterInternal> result = new Collection<CommandParameterInternal>();
foreach (CommandParameterInternal argument in arguments)
{
if (!argument.ParameterNameSpecified)
{
result.Add(argument);
continue;
}
// We don't want to throw an exception yet because
// the parameter might be a positional argument
MergedCompiledCommandParameter parameter =
BindableParameters.GetMatchingParameter(
argument.ParameterName,
false, true,
new InvocationInfo(this.InvocationInfo.MyCommand, argument.ParameterExtent));
// If the parameter is not in the specified parameter set,
// throw a binding exception
if (parameter != null)
{
// Now check to make sure it hasn't already been
// bound by looking in the boundParameters collection
if (BoundParameters.ContainsKey(parameter.Parameter.Name))
{
ParameterBindingException bindingException =
new ParameterBindingException(
ErrorCategory.InvalidArgument,
this.InvocationInfo,
GetParameterErrorExtent(argument),
argument.ParameterName,
null,
null,
ParameterBinderStrings.ParameterAlreadyBound,
nameof(ParameterBinderStrings.ParameterAlreadyBound));
throw bindingException;
}
BindParameter(uint.MaxValue, argument, parameter, ParameterBindingFlags.ShouldCoerceType);
}
else if (argument.ParameterName.Equals(Language.Parser.VERBATIM_PARAMETERNAME, StringComparison.Ordinal))
{
// We sometimes send a magic parameter from a remote machine with the values referenced via
// a using expression ($using:x). We then access these values via PSBoundParameters, so
// "bind" them here.
DefaultParameterBinder.CommandLineParameters.SetImplicitUsingParameters(argument.ArgumentValue);
}
else
{
result.Add(argument);
}
}
return result;
}
/// <summary>
/// Takes the remaining arguments that haven't been bound, and binds
/// them to $args.
@@ -0,0 +1,370 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe "Hashtable Splatting Parameter Binding Tests" -Tags "CI" {
BeforeAll {
function SimpleTest {
param(
[Alias('Key')]
$Name,
$Path
)
"Key: $Name; Path: $Path; Args: $args"
}
}
Context "Basic Hashtable Splatting" {
It "works on cmdlet" {
$hash = @{ Verb = "Get"; OutVariable = "zoo" }
Get-Verb @hash > $null
$zoo | Should -BeOfType System.Management.Automation.VerbInfo
$zoo.Verb | Should -BeExactly 'Get'
}
It "works on simple function" {
$hash = @{ Name = "Hello"; Blah = "World" }
SimpleTest @hash | Should -BeExactly 'Key: Hello; Path: ; Args: -Blah: World'
$hash = @{ Name = "Hello"; Path = "World" }
SimpleTest @hash | Should -BeExactly 'Key: Hello; Path: World; Args: '
$hash = @{ Name = "Hello" }
SimpleTest @hash -Path "Yeah" | Should -BeExactly 'Key: Hello; Path: Yeah; Args: '
SimpleTest -Path "Yeah" @hash | Should -BeExactly 'Key: Hello; Path: Yeah; Args: '
$hash = @{ Key = "Hello" }
SimpleTest @hash | Should -BeExactly 'Key: Hello; Path: ; Args: '
}
It "works on ScriptBlock.GetPowerShell" {
$hash = @{ Verb = "Get"; OutVariable = "zoo" }
$ps = { param($hash) Get-Verb @hash; Get-Variable zoo }.GetPowerShell($hash)
try {
$result = $ps.Invoke()
$result[0] | Should -BeOfType System.Management.Automation.VerbInfo
$result[0].Verb | Should -BeExactly 'Get'
} finally {
$ps.Dispose()
}
}
It "works on steppable pipeline" {
$hash = @{ Verb = "Get"; OutVariable = "zoo" }
$sp = { Get-Verb @hash }.GetSteppablePipeline()
try {
$sp.Begin($false)
$result = $sp.Process()
$result | Should -BeOfType System.Management.Automation.VerbInfo
$result.Verb | Should -BeExactly 'Get'
$zoo | Should -BeOfType System.Management.Automation.VerbInfo
$zoo.Verb | Should -BeExactly 'Get'
$sp.End()
} finally {
$sp.Dispose()
}
}
}
Context "Explicitly specified named parameter supersedes the same one in Hashtable splatting" {
It "works with the same parameter name" {
## Regular use with cmdlet
$hash = @{ Verb = "Get"; OutVariable = "zoo" }
Get-Verb @hash -Verb "Send" > $null
$zoo | Should -BeOfType System.Management.Automation.VerbInfo
$zoo.Verb | Should -BeExactly "Send"
$zoo = $null
Get-Verb -Verb "Send" @hash > $null
$zoo | Should -BeOfType System.Management.Automation.VerbInfo
$zoo.Verb | Should -BeExactly "Send"
## GetPowerShell
$ps = { param($hash) Get-Verb @hash -Verb "Send"; Get-Variable zoo }.GetPowerShell($hash)
try {
$result = $ps.Invoke()
$result[0] | Should -BeOfType System.Management.Automation.VerbInfo
$result[0].Verb | Should -BeExactly 'Send'
} finally {
$ps.Dispose()
}
## Steppable pipeline
$zoo = $null
$sp = { Get-Verb @hash -Verb "Send" }.GetSteppablePipeline()
try {
$sp.Begin($false)
$result = $sp.Process()
$result | Should -BeOfType System.Management.Automation.VerbInfo
$result.Verb | Should -BeExactly 'Send'
$zoo | Should -BeOfType System.Management.Automation.VerbInfo
$zoo.Verb | Should -BeExactly 'Send'
$sp.End()
} finally {
$sp.Dispose()
}
## Regular use with simple function
$hash = @{ Name = "Hello"; Path = "World" }
SimpleTest @hash -Path "Yeah" | Should -BeExactly 'Key: Hello; Path: Yeah; Args: '
SimpleTest -Path "Yeah" @hash | Should -BeExactly 'Key: Hello; Path: Yeah; Args: '
$hash = @{ Name = "Hello"; Blah = "World" }
SimpleTest @hash -Name "Yeah" | Should -BeExactly 'Key: Yeah; Path: ; Args: -Blah: World'
}
It "works with the same alias name" {
## Regular use with cmdlet
$hash = @{ Verb = "Get"; ov = "zoo" }
Get-Verb @hash -Verb "Send" -ov "bar" > $null
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly "Send"
## GetPowerShell
$ps = { param($hash) Get-Verb @hash -Verb "Send" -ov "bar"; Get-Variable bar }.GetPowerShell($hash)
try {
$result = $ps.Invoke()
$result[0] | Should -BeOfType System.Management.Automation.VerbInfo
$result[0].Verb | Should -BeExactly 'Send'
} finally {
$ps.Dispose()
}
## Steppable pipeline
$bar = $null
$sp = { Get-Verb @hash -Verb "Send" -ov "bar" }.GetSteppablePipeline()
try {
$sp.Begin($false)
$result = $sp.Process()
$result | Should -BeOfType System.Management.Automation.VerbInfo
$result.Verb | Should -BeExactly 'Send'
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly 'Send'
$sp.End()
} finally {
$sp.Dispose()
}
## Regular use with simple function
$hash = @{ key = "Hello"; Path = "World" }
SimpleTest @hash -Key "Yeah" | Should -BeExactly 'Key: Yeah; Path: World; Args: '
}
It "works with parameter name and alias name mixed" {
## Regular use with cmdlet
$hash = @{ Verb = "Get"; OutVariable = "zoo" }
Get-Verb @hash -Verb "Send" -ov "bar" > $null
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly "Send"
## GetPowerShell
$ps = { param($hash) Get-Verb @hash -Verb "Send" -ov "bar"; Get-Variable bar }.GetPowerShell($hash)
try {
$result = $ps.Invoke()
$result[0] | Should -BeOfType System.Management.Automation.VerbInfo
$result[0].Verb | Should -BeExactly 'Send'
} finally {
$ps.Dispose()
}
## Steppable pipeline
$bar = $null
$sp = { Get-Verb @hash -Verb "Send" -ov "bar" }.GetSteppablePipeline()
try {
$sp.Begin($false)
$result = $sp.Process()
$result | Should -BeOfType System.Management.Automation.VerbInfo
$result.Verb | Should -BeExactly 'Send'
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly 'Send'
$sp.End()
} finally {
$sp.Dispose()
}
## Regular use with simple function
$hash = @{ Name = "Hello"; Path = "World" }
SimpleTest @hash -Key "Yeah" | Should -BeExactly 'Key: Yeah; Path: World; Args: '
}
It "works with unambiguous prefix and parameter name mixed - prefix explicitly specified" {
## Regular use with cmdlet
$hash = @{ Verb = "Get"; OutVariable = "zoo" }
Get-Verb @hash -Verb "Send" -outv "bar" > $null
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly "Send"
## GetPowerShell
$ps = { param($hash) Get-Verb @hash -Verb "Send" -outv "bar"; Get-Variable bar }.GetPowerShell($hash)
try {
$result = $ps.Invoke()
$result[0] | Should -BeOfType System.Management.Automation.VerbInfo
$result[0].Verb | Should -BeExactly 'Send'
} finally {
$ps.Dispose()
}
## Steppable pipeline
$bar = $null
$sp = { Get-Verb @hash -Verb "Send" -outv "bar" }.GetSteppablePipeline()
try {
$sp.Begin($false)
$result = $sp.Process()
$result | Should -BeOfType System.Management.Automation.VerbInfo
$result.Verb | Should -BeExactly 'Send'
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly 'Send'
$sp.End()
} finally {
$sp.Dispose()
}
## Regular use with simple function
$hash = @{ Name = "Hello"; Path = "World" }
SimpleTest @hash -n "Yeah" | Should -BeExactly 'Key: Yeah; Path: World; Args: '
}
It "works with unambiguous prefix and parameter name mixed - prefix in splatting hashtable" {
## Regular use with cmdlet
$hash = @{ Verb = "Get"; outv = "zoo" }
Get-Verb @hash -Verb "Send" -OutVariable "bar" > $null
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly "Send"
## GetPowerShell
$ps = { param($hash) Get-Verb @hash -Verb "Send" -OutVariable "bar"; Get-Variable bar }.GetPowerShell($hash)
try {
$result = $ps.Invoke()
$result[0] | Should -BeOfType System.Management.Automation.VerbInfo
$result[0].Verb | Should -BeExactly 'Send'
} finally {
$ps.Dispose()
}
## Steppable pipeline
$bar = $null
$sp = { Get-Verb @hash -Verb "Send" -OutVariable "bar" }.GetSteppablePipeline()
try {
$sp.Begin($false)
$result = $sp.Process()
$result | Should -BeOfType System.Management.Automation.VerbInfo
$result.Verb | Should -BeExactly 'Send'
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly 'Send'
$sp.End()
} finally {
$sp.Dispose()
}
## Regular use with simple function
$hash = @{ n = "Hello"; Path = "World" }
SimpleTest @hash -Name "Yeah" | Should -BeExactly 'Key: Yeah; Path: World; Args: '
}
It "works with unambiguous prefix and alias name mixed - prefix explicitly specified" {
## Regular use with cmdlet
$hash = @{ Verb = "Get"; ov = "zoo" }
Get-Verb @hash -Verb "Send" -outv "bar" > $null
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly "Send"
## GetPowerShell
$ps = { param($hash) Get-Verb @hash -Verb "Send" -outv "bar"; Get-Variable bar }.GetPowerShell($hash)
try {
$result = $ps.Invoke()
$result[0] | Should -BeOfType System.Management.Automation.VerbInfo
$result[0].Verb | Should -BeExactly 'Send'
} finally {
$ps.Dispose()
}
## Steppable pipeline
$bar = $null
$sp = { Get-Verb @hash -Verb "Send" -outv "bar" }.GetSteppablePipeline()
try {
$sp.Begin($false)
$result = $sp.Process()
$result | Should -BeOfType System.Management.Automation.VerbInfo
$result.Verb | Should -BeExactly 'Send'
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly 'Send'
$sp.End()
} finally {
$sp.Dispose()
}
## Regular use with simple function
$hash = @{ key = "Hello"; Path = "World" }
SimpleTest @hash -n "Yeah" | Should -BeExactly 'Key: Yeah; Path: World; Args: '
}
It "works with unambiguous prefix and alias name mixed - prefix in splatting hashtable" {
## Regular use with cmdlet
$hash = @{ Verb = "Get"; outv = "zoo" }
Get-Verb @hash -Verb "Send" -ov "bar" > $null
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly "Send"
## GetPowerShell
$ps = { param($hash) Get-Verb @hash -Verb "Send" -ov "bar"; Get-Variable bar }.GetPowerShell($hash)
try {
$result = $ps.Invoke()
$result[0] | Should -BeOfType System.Management.Automation.VerbInfo
$result[0].Verb | Should -BeExactly 'Send'
} finally {
$ps.Dispose()
}
## Steppable pipeline
$bar = $null
$sp = { Get-Verb @hash -Verb "Send" -ov "bar" }.GetSteppablePipeline()
try {
$sp.Begin($false)
$result = $sp.Process()
$result | Should -BeOfType System.Management.Automation.VerbInfo
$result.Verb | Should -BeExactly 'Send'
$zoo | Should -BeNullOrEmpty
$bar | Should -BeOfType System.Management.Automation.VerbInfo
$bar.Verb | Should -BeExactly 'Send'
$sp.End()
} finally {
$sp.Dispose()
}
## Regular use with simple function
$hash = @{ n = "Hello"; Path = "World" }
SimpleTest @hash -key "Yeah" | Should -BeExactly 'Key: Yeah; Path: World; Args: '
}
}
}