diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
index 7750585c0..fc4f4e389 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
@@ -156,23 +156,19 @@ private static PSCommand BuildPSCommandFromArguments(string command, IReadOnlyLi
return new PSCommand().AddCommand(command);
}
- // We are forced to use a hack here so that we can reuse PowerShell's parameter binding
+ // HACK: We use AddScript instead of AddArgument/AddParameter to reuse Powershell parameter binding logic.
+ // We quote the command parameter so that expressions can still be used in the arguments.
var sb = new StringBuilder()
- .Append("& ")
- .Append(StringEscaping.SingleQuoteAndEscape(command));
+ .Append('&')
+ .Append('"')
+ .Append(command)
+ .Append('"');
foreach (string arg in arguments)
{
- sb.Append(' ');
-
- if (StringEscaping.PowerShellArgumentNeedsEscaping(arg))
- {
- sb.Append(StringEscaping.SingleQuoteAndEscape(arg));
- }
- else
- {
- sb.Append(arg);
- }
+ sb
+ .Append(' ')
+ .Append(ArgumentEscaping.Escape(arg));
}
return new PSCommand().AddScript(sb.ToString());
diff --git a/src/PowerShellEditorServices/Utility/ArgumentUtils.cs b/src/PowerShellEditorServices/Utility/ArgumentUtils.cs
new file mode 100644
index 000000000..dd73f4d52
--- /dev/null
+++ b/src/PowerShellEditorServices/Utility/ArgumentUtils.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text;
+using System.Management.Automation.Language;
+
+namespace Microsoft.PowerShell.EditorServices.Utility
+{
+ internal static class ArgumentEscaping
+ {
+ ///
+ /// Escape a PowerShell argument while still making it able to be evaluated in AddScript.
+ ///
+ /// NOTE: This does not "sanitize" parameters, e.g., a pipe in one argument might affect another argument.
+ /// This is intentional to give flexibility to specifying arguments.
+ /// It also does not try to fix invalid PowerShell syntax, e.g., a single quote in a string literal.
+ ///
+ public static string Escape(string Arg)
+ {
+ // if argument is a scriptblock return as-is
+ if (Arg.StartsWith("{") && Arg.EndsWith("}"))
+ {
+ return Arg;
+ }
+
+ // If argument has a space enclose it in quotes unless it is already quoted
+ if (Arg.Contains(" "))
+ {
+ if (Arg.StartsWith("\"") && Arg.EndsWith("\"") || Arg.StartsWith("'") && Arg.EndsWith("'"))
+ {
+ return Arg;
+ }
+
+ return "\"" + Arg + "\"";
+ }
+
+ return Arg;
+ }
+ }
+}
diff --git a/src/PowerShellEditorServices/Utility/PathUtils.cs b/src/PowerShellEditorServices/Utility/PathUtils.cs
index cdfed6df1..4342034b9 100644
--- a/src/PowerShellEditorServices/Utility/PathUtils.cs
+++ b/src/PowerShellEditorServices/Utility/PathUtils.cs
@@ -38,48 +38,22 @@ public static string NormalizePathSeparators(string path)
return string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator);
}
- public static string WildcardEscape(string path)
- {
- return WildcardPattern.Escape(path);
- }
-
///
/// Return the given path with all PowerShell globbing characters escaped,
/// plus optionally the whitespace.
///
/// The path to process.
/// Specify True to escape spaces in the path, otherwise False.
- /// The path with [ and ] escaped.
+ /// The path with *, ?, [, and ] escaped, including spaces if required
internal static string WildcardEscapePath(string path, bool escapeSpaces = false)
{
- var sb = new StringBuilder();
- for (int i = 0; i < path.Length; i++)
- {
- char curr = path[i];
- switch (curr)
- {
- // Escape '[', ']', '?' and '*' with '`'
- case '[':
- case ']':
- case '*':
- case '?':
- case '`':
- sb.Append('`').Append(curr);
- break;
+ var wildcardEscapedPath = WildcardPattern.Escape(path);
- default:
- // Escape whitespace if required
- if (escapeSpaces && char.IsWhiteSpace(curr))
- {
- sb.Append('`').Append(curr);
- break;
- }
- sb.Append(curr);
- break;
- }
+ if (escapeSpaces)
+ {
+ wildcardEscapedPath = wildcardEscapedPath.Replace(" ", "` ");
}
-
- return sb.ToString();
+ return wildcardEscapedPath;
}
}
}
diff --git a/src/PowerShellEditorServices/Utility/StringEscaping.cs b/src/PowerShellEditorServices/Utility/StringEscaping.cs
deleted file mode 100644
index 5736a9aad..000000000
--- a/src/PowerShellEditorServices/Utility/StringEscaping.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Microsoft.PowerShell.EditorServices.Utility
-{
- internal static class StringEscaping
- {
- public static StringBuilder SingleQuoteAndEscape(string s)
- {
- return new StringBuilder(s.Length)
- .Append("'")
- .Append(s.Replace("'", "''"))
- .Append("'");
- }
-
- public static bool PowerShellArgumentNeedsEscaping(string argument)
- {
- foreach (char c in argument)
- {
- switch (c)
- {
- case '\'':
- case '"':
- case '|':
- case '&':
- case ';':
- case ':':
- case char w when char.IsWhiteSpace(w):
- return true;
- }
- }
-
- return false;
- }
- }
-}
diff --git a/test/PowerShellEditorServices.Test.Shared/scriptassets/[Truly] b&d Name_4_script.ps1 b/test/PowerShellEditorServices.Test.Shared/scriptassets/[Truly] b&d `N$(){}me_4_script.ps1
similarity index 100%
rename from test/PowerShellEditorServices.Test.Shared/scriptassets/[Truly] b&d Name_4_script.ps1
rename to test/PowerShellEditorServices.Test.Shared/scriptassets/[Truly] b&d `N$(){}me_4_script.ps1
diff --git a/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs b/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs
index 495166e09..abc1605de 100644
--- a/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs
+++ b/test/PowerShellEditorServices.Test/Session/PathEscapingTests.cs
@@ -2,16 +2,12 @@
// Licensed under the MIT License.
using Xunit;
-using System.IO;
-using Microsoft.PowerShell.EditorServices.Services;
using Microsoft.PowerShell.EditorServices.Utility;
namespace Microsoft.PowerShell.EditorServices.Test.Session
{
public class PathEscapingTests
{
- private const string ScriptAssetPath = @"..\..\..\..\PowerShellEditorServices.Test.Shared\scriptassets";
-
[Trait("Category", "PathEscaping")]
[Theory]
[InlineData("DebugTest.ps1", "DebugTest.ps1")]
@@ -53,68 +49,5 @@ public void CorrectlyWildcardEscapesPaths_Spaces(string unescapedPath, string es
string extensionEscapedPath = PathUtils.WildcardEscapePath(unescapedPath, escapeSpaces: true);
Assert.Equal(escapedPath, extensionEscapedPath);
}
-
- [Trait("Category", "PathEscaping")]
- [Theory]
- [InlineData("DebugTest.ps1", "'DebugTest.ps1'")]
- [InlineData("../../DebugTest.ps1", "'../../DebugTest.ps1'")]
- [InlineData("C:\\Users\\me\\Documents\\DebugTest.ps1", "'C:\\Users\\me\\Documents\\DebugTest.ps1'")]
- [InlineData("/home/me/Documents/weird&folder/script.ps1", "'/home/me/Documents/weird&folder/script.ps1'")]
- [InlineData("./path/with some/spaces", "'./path/with some/spaces'")]
- [InlineData("C:\\path\\with[some]brackets\\file.ps1", "'C:\\path\\with[some]brackets\\file.ps1'")]
- [InlineData("C:\\look\\an*\\here.ps1", "'C:\\look\\an*\\here.ps1'")]
- [InlineData("/Users/me/Documents/?here.ps1", "'/Users/me/Documents/?here.ps1'")]
- [InlineData("/Brackets [and s]paces/path.ps1", "'/Brackets [and s]paces/path.ps1'")]
- [InlineData("/file path/that isn't/normal/", "'/file path/that isn''t/normal/'")]
- [InlineData("/CJK.chars/脚本/hello.ps1", "'/CJK.chars/脚本/hello.ps1'")]
- [InlineData("/CJK chars/脚本/[hello].ps1", "'/CJK chars/脚本/[hello].ps1'")]
- [InlineData("C:\\Animal s\\утка\\quack.ps1", "'C:\\Animal s\\утка\\quack.ps1'")]
- [InlineData("C:\\&nimals\\утка\\qu*ck?.ps1", "'C:\\&nimals\\утка\\qu*ck?.ps1'")]
- public void CorrectlyQuoteEscapesPaths(string unquotedPath, string expectedQuotedPath)
- {
- string extensionQuotedPath = StringEscaping.SingleQuoteAndEscape(unquotedPath).ToString();
- Assert.Equal(expectedQuotedPath, extensionQuotedPath);
- }
-
- [Trait("Category", "PathEscaping")]
- [Theory]
- [InlineData("DebugTest.ps1", "'DebugTest.ps1'")]
- [InlineData("../../DebugTest.ps1", "'../../DebugTest.ps1'")]
- [InlineData("C:\\Users\\me\\Documents\\DebugTest.ps1", "'C:\\Users\\me\\Documents\\DebugTest.ps1'")]
- [InlineData("/home/me/Documents/weird&folder/script.ps1", "'/home/me/Documents/weird&folder/script.ps1'")]
- [InlineData("./path/with some/spaces", "'./path/with some/spaces'")]
- [InlineData("C:\\path\\with[some]brackets\\file.ps1", "'C:\\path\\with`[some`]brackets\\file.ps1'")]
- [InlineData("C:\\look\\an*\\here.ps1", "'C:\\look\\an`*\\here.ps1'")]
- [InlineData("/Users/me/Documents/?here.ps1", "'/Users/me/Documents/`?here.ps1'")]
- [InlineData("/Brackets [and s]paces/path.ps1", "'/Brackets `[and s`]paces/path.ps1'")]
- [InlineData("/file path/that isn't/normal/", "'/file path/that isn''t/normal/'")]
- [InlineData("/CJK.chars/脚本/hello.ps1", "'/CJK.chars/脚本/hello.ps1'")]
- [InlineData("/CJK chars/脚本/[hello].ps1", "'/CJK chars/脚本/`[hello`].ps1'")]
- [InlineData("C:\\Animal s\\утка\\quack.ps1", "'C:\\Animal s\\утка\\quack.ps1'")]
- [InlineData("C:\\&nimals\\утка\\qu*ck?.ps1", "'C:\\&nimals\\утка\\qu`*ck`?.ps1'")]
- public void CorrectlyFullyEscapesPaths(string unescapedPath, string escapedPath)
- {
- string extensionEscapedPath = StringEscaping.SingleQuoteAndEscape(PathUtils.WildcardEscapePath(unescapedPath)).ToString();
- Assert.Equal(escapedPath, extensionEscapedPath);
- }
-
- [Trait("Category", "PathEscaping")]
- [Theory]
- [InlineData("NormalScript.ps1")]
- [InlineData("Bad&name4script.ps1")]
- [InlineData("[Truly] b&d Name_4_script.ps1")]
- public void CanDotSourcePath(string rawFileName)
- {
- string fullPath = Path.Combine(ScriptAssetPath, rawFileName);
- string quotedPath = StringEscaping.SingleQuoteAndEscape(fullPath).ToString();
-
- var psCommand = new System.Management.Automation.PSCommand().AddScript($". {quotedPath}");
-
- using (var pwsh = System.Management.Automation.PowerShell.Create())
- {
- pwsh.Commands = psCommand;
- pwsh.Invoke();
- }
- }
}
}
diff --git a/test/PowerShellEditorServices.Test/Utility/ArgumentEscapingTests.cs b/test/PowerShellEditorServices.Test/Utility/ArgumentEscapingTests.cs
new file mode 100644
index 000000000..d6c155211
--- /dev/null
+++ b/test/PowerShellEditorServices.Test/Utility/ArgumentEscapingTests.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Xunit;
+using Microsoft.PowerShell.EditorServices.Utility;
+using System.IO;
+using System.Management.Automation;
+using System.Linq;
+
+namespace Microsoft.PowerShell.EditorServices.Test.Session
+{
+ public class ArgumentEscapingTests
+ {
+ [Trait("Category", "ArgumentEscaping")]
+ [Theory]
+ [InlineData(" has spaces", "\" has spaces\"")]
+ [InlineData("-Parameter", "-Parameter")]
+ [InlineData("' single quote left alone'", "' single quote left alone'")]
+ [InlineData("\"double quote left alone\"", "\"double quote left alone\"")]
+ [InlineData("/path/to/fi le", "\"/path/to/fi le\"")]
+ [InlineData("'/path/to/fi le'", "'/path/to/fi le'")]
+ [InlineData("|pipeline", "|pipeline")]
+ [InlineData("am&pe rsand", "\"am&pe rsand\"")]
+ [InlineData("semicolon ;", "\"semicolon ;\"")]
+ [InlineData(": colon", "\": colon\"")]
+ [InlineData("$(expressions should be quoted)", "\"$(expressions should be quoted)\"")]
+ [InlineData("{scriptBlocks should not have escaped-spaces}", "{scriptBlocks should not have escaped-spaces}")]
+ [InlineData("-Parameter test", "\"-Parameter test\"")] //This is invalid, but should be obvious enough looking at the PSIC invocation
+ public void CorrectlyEscapesPowerShellArguments(string Arg, string expectedArg)
+ {
+ string quotedArg = ArgumentEscaping.Escape(Arg);
+ Assert.Equal(expectedArg, quotedArg);
+ }
+
+ [Trait("Category", "ArgumentEscaping")]
+ [Theory]
+ [InlineData("/path/t o/file", "/path/t o/file")]
+ [InlineData("/path/with/$(echo 'expression')inline", "/path/with/expressioninline")]
+ [InlineData("/path/with/$(echo 'expression') inline", "/path/with/expression inline")]
+ [InlineData("am&per sand", "am&per sand")]
+ [InlineData("'inner\"\"quotes'", "inner\"\"quotes")]
+ public void CanEvaluateArguments(string Arg, string expectedOutput)
+ {
+ var escapedArg = ArgumentEscaping.Escape(Arg);
+ var psCommand = new PSCommand().AddScript($"& Write-Output {escapedArg}");
+ using var pwsh = System.Management.Automation.PowerShell.Create();
+ pwsh.Commands = psCommand;
+ var scriptOutput = pwsh.Invoke().First();
+ Assert.Equal(expectedOutput, scriptOutput);
+ }
+
+ [Trait("Category", "ArgumentEscaping")]
+ [Theory]
+ [InlineData("NormalScript.ps1")]
+ [InlineData("Bad&name4script.ps1")]
+ [InlineData("[Truly] b&d `Name_4_script.ps1")]
+ public void CanDotSourcePath(string rawFileName)
+ {
+ var ScriptAssetPath = @"..\..\..\..\PowerShellEditorServices.Test.Shared\scriptassets";
+ var fullPath = Path.Combine(ScriptAssetPath, rawFileName);
+ var escapedPath = PathUtils.WildcardEscapePath(fullPath).ToString();
+ var psCommand = new PSCommand().AddScript($"& \"{escapedPath}\"");
+
+ using var pwsh = System.Management.Automation.PowerShell.Create();
+ pwsh.Commands = psCommand;
+ pwsh.Invoke();
+ }
+ }
+}