diff --git a/rust/hg-core/src/config/layer.rs b/rust/hg-core/src/config/layer.rs --- a/rust/hg-core/src/config/layer.rs +++ b/rust/hg-core/src/config/layer.rs @@ -150,6 +150,7 @@ let line = Some(index + 1); if let Some(m) = INCLUDE_RE.captures(&bytes) { let filename_bytes = &m[1]; + let filename_bytes = crate::utils::expand_vars(filename_bytes); // `Path::parent` only fails for the root directory, // which `src` can’t be since we’ve managed to open it as a // file. diff --git a/rust/hg-core/src/utils.rs b/rust/hg-core/src/utils.rs --- a/rust/hg-core/src/utils.rs +++ b/rust/hg-core/src/utils.rs @@ -241,6 +241,59 @@ }) } +/// Expand `$FOO` and `${FOO}` environment variables in the given byte string +pub fn expand_vars(s: &[u8]) -> std::borrow::Cow<[u8]> { + lazy_static::lazy_static! { + /// https://github.com/python/cpython/blob/3.9/Lib/posixpath.py#L301 + /// The `x` makes whitespace ignored. + /// `-u` disables the Unicode flag, which makes `\w` like Python with the ASCII flag. + static ref VAR_RE: regex::bytes::Regex = + regex::bytes::Regex::new(r"(?x-u) + \$ + (?: + (\w+) + | + \{ + ([^}]*) + \} + ) + ").unwrap(); + } + VAR_RE.replace_all(s, |captures: ®ex::bytes::Captures| { + let var_name = files::get_os_str_from_bytes( + captures + .get(1) + .or_else(|| captures.get(2)) + .expect("either side of `|` must participate in match") + .as_bytes(), + ); + std::env::var_os(var_name) + .map(files::get_bytes_from_os_str) + .unwrap_or_else(|| { + // Referencing an environment variable that does not exist. + // Leave the $FOO reference as-is. + captures[0].to_owned() + }) + }) +} + +#[test] +fn test_expand_vars() { + // Modifying process-global state in a test isn’t great, + // but hopefully this won’t collide with anything. + std::env::set_var("TEST_EXPAND_VAR", "1"); + assert_eq!( + expand_vars(b"before/$TEST_EXPAND_VAR/after"), + &b"before/1/after"[..] + ); + assert_eq!( + expand_vars(b"before${TEST_EXPAND_VAR}${TEST_EXPAND_VAR}${TEST_EXPAND_VAR}after"), + &b"before111after"[..] + ); + let s = b"before $SOME_LONG_NAME_THAT_WE_ASSUME_IS_NOT_AN_ACTUAL_ENV_VAR after"; + assert_eq!(expand_vars(s), &s[..]); +} + pub(crate) enum MergeResult { UseLeftValue, UseRightValue, diff --git a/rust/hg-core/src/utils/files.rs b/rust/hg-core/src/utils/files.rs --- a/rust/hg-core/src/utils/files.rs +++ b/rust/hg-core/src/utils/files.rs @@ -23,7 +23,7 @@ use std::ops::Deref; use std::path::{Path, PathBuf}; -pub fn get_path_from_bytes(bytes: &[u8]) -> &Path { +pub fn get_os_str_from_bytes(bytes: &[u8]) -> &OsStr { let os_str; #[cfg(unix)] { @@ -33,8 +33,11 @@ // TODO Handle other platforms // TODO: convert from WTF8 to Windows MBCS (ANSI encoding). // Perhaps, the return type would have to be Result. + os_str +} - Path::new(os_str) +pub fn get_path_from_bytes(bytes: &[u8]) -> &Path { + Path::new(get_os_str_from_bytes(bytes)) } // TODO: need to convert from WTF8 to MBCS bytes on Windows.