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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: "PSSubsystemPluginModel",
description: "A plugin model for registering and un-registering PowerShell subsystems"),
new ExperimentalFeature(
name: "PSEscapeDoubleQuotedStringForNativeExecutables",
description: "Alternative approach for escaping quotes when calling a native executable"),
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,28 @@ internal void BindParameters(Collection<CommandParameterInternal> parameters)
break;
}

// Prior to PSNativePSPathResolution experimental feature, a single quote worked the same as a double quote
// so if the feature is not enabled, we treat any quotes as double quotes. When this feature is no longer
// experimental, this code here needs to be removed.
if (!ExperimentalFeature.IsEnabled("PSNativePSPathResolution") && stringConstantType == StringConstantType.SingleQuoted)
// We need to search the string for a double quote.
// If the double quote is not preceded by a "\", we need to add one
string valueString = argValue as string;
if ( ExperimentalFeature.IsEnabled("PSEscapeDoubleQuotedStringForNativeExecutables") && !string.IsNullOrEmpty(valueString) && valueString.IndexOf('"') >= 0)
{
stringConstantType = StringConstantType.DoubleQuoted;
StringBuilder parameterValue = new StringBuilder(valueString);
for(int i = valueString.Length - 1; i >= 0; i--)
{
if (i == 0)
{
if (valueString[0] == '"')
{
parameterValue.Insert(0,'\\');
}
}
else if (parameterValue[i] == '"' && parameterValue[i - 1] != '\\')
{
parameterValue.Insert(i, '\\');
}
}

argValue = parameterValue.ToString();
}

AppendOneNativeArgument(Context, argValue, arrayLiteralAst, sawVerbatimArgumentMarker, stringConstantType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,26 @@ private ProcessStartInfo GetProcessStartInfo(bool redirectOutput, bool redirectE
startInfo.CreateNoWindow = mpc.NonInteractive;
}

startInfo.Arguments = NativeParameterBinderController.Arguments;
if (ExperimentalFeature.IsEnabled("PSEscapeDoubleQuotedStringForNativeExecutables"))
{
foreach(var p in arguments)
{
if (p.ParameterExtent.Text != "")
{
startInfo.ArgumentList.Add(p.ParameterExtent.Text);
}
string argValue = p.ArgumentValue as string;
if (argValue != null && ! string.IsNullOrEmpty(argValue))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty (but non-null, of course) arguments are totally legitimate for command lines, so IMO there's no need of filtering them out.

{
startInfo.ArgumentList.Add(argValue);
}
}

}
else
{
startInfo.Arguments = NativeParameterBinderController.Arguments;
}

ExecutionContext context = this.Command.Context;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe "Native Command Arguments" -tags "CI" {
BeforeAll {
[bool]$skipQuoteTests = ! (Get-ExperimentalFeature PSEscapeDoubleQuotedStringForNativeExecutables).Enabled
}
# 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).
Expand All @@ -11,11 +14,43 @@ Describe "Native Command Arguments" -tags "CI" {
$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>'
if ( $skipQuoteTests ) {
$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 <ab cd>'
}
else {
$lines[0] | Should -BeExactly 'Arg 0 is <a"b c"d>'
$lines[1] | Should -BeExactly 'Arg 1 is <a"b c"d>'
# the following is the only example of where the parser removes the quotes
# this is because the whole string is not quoted
$lines[2] | Should -BeExactly 'Arg 2 is <ab cd>'
$lines[3] | Should -BeExactly 'Arg 3 is <a"b c"d>'
}
}

# tests for the new experimental feature of escaping double quotes
It "Should handle example: '<argument>'" -Skip:$skipQuoteTests -TestCases @(
@{ argument = '"This is" "a test-1"'; expected = '"This is" "a test-1"' }
@{ argument = """This is"" ""a test-2""" ; expected = '"This is" "a test-2"'}
@{ argument = '"this" is "a test"'; expected = '"this" is "a test"' }
@{ argument = '""""this is a test'; expected = '""""this is a test' }
@{ argument = '"""this is a test'; expected = '"""this is a test' }
@{ argument = '`"this is a test'; expected = '`"this is a test' }
@{ argument = "`"this is a test (first quote with ``)"; expected = '"this is a test (first quote with `)' }
) {
param ( $argument, $expected )
$lines = testexe -echoargs $argument
$lines.Count | Should -Be 1
$expected = 'Arg 0 is <{0}>' -f $expected
@($lines)[0] | Should -BeExactly $expected
}

# The following tests should work with or without the experimental feature
# which checks to see if there is already a "\" in the string and then
# does not add the escape
#
# 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
Expand Down