diff --git a/src/System.Management.Automation/engine/CommandBase.cs b/src/System.Management.Automation/engine/CommandBase.cs
index da2e97e516b..2e1c5cda56c 100644
--- a/src/System.Management.Automation/engine/CommandBase.cs
+++ b/src/System.Management.Automation/engine/CommandBase.cs
@@ -272,6 +272,20 @@ internal void InternalDispose(bool isDisposing)
namespace System.Management.Automation
{
+ #region NativeArgumentPassingStyle
+ ///
+ /// Defines the different native command argument parsing options.
+ ///
+ public enum NativeArgumentPassingStyle
+ {
+ /// Use legacy argument parsing via ProcessStartInfo.Arguments.
+ Legacy = 0,
+
+ /// Use new style argument parsing via ProcessStartInfo.ArgumentList.
+ Standard = 1
+ }
+ #endregion NativeArgumentPassingStyle
+
#region ErrorView
///
/// Defines the potential ErrorView options.
diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
index 5eeaf814e10..7e09e9997fa 100644
--- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
+++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
@@ -22,6 +22,7 @@ public class ExperimentalFeature
internal const string EngineSource = "PSEngine";
internal const string PSAnsiProgressFeatureName = "PSAnsiProgress";
+ internal const string PSNativeCommandArgumentPassingFeatureName = "PSNativeCommandArgumentPassing";
#endregion
@@ -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(engineFeatures);
diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs
index 257cb79ce7b..3294c19ac97 100644
--- a/src/System.Management.Automation/engine/InitialSessionState.cs
+++ b/src/System.Management.Automation/engine/InitialSessionState.cs
@@ -1308,6 +1308,32 @@ private static void MakeDisallowedEntriesPrivate(InitialSessionStateEntryColl
}
}
+ #region VariableHelper
+ ///
+ /// A helper for adding variables to session state.
+ /// Experimental features can be handled here.
+ ///
+ /// The variables to add to session state.
+ private void AddVariables(IEnumerable 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
+
///
/// Creates an initial session state from a PSSC configuration file.
///
@@ -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 proxyFunction in CommandMetadata.GetRestrictedCommands(SessionCapabilities.RemoteServer))
@@ -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;
@@ -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);
@@ -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);
@@ -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());
@@ -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.
@@ -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
diff --git a/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs b/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs
index e0c643c3d3b..1c74043b00a 100644
--- a/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs
+++ b/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs
@@ -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;
@@ -82,7 +83,7 @@ internal void BindParameters(Collection 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)
{
@@ -130,7 +131,7 @@ internal void BindParameters(Collection parameters)
stringConstantType = StringConstantType.DoubleQuoted;
}
- AppendOneNativeArgument(Context, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, stringConstantType);
+ AppendOneNativeArgument(Context, parameter, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, stringConstantType);
}
}
}
@@ -151,6 +152,65 @@ internal string Arguments
private readonly StringBuilder _arguments = new StringBuilder();
+ internal string[] ArgumentList
+ {
+ get
+ {
+ return _argumentList.ToArray();
+ }
+ }
+
+ ///
+ /// 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".
+ ///
+ /// The parameter associated with the operation.
+ /// The value used with parameter.
+ 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 _argumentList = new List();
+
+ ///
+ /// Gets a value indicating whether to use an ArgumentList or string for arguments when invoking a native executable.
+ ///
+ 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(
+ 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
@@ -161,24 +221,27 @@ internal string Arguments
/// each of which will be stringized.
///
/// Execution context instance.
+ /// The parameter associated with the operation.
/// The object to append.
/// If the argument was an array literal, the Ast, otherwise null.
/// True if the argument occurs after --%.
/// Bare, SingleQuoted, or DoubleQuoted.
- 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
{
@@ -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)
@@ -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
{
@@ -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
@@ -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);
}
@@ -257,8 +345,9 @@ private void AppendOneNativeArgument(ExecutionContext context, object obj, Array
/// On Unix, do globbing as appropriate, otherwise just append .
///
/// The argument that possibly needs expansion.
+ /// The parameter associated with the operation.
/// Bare, SingleQuoted, or DoubleQuoted.
- private void PossiblyGlobArg(string arg, StringConstantType stringConstantType)
+ private void PossiblyGlobArg(string arg, CommandParameterInternal parameter, StringConstantType stringConstantType)
{
var argExpanded = false;
@@ -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;
@@ -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;
}
}
@@ -351,6 +444,7 @@ private void PossiblyGlobArg(string arg, StringConstantType stringConstantType)
if (!argExpanded)
{
_arguments.Append(arg);
+ AddToArgumentList(parameter, arg);
}
}
diff --git a/src/System.Management.Automation/engine/NativeCommandParameterBinderController.cs b/src/System.Management.Automation/engine/NativeCommandParameterBinderController.cs
index ecdd4554abe..933c71f5503 100644
--- a/src/System.Management.Automation/engine/NativeCommandParameterBinderController.cs
+++ b/src/System.Management.Automation/engine/NativeCommandParameterBinderController.cs
@@ -38,6 +38,28 @@ internal string Arguments
}
}
+ ///
+ /// Gets the value of the command arguments as an array of strings.
+ ///
+ internal string[] ArgumentList
+ {
+ get
+ {
+ return ((NativeCommandParameterBinder)DefaultParameterBinder).ArgumentList;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether to use the new API for StartInfo.
+ ///
+ internal bool UseArgumentList
+ {
+ get
+ {
+ return ((NativeCommandParameterBinder)DefaultParameterBinder).UseArgumentList;
+ }
+ }
+
///
/// Passes the binding directly through to the parameter binder.
/// It does no verification against metadata.
@@ -49,8 +71,7 @@ internal string Arguments
/// Ignored.
///
///
- /// True if the parameter was successfully bound. Any error condition
- /// produces an exception.
+ /// True if the parameter was successfully bound. Any error condition produces an exception.
///
internal override bool BindParameter(
CommandParameterInternal argument,
diff --git a/src/System.Management.Automation/engine/NativeCommandProcessor.cs b/src/System.Management.Automation/engine/NativeCommandProcessor.cs
index 96ad84a6c46..6899a1de0ef 100644
--- a/src/System.Management.Automation/engine/NativeCommandProcessor.cs
+++ b/src/System.Management.Automation/engine/NativeCommandProcessor.cs
@@ -357,7 +357,7 @@ internal override void ProcessRecord()
///
/// Indicate if we have called 'NotifyBeginApplication()' on the host, so that
- /// we can call the counterpart 'NotifyEndApplication' as approriate.
+ /// we can call the counterpart 'NotifyEndApplication' as appropriate.
///
private bool _hasNotifiedBeginApplication;
@@ -1157,10 +1157,35 @@ private ProcessStartInfo GetProcessStartInfo(bool redirectOutput, bool redirectE
startInfo.CreateNoWindow = mpc.NonInteractive;
}
- startInfo.Arguments = NativeParameterBinderController.Arguments;
-
ExecutionContext context = this.Command.Context;
+ // We provide the user a way to select the new behavior via a new preference variable
+ using (ParameterBinderBase.bindingTracer.TraceScope("BIND NAMED native application line args [{0}]", this.Path))
+ {
+ if (!NativeParameterBinderController.UseArgumentList)
+ {
+ using (ParameterBinderBase.bindingTracer.TraceScope("BIND argument [{0}]", NativeParameterBinderController.Arguments))
+ {
+ startInfo.Arguments = NativeParameterBinderController.Arguments;
+ }
+ }
+ else
+ {
+ // Use new API for running native application
+ int position = 0;
+ foreach (string nativeArgument in NativeParameterBinderController.ArgumentList)
+ {
+ if (nativeArgument != null)
+ {
+ using (ParameterBinderBase.bindingTracer.TraceScope("BIND cmd line arg [{0}] to position [{1}]", nativeArgument, position++))
+ {
+ startInfo.ArgumentList.Add(nativeArgument);
+ }
+ }
+ }
+ }
+ }
+
// Start command in the current filesystem directory
string rawPath =
context.EngineSessionState.GetNamespaceCurrentLocation(
diff --git a/src/System.Management.Automation/engine/SpecialVariables.cs b/src/System.Management.Automation/engine/SpecialVariables.cs
index ba757722e91..0f95349664f 100644
--- a/src/System.Management.Automation/engine/SpecialVariables.cs
+++ b/src/System.Management.Automation/engine/SpecialVariables.cs
@@ -257,6 +257,11 @@ internal static class SpecialVariables
#endregion Preference Variables
+ // Native command argument passing style
+ internal const string NativeArgumentPassing = "PSNativeCommandArgumentPassing";
+
+ internal static readonly VariablePath NativeArgumentPassingVarPath = new VariablePath(NativeArgumentPassing);
+
internal const string ErrorView = "ErrorView";
internal static readonly VariablePath ErrorViewVarPath = new VariablePath(ErrorView);
diff --git a/src/System.Management.Automation/resources/RunspaceInit.resx b/src/System.Management.Automation/resources/RunspaceInit.resx
index ac900465d7d..a2497961901 100644
--- a/src/System.Management.Automation/resources/RunspaceInit.resx
+++ b/src/System.Management.Automation/resources/RunspaceInit.resx
@@ -189,6 +189,9 @@
If true, WhatIf is considered to be enabled for all commands.
+
+ Dictates how arguments are passed to native executables.
+
Dictates the limit of enumeration on formatting IEnumerable objects
diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1
index 390b461fb90..e57b6357c70 100644
--- a/test/powershell/Host/ConsoleHost.Tests.ps1
+++ b/test/powershell/Host/ConsoleHost.Tests.ps1
@@ -235,11 +235,16 @@ Describe "ConsoleHost unit tests" -tags "Feature" {
$observed | Should -BeExactly "h-llo"
}
- It "Empty command should fail" {
- & $powershell -noprofile -c ''
+ It "Missing command should fail" {
+ & $powershell -noprofile -c
$LASTEXITCODE | Should -Be 64
}
+ It "Empty space command should succeed" {
+ & $powershell -noprofile -c '' | Should -BeNullOrEmpty
+ $LASTEXITCODE | Should -Be 0
+ }
+
It "Whitespace command should succeed" {
& $powershell -noprofile -c ' ' | Should -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
diff --git a/test/powershell/Host/Startup.Tests.ps1 b/test/powershell/Host/Startup.Tests.ps1
index 6ce9dfc463d..3c49dbf3152 100644
--- a/test/powershell/Host/Startup.Tests.ps1
+++ b/test/powershell/Host/Startup.Tests.ps1
@@ -90,7 +90,7 @@ Describe "Validate start of console host" -Tag CI {
Remove-Item $profileDataFile -Force
}
- $loadedAssemblies = & "$PSHOME/pwsh" -noprofile -command '([System.AppDomain]::CurrentDomain.GetAssemblies()).manifestmodule | Where-Object { $_.Name -notlike ""<*>"" } | ForEach-Object { $_.Name }'
+ $loadedAssemblies = & "$PSHOME/pwsh" -noprofile -command '([System.AppDomain]::CurrentDomain.GetAssemblies()).manifestmodule | Where-Object { $_.Name -notlike "<*>" } | ForEach-Object { $_.Name }'
}
It "No new assemblies are loaded" {
diff --git a/test/powershell/Language/Scripting/NativeExecution/NativeCommandArguments.Tests.ps1 b/test/powershell/Language/Scripting/NativeExecution/NativeCommandArguments.Tests.ps1
index df71542a6bd..b279f04c9e8 100644
--- a/test/powershell/Language/Scripting/NativeExecution/NativeCommandArguments.Tests.ps1
+++ b/test/powershell/Language/Scripting/NativeExecution/NativeCommandArguments.Tests.ps1
@@ -1,70 +1,112 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
-Describe "Native Command Arguments" -tags "CI" {
- # When passing arguments to native commands, quoted segments that contain
- # spaces need to be quoted with '"' characters when they are passed to the
- # native command (or to bash or sh on Linux).
- #
- # This test checks that the proper quoting is occuring by passing arguments
- # to the testexe native command and looking at how it got the arguments.
- It "Should handle quoted spaces correctly" {
- $a = 'a"b c"d'
- $lines = testexe -echoargs $a 'a"b c"d' a"b c"d
- $lines.Count | Should -Be 3
- $lines[0] | Should -BeExactly 'Arg 0 is '
- $lines[1] | Should -BeExactly 'Arg 1 is '
- $lines[2] | Should -BeExactly 'Arg 2 is '
+[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
+param()
+Describe "Will error correctly if an attempt to set variable to improper value" {
+ It "will error when setting variable incorrectly" {
+ if ($EnabledExperimentalFeatures -contains 'PSNativeCommandArgumentPassing') {
+ { $global:PSNativeCommandArgumentPassing = "zzz" } | Should -Throw -ExceptionType System.Management.Automation.ArgumentTransformationMetadataException
+ }
+ else {
+ Set-Test -State skipped -Because "Experimental feature 'PSNativeCommandArgumentPassing' is not enabled"
+ }
}
+}
- # In order to pass '"' characters so they are actually part of command line
- # arguments for native commands, they need to be escaped with a '\' (this
- # is in addition to the '`' escaping needed inside '"' quoted strings in
- # PowerShell).
- #
- # This functionality was broken in PowerShell 5.0 and 5.1, so this test
- # will fail on those versions unless the fix is backported to them.
- #
- # This test checks that the proper quoting and escaping is occurring by
- # passing arguments with escaped quotes to the testexe native command and
- # looking at how it got the arguments.
- It "Should handle spaces between escaped quotes" {
- $lines = testexe -echoargs 'a\"b c\"d' "a\`"b c\`"d"
- $lines.Count | Should -Be 2
- $lines[0] | Should -BeExactly 'Arg 0 is '
- $lines[1] | Should -BeExactly 'Arg 1 is '
- }
+foreach ( $argumentListValue in "Standard","Legacy" ) {
+ $PSNativeCommandArgumentPassing = $argumentListValue
+ Describe "Native Command Arguments (${PSNativeCommandArgumentPassing})" -tags "CI" {
+ # When passing arguments to native commands, quoted segments that contain
+ # spaces need to be quoted with '"' characters when they are passed to the
+ # native command (or to bash or sh on Linux).
+ #
+ # This test checks that the proper quoting is occuring by passing arguments
+ # to the testexe native command and looking at how it got the arguments.
+ It "Should handle quoted spaces correctly (ArgumentList=${PSNativeCommandArgumentPassing})" {
+ $a = 'a"b c"d'
+ $lines = testexe -echoargs $a 'a"b c"d' a"b c"d "a'b c'd"
+ $lines.Count | Should -Be 4
+ if (($EnabledExperimentalFeatures -contains 'PSNativeCommandArgumentPassing') -and $PSNativeCommandArgumentPassing -ne "Legacy") {
+ $lines[0] | Should -BeExactly 'Arg 0 is '
+ $lines[1] | Should -BeExactly 'Arg 1 is '
+ }
+ else {
+ $lines[0] | Should -BeExactly 'Arg 0 is '
+ $lines[1] | Should -BeExactly 'Arg 1 is '
+ }
+ $lines[2] | Should -BeExactly 'Arg 2 is '
+ $lines[3] | Should -BeExactly 'Arg 3 is '
+ }
- It "Should correctly quote paths with spaces: " -TestCases @(
- @{arguments = "'.\test 1\' `".\test 2\`"" ; expected = @(".\test 1\",".\test 2\")},
- @{arguments = "'.\test 1\\\' `".\test 2\\`""; expected = @(".\test 1\\\",".\test 2\\")}
- ) {
- param($arguments, $expected)
- $lines = Invoke-Expression "testexe -echoargs $arguments"
- $lines.Count | Should -Be $expected.Count
- for ($i = 0; $i -lt $lines.Count; $i++) {
- $lines[$i] | Should -BeExactly "Arg $i is <$($expected[$i])>"
+ # In order to pass '"' characters so they are actually part of command line
+ # arguments for native commands, they need to be escaped with a '\' (this
+ # is in addition to the '`' escaping needed inside '"' quoted strings in
+ # PowerShell).
+ #
+ # This functionality was broken in PowerShell 5.0 and 5.1, so this test
+ # will fail on those versions unless the fix is backported to them.
+ #
+ # This test checks that the proper quoting and escaping is occurring by
+ # passing arguments with escaped quotes to the testexe native command and
+ # looking at how it got the arguments.
+ It "Should handle spaces between escaped quotes (ArgumentList=${PSNativeCommandArgumentPassing})" {
+ $lines = testexe -echoargs 'a\"b c\"d' "a\`"b c\`"d"
+ $lines.Count | Should -Be 2
+ if (($EnabledExperimentalFeatures -contains 'PSNativeCommandArgumentPassing') -and $PSNativeCommandArgumentPassing -ne "Legacy") {
+ $lines[0] | Should -BeExactly 'Arg 0 is '
+ $lines[1] | Should -BeExactly 'Arg 1 is '
+ }
+ else {
+ $lines[0] | Should -BeExactly 'Arg 0 is '
+ $lines[1] | Should -BeExactly 'Arg 1 is '
+ }
}
- }
- It "Should handle PowerShell arrays with or without spaces correctly: " -TestCases @(
- @{arguments = "1,2"; expected = @("1,2")}
- @{arguments = "1,2,3"; expected = @("1,2,3")}
- @{arguments = "1, 2"; expected = "1,", "2"}
- @{arguments = "1 ,2"; expected = "1", ",2"}
- @{arguments = "1 , 2"; expected = "1", ",", "2"}
- @{arguments = "1, 2,3"; expected = "1,", "2,3"}
- @{arguments = "1 ,2,3"; expected = "1", ",2,3"}
- @{arguments = "1 , 2,3"; expected = "1", ",", "2,3"}
- ) {
- param($arguments, $expected)
- $lines = @(Invoke-Expression "testexe -echoargs $arguments")
- $lines.Count | Should -Be $expected.Count
- for ($i = 0; $i -lt $expected.Count; $i++) {
- $lines[$i] | Should -BeExactly "Arg $i is <$($expected[$i])>"
+ It "Should correctly quote paths with spaces (ArgumentList=${PSNativeCommandArgumentPassing}): " -TestCases @(
+ @{arguments = "'.\test 1\' `".\test 2\`"" ; expected = @(".\test 1\",".\test 2\")},
+ @{arguments = "'.\test 1\\\' `".\test 2\\`""; expected = @(".\test 1\\\",".\test 2\\")}
+ ) {
+ param($arguments, $expected)
+ $lines = Invoke-Expression "testexe -echoargs $arguments"
+ $lines.Count | Should -Be $expected.Count
+ for ($i = 0; $i -lt $lines.Count; $i++) {
+ $lines[$i] | Should -BeExactly "Arg $i is <$($expected[$i])>"
+ }
+ }
+
+ It "Should handle arguments that include commas without spaces (windbg example)" {
+ $lines = testexe -echoargs -k com:port=\\devbox\pipe\debug,pipe,resets=0,reconnect
+ $lines.Count | Should -Be 2
+ $lines[0] | Should -BeExactly "Arg 0 is <-k>"
+ $lines[1] | Should -BeExactly "Arg 1 is "
+ }
+
+ It "Should handle DOS style arguments" {
+ $lines = testexe -echoargs /arg1 /c:"a string"
+ $lines.Count | Should -Be 2
+ $lines[0] | Should -BeExactly "Arg 0 is "
+ $lines[1] | Should -BeExactly "Arg 1 is "
+ }
+
+ It "Should handle PowerShell arrays with or without spaces correctly (ArgumentList=${PSNativeCommandArgumentPassing}): " -TestCases @(
+ @{arguments = "1,2"; expected = @("1,2")}
+ @{arguments = "1,2,3"; expected = @("1,2,3")}
+ @{arguments = "1, 2"; expected = "1,", "2"}
+ @{arguments = "1 ,2"; expected = "1", ",2"}
+ @{arguments = "1 , 2"; expected = "1", ",", "2"}
+ @{arguments = "1, 2,3"; expected = "1,", "2,3"}
+ @{arguments = "1 ,2,3"; expected = "1", ",2,3"}
+ @{arguments = "1 , 2,3"; expected = "1", ",", "2,3"}
+ ) {
+ param($arguments, $expected)
+ $lines = @(Invoke-Expression "testexe -echoargs $arguments")
+ $lines.Count | Should -Be $expected.Count
+ for ($i = 0; $i -lt $expected.Count; $i++) {
+ $lines[$i] | Should -BeExactly "Arg $i is <$($expected[$i])>"
+ }
}
}
}
-
Describe 'PSPath to native commands' {
BeforeAll {
$featureEnabled = $EnabledExperimentalFeatures.Contains('PSNativePSPathResolution')
diff --git a/test/powershell/Language/Scripting/NativeExecution/NativeStreams.Tests.ps1 b/test/powershell/Language/Scripting/NativeExecution/NativeStreams.Tests.ps1
index 59702938375..755ea27f794 100644
--- a/test/powershell/Language/Scripting/NativeExecution/NativeStreams.Tests.ps1
+++ b/test/powershell/Language/Scripting/NativeExecution/NativeStreams.Tests.ps1
@@ -12,9 +12,9 @@ Describe "Native streams behavior with PowerShell" -Tags 'CI' {
$error.Clear()
$command = [string]::Join('', @(
- '[Console]::Error.Write(\"foo`n`nbar`n`nbaz\"); ',
- '[Console]::Error.Write(\"middle\"); ',
- '[Console]::Error.Write(\"foo`n`nbar`n`nbaz\")'
+ '[Console]::Error.Write("foo`n`nbar`n`nbaz"); ',
+ '[Console]::Error.Write("middle"); ',
+ '[Console]::Error.Write("foo`n`nbar`n`nbaz")'
))
$out = & $powershell -noprofile -command $command 2>&1