Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-stdx/src/env.rs')
| -rw-r--r-- | helix-stdx/src/env.rs | 126 |
1 files changed, 124 insertions, 2 deletions
diff --git a/helix-stdx/src/env.rs b/helix-stdx/src/env.rs index 51450d22..d29a98fd 100644 --- a/helix-stdx/src/env.rs +++ b/helix-stdx/src/env.rs @@ -1,9 +1,12 @@ use std::{ - ffi::OsStr, + borrow::Cow, + ffi::{OsStr, OsString}, path::{Path, PathBuf}, sync::RwLock, }; +use once_cell::sync::Lazy; + static CWD: RwLock<Option<PathBuf>> = RwLock::new(None); // Get the current working directory. @@ -59,6 +62,93 @@ pub fn which<T: AsRef<OsStr>>( }) } +fn find_brace_end(src: &[u8]) -> Option<usize> { + use regex_automata::meta::Regex; + + static REGEX: Lazy<Regex> = 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<OsString>) -> Cow<OsStr> { + use regex_automata::meta::Regex; + + static REGEX: Lazy<Regex> = 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: +/// +/// * `$<var>`, `${<var>}` +/// * `${<var>:-<default>}`, `${<var>-<default>}` +/// * `${<var>:=<default>}`, `${<var>=default}` +/// +pub fn expand<S: AsRef<OsStr> + ?Sized>(src: &S) -> Cow<OsStr> { + expand_impl(src.as_ref(), |var| std::env::var_os(var)) +} + #[derive(Debug)] pub struct ExecutableNotFoundError { command: String, @@ -75,7 +165,9 @@ impl std::error::Error for ExecutableNotFoundError {} #[cfg(test)] mod tests { - use super::{current_working_dir, set_current_working_dir}; + use std::ffi::{OsStr, OsString}; + + use super::{current_working_dir, expand_impl, set_current_working_dir}; #[test] fn current_dir_is_set() { @@ -88,4 +180,34 @@ mod tests { 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<OsString> { + 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"); + } } |