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
132 changes: 132 additions & 0 deletions src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,135 @@ impl PathExt for Path {
self.as_os_str().is_empty()
}
}

pub(crate) enum PathEscape {
Unix,
EscapeBackslash,
}

pub(crate) fn to_path_list(escapes: &[PathEscape], path: &str) -> String {
let mut out = path.to_string();
for escape in escapes {
match escape {
PathEscape::Unix => {
#[cfg(windows)]
{
if windows_path::should_use_unix_path() {
out = windows_path::to_unix_path_list(&out);
}
}
}
PathEscape::EscapeBackslash => {
out = out.replace('\\', r#"\\"#);
}
}
}
out
}

#[cfg(windows)]
mod windows_path {
use once_cell::sync::Lazy;
use which::which;

/// Check Unix-like shell env first, then cygpath.exe only if needed.
/// Only support Unix-like path conversion for MSYS2/Git Bash (MSYSTEM).
/// Cygwin is NOT supported.
static SHOULD_USE_UNIX_PATH: Lazy<bool> =
Lazy::new(|| std::env::var("MSYSTEM").is_ok() && which("cygpath").is_ok());

pub(super) fn should_use_unix_path() -> bool {
*SHOULD_USE_UNIX_PATH
}

/// Returns true if the path is a canonical Windows drive path (e.g. C:/foo/bar or D:\bar)
fn is_canonical_windows_drive_path(p: &str) -> bool {
let p = p.trim();
p.len() >= 3
&& matches!(p.chars().next(), Some(c) if c.is_ascii_alphabetic())
&& p.chars().nth(1) == Some(':')
&& matches!(p.chars().nth(2), Some('/') | Some('\\'))
}

/// Converts a Windows-style path list to Unix-style.
/// Optimizes for common patterns like C:/foo or D:/bar without calling cygpath.
/// Falls back to cygpath for other cases.
/// If cygpath fails, returns the original path string.
pub(super) fn to_unix_path_list(path: &str) -> String {
// If all paths are Windows-style (e.g. C:/foo), convert them manually
if path.split(';').all(is_canonical_windows_drive_path) {
let unix_paths: Vec<String> = path
.split(';')
.map(|p| {
let p = p.trim().replace('\\', "/");
if let Some(drive) = p.chars().next() {
if p.chars().nth(1) == Some(':') && p.chars().nth(2) == Some('/') {
// C:/foo/bar → /c/foo/bar
format!("/{}{}", drive.to_ascii_lowercase(), &p[2..])
} else {
p
}
} else {
p
}
})
.collect();
return unix_paths.join(":");
}

// Otherwise, fallback to cygpath (slow, external process)
if let Ok(output) = std::process::Command::new("cygpath")
.args(["-u", "-p", path])
.output()
{
if output.status.success() {
if let Ok(s) = String::from_utf8(output.stdout) {
return s.trim().to_string();
}
}
}
// Fallback: return the original path string if conversion fails
String::from(path)
}
}

#[cfg(test)]
mod tests {
use super::{PathEscape, to_path_list};

#[test]
fn test_to_path_list_backslash() {
let input = r"\foo\bar";
let output = to_path_list(&[PathEscape::EscapeBackslash], input);
assert_eq!(output, r"\\foo\\bar");
}

#[cfg(windows)]
mod windows_tests {
use super::super::windows_path;
use super::{PathEscape, to_path_list};

#[test]
fn test_to_path_list_unix() {
let input = "C:\\foo;D:\\bar";
let output = to_path_list(&[PathEscape::Unix], input);
if windows_path::should_use_unix_path() {
assert_eq!(output, "/c/foo:/d/bar");
} else {
assert_eq!(output, input);
}
}
}

#[cfg(not(windows))]
mod unix_tests {
use super::{PathEscape, to_path_list};

#[test]
fn test_to_path_list_unix() {
let input = "/foo:/bar";
let output = to_path_list(&[PathEscape::Unix], input);
assert_eq!(output, input);
}
}
}
19 changes: 16 additions & 3 deletions src/shell/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::fmt::Display;
use indoc::formatdoc;

use crate::config::Settings;
use crate::path::{PathEscape, to_path_list};
use crate::shell::{ActivateOptions, Shell};

#[derive(Default)]
Expand All @@ -15,7 +16,7 @@ impl Shell for Bash {
let exe = opts.exe;
let flags = opts.flags;
let settings = Settings::get();
let exe = exe.to_string_lossy();
let exe = to_path_list(&[PathEscape::Unix], &exe.to_string_lossy());

let mut out = String::new();
out.push_str(&self.format_activate_prelude(&opts.prelude));
Expand Down Expand Up @@ -104,18 +105,30 @@ impl Shell for Bash {
}

fn set_env(&self, k: &str, v: &str) -> String {
let k = shell_escape::unix::escape(k.into());
let v = shell_escape::unix::escape(v.into());
let (k, v) = self.escape_env_pair(k, v);
format!("export {k}={v}\n")
}

fn prepend_env(&self, k: &str, v: &str) -> String {
let (k, v) = self.escape_env_pair(k, v);
format!("export {k}=\"{v}:${k}\"\n")
}

fn unset_env(&self, k: &str) -> String {
format!("unset {k}\n", k = shell_escape::unix::escape(k.into()))
}

fn escape_env_pair(&self, k: &str, v: &str) -> (String, String) {
let v = match k {
"PATH" => to_path_list(&[PathEscape::Unix], v),
_ => v.to_string(),
};

let k = shell_escape::unix::escape(k.into());
let v = shell_escape::unix::escape(v.into());

(k.to_string(), v.to_string())
}
}

impl Display for Bash {
Expand Down
21 changes: 16 additions & 5 deletions src/shell/elvish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#![allow(clippy::literal_string_with_formatting_args)]
use std::fmt::Display;

use crate::path::{PathEscape, to_path_list};
use crate::shell::{ActivateOptions, Shell};
use indoc::formatdoc;

Expand All @@ -12,7 +13,7 @@ impl Shell for Elvish {
fn activate(&self, opts: ActivateOptions) -> String {
let exe = opts.exe;
let flags = opts.flags;
let exe = exe.to_string_lossy();
let exe = to_path_list(&[PathEscape::Unix], &exe.to_string_lossy());

let mut out = String::new();
out.push_str(&self.format_activate_prelude(&opts.prelude));
Expand Down Expand Up @@ -73,21 +74,31 @@ impl Shell for Elvish {
}

fn set_env(&self, k: &str, v: &str) -> String {
let k = shell_escape::unix::escape(k.into());
let v = shell_escape::unix::escape(v.into());
let (k, v) = self.escape_env_pair(k, v);
let v = v.replace("\\n", "\n");
format!("set-env {k} {v}\n")
}

fn prepend_env(&self, k: &str, v: &str) -> String {
let k = shell_escape::unix::escape(k.into());
let v = shell_escape::unix::escape(v.into());
let (k, v) = self.escape_env_pair(k, v);
format!("set-env {k} {v}(get-env {k})\n")
}

fn unset_env(&self, k: &str) -> String {
format!("unset-env {k}\n", k = shell_escape::unix::escape(k.into()))
}

fn escape_env_pair(&self, k: &str, v: &str) -> (String, String) {
let v = match k {
"PATH" => to_path_list(&[PathEscape::Unix], v),
_ => v.to_string(),
};

let k = shell_escape::unix::escape(k.into());
let v = shell_escape::unix::escape(v.into());

(k.to_string(), v.to_string())
}
}

impl Display for Elvish {
Expand Down
22 changes: 17 additions & 5 deletions src/shell/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::fmt::{Display, Formatter};

use crate::config::Settings;
use crate::path::{PathEscape, to_path_list};
use crate::shell::{ActivateOptions, Shell};
use indoc::formatdoc;
use shell_escape::unix::escape;
Expand All @@ -14,7 +15,8 @@ impl Shell for Fish {
fn activate(&self, opts: ActivateOptions) -> String {
let exe = opts.exe;
let flags = opts.flags;
let exe = exe.to_string_lossy();
let exe = to_path_list(&[PathEscape::Unix], &exe.to_string_lossy());

let description = "'Update mise environment when changing directories'";
let mut out = String::new();
out.push_str(&self.format_activate_prelude(&opts.prelude));
Expand Down Expand Up @@ -121,20 +123,30 @@ impl Shell for Fish {
}

fn set_env(&self, key: &str, v: &str) -> String {
let k = escape(key.into());
let v = escape(v.into());
let (k, v) = self.escape_env_pair(key, v);
format!("set -gx {k} {v}\n")
}

fn prepend_env(&self, key: &str, v: &str) -> String {
let k = escape(key.into());
let v = escape(v.into());
let (k, v) = self.escape_env_pair(key, v);
format!("set -gx {k} {v} ${k}\n")
}

fn unset_env(&self, k: &str) -> String {
format!("set -e {k}\n", k = escape(k.into()))
}

fn escape_env_pair(&self, k: &str, v: &str) -> (String, String) {
let v = match k {
"PATH" => to_path_list(&[PathEscape::Unix], v),
_ => v.to_string(),
};

let k = shell_escape::unix::escape(k.into());
let v = shell_escape::unix::escape(v.into());

(k.to_string(), v.to_string())
}
}

impl Display for Fish {
Expand Down
1 change: 1 addition & 0 deletions src/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub trait Shell: Display {
fn set_env(&self, k: &str, v: &str) -> String;
fn prepend_env(&self, k: &str, v: &str) -> String;
fn unset_env(&self, k: &str) -> String;
fn escape_env_pair(&self, k: &str, v: &str) -> (String, String);

fn format_activate_prelude(&self, prelude: &[ActivatePrelude]) -> String {
prelude
Expand Down
23 changes: 19 additions & 4 deletions src/shell/nushell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::fmt::Display;

use indoc::formatdoc;

use crate::path::{PathEscape, to_path_list};
use crate::shell::{ActivateOptions, ActivatePrelude, Shell};
use itertools::Itertools;

Expand Down Expand Up @@ -48,7 +49,10 @@ impl Shell for Nushell {
fn activate(&self, opts: ActivateOptions) -> String {
let exe = opts.exe;
let flags = opts.flags;
let exe = exe.to_string_lossy().replace('\\', r#"\\"#);
let exe = to_path_list(
&[PathEscape::Unix, PathEscape::EscapeBackslash],
&exe.to_string_lossy(),
);

let mut out = String::new();
out.push_str(&self.format_activate_prelude_inline(&opts.prelude));
Expand Down Expand Up @@ -123,20 +127,31 @@ impl Shell for Nushell {
}

fn set_env(&self, k: &str, v: &str) -> String {
let k = Nushell::escape_csv_value(k);
let v = Nushell::escape_csv_value(v);

let (k, v) = self.escape_env_pair(k, v);
EnvOp::Set { key: &k, val: &v }.to_string()
}

fn prepend_env(&self, k: &str, v: &str) -> String {
let (k, v) = self.escape_env_pair(k, v);
format!("$env.{k} = ($env.{k} | prepend '{v}')\n")
}

fn unset_env(&self, k: &str) -> String {
let k = Nushell::escape_csv_value(k);
EnvOp::Hide { key: k.as_ref() }.to_string()
}

fn escape_env_pair(&self, k: &str, v: &str) -> (String, String) {
let v = match k {
"PATH" => to_path_list(&[PathEscape::Unix], v),
_ => v.to_string(),
};

let k = Nushell::escape_csv_value(k);
let v = Nushell::escape_csv_value(&v);

(k.to_string(), v.to_string())
}
}

impl Display for Nushell {
Expand Down
12 changes: 8 additions & 4 deletions src/shell/pwsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,21 +156,25 @@ impl Shell for Pwsh {
}

fn set_env(&self, k: &str, v: &str) -> String {
let k = powershell_escape(k.into());
let v = powershell_escape(v.into());
let (k, v) = self.escape_env_pair(k, v);
format!("$Env:{k}='{v}'\n")
}

fn prepend_env(&self, k: &str, v: &str) -> String {
let k = powershell_escape(k.into());
let v = powershell_escape(v.into());
let (k, v) = self.escape_env_pair(k, v);
format!("$Env:{k}='{v}'+[IO.Path]::PathSeparator+$env:{k}\n")
}

fn unset_env(&self, k: &str) -> String {
let k = powershell_escape(k.into());
format!("Remove-Item -ErrorAction SilentlyContinue -Path Env:/{k}\n")
}

fn escape_env_pair(&self, k: &str, v: &str) -> (String, String) {
let k = powershell_escape(k.into());
let v = powershell_escape(v.into());
(k.to_string(), v.to_string())
}
}

impl Display for Pwsh {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: src/shell/bash.rs
expression: "replace_path(&bash.prepend_env(\"PATH\", \"/some/dir:/2/dir\"))"
snapshot_kind: text
---
export PATH="/some/dir:/2/dir:$PATH"
export PATH="'/some/dir:/2/dir':$PATH"
Loading
Loading