From 2ebcef9ad8422949fe63c19b0cd87c64edccbcd9 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Tue, 5 Mar 2024 22:29:50 -0500 Subject: [PATCH] feat: cmd.exe detection heuristic (#2226) ## Summary Follow up from discussion in https://github.com/astral-sh/uv/pull/2223 Detect CMD.exe by checking if `PROMPT` env var is set on windows, otherwise assume it's PowerShell. Note, this will not work if user modifies their system env vars to include `PROMPT` by default or if they launch nested PowerShell from Command Prompt (e.g. `Developer PowerShell for VS 2022`). ## Test Plan Only tested locally, although we try to add some CI tests that specifically use CMD.exe Command Prompt ``` Microsoft Windows [Version 10.0.19044.3086] (c) Microsoft Corporation. All rights reserved. Z:\Users\samypr100\dev\uv>Z:\Users\samypr100\.cargo\bin\cargo.exe +stable run --color=always -- venv "Foo Bar" Finished dev [unoptimized + debuginfo] target(s) in 0.69s Running `target\debug\uv.exe venv "Foo Bar"` Using Python 3.12.2 interpreter at: Z:\Users\samypr100\AppData\Local\Programs\Python\Python312\python.exe Creating virtualenv at: Foo Bar Activate with: "Foo Bar\Scripts\activate" ``` Power Shell ``` Windows PowerShell Copyright (C) Microsoft Corporation. All rights reserved. Try the new cross-platform PowerShell https://aka.ms/pscore6 PS Z:\Users\samypr100\dev\uv>Z:\Users\samypr100\.cargo\bin\cargo.exe +stable run --color=always -- venv "Foo Bar" Finished dev [unoptimized + debuginfo] target(s) in 0.63s Running `target\debug\uv.exe venv "Foo Bar"` Using Python 3.12.2 interpreter at: Z:\Users\samypr100\AppData\Local\Programs\Python\Python312\python.exe Creating virtualenv at: Foo Bar Activate with: & "Foo Bar\Scripts\activate" ``` --- crates/uv/src/commands/venv.rs | 24 ++++++++++++++++++------ crates/uv/src/shell.rs | 11 ++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 015465d95..7ff396c13 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -230,7 +230,14 @@ async fn venv_impl( "source {}", shlex_posix(path.join("bin").join("activate.csh")) )), - Some(Shell::Powershell) => Some(shlex_windows(path.join("Scripts").join("activate"))), + Some(Shell::Powershell) => Some(shlex_windows( + path.join("Scripts").join("activate"), + Shell::Powershell, + )), + Some(Shell::Cmd) => Some(shlex_windows( + path.join("Scripts").join("activate"), + Shell::Cmd, + )), }; if let Some(act) = activation { writeln!(printer.stderr(), "Activate with: {}", act.green()).into_diagnostic()?; @@ -254,15 +261,20 @@ fn shlex_posix(executable: impl AsRef) -> String { } } -/// Quote a path, if necessary, for safe use in `PowerShell`. -fn shlex_windows(executable: impl AsRef) -> String { +/// Quote a path, if necessary, for safe use in `PowerShell` and `cmd`. +fn shlex_windows(executable: impl AsRef, shell: Shell) -> String { // Convert to a display path. let executable = executable.as_ref().simplified_display().to_string(); - // Wrap the executable in quotes (and a `&` invocation) if it contains spaces. - // TODO(charlie): This won't work in `cmd.exe`. + // Wrap the executable in quotes (and a `&` invocation on PowerShell), if it contains spaces. if executable.contains(' ') { - format!("& \"{executable}\"") + if shell == Shell::Powershell { + // For PowerShell, wrap in a `&` invocation. + format!("& \"{executable}\"") + } else { + // Otherwise, assume `cmd`, which doesn't need the `&`. + format!("\"{executable}\"") + } } else { executable } diff --git a/crates/uv/src/shell.rs b/crates/uv/src/shell.rs index cc1cdbdb5..d1ec39307 100644 --- a/crates/uv/src/shell.rs +++ b/crates/uv/src/shell.rs @@ -9,6 +9,8 @@ pub(crate) enum Shell { Fish, /// PowerShell Powershell, + /// Cmd (Command Prompt) + Cmd, /// Z SHell (zsh) Zsh, /// Nushell @@ -34,7 +36,14 @@ impl Shell { } else if let Some(env_shell) = std::env::var_os("SHELL") { Shell::from_shell_path(env_shell) } else if cfg!(windows) { - Some(Shell::Powershell) + // Command Prompt relies on PROMPT for its appearance whereas PowerShell does not. + // See: https://stackoverflow.com/a/66415037. + if std::env::var_os("PROMPT").is_some() { + Some(Shell::Cmd) + } else { + // Fallback to PowerShell if the PROMPT environment variable is not set. + Some(Shell::Powershell) + } } else { None }