use std::{ borrow::Cow, ffi::{OsStr, OsString}, path::{Path, PathBuf}, sync::RwLock, }; use once_cell::sync::Lazy; static CWD: RwLock> = RwLock::new(None); // Get the current working directory. // This information is managed internally as the call to std::env::current_dir // might fail if the cwd has been deleted. pub fn current_working_dir() -> PathBuf { if let Some(path) = &*CWD.read().unwrap() { return path.clone(); } // implementation of crossplatform pwd -L // we want pwd -L so that symlinked directories are handled correctly let mut cwd = std::env::current_dir().expect("Couldn't determine current working directory"); let pwd = std::env::var_os("PWD"); #[cfg(windows)] let pwd = pwd.or_else(|| std::env::var_os("CD")); if let Some(pwd) = pwd.map(PathBuf::from) { if pwd.canonicalize().ok().as_ref() == Some(&cwd) { cwd = pwd; } } let mut dst = CWD.write().unwrap(); *dst = Some(cwd.clone()); cwd } pub fn set_current_working_dir(path: impl AsRef) -> std::io::Result<()> { let path = crate::path::canonicalize(path); std::env::set_current_dir(&path)?; let mut cwd = CWD.write().unwrap(); *cwd = Some(path); Ok(()) } pub fn env_var_is_set(env_var_name: &str) -> bool { std::env::var_os(env_var_name).is_some() } pub fn binary_exists>(binary_name: T) -> bool { which::which(binary_name).is_ok() } pub fn which>( binary_name: T, ) -> Result { let binary_name = binary_name.as_ref(); which::which(binary_name).map_err(|err| ExecutableNotFoundError { command: binary_name.to_string_lossy().into_owned(), inner: err, }) } fn find_brace_end(src: &[u8]) -> Option { use regex_automata::meta::Regex; static REGEX: Lazy = Lazy::new(|| Regex::builder().build("[{}]").unwrap()); let mut depth = 0; for mat in REGEX.find_iter(src) { let pos = mat.start(); match src[pos] { b'{' => depth += 1, b'}' if depth == 0 => return Some(pos), b'}' => depth -= 1, _ => unreachable!(), } } None } fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option) -> Cow { use regex_automata::meta::Regex; static REGEX: Lazy = Lazy::new(|| { Regex::builder() .build_many(&[ r"\$\{([^\}:]+):-", r"\$\{([^\}:]+):=", r"\$\{([^\}-]+)-", r"\$\{([^\}=]+)=", r"\$\{([^\}]+)", r"\$(\w+)", ]) .unwrap() }); let bytes = src.as_encoded_bytes(); let mut res = Vec::with_capacity(bytes.len()); let mut pos = 0; for captures in REGEX.captures_iter(bytes) { let mat = captures.get_match().unwrap(); let pattern_id = mat.pattern().as_usize(); let mut range = mat.range(); let var = &bytes[captures.get_group(1).unwrap().range()]; let default = if pattern_id != 5 { let Some(bracket_pos) = find_brace_end(&bytes[range.end..]) else { break; }; let default = &bytes[range.end..range.end + bracket_pos]; range.end += bracket_pos + 1; default } else { &[] }; // safety: this is a codepoint aligned substring of an osstr (always valid) let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) }; let expansion = resolve(var); let expansion = match &expansion { Some(val) => { if val.is_empty() && pattern_id < 2 { default } else { val.as_encoded_bytes() } } None => default, }; res.extend_from_slice(&bytes[pos..range.start]); pos = range.end; res.extend_from_slice(expansion); } if pos == 0 { src.into() } else { res.extend_from_slice(&bytes[pos..]); // safety: this is a composition of valid osstr (and codepoint aligned slices which are also valid) unsafe { OsString::from_encoded_bytes_unchecked(res) }.into() } } /// performs substitution of enviorment variables. Supports the following (POSIX) syntax: /// /// * `$`, `${}` /// * `${:-}`, `${-}` /// * `${:=}`, `${=default}` /// pub fn expand + ?Sized>(src: &S) -> Cow { expand_impl(src.as_ref(), |var| std::env::var_os(var)) } #[derive(Debug)] pub struct ExecutableNotFoundError { command: String, inner: which::Error, } impl std::fmt::Display for ExecutableNotFoundError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "command '{}' not found: {}", self.command, self.inner) } } impl std::error::Error for ExecutableNotFoundError {} #[cfg(test)] mod tests { use std::ffi::{OsStr, OsString}; use super::{current_working_dir, expand_impl, set_current_working_dir}; #[test] fn current_dir_is_set() { let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap(); let cwd = current_working_dir(); assert_ne!(cwd, new_path); set_current_working_dir(&new_path).expect("Couldn't set new path"); let cwd = current_working_dir(); assert_eq!(cwd, new_path); } macro_rules! assert_env_expand { ($env: expr, $lhs: expr, $rhs: expr) => { assert_eq!(&*expand_impl($lhs.as_ref(), $env), OsStr::new($rhs)); }; } /// paths that should work on all platforms #[test] fn test_env_expand() { let env = |var: &OsStr| -> Option { match var.to_str().unwrap() { "FOO" => Some("foo".into()), "EMPTY" => Some("".into()), _ => None, } }; assert_env_expand!(env, "pass_trough", "pass_trough"); assert_env_expand!(env, "$FOO", "foo"); assert_env_expand!(env, "bar/$FOO/baz", "bar/foo/baz"); assert_env_expand!(env, "bar/${FOO}/baz", "bar/foo/baz"); assert_env_expand!(env, "baz/${BAR:-bar}/foo", "baz/bar/foo"); assert_env_expand!(env, "baz/${BAR:=bar}/foo", "baz/bar/foo"); assert_env_expand!(env, "baz/${BAR-bar}/foo", "baz/bar/foo"); assert_env_expand!(env, "baz/${BAR=bar}/foo", "baz/bar/foo"); assert_env_expand!(env, "baz/${EMPTY:-bar}/foo", "baz/bar/foo"); assert_env_expand!(env, "baz/${EMPTY:=bar}/foo", "baz/bar/foo"); assert_env_expand!(env, "baz/${EMPTY-bar}/foo", "baz//foo"); assert_env_expand!(env, "baz/${EMPTY=bar}/foo", "baz//foo"); } }