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.rs126
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");
+ }
}