Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/System.Management.Automation/engine/CommonCommandParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,22 @@ public string PipelineVariable
set { _commandRuntime.PipelineVariable = value; }
}

/// <summary>
/// Gets or sets the Splat parameter for the cmdlet.
/// </summary>
/// <remarks>
/// This parameter allows scripters to splat mulitple parameters at once from one
/// or more IDictionary instances. These can be passed in inline, or referenced
/// from a property, or returned from a command, method invocation, or subexpression.
/// </remarks>
[Parameter]
[Experimental("PSCommonSplatParameter", ExperimentAction.Show)]
public IDictionary[] Splat
{
get { return _commandRuntime.Splat; }
set { _commandRuntime.Splat = value; }
}

#endregion parameters

private MshCommandRuntime _commandRuntime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,11 @@ static ExperimentalFeature()
#if !UNIX
new ExperimentalFeature(
name: "PSWindowsPowerShellCompatibility",
description: "Load non-PSCore-compartible modules into Windows PowerShell over PS Remoting")
description: "Load non-PSCore-compartible modules into Windows PowerShell over PS Remoting"),
#endif
new ExperimentalFeature(
name: "PSCommonSplatParameter",
description: "Add -splat common parameter to PowerShell for inline splatting"),
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

Expand Down
5 changes: 5 additions & 0 deletions src/System.Management.Automation/engine/MshCommandRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ internal bool IsStopping
get { return (this.PipelineProcessor != null && this.PipelineProcessor.Stopping); }
}

/// <summary>
/// Gets or sets the objects that will be splat into the invocation.
/// </summary>
internal IDictionary[] Splat { get; set; }

#region Write

// Trust: WriteObject needs to respect EmitTrustCategory
Expand Down
136 changes: 100 additions & 36 deletions src/System.Management.Automation/engine/ParameterBinderController.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Text;

Expand Down Expand Up @@ -138,48 +140,65 @@ internal void ReparseUnboundArguments()
{
CommandParameterInternal argument = UnboundArguments[index];

// If the parameter name is not specified, or if it is specified _and_ there is an
// argument, we have nothing to reparse for this argument.
if (!argument.ParameterNameSpecified || argument.ArgumentSpecified)
// If the parameter name is not specified, we have nothing to reparse for this argument.
if (!argument.ParameterNameSpecified)
{
result.Add(argument);
continue;
}

Diagnostics.Assert(argument.ParameterNameSpecified && !argument.ArgumentSpecified,
"At this point, we only process parameters with no arguments");

// Now check the argument name with the binder.

string parameterName = argument.ParameterName;
// Now check the parameter name with the binder.
MergedCompiledCommandParameter matchingParameter =
_bindableParameters.GetMatchingParameter(
parameterName,
false,
true,
new InvocationInfo(this.InvocationInfo.MyCommand, argument.ParameterExtent));
argument.ParameterName,
throwOnParameterNotFound: false,
tryExactMatching: true,
new InvocationInfo(InvocationInfo.MyCommand, argument.ParameterExtent));

// If we can't find a match, just add the argument as it was and continue.
if (matchingParameter == null)
{
// Since we couldn't find a match, just add the argument as it was
// and continue
result.Add(argument);
continue;
}

// Now that we know we have a single match for the parameter name,
// see if we can figure out what the argument value for the parameter is.
// Update the argument with its matching parameter name.
argument.ParameterName = matchingParameter.Parameter.Name;

// If there is an argument already, we have nothing to reparse.
if (argument.ArgumentSpecified)
{
result.Add(argument);

// If this is the "-splat" common parameter, splat the argument values into the
// command as well.
if (argument.ParameterName.Equals(nameof(CommonParameters.Splat)))
{
foreach (var splattedArgument in SplatCommonParameterValue(argument))
{
result.Add(splattedArgument);
}
}

continue;
}

Diagnostics.Assert(argument.ParameterNameSpecified && !argument.ArgumentSpecified,
"At this point, we only process parameters with no arguments");

// If its a bool or switch parameter, then set the value to true and continue
// Now that we know we have a single match for the parameter name, but no
// argument, so let's see if we can figure out what the argument value for
// the parameter is.

if (IsSwitchAndSetValue(parameterName, argument, matchingParameter.Parameter))
// If its a switch parameter, then set the value to true and continue.
if (matchingParameter.Parameter.Type == typeof(SwitchParameter))
{
argument.SetArgumentValue(null, SwitchParameter.Present);
result.Add(argument);
continue;
}

// Since it's not a bool or a SwitchParameter we need to check the next
// argument.
// Since it's not a SwitchParameter we need to check the next argument.

if (UnboundArguments.Count - 1 > index)
{
Expand Down Expand Up @@ -229,13 +248,24 @@ internal void ReparseUnboundArguments()
continue;
}

// The next argument appears to be the value for this parameter. Set the value,
// increment the index and continue

// The next argument appears to be the value for this parameter, so increment
// the counter and set the value in the argument.
++index;
argument.ParameterName = matchingParameter.Parameter.Name;
argument.SetArgumentValue(nextArgument.ArgumentAst, nextArgument.ArgumentValue);

// Bind the argument to the command.
result.Add(argument);

// If this is the "-splat" common parameter, splat the argument values into the
// command as well.
if (argument.ParameterName.Equals(nameof(CommonParameters.Splat)))
{
foreach (var splattedArgument in SplatCommonParameterValue(argument))
{
result.Add(splattedArgument);
}
}
}
else
{
Expand All @@ -260,21 +290,55 @@ internal void ReparseUnboundArguments()
UnboundArguments = result;
}

private static bool IsSwitchAndSetValue(
string argumentName,
CommandParameterInternal argument,
CompiledCommandParameter matchingParameter)
private IEnumerable<CommandParameterInternal> SplatCommonParameterValue(CommandParameterInternal splatParameter)
{
bool result = false;

if (matchingParameter.Type == typeof(SwitchParameter))
switch (splatParameter.ArgumentValue)
{
argument.ParameterName = argumentName;
argument.SetArgumentValue(null, SwitchParameter.Present);
result = true;
}
case IDictionary dictionaryArgument:
// If an IDictionary value was passed into the splat parameter, splat that value as is into the command.
foreach (var splattedCpi in PipelineOps.Splat(dictionaryArgument, splatParameter.ArgumentAst))
{
yield return splattedCpi;
}

return result;
break;

case Array arrayArgument:
// If an array was passed into the splat parameter, splat in the individual dictionaries in that array.
int splatIndex = 0;
var arrayLiteralAst = splatParameter.ArgumentAst as ArrayLiteralAst;
foreach (var splattedValue in arrayArgument)
{
if (splattedValue is IDictionary)
{
foreach (var splattedCpi in PipelineOps.Splat(splattedValue, arrayLiteralAst?.Elements[splatIndex++] ?? splatParameter.ArgumentAst))
{
yield return splattedCpi;
}

continue;
}

goto default;
}

break;

default:
ParameterBindingException exception = new ParameterBindingException(
ErrorCategory.InvalidArgument,
InvocationInfo,
GetParameterErrorExtent(splatParameter),
nameof(CommonParameters.Splat),
typeof(IDictionary[]),
splatParameter.ArgumentValue?.GetType(),
ParameterBinderStrings.CannotConvertArgument,
"CannotConvertArgument",
splatParameter.ArgumentValue,
string.Empty);

throw exception;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Management.Automation.Internal;
Expand Down Expand Up @@ -200,6 +201,7 @@ static ReflectionParameterBinder()
s_setterMethods.TryAdd(Tuple.Create(typeof(CommonParameters), "OutVariable"), (o, v) => ((CommonParameters)o).OutVariable = (string)v);
s_setterMethods.TryAdd(Tuple.Create(typeof(CommonParameters), "OutBuffer"), (o, v) => ((CommonParameters)o).OutBuffer = (int)v);
s_setterMethods.TryAdd(Tuple.Create(typeof(CommonParameters), "PipelineVariable"), (o, v) => ((CommonParameters)o).PipelineVariable = (string)v);
s_setterMethods.TryAdd(Tuple.Create(typeof(CommonParameters), "Splat"), (o, v) => ((CommonParameters)o).Splat = (IDictionary[])v);
}

private static readonly ConcurrentDictionary<Tuple<Type, string>, Func<object, object>> s_getterMethods
Expand Down
24 changes: 20 additions & 4 deletions src/System.Management.Automation/engine/cmdlet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,26 @@ public static HashSet<string> CommonParameters
private static Lazy<HashSet<string>> s_commonParameters = new Lazy<HashSet<string>>(
() =>
{
return new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
"Verbose", "Debug", "ErrorAction", "WarningAction", "InformationAction",
"ErrorVariable", "WarningVariable", "OutVariable",
"OutBuffer", "PipelineVariable", "InformationVariable" };
if (!ExperimentalFeature.EnabledExperimentalFeatureNames.Contains("PSCommonSplatParameter"))
{
return new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Verbose", "Debug", "ErrorAction", "WarningAction", "InformationAction",
"ErrorVariable", "WarningVariable", "OutVariable",
"OutBuffer", "PipelineVariable", "InformationVariable"
};
}

// If you add new common parameters, please manually sort them into this list
// so that they appear in sorted order to anyone who wants to view the common
// parameters in PowerShell by invoking the following command:
// [System.Management.Automation.Cmdlet]::CommonParameters
return new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Debug", "ErrorAction", "ErrorVariable", "InformationAction",
"InformationVariable", "OutBuffer", "OutVariable", "PipelineVariable",
"Splat", "Verbose", "WarningAction", "WarningVariable"
};
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@
<data name="BaseCmdletInformation" xml:space="preserve">
<value> This cmdlet supports the common parameters: Verbose, Debug,
ErrorAction, ErrorVariable, WarningAction, WarningVariable,
OutBuffer, PipelineVariable, and OutVariable. For more information, see
OutBuffer, Splat, PipelineVariable, and OutVariable. For more information, see
about_CommonParameters (https://go.microsoft.com/fwlink/?LinkID=113216). </value>
</data>
<data name="ParameterRequired" xml:space="preserve">
Expand Down
Loading