Skip to content

Commit

Permalink
Native invocation using ArgumentList (#14692)
Browse files Browse the repository at this point in the history
Co-authored-by: Ilya <darpa@yandex.ru>
  • Loading branch information
JamesWTruher and iSazonov committed Apr 1, 2021
1 parent cf6876e commit a520e2c
Show file tree
Hide file tree
Showing 12 changed files with 335 additions and 84 deletions.
14 changes: 14 additions & 0 deletions src/System.Management.Automation/engine/CommandBase.cs
Expand Up @@ -272,6 +272,20 @@ internal void InternalDispose(bool isDisposing)

namespace System.Management.Automation
{
#region NativeArgumentPassingStyle
/// <summary>
/// Defines the different native command argument parsing options.
/// </summary>
public enum NativeArgumentPassingStyle
{
/// <summary>Use legacy argument parsing via ProcessStartInfo.Arguments.</summary>
Legacy = 0,

/// <summary>Use new style argument parsing via ProcessStartInfo.ArgumentList.</summary>
Standard = 1
}
#endregion NativeArgumentPassingStyle

#region ErrorView
/// <summary>
/// Defines the potential ErrorView options.
Expand Down
Expand Up @@ -22,6 +22,7 @@ public class ExperimentalFeature

internal const string EngineSource = "PSEngine";
internal const string PSAnsiProgressFeatureName = "PSAnsiProgress";
internal const string PSNativeCommandArgumentPassingFeatureName = "PSNativeCommandArgumentPassing";

#endregion

Expand Down Expand Up @@ -136,6 +137,9 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: PSAnsiProgressFeatureName,
description: "Enable lightweight progress bar that leverages ANSI codes for rendering"),
new ExperimentalFeature(
name: PSNativeCommandArgumentPassingFeatureName,
description: "Use ArgumentList when invoking a native command"),
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

Expand Down
56 changes: 47 additions & 9 deletions src/System.Management.Automation/engine/InitialSessionState.cs
Expand Up @@ -1308,6 +1308,32 @@ private static void MakeDisallowedEntriesPrivate<T>(InitialSessionStateEntryColl
}
}

#region VariableHelper
/// <summary>
/// A helper for adding variables to session state.
/// Experimental features can be handled here.
/// </summary>
/// <param name="variables">The variables to add to session state.</param>
private void AddVariables(IEnumerable<SessionStateVariableEntry> variables)
{
Variables.Add(variables);

// If the PSNativeCommandArgumentPassing feature is enabled, create the variable which controls the behavior
// Since the BuiltInVariables list is static, and this should be done dynamically
// we need to do this here.
if (ExperimentalFeature.IsEnabled("PSNativeCommandArgumentPassing"))
{
Variables.Add(
new SessionStateVariableEntry(
SpecialVariables.NativeArgumentPassing,
NativeArgumentPassingStyle.Standard,
RunspaceInit.NativeCommandArgumentPassingDescription,
ScopedItemOptions.None,
new ArgumentTypeConverterAttribute(typeof(NativeArgumentPassingStyle))));
}
}
#endregion

/// <summary>
/// Creates an initial session state from a PSSC configuration file.
/// </summary>
Expand Down Expand Up @@ -1413,7 +1439,7 @@ private static InitialSessionState CreateRestrictedForRemoteServer()
}

// Add built-in variables.
iss.Variables.Add(BuiltInVariables);
iss.AddVariables(BuiltInVariables);

// wrap some commands in a proxy function to restrict their parameters
foreach (KeyValuePair<string, CommandMetadata> proxyFunction in CommandMetadata.GetRestrictedCommands(SessionCapabilities.RemoteServer))
Expand Down Expand Up @@ -1477,7 +1503,7 @@ public static InitialSessionState Create()
// be causing test failures - i suspect due to lack test isolation - brucepay Mar 06/2008
#if false
// Add the default variables and make them private...
iss.Variables.Add(BuiltInVariables);
iss.AddVariables(BuiltInVariables);
foreach (SessionStateVariableEntry v in iss.Variables)
{
v.Visibility = SessionStateEntryVisibility.Private;
Expand All @@ -1500,7 +1526,7 @@ public static InitialSessionState CreateDefault()

InitialSessionState ss = new InitialSessionState();

ss.Variables.Add(BuiltInVariables);
ss.AddVariables(BuiltInVariables);
ss.Commands.Add(new SessionStateApplicationEntry("*"));
ss.Commands.Add(new SessionStateScriptEntry("*"));
ss.Commands.Add(BuiltInFunctions);
Expand Down Expand Up @@ -1567,7 +1593,7 @@ public static InitialSessionState CreateDefault2()
{
InitialSessionState ss = new InitialSessionState();

ss.Variables.Add(BuiltInVariables);
ss.AddVariables(BuiltInVariables);
ss.Commands.Add(new SessionStateApplicationEntry("*"));
ss.Commands.Add(new SessionStateScriptEntry("*"));
ss.Commands.Add(BuiltInFunctions);
Expand Down Expand Up @@ -1608,7 +1634,7 @@ public InitialSessionState Clone()
{
InitialSessionState ss = new InitialSessionState();

ss.Variables.Add(this.Variables.Clone());
ss.AddVariables(this.Variables.Clone());
ss.EnvironmentVariables.Add(this.EnvironmentVariables.Clone());
ss.Commands.Add(this.Commands.Clone());
ss.Assemblies.Add(this.Assemblies.Clone());
Expand Down Expand Up @@ -4272,7 +4298,13 @@ .FORWARDHELPCATEGORY Cmdlet
}
else {
$pagerCommand = 'less'
$pagerArgs = '-Ps""Page %db?B of %D:.\. Press h for help or q to quit\.$""'
# PSNativeCommandArgumentPassing arguments should be constructed differently.
if ($EnabledExperimentalFeatures -contains 'PSNativeCommandArgumentPassing') {
$pagerArgs = '-s','-P','Page %db?B of %D:.\. Press h for help or q to quit\.'
}
else {
$pagerArgs = '-Ps""Page %db?B of %D:.\. Press h for help or q to quit\.$""'
}
}
# Respect PAGER environment variable which allows user to specify a custom pager.
Expand Down Expand Up @@ -4312,10 +4344,16 @@ .FORWARDHELPCATEGORY Cmdlet
$consoleWidth = [System.Math]::Max([System.Console]::WindowWidth, 20)
if ($pagerArgs) {
# Supply pager arguments to an application without any PowerShell parsing of the arguments.
# Start the pager arguments directly if the PSNativeCommandArgumentPassing feature is enabled.
# Otherwise, supply pager arguments to an application without any PowerShell parsing of the arguments.
# Leave environment variable to help user debug arguments supplied in $env:PAGER.
$env:__PSPAGER_ARGS = $pagerArgs
$help | Out-String -Stream -Width ($consoleWidth - 1) | & $pagerCommand --% %__PSPAGER_ARGS%
if ($EnabledExperimentalFeatures -contains 'PSNativeCommandArgumentPassing') {
$help | Out-String -Stream -Width ($consoleWidth - 1) | & $pagerCommand $pagerArgs
}
else {
$env:__PSPAGER_ARGS = $pagerArgs
$help | Out-String -Stream -Width ($consoleWidth - 1) | & $pagerCommand --% %__PSPAGER_ARGS%
}
}
else {
$help | Out-String -Stream -Width ($consoleWidth - 1) | & $pagerCommand
Expand Down
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -82,7 +83,7 @@ internal void BindParameters(Collection<CommandParameterInternal> parameters)
if (parameter.ParameterNameSpecified)
{
Diagnostics.Assert(!parameter.ParameterText.Contains(' '), "Parameters cannot have whitespace");
PossiblyGlobArg(parameter.ParameterText, StringConstantType.BareWord);
PossiblyGlobArg(parameter.ParameterText, parameter, StringConstantType.BareWord);

if (parameter.SpaceAfterParameter)
{
Expand Down Expand Up @@ -130,7 +131,7 @@ internal void BindParameters(Collection<CommandParameterInternal> parameters)
stringConstantType = StringConstantType.DoubleQuoted;
}

AppendOneNativeArgument(Context, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, stringConstantType);
AppendOneNativeArgument(Context, parameter, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, stringConstantType);
}
}
}
Expand All @@ -151,6 +152,65 @@ internal string Arguments

private readonly StringBuilder _arguments = new StringBuilder();

internal string[] ArgumentList
{
get
{
return _argumentList.ToArray();
}
}

/// <summary>
/// Add an argument to the ArgumentList.
/// We may need to construct the argument out of the parameter text and the argument
/// in the case that we have a parameter that appears as "-switch:value".
/// </summary>
/// <param name="parameter">The parameter associated with the operation.</param>
/// <param name="argument">The value used with parameter.</param>
internal void AddToArgumentList(CommandParameterInternal parameter, string argument)
{
if (parameter.ParameterNameSpecified && parameter.ParameterText.EndsWith(":"))
{
if (argument != parameter.ParameterText)
{
_argumentList.Add(parameter.ParameterText + argument);
}
}
else
{
_argumentList.Add(argument);
}
}

private List<string> _argumentList = new List<string>();

/// <summary>
/// Gets a value indicating whether to use an ArgumentList or string for arguments when invoking a native executable.
/// </summary>
internal bool UseArgumentList
{
get
{
if (ExperimentalFeature.IsEnabled("PSNativeCommandArgumentPassing"))
{
try
{
// This will default to the new behavior if it is set to anything other than Legacy
var preference = LanguagePrimitives.ConvertTo<NativeArgumentPassingStyle>(
Context.GetVariableValue(new VariablePath(SpecialVariables.NativeArgumentPassing), NativeArgumentPassingStyle.Standard));
return preference != NativeArgumentPassingStyle.Legacy;
}
catch
{
// The value is not convertable send back true
return true;
}
}

return false;
}
}

#endregion internal members

#region private members
Expand All @@ -161,24 +221,27 @@ internal string Arguments
/// each of which will be stringized.
/// </summary>
/// <param name="context">Execution context instance.</param>
/// <param name="parameter">The parameter associated with the operation.</param>
/// <param name="obj">The object to append.</param>
/// <param name="argArrayAst">If the argument was an array literal, the Ast, otherwise null.</param>
/// <param name="sawVerbatimArgumentMarker">True if the argument occurs after --%.</param>
/// <param name="stringConstantType">Bare, SingleQuoted, or DoubleQuoted.</param>
private void AppendOneNativeArgument(ExecutionContext context, object obj, ArrayLiteralAst argArrayAst, bool sawVerbatimArgumentMarker, StringConstantType stringConstantType)
private void AppendOneNativeArgument(ExecutionContext context, CommandParameterInternal parameter, object obj, ArrayLiteralAst argArrayAst, bool sawVerbatimArgumentMarker, StringConstantType stringConstantType)
{
IEnumerator list = LanguagePrimitives.GetEnumerator(obj);

Diagnostics.Assert((argArrayAst == null) || obj is object[] && ((object[])obj).Length == argArrayAst.Elements.Count, "array argument and ArrayLiteralAst differ in number of elements");
Diagnostics.Assert((argArrayAst == null) || (obj is object[] && ((object[])obj).Length == argArrayAst.Elements.Count), "array argument and ArrayLiteralAst differ in number of elements");

int currentElement = -1;
string separator = string.Empty;
do
{
string arg;
object currentObj;
if (list == null)
{
arg = PSObject.ToStringParser(context, obj);
currentObj = obj;
}
else
{
Expand All @@ -187,7 +250,8 @@ private void AppendOneNativeArgument(ExecutionContext context, object obj, Array
break;
}

arg = PSObject.ToStringParser(context, ParserOps.Current(null, list));
currentObj = ParserOps.Current(null, list);
arg = PSObject.ToStringParser(context, currentObj);

currentElement += 1;
if (currentElement != 0)
Expand All @@ -198,12 +262,16 @@ private void AppendOneNativeArgument(ExecutionContext context, object obj, Array

if (!string.IsNullOrEmpty(arg))
{
// Only add the separator to the argument string rather than adding a separator to the ArgumentList.
_arguments.Append(separator);

if (sawVerbatimArgumentMarker)
{
arg = Environment.ExpandEnvironmentVariables(arg);
_arguments.Append(arg);

// we need to split the argument on spaces
_argumentList.AddRange(arg.Split(' ', StringSplitOptions.RemoveEmptyEntries));
}
else
{
Expand All @@ -227,10 +295,12 @@ private void AppendOneNativeArgument(ExecutionContext context, object obj, Array
if (stringConstantType == StringConstantType.DoubleQuoted)
{
_arguments.Append(ResolvePath(arg, Context));
AddToArgumentList(parameter, ResolvePath(arg, Context));
}
else
{
_arguments.Append(arg);
AddToArgumentList(parameter, arg);
}

// need to escape all trailing backslashes so the native command receives it correctly
Expand All @@ -244,10 +314,28 @@ private void AppendOneNativeArgument(ExecutionContext context, object obj, Array
}
else
{
PossiblyGlobArg(arg, stringConstantType);
if (argArrayAst != null && UseArgumentList)
{
// We have a literal array, so take the extent, break it on spaces and add them to the argument list.
foreach (string element in argArrayAst.Extent.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
PossiblyGlobArg(element, parameter, stringConstantType);
}

break;
}
else
{
PossiblyGlobArg(arg, parameter, stringConstantType);
}
}
}
}
else if (UseArgumentList && currentObj != null)
{
// add empty strings to arglist, but not nulls
AddToArgumentList(parameter, arg);
}
}
while (list != null);
}
Expand All @@ -257,8 +345,9 @@ private void AppendOneNativeArgument(ExecutionContext context, object obj, Array
/// On Unix, do globbing as appropriate, otherwise just append <paramref name="arg"/>.
/// </summary>
/// <param name="arg">The argument that possibly needs expansion.</param>
/// <param name="parameter">The parameter associated with the operation.</param>
/// <param name="stringConstantType">Bare, SingleQuoted, or DoubleQuoted.</param>
private void PossiblyGlobArg(string arg, StringConstantType stringConstantType)
private void PossiblyGlobArg(string arg, CommandParameterInternal parameter, StringConstantType stringConstantType)
{
var argExpanded = false;

Expand Down Expand Up @@ -311,10 +400,12 @@ private void PossiblyGlobArg(string arg, StringConstantType stringConstantType)
_arguments.Append('"');
_arguments.Append(expandedPath);
_arguments.Append('"');
AddToArgumentList(parameter, expandedPath);
}
else
{
_arguments.Append(expandedPath);
AddToArgumentList(parameter, expandedPath);
}

argExpanded = true;
Expand All @@ -331,12 +422,14 @@ private void PossiblyGlobArg(string arg, StringConstantType stringConstantType)
if (string.Equals(arg, "~"))
{
_arguments.Append(home);
AddToArgumentList(parameter, home);
argExpanded = true;
}
else if (arg.StartsWith("~/", StringComparison.OrdinalIgnoreCase))
{
var replacementString = home + arg.Substring(1);
_arguments.Append(replacementString);
AddToArgumentList(parameter, replacementString);
argExpanded = true;
}
}
Expand All @@ -351,6 +444,7 @@ private void PossiblyGlobArg(string arg, StringConstantType stringConstantType)
if (!argExpanded)
{
_arguments.Append(arg);
AddToArgumentList(parameter, arg);
}
}

Expand Down

0 comments on commit a520e2c

Please sign in to comment.