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