Native invocation using ArgumentList (#14692)

Co-authored-by: Ilya <darpa@yandex.ru>
This commit is contained in:
James Truher [MSFT] 2021-04-01 14:54:10 -07:00 committed by GitHub
parent cf6876e716
commit a520e2c982
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 338 additions and 87 deletions

View file

@ -272,6 +272,20 @@ namespace System.Management.Automation.Internal
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.

View file

@ -22,6 +22,7 @@ namespace System.Management.Automation
internal const string EngineSource = "PSEngine";
internal const string PSAnsiProgressFeatureName = "PSAnsiProgress";
internal const string PSNativeCommandArgumentPassingFeatureName = "PSNativeCommandArgumentPassing";
#endregion
@ -136,6 +137,9 @@ namespace System.Management.Automation
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);

View file

@ -1308,6 +1308,32 @@ namespace System.Management.Automation.Runspaces
}
}
#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>
@ -1413,7 +1439,7 @@ namespace System.Management.Automation.Runspaces
}
// 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))
@ -1477,7 +1503,7 @@ namespace System.Management.Automation.Runspaces
// 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 @@ namespace System.Management.Automation.Runspaces
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 @@ namespace System.Management.Automation.Runspaces
{
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 @@ namespace System.Management.Automation.Runspaces
{
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 @@ param(
}
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 @@ param(
$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

View file

@ -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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
stringConstantType = StringConstantType.DoubleQuoted;
}
AppendOneNativeArgument(Context, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, stringConstantType);
AppendOneNativeArgument(Context, parameter, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, stringConstantType);
}
}
}
@ -151,6 +152,65 @@ namespace System.Management.Automation
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
@ -161,24 +221,27 @@ namespace System.Management.Automation
/// 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
{
@ -187,7 +250,8 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
}
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 @@ namespace System.Management.Automation
/// 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;
@ -311,10 +400,12 @@ namespace System.Management.Automation
_arguments.Append('"');
_arguments.Append(expandedPath);
_arguments.Append('"');
AddToArgumentList(parameter, expandedPath);
}
else
{
_arguments.Append(expandedPath);
AddToArgumentList(parameter, expandedPath);
}
argExpanded = true;
@ -331,12 +422,14 @@ namespace System.Management.Automation
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 @@ namespace System.Management.Automation
if (!argExpanded)
{
_arguments.Append(arg);
AddToArgumentList(parameter, arg);
}
}

View file

@ -38,6 +38,28 @@ namespace System.Management.Automation
}
}
/// <summary>
/// Gets the value of the command arguments as an array of strings.
/// </summary>
internal string[] ArgumentList
{
get
{
return ((NativeCommandParameterBinder)DefaultParameterBinder).ArgumentList;
}
}
/// <summary>
/// Gets a value indicating whether to use the new API for StartInfo.
/// </summary>
internal bool UseArgumentList
{
get
{
return ((NativeCommandParameterBinder)DefaultParameterBinder).UseArgumentList;
}
}
/// <summary>
/// Passes the binding directly through to the parameter binder.
/// It does no verification against metadata.
@ -49,8 +71,7 @@ namespace System.Management.Automation
/// Ignored.
/// </param>
/// <returns>
/// 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.
/// </returns>
internal override bool BindParameter(
CommandParameterInternal argument,

View file

@ -357,7 +357,7 @@ namespace System.Management.Automation
/// <summary>
/// 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.
/// </summary>
private bool _hasNotifiedBeginApplication;
@ -1157,10 +1157,35 @@ namespace System.Management.Automation
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(

View file

@ -257,6 +257,11 @@ namespace System.Management.Automation
#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);

View file

@ -189,6 +189,9 @@
<data name="WhatIfPreferenceDescription" xml:space="preserve">
<value>If true, WhatIf is considered to be enabled for all commands.</value>
</data>
<data name="NativeCommandArgumentPassingDescription" xml:space="preserve">
<value>Dictates how arguments are passed to native executables.</value>
</data>
<data name="FormatEnumerationLimitDescription" xml:space="preserve">
<value>Dictates the limit of enumeration on formatting IEnumerable objects</value>
</data>

View file

@ -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

View file

@ -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" {

View file

@ -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 <ab cd>'
$lines[1] | Should -BeExactly 'Arg 1 is <ab cd>'
$lines[2] | Should -BeExactly 'Arg 2 is <ab cd>'
}
# 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 <a"b c"d>'
$lines[1] | Should -BeExactly 'Arg 1 is <a"b c"d>'
}
It "Should correctly quote paths with spaces: <arguments>" -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])>"
[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
}
}
It "Should handle PowerShell arrays with or without spaces correctly: <arguments>" -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])>"
else {
Set-Test -State skipped -Because "Experimental feature 'PSNativeCommandArgumentPassing' is not enabled"
}
}
}
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 <a"b c"d>'
$lines[1] | Should -BeExactly 'Arg 1 is <a"b c"d>'
}
else {
$lines[0] | Should -BeExactly 'Arg 0 is <ab cd>'
$lines[1] | Should -BeExactly 'Arg 1 is <ab cd>'
}
$lines[2] | Should -BeExactly 'Arg 2 is <ab cd>'
$lines[3] | Should -BeExactly 'Arg 3 is <a''b c''d>'
}
# 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 <a\"b c\"d>'
$lines[1] | Should -BeExactly 'Arg 1 is <a\"b c\"d>'
}
else {
$lines[0] | Should -BeExactly 'Arg 0 is <a"b c"d>'
$lines[1] | Should -BeExactly 'Arg 1 is <a"b c"d>'
}
}
It "Should correctly quote paths with spaces (ArgumentList=${PSNativeCommandArgumentPassing}): <arguments>" -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 <com:port=\\devbox\pipe\debug,pipe,resets=0,reconnect>"
}
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 </arg1>"
$lines[1] | Should -BeExactly "Arg 1 is </c:a string>"
}
It "Should handle PowerShell arrays with or without spaces correctly (ArgumentList=${PSNativeCommandArgumentPassing}): <arguments>" -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')

View file

@ -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