1use std::{
3 borrow::Cow,
4 ffi::{OsStr, OsString},
5 path::{Path, PathBuf},
6 sync::RwLock,
7};
8
9use std::sync::LazyLock;
10
11static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
13
14pub fn current_working_dir() -> PathBuf {
18 if let Some(path) = &*CWD.read().unwrap() {
19 return path.clone();
20 }
21
22 let mut cwd = std::env::current_dir().expect("Couldn't determine current working directory");
25
26 let pwd = std::env::var_os("PWD");
27 #[cfg(windows)]
28 let pwd = pwd.or_else(|| std::env::var_os("CD"));
29
30 if let Some(pwd) = pwd.map(PathBuf::from) {
31 if pwd.canonicalize().ok().as_ref() == Some(&cwd) {
32 cwd = pwd;
33 }
34 }
35 let mut dst = CWD.write().unwrap();
36 *dst = Some(cwd.clone());
37
38 cwd
39}
40
41pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<Option<PathBuf>> {
43 let path = crate::path::canonicalize(path);
44 std::env::set_current_dir(&path)?;
45 let mut cwd = CWD.write().unwrap();
46
47 Ok(cwd.replace(path))
48}
49
50pub fn env_var_is_set(env_var_name: &str) -> bool {
52 std::env::var_os(env_var_name).is_some()
53}
54
55pub fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
57 which::which(binary_name).is_ok()
58}
59
60pub fn which<T: AsRef<OsStr>>(
62 binary_name: T,
63) -> Result<std::path::PathBuf, ExecutableNotFoundError> {
64 let binary_name = binary_name.as_ref();
65 which::which(binary_name).map_err(|err| ExecutableNotFoundError {
66 command: binary_name.to_string_lossy().into_owned(),
67 inner: err,
68 })
69}
70
71fn find_brace_end(src: &[u8]) -> Option<usize> {
72 use regex_automata::meta::Regex;
73
74 static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::builder().build("[{}]").unwrap());
75 let mut depth = 0;
76 for mat in REGEX.find_iter(src) {
77 let pos = mat.start();
78 match src[pos] {
79 b'{' => depth += 1,
80 b'}' if depth == 0 => return Some(pos),
81 b'}' => depth -= 1,
82 _ => unreachable!(),
83 }
84 }
85 None
86}
87
88fn expand_impl(src: &OsStr, mut resolve: impl FnMut(&OsStr) -> Option<OsString>) -> Cow<'_, OsStr> {
89 use regex_automata::meta::Regex;
90
91 static REGEX: LazyLock<Regex> = LazyLock::new(|| {
92 Regex::builder()
93 .build_many(&[
94 r"\$\{([^\}:]+):-",
95 r"\$\{([^\}:]+):=",
96 r"\$\{([^\}-]+)-",
97 r"\$\{([^\}=]+)=",
98 r"\$\{([^\}]+)",
99 r"\$(\w+)",
100 ])
101 .unwrap()
102 });
103
104 let bytes = src.as_encoded_bytes();
105 let mut res = Vec::with_capacity(bytes.len());
106 let mut pos = 0;
107 for captures in REGEX.captures_iter(bytes) {
108 let mat = captures.get_match().unwrap();
109 let pattern_id = mat.pattern().as_usize();
110 let mut range = mat.range();
111 if range.start < pos {
115 continue;
116 }
117 let var = &bytes[captures.get_group(1).unwrap().range()];
118 let default = if pattern_id != 5 {
119 let Some(bracket_pos) = find_brace_end(&bytes[range.end..]) else {
120 break;
121 };
122 let default = &bytes[range.end..range.end + bracket_pos];
123 range.end += bracket_pos + 1;
124 default
125 } else {
126 &[]
127 };
128 let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
130 let expansion = resolve(var);
131 let expansion = match &expansion {
132 Some(val) => {
133 if val.is_empty() && pattern_id < 2 {
134 default
135 } else {
136 val.as_encoded_bytes()
137 }
138 }
139 None => default,
140 };
141 res.extend_from_slice(&bytes[pos..range.start]);
142 pos = range.end;
143 res.extend_from_slice(expansion);
144 }
145 if pos == 0 {
146 src.into()
147 } else {
148 res.extend_from_slice(&bytes[pos..]);
149 unsafe { OsString::from_encoded_bytes_unchecked(res) }.into()
151 }
152}
153
154pub fn expand<S: AsRef<OsStr> + ?Sized>(src: &S) -> Cow<'_, OsStr> {
161 expand_impl(src.as_ref(), |var| std::env::var_os(var))
162}
163
164#[derive(Debug)]
165pub struct ExecutableNotFoundError {
166 command: String,
167 inner: which::Error,
168}
169
170impl std::fmt::Display for ExecutableNotFoundError {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 write!(f, "command '{}' not found: {}", self.command, self.inner)
173 }
174}
175
176impl std::error::Error for ExecutableNotFoundError {}
177
178#[cfg(test)]
179mod tests {
180 use std::ffi::{OsStr, OsString};
181
182 use super::{current_working_dir, expand_impl, set_current_working_dir};
183
184 #[test]
185 fn current_dir_is_set() {
186 let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
187 let cwd = current_working_dir();
188 assert_ne!(cwd, new_path);
189
190 set_current_working_dir(&new_path).expect("Couldn't set new path");
191
192 let cwd = current_working_dir();
193 assert_eq!(cwd, new_path);
194 }
195
196 macro_rules! assert_env_expand {
197 ($env: expr, $lhs: expr, $rhs: expr) => {
198 assert_eq!(&*expand_impl($lhs.as_ref(), $env), OsStr::new($rhs));
199 };
200 }
201
202 #[test]
204 fn test_env_expand() {
205 let env = |var: &OsStr| -> Option<OsString> {
206 match var.to_str().unwrap() {
207 "FOO" => Some("foo".into()),
208 "EMPTY" => Some("".into()),
209 _ => None,
210 }
211 };
212 assert_env_expand!(env, "pass_trough", "pass_trough");
213 assert_env_expand!(env, "$FOO", "foo");
214 assert_env_expand!(env, "bar/$FOO/baz", "bar/foo/baz");
215 assert_env_expand!(env, "bar/${FOO}/baz", "bar/foo/baz");
216 assert_env_expand!(env, "baz/${BAR:-bar}/foo", "baz/bar/foo");
217 assert_env_expand!(env, "baz/${FOO:-$FOO}/foo", "baz/foo/foo");
218 assert_env_expand!(env, "baz/${BAR:=bar}/foo", "baz/bar/foo");
219 assert_env_expand!(env, "baz/${BAR-bar}/foo", "baz/bar/foo");
220 assert_env_expand!(env, "baz/${BAR=bar}/foo", "baz/bar/foo");
221 assert_env_expand!(env, "baz/${EMPTY:-bar}/foo", "baz/bar/foo");
222 assert_env_expand!(env, "baz/${EMPTY:=bar}/foo", "baz/bar/foo");
223 assert_env_expand!(env, "baz/${EMPTY-bar}/foo", "baz//foo");
224 assert_env_expand!(env, "baz/${EMPTY=bar}/foo", "baz//foo");
225 }
226}