terminal: Escape strings with backticks rather than backslashes in PowerShell (#39657)

Jakub Konka created

Closes #39007 

Strings should be escaped with backticks in PowerShell, so the following

```
\"pwsh.exe -C pytest -m \\\"some_test\\\"\"
```

becomes

```
\"pwsh.exe -C pytest -m `\"some_test`\"\"
```

Otherwise PowerShell will misinterpret the invocation resulting in
weirdness all-around such as the issue linked above.

Release Notes:

- N/A

Change summary

crates/project/src/terminals.rs |  5 +++--
crates/util/src/shell.rs        | 13 ++++++++++++-
2 files changed, 15 insertions(+), 3 deletions(-)

Detailed changes

crates/project/src/terminals.rs 🔗

@@ -145,7 +145,7 @@ impl Project {
             project.update(cx, move |this, cx| {
                 let format_to_run = || {
                     if let Some(command) = &spawn_task.command {
-                        let mut command: Option<Cow<str>> = shlex::try_quote(command).ok();
+                        let mut command: Option<Cow<str>> = shell_kind.try_quote(command);
                         if let Some(command) = &mut command
                             && command.starts_with('"')
                             && let Some(prefix) = shell_kind.command_prefix()
@@ -156,7 +156,8 @@ impl Project {
                         let args = spawn_task
                             .args
                             .iter()
-                            .filter_map(|arg| shlex::try_quote(arg).ok());
+                            .filter_map(|arg| shell_kind.try_quote(&arg));
+
                         command.into_iter().chain(args).join(" ")
                     } else {
                         // todo: this breaks for remotes to windows

crates/util/src/shell.rs 🔗

@@ -1,4 +1,4 @@
-use std::{fmt, path::Path, sync::LazyLock};
+use std::{borrow::Cow, fmt, path::Path, sync::LazyLock};
 
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
 pub enum ShellKind {
@@ -241,6 +241,7 @@ impl ShellKind {
             input.into()
         }
     }
+
     fn to_powershell_variable(input: &str) -> String {
         if let Some(var_str) = input.strip_prefix("${") {
             if var_str.find(':').is_none() {
@@ -359,4 +360,14 @@ impl ShellKind {
             _ => None,
         }
     }
+
+    pub fn try_quote<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
+        shlex::try_quote(arg).ok().map(|arg| match self {
+            // If we are running in PowerShell, we want to take extra care when escaping strings.
+            // In particular, we want to escape strings with a backtick (`) rather than a backslash (\).
+            // TODO double escaping backslashes is not necessary in PowerShell and probably CMD
+            ShellKind::PowerShell => Cow::Owned(arg.replace("\\\"", "`\"")),
+            _ => arg,
+        })
+    }
 }