Repository: microsoft/terminal (language: cpp)

Issue:
After upgrade to 1.12.3472.0 startingDirectory with forward slashes does not work for WSL any more
### Windows Terminal version

1.12.3472.0

### Windows build number

10.0.19043.0

### Other Software

I have both WSL2 and WSL1 with Ubuntu 20.04 images. For both of them after Windows Terminal upgraded form 1.11 preview to 1.12 preview my session starts in the Ubuntu's root directory instead of the directory set in the Terminal's profile:
``` JSON
            {
                "guid": "{2c4de342-38b7-51cf-b940-2309a097f518}",
                "hidden": false,
                "name": "Ubuntu",
                "source": "Windows.Terminal.Wsl",
                "startingDirectory": "//wsl$/Ubuntu/home/vbrozik/"
            },
            {
                "guid": "{07b52e3e-de2c-5db4-bd2d-ba144ed6c273}",
                "hidden": false,
                "name": "Ubuntu-20.04",
                "source": "Windows.Terminal.Wsl",
                "startingDirectory": "//wsl$/Ubuntu-20.04/home/vbrozik/"
            },
```
The directories exist:
``` terminal
PS C:\Users\vaclav.brozik> cd //wsl$/Ubuntu/home/vbrozik/
PS Microsoft.PowerShell.Core\FileSystem::\\wsl$\Ubuntu\home\vbrozik> cd //wsl$/Ubuntu-20.04/home/vbrozik/
PS Microsoft.PowerShell.Core\FileSystem::\\wsl$\Ubuntu-20.04\home\vbrozik>
```
The same bug is in the `wsl` utility itself as I noticed that the `--cd` switch fails to process the path with forward slashes while backslashes work fine:
```
PS C:\Users\vaclav.brozik> wsl --cd //wsl$/Ubuntu/home/vbrozik/
vbrozik@EUCZPC6DRGJM2:/$ pwd
/
vbrozik@EUCZPC6DRGJM2:/$ logout
PS C:\Users\vaclav.brozik> wsl --cd \\wsl$\Ubuntu\home\vbrozik\
vbrozik@EUCZPC6DRGJM2:~$ pwd
/home/vbrozik
```
So if Windows Terminal uses this buggy functionality of the `wsl` tool the bug propagates to Windows Terminal.

### Workaround
After I replaced the slashes with backslashes everything works as expected:
``` JSON
            {
                "guid": "{2c4de342-38b7-51cf-b940-2309a097f518}",
                "hidden": false,
                "name": "Ubuntu",
                "source": "Windows.Terminal.Wsl",
                "startingDirectory": "\\\\wsl$\\Ubuntu\\home\\vbrozik\\"
            },
            {
                "guid": "{07b52e3e-de2c-5db4-bd2d-ba144ed6c273}",
                "hidden": false,
                "name": "Ubuntu-20.04",
                "source": "Windows.Terminal.Wsl",
                "startingDirectory": "\\\\wsl$\\Ubuntu-20.04\\home\\vbrozik\\"
            },
```

### Steps to reproduce

Use forward slashes in the `startingDirectory` property of a WSL profile in Windows Terminal - for example `//wsl$/Ubuntu/home/username/`.

### Expected Behavior

The interactive shell running in WSL should have the working directory as configured in the `startingDirectory` property.

### Actual Behavior

The interactive shell running in WSL has the root directory as the working directory.


Discussion / hints:
I'm having this same issue, and attempting the workaround with backslashes doesn't work for me either, I just get an error: 

```
The system cannot find the path specified.

[process exited with code 4294967295]
```

I've tried a number of different permutations and regardless, I still just get it defaulting to root `/` instead of `~`. 
solution in https://github.com/microsoft/terminal/issues/11812#issuecomment-977198964
Thank you for the link. I was unable to find the ticket before opening this one (probably I searched only for opened cases...)

Anyway I do not think that removing support of forward slashes in the path is a good decision. Windows normally support forward slashes in the path. Why Windows Terminal should support only the (weird - as I see it) file path with backslashes?

I did not notice this breaking change in the release notes but it is there ([v1.11.2421.0](https://github.com/microsoft/terminal/releases/tag/v1.11.2421.0)):
* Terminal will now try to launch WSL by moving the `startingDirectory` to the command line (#9223)
  * If you are on a newer version of Windows, you will be able to set the starting directory to `~` or any Linux path!

It should have been mentioned that the change breaks things.

I think that a proper implementation should clearly differentiate the path specification for Windows and for the target system. It should not be mixed in the same property.
BTW

Putting `~` into `startingDirectory` fails:
```
The system cannot find the file specified.

[process exited with code 4294967295 (0xffffffff)]
```
While `wsl.exe` accepts it:
```
PS C:\Users\vaclav.brozik> wsl --cd ~
vbrozik@EUCZPC6DRGJM2:~$
```
This should be a separate bug-issue for Windows Terminal.
idea: we add `//wsl$` to the heuristic - if we see that, clearly, the user wanted a windows-style path. 


The `~` thing is tracked elsewhere on the repo already
Is there anyone working on this? Just ran into these issues after following the instructions here: https://docs.microsoft.com/en-us/windows/terminal/troubleshooting. 
Also wanted to know going forward if the recommended method of setting startingDirectory is via unix style paths or windows style especially since windows style paths is broken right now.

I wanted to just set startingDirectory to '~' but it seems like from #11432 it will take a while for that capability to land. 
> Is there anyone working on this? 

not currently, we're all out for the holidays. This (and #11432) are both things we should resolve before shipping 1.13 (or moving this feature to stable)
Oh of course, I didn't expect anyone to be working during the holidays. I just wanted to know if someone was already working on a fix or was already assigned to work on this. 

Was just looking for a good project during the break and thought this might be a good way to get my feet wet with contributing to this project.

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index ded141d0021..84fcd8e14b5 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -2391,6 +2391,7 @@ TIcon
 tif
 tilunittests
 Timeline
+timelines
 titlebar
 TITLEISLINKNAME
 TJson
diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp
index a6dcbd3b435..acde96d3de5 100644
--- a/src/cascadia/TerminalConnection/ConptyConnection.cpp
+++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp
@@ -61,72 +61,6 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
         return S_OK;
     }
 
-    // Function Description:
-    // - Promotes a starting directory provided to a WSL invocation to a commandline argument.
-    //   This is necessary because WSL has some modicum of support for linux-side directories (!) which
-    //   CreateProcess never will.
-    static std::tuple<std::wstring, std::wstring> _tryMangleStartingDirectoryForWSL(std::wstring_view commandLine, std::wstring_view startingDirectory)
-    {
-        do
-        {
-            if (startingDirectory.size() > 0 && commandLine.size() >= 3)
-            { // "wsl" is three characters; this is a safe bet. no point in doing it if there's no starting directory though!
-                // Find the first space, quote or the end of the string -- we'll look for wsl before that.
-                const auto terminator{ commandLine.find_first_of(LR"(" )", 1) }; // look past the first character in case it starts with "
-                const auto start{ til::at(commandLine, 0) == L'"' ? 1 : 0 };
-                const std::filesystem::path executablePath{ commandLine.substr(start, terminator - start) };
-                const auto executableFilename{ executablePath.filename().wstring() };
-                if (executableFilename == L"wsl" || executableFilename == L"wsl.exe")
-                {
-                    // We've got a WSL -- let's just make sure it's the right one.
-                    if (executablePath.has_parent_path())
-                    {
-                        std::wstring systemDirectory{};
-                        if (FAILED(wil::GetSystemDirectoryW(systemDirectory)))
-                        {
-                            break; // just bail out.
-                        }
-                        if (executablePath.parent_path().wstring() != systemDirectory)
-                        {
-                            break; // it wasn't in system32!
-                        }
-                    }
-                    else
-                    {
-                        // assume that unqualified WSL is the one in system32 (minor danger)
-                    }
-
-                    const auto arguments{ terminator == std::wstring_view::npos ? std::wstring_view{} : commandLine.substr(terminator + 1) };
-                    if (arguments.find(L"--cd") != std::wstring_view::npos)
-                    {
-                        break; // they've already got a --cd!
-                    }
-
-                    const auto tilde{ arguments.find_first_of(L'~') };
-                    if (tilde != std::wstring_view::npos)
-                    {
-                        if (tilde + 1 == arguments.size() || til::at(arguments, tilde + 1) == L' ')
-                        {
-                            // We want to suppress --cd if they have added a bare ~ to their commandline (they conflict).
-                            break;
-                        }
-                        // Tilde followed by non-space should be okay (like, wsl -d Debian ~/blah.sh)
-                    }
-
-                    return {
-                        fmt::format(LR"("{}" --cd "{}" {})", executablePath.wstring(), startingDirectory, arguments),
-                        std::wstring{}
-                    };
-                }
-            }
-        } while (false);
-
-        return {
-            std::wstring{ commandLine },
-            std::wstring{ startingDirectory }
-        };
-    }
-
     // Function Description:
     // - launches the client application attached to the new pseudoconsole
     HRESULT ConptyConnection::_LaunchAttachedClient() noexcept
@@ -233,7 +167,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
             siEx.StartupInfo.lpTitle = mutableTitle.data();
         }
 
-        auto [newCommandLine, newStartingDirectory] = _tryMangleStartingDirectoryForWSL(cmdline, _startingDirectory);
+        auto [newCommandLine, newStartingDirectory] = Utils::MangleStartingDirectoryForWSL(cmdline, _startingDirectory);
         const wchar_t* const startingDirectory = newStartingDirectory.size() > 0 ? newStartingDirectory.c_str() : nullptr;
 
         RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(
diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp
index 9bd1c3141b3..7a5eca2ff36 100644
--- a/src/types/inc/utils.hpp
+++ b/src/types/inc/utils.hpp
@@ -95,4 +95,13 @@ namespace Microsoft::Console::Utils
     GUID CreateV5Uuid(const GUID& namespaceGuid, const gsl::span<const gsl::byte> name);
 
     bool IsElevated();
+
+    // This function is only ever used by the ConPTY connection in
+    // TerminalConnection. However, that library does not have a good system of
+    // tests set up. Since this function has a plethora of edge cases that would
+    // be beneficial to have tests for, we're hosting it in this lib, so it can
+    // be easily tested.
+    std::tuple<std::wstring, std::wstring> MangleStartingDirectoryForWSL(std::wstring_view commandLine,
+                                                                         std::wstring_view startingDirectory);
+
 }
diff --git a/src/types/ut_types/UtilsTests.cpp b/src/types/ut_types/UtilsTests.cpp
index ac8b5e97406..e4395b51ee0 100644
--- a/src/types/ut_types/UtilsTests.cpp
+++ b/src/types/ut_types/UtilsTests.cpp
@@ -26,6 +26,8 @@ class UtilsTests
     TEST_METHOD(TestStringToUint);
     TEST_METHOD(TestColorFromXTermColor);
 
+    TEST_METHOD(TestMangleWSLPaths);
+
     void _VerifyXTermColorResult(const std::wstring_view wstr, DWORD colorValue);
     void _VerifyXTermColorInvalid(const std::wstring_view wstr);
 };
@@ -332,3 +334,145 @@ void UtilsTests::_VerifyXTermColorInvalid(const std::wstring_view wstr)
     std::optional<til::color> color = ColorFromXTermColor(wstr);
     VERIFY_IS_FALSE(color.has_value());
 }
+
+void UtilsTests::TestMangleWSLPaths()
+{
+    // Continue on failures
+    const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope;
+
+    const auto startingDirectory{ L"SENTINEL" };
+    // MUST MANGLE
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "SENTINEL" )", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl -d X)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "SENTINEL" -d X)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl -d X ~/bin/sh)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "SENTINEL" -d X ~/bin/sh)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl.exe)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl.exe" --cd "SENTINEL" )", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl.exe -d X)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl.exe" --cd "SENTINEL" -d X)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl.exe -d X ~/bin/sh)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl.exe" --cd "SENTINEL" -d X ~/bin/sh)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"("wsl")", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "SENTINEL" )", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"("wsl.exe")", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl.exe" --cd "SENTINEL" )", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"("wsl" -d X)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "SENTINEL"  -d X)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"("wsl.exe" -d X)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl.exe" --cd "SENTINEL"  -d X)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"("C:\Windows\system32\wsl.exe" -d X)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("C:\Windows\system32\wsl.exe" --cd "SENTINEL"  -d X)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"("C:\windows\system32\wsl" -d X)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("C:\windows\system32\wsl" --cd "SENTINEL"  -d X)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl ~/bin)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "SENTINEL" ~/bin)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    // MUST NOT MANGLE
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"("C:\wsl.exe" -d X)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"("C:\wsl.exe" -d X)", commandline);
+        VERIFY_ARE_EQUAL(startingDirectory, path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(C:\wsl.exe)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"(C:\wsl.exe)", commandline);
+        VERIFY_ARE_EQUAL(startingDirectory, path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl --cd C:\)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"(wsl --cd C:\)", commandline);
+        VERIFY_ARE_EQUAL(startingDirectory, path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl ~)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"(wsl ~)", commandline);
+        VERIFY_ARE_EQUAL(startingDirectory, path);
+    }
+
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl ~ -d Ubuntu)", startingDirectory);
+        VERIFY_ARE_EQUAL(LR"(wsl ~ -d Ubuntu)", commandline);
+        VERIFY_ARE_EQUAL(startingDirectory, path);
+    }
+
+    {
+        // Test for GH#11994 - make sure `//wsl$/` paths get mangled back to
+        // `\\wsl$\`, to workaround a potential bug in `wsl --cd`
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl -d Ubuntu)", LR"(//wsl$/Ubuntu/home/user)");
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "\\wsl$\Ubuntu\home\user" -d Ubuntu)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl -d Ubuntu)", LR"(\\wsl$\Ubuntu\home\user)");
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "\\wsl$\Ubuntu\home\user" -d Ubuntu)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+
+    {
+        // Same, but with `wsl.localhost`
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl -d Ubuntu)", LR"(//wsl.localhost/Ubuntu/home/user)");
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "\\wsl.localhost\Ubuntu\home\user" -d Ubuntu)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+    {
+        auto [commandline, path] = MangleStartingDirectoryForWSL(LR"(wsl -d Ubuntu)", LR"(\\wsl.localhost\Ubuntu\home\user)");
+        VERIFY_ARE_EQUAL(LR"("wsl" --cd "\\wsl.localhost\Ubuntu\home\user" -d Ubuntu)", commandline);
+        VERIFY_ARE_EQUAL(L"", path);
+    }
+}
diff --git a/src/types/utils.cpp b/src/types/utils.cpp
index 1dc18a68e13..45c6b22c9d0 100644
--- a/src/types/utils.cpp
+++ b/src/types/utils.cpp
@@ -6,6 +6,7 @@
 #include "inc/colorTable.hpp"
 
 #include <wil/token_helpers.h>
+#include <til/string.h>
 
 using namespace Microsoft::Console;
 
@@ -591,3 +592,86 @@ bool Utils::IsElevated()
     }();
     return isElevated;
 }
+
+// Function Description:
+// - Promotes a starting directory provided to a WSL invocation to a commandline argument.
+//   This is necessary because WSL has some modicum of support for linux-side directories (!) which
+//   CreateProcess never will.
+std::tuple<std::wstring, std::wstring> Utils::MangleStartingDirectoryForWSL(std::wstring_view commandLine,
+                                                                            std::wstring_view startingDirectory)
+{
+    do
+    {
+        if (startingDirectory.size() > 0 && commandLine.size() >= 3)
+        { // "wsl" is three characters; this is a safe bet. no point in doing it if there's no starting directory though!
+            // Find the first space, quote or the end of the string -- we'll look for wsl before that.
+            const auto terminator{ commandLine.find_first_of(LR"(" )", 1) }; // look past the first character in case it starts with "
+            const auto start{ til::at(commandLine, 0) == L'"' ? 1 : 0 };
+            const std::filesystem::path executablePath{ commandLine.substr(start, terminator - start) };
+            const auto executableFilename{ executablePath.filename().wstring() };
+            if (executableFilename == L"wsl" || executableFilename == L"wsl.exe")
+            {
+                // We've got a WSL -- let's just make sure it's the right one.
+                if (executablePath.has_parent_path())
+                {
+                    std::wstring systemDirectory{};
+                    if (FAILED(wil::GetSystemDirectoryW(systemDirectory)))
+                    {
+                        break; // just bail out.
+                    }
+
+                    if (!til::equals_insensitive_ascii(executablePath.parent_path().c_str(), systemDirectory))
+                    {
+                        break; // it wasn't in system32!
+                    }
+                }
+                else
+                {
+                    // assume that unqualified WSL is the one in system32 (minor danger)
+                }
+
+                const auto arguments{ terminator == std::wstring_view::npos ? std::wstring_view{} : commandLine.substr(terminator + 1) };
+                if (arguments.find(L"--cd") != std::wstring_view::npos)
+                {
+                    break; // they've already got a --cd!
+                }
+
+                const auto tilde{ arguments.find_first_of(L'~') };
+                if (tilde != std::wstring_view::npos)
+                {
+                    if (tilde + 1 == arguments.size() || til::at(arguments, tilde + 1) == L' ')
+                    {
+                        // We want to suppress --cd if they have added a bare ~ to their commandline (they conflict).
+                        break;
+                    }
+                    // Tilde followed by non-space should be okay (like, wsl -d Debian ~/blah.sh)
+                }
+
+                // GH#11994 - If the path starts with //wsl$, then the user is
+                // likely passing a Windows-style path to the WSL filesystem,
+                // but with forward slashes instead of backslashes.
+                // Unfortunately, `wsl --cd` will try to treat this as a
+                // linux-relative path, which will fail to do the expected
+                // thing.
+                //
+                // In that case, manually mangle the startingDirectory to use
+                // backslashes as the path separator instead.
+                std::wstring mangledDirectory{ startingDirectory };
+                if (til::starts_with(mangledDirectory, L"//wsl$") || til::starts_with(mangledDirectory, L"//wsl.localhost"))
+                {
+                    mangledDirectory = std::filesystem::path{ startingDirectory }.make_preferred().wstring();
+                }
+
+                return {
+                    fmt::format(LR"("{}" --cd "{}" {})", executablePath.wstring(), mangledDirectory, arguments),
+                    std::wstring{}
+                };
+            }
+        }
+    } while (false);
+
+    return {
+        std::wstring{ commandLine },
+        std::wstring{ startingDirectory }
+    };
+}


Test patch: