# HG changeset patch # User Mitchell Kember # Date 1737051302 18000 # Node ID 94e2547e6f3d96ec25fa5998773b004c317659cd # Parent 36d39726c0af2fea0caaacc64223c37aaffb016f rust: move code from utils to utils::strings This moves string-related functions in hg::utils into the recently added hg::utils::strings module. diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/config/layer.rs --- a/rust/hg-core/src/config/layer.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/config/layer.rs Thu Jan 16 13:15:02 2025 -0500 @@ -57,7 +57,7 @@ cli_config_args: impl IntoIterator>, ) -> Result, ConfigError> { fn parse_one(arg: &[u8]) -> Option<(Vec, Vec, Vec)> { - use crate::utils::SliceExt; + use crate::utils::strings::SliceExt; let (section_and_item, value) = arg.split_2(b'=')?; let (section, item) = section_and_item.trim().split_2(b'.')?; @@ -169,7 +169,8 @@ 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); + let filename_bytes = + crate::utils::strings::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 -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/config/values.rs --- a/rust/hg-core/src/config/values.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/config/values.rs Thu Jan 16 13:15:02 2025 -0500 @@ -8,7 +8,7 @@ //! details about where the value came from (but omits details of what’s //! invalid inside the value). -use crate::utils::SliceExt; +use crate::utils::strings::SliceExt; pub(super) fn parse_bool(v: &[u8]) -> Option { match v.to_ascii_lowercase().as_slice() { diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/encoding.rs --- a/rust/hg-core/src/encoding.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/encoding.rs Thu Jan 16 13:15:02 2025 -0500 @@ -3,7 +3,7 @@ use core::str; use std::borrow::Cow; -use crate::{errors::HgError, utils::Escaped}; +use crate::{errors::HgError, utils::strings::Escaped}; use unicode_width::UnicodeWidthStr as _; /// String encoder and decoder. diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/filepatterns.rs --- a/rust/hg-core/src/filepatterns.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/filepatterns.rs Thu Jan 16 13:15:02 2025 -0500 @@ -12,7 +12,7 @@ utils::{ files::{canonical_path, get_bytes_from_path, get_path_from_bytes}, hg_path::{path_to_hg_path_buf, HgPathBuf, HgPathError}, - SliceExt, + strings::SliceExt, }, FastHashMap, }; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/matchers.rs --- a/rust/hg-core/src/matchers.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/matchers.rs Thu Jan 16 13:15:02 2025 -0500 @@ -24,7 +24,7 @@ utils::{ files::{dir_ancestors, find_dirs}, hg_path::{HgPath, HgPathBuf, HgPathError}, - Escaped, + strings::Escaped, }, FastHashMap, }; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/repo.rs --- a/rust/hg-core/src/repo.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/repo.rs Thu Jan 16 13:15:02 2025 -0500 @@ -15,7 +15,7 @@ use crate::utils::debug::debug_wait_for_file_or_print; use crate::utils::files::get_path_from_bytes; use crate::utils::hg_path::HgPath; -use crate::utils::SliceExt; +use crate::utils::strings::SliceExt; use crate::vfs::{is_dir, is_file, Vfs, VfsImpl}; use crate::{exit_codes, requirements, NodePrefix, UncheckedRevision}; use std::cell::{Ref, RefCell, RefMut}; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/requirements.rs --- a/rust/hg-core/src/requirements.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/requirements.rs Thu Jan 16 13:15:02 2025 -0500 @@ -1,6 +1,6 @@ use crate::errors::{HgError, HgResultExt}; use crate::repo::Repo; -use crate::utils::join_display; +use crate::utils::strings::join_display; use crate::vfs::VfsImpl; use std::collections::HashSet; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/revlog/filelog.rs --- a/rust/hg-core/src/revlog/filelog.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/revlog/filelog.rs Thu Jan 16 13:15:02 2025 -0500 @@ -8,7 +8,7 @@ use crate::revlog::{Revlog, RevlogError}; use crate::utils::files::get_path_from_bytes; use crate::utils::hg_path::HgPath; -use crate::utils::SliceExt; +use crate::utils::strings::SliceExt; use crate::Graph; use crate::GraphError; use crate::Node; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/revlog/manifest.rs --- a/rust/hg-core/src/revlog/manifest.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/revlog/manifest.rs Thu Jan 16 13:15:02 2025 -0500 @@ -4,7 +4,7 @@ use crate::revlog::{Node, NodePrefix}; use crate::revlog::{Revlog, RevlogError}; use crate::utils::hg_path::HgPath; -use crate::utils::SliceExt; +use crate::utils::strings::SliceExt; use crate::vfs::VfsImpl; use crate::{Graph, GraphError, Revision, UncheckedRevision}; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/sparse.rs --- a/rust/hg-core/src/sparse.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/sparse.rs Thu Jan 16 13:15:02 2025 -0500 @@ -17,7 +17,7 @@ operations::cat, repo::Repo, requirements::SPARSE_REQUIREMENT, - utils::{hg_path::HgPath, SliceExt}, + utils::{hg_path::HgPath, strings::SliceExt}, Revision, NULL_REVISION, }; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/utils.rs --- a/rust/hg-core/src/utils.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/utils.rs Thu Jan 16 13:15:02 2025 -0500 @@ -8,15 +8,11 @@ //! Contains useful functions, traits, structs, etc. for use in core. use crate::errors::{HgError, IoErrorContext}; -use crate::utils::hg_path::HgPath; use im_rc::ordmap::DiffItem; use im_rc::ordmap::OrdMap; use itertools::EitherOrBoth; use itertools::Itertools; -use std::cell::Cell; use std::cmp::Ordering; -use std::fmt; -use std::{io::Write, ops::Deref}; pub mod debug; pub mod files; @@ -24,208 +20,6 @@ pub mod path_auditor; pub mod strings; -/// Useful until rust/issues/56345 is stable -/// -/// # Examples -/// -/// ``` -/// use crate::hg::utils::find_slice_in_slice; -/// -/// let haystack = b"This is the haystack".to_vec(); -/// assert_eq!(find_slice_in_slice(&haystack, b"the"), Some(8)); -/// assert_eq!(find_slice_in_slice(&haystack, b"not here"), None); -/// ``` -pub fn find_slice_in_slice(slice: &[T], needle: &[T]) -> Option -where - for<'a> &'a [T]: PartialEq, -{ - slice - .windows(needle.len()) - .position(|window| window == needle) -} - -/// Replaces the `from` slice with the `to` slice inside the `buf` slice. -/// -/// # Examples -/// -/// ``` -/// use crate::hg::utils::replace_slice; -/// let mut line = b"I hate writing tests!".to_vec(); -/// replace_slice(&mut line, b"hate", b"love"); -/// assert_eq!( -/// line, -/// b"I love writing tests!".to_vec() -/// ); -/// ``` -pub fn replace_slice(buf: &mut [T], from: &[T], to: &[T]) -where - T: Clone + PartialEq, -{ - if buf.len() < from.len() || from.len() != to.len() { - return; - } - for i in 0..=buf.len() - from.len() { - if buf[i..].starts_with(from) { - buf[i..(i + from.len())].clone_from_slice(to); - } - } -} - -pub trait SliceExt { - fn trim_end(&self) -> &Self; - fn trim_start(&self) -> &Self; - fn trim_end_matches(&self, f: impl FnMut(u8) -> bool) -> &Self; - fn trim_start_matches(&self, f: impl FnMut(u8) -> bool) -> &Self; - fn trim(&self) -> &Self; - fn drop_prefix(&self, needle: &Self) -> Option<&Self>; - fn split_2(&self, separator: u8) -> Option<(&[u8], &[u8])>; - fn split_2_by_slice(&self, separator: &[u8]) -> Option<(&[u8], &[u8])>; -} - -impl SliceExt for [u8] { - fn trim_end(&self) -> &[u8] { - self.trim_end_matches(|byte| byte.is_ascii_whitespace()) - } - - fn trim_start(&self) -> &[u8] { - self.trim_start_matches(|byte| byte.is_ascii_whitespace()) - } - - fn trim_end_matches(&self, mut f: impl FnMut(u8) -> bool) -> &Self { - if let Some(last) = self.iter().rposition(|&byte| !f(byte)) { - &self[..=last] - } else { - &[] - } - } - - fn trim_start_matches(&self, mut f: impl FnMut(u8) -> bool) -> &Self { - if let Some(first) = self.iter().position(|&byte| !f(byte)) { - &self[first..] - } else { - &[] - } - } - - /// ``` - /// use hg::utils::SliceExt; - /// assert_eq!( - /// b" to trim ".trim(), - /// b"to trim" - /// ); - /// assert_eq!( - /// b"to trim ".trim(), - /// b"to trim" - /// ); - /// assert_eq!( - /// b" to trim".trim(), - /// b"to trim" - /// ); - /// ``` - fn trim(&self) -> &[u8] { - self.trim_start().trim_end() - } - - fn drop_prefix(&self, needle: &Self) -> Option<&Self> { - if self.starts_with(needle) { - Some(&self[needle.len()..]) - } else { - None - } - } - - fn split_2(&self, separator: u8) -> Option<(&[u8], &[u8])> { - let pos = memchr::memchr(separator, self)?; - Some((&self[..pos], &self[pos + 1..])) - } - - fn split_2_by_slice(&self, separator: &[u8]) -> Option<(&[u8], &[u8])> { - find_slice_in_slice(self, separator) - .map(|pos| (&self[..pos], &self[pos + separator.len()..])) - } -} - -pub trait Escaped { - /// Return bytes escaped for display to the user - fn escaped_bytes(&self) -> Vec; -} - -impl Escaped for u8 { - fn escaped_bytes(&self) -> Vec { - let mut acc = vec![]; - match self { - c @ b'\'' | c @ b'\\' => { - acc.push(b'\\'); - acc.push(*c); - } - b'\t' => { - acc.extend(br"\\t"); - } - b'\n' => { - acc.extend(br"\\n"); - } - b'\r' => { - acc.extend(br"\\r"); - } - c if (*c < b' ' || *c >= 127) => { - write!(acc, "\\x{:x}", self).unwrap(); - } - c => { - acc.push(*c); - } - } - acc - } -} - -impl<'a, T: Escaped> Escaped for &'a [T] { - fn escaped_bytes(&self) -> Vec { - self.iter().flat_map(Escaped::escaped_bytes).collect() - } -} - -impl Escaped for Vec { - fn escaped_bytes(&self) -> Vec { - self.deref().escaped_bytes() - } -} - -impl<'a> Escaped for &'a HgPath { - fn escaped_bytes(&self) -> Vec { - self.as_bytes().escaped_bytes() - } -} - -#[cfg(unix)] -pub fn shell_quote(value: &[u8]) -> Vec { - if value.iter().all(|&byte| { - matches!( - byte, - b'a'..=b'z' - | b'A'..=b'Z' - | b'0'..=b'9' - | b'.' - | b'_' - | b'/' - | b'+' - | b'-' - ) - }) { - value.to_owned() - } else { - let mut quoted = Vec::with_capacity(value.len() + 2); - quoted.push(b'\''); - for &byte in value { - if byte == b'\'' { - quoted.push(b'\\'); - } - quoted.push(byte); - } - quoted.push(b'\''); - quoted - } -} - pub fn current_dir() -> Result { std::env::current_dir().map_err(|error| HgError::IoError { error, @@ -240,59 +34,6 @@ }) } -/// 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 { Left, Right, @@ -441,46 +182,6 @@ } } -/// Join items of the iterable with the given separator, similar to Python’s -/// `separator.join(iter)`. -/// -/// Formatting the return value consumes the iterator. -/// Formatting it again will produce an empty string. -pub fn join_display( - iter: impl IntoIterator, - separator: impl fmt::Display, -) -> impl fmt::Display { - JoinDisplay { - iter: Cell::new(Some(iter.into_iter())), - separator, - } -} - -struct JoinDisplay { - iter: Cell>, - separator: S, -} - -impl fmt::Display for JoinDisplay -where - I: Iterator, - T: fmt::Display, - S: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(mut iter) = self.iter.take() { - if let Some(first) = iter.next() { - first.fmt(f)?; - } - for value in iter { - self.separator.fmt(f)?; - value.fmt(f)?; - } - } - Ok(()) - } -} - /// Like `Iterator::filter_map`, but over a fallible iterator of `Result`s. /// /// The callback is only called for incoming `Ok` values. Errors are passed diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/utils/files.rs --- a/rust/hg-core/src/utils/files.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/utils/files.rs Thu Jan 16 13:15:02 2025 -0500 @@ -12,7 +12,7 @@ use crate::utils::{ hg_path::{path_to_hg_path_buf, HgPath, HgPathBuf, HgPathError}, path_auditor::PathAuditor, - replace_slice, + strings::replace_slice, }; use lazy_static::lazy_static; use same_file::is_same_file; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/utils/hg_path.rs --- a/rust/hg-core/src/utils/hg_path.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/utils/hg_path.rs Thu Jan 16 13:15:02 2025 -0500 @@ -6,7 +6,7 @@ // GNU General Public License version 2 or any later version. use crate::errors::HgError; -use crate::utils::SliceExt; +use crate::utils::strings::SliceExt; use std::borrow::Borrow; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/utils/path_auditor.rs --- a/rust/hg-core/src/utils/path_auditor.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/utils/path_auditor.rs Thu Jan 16 13:15:02 2025 -0500 @@ -8,8 +8,8 @@ use crate::utils::{ files::lower_clean, - find_slice_in_slice, hg_path::{hg_path_to_path_buf, HgPath, HgPathBuf, HgPathError}, + strings::find_slice_in_slice, }; use std::collections::HashSet; use std::path::{Path, PathBuf}; diff -r 36d39726c0af -r 94e2547e6f3d rust/hg-core/src/utils/strings.rs --- a/rust/hg-core/src/utils/strings.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/hg-core/src/utils/strings.rs Thu Jan 16 13:15:02 2025 -0500 @@ -1,3 +1,286 @@ +//! Contains string-related utilities. + +use crate::utils::hg_path::HgPath; +use std::{cell::Cell, fmt, io::Write as _, ops::Deref as _}; + +/// Useful until rust/issues/56345 is stable +/// +/// # Examples +/// +/// ``` +/// use hg::utils::strings::find_slice_in_slice; +/// +/// let haystack = b"This is the haystack".to_vec(); +/// assert_eq!(find_slice_in_slice(&haystack, b"the"), Some(8)); +/// assert_eq!(find_slice_in_slice(&haystack, b"not here"), None); +/// ``` +pub fn find_slice_in_slice(slice: &[T], needle: &[T]) -> Option +where + for<'a> &'a [T]: PartialEq, +{ + slice + .windows(needle.len()) + .position(|window| window == needle) +} + +/// Replaces the `from` slice with the `to` slice inside the `buf` slice. +/// +/// # Examples +/// +/// ``` +/// use hg::utils::strings::replace_slice; +/// let mut line = b"I hate writing tests!".to_vec(); +/// replace_slice(&mut line, b"hate", b"love"); +/// assert_eq!( +/// line, +/// b"I love writing tests!".to_vec() +/// ); +/// ``` +pub fn replace_slice(buf: &mut [T], from: &[T], to: &[T]) +where + T: Clone + PartialEq, +{ + if buf.len() < from.len() || from.len() != to.len() { + return; + } + for i in 0..=buf.len() - from.len() { + if buf[i..].starts_with(from) { + buf[i..(i + from.len())].clone_from_slice(to); + } + } +} + +pub trait SliceExt { + fn trim_end(&self) -> &Self; + fn trim_start(&self) -> &Self; + fn trim_end_matches(&self, f: impl FnMut(u8) -> bool) -> &Self; + fn trim_start_matches(&self, f: impl FnMut(u8) -> bool) -> &Self; + fn trim(&self) -> &Self; + fn drop_prefix(&self, needle: &Self) -> Option<&Self>; + fn split_2(&self, separator: u8) -> Option<(&[u8], &[u8])>; + fn split_2_by_slice(&self, separator: &[u8]) -> Option<(&[u8], &[u8])>; +} + +impl SliceExt for [u8] { + fn trim_end(&self) -> &[u8] { + self.trim_end_matches(|byte| byte.is_ascii_whitespace()) + } + + fn trim_start(&self) -> &[u8] { + self.trim_start_matches(|byte| byte.is_ascii_whitespace()) + } + + fn trim_end_matches(&self, mut f: impl FnMut(u8) -> bool) -> &Self { + if let Some(last) = self.iter().rposition(|&byte| !f(byte)) { + &self[..=last] + } else { + &[] + } + } + + fn trim_start_matches(&self, mut f: impl FnMut(u8) -> bool) -> &Self { + if let Some(first) = self.iter().position(|&byte| !f(byte)) { + &self[first..] + } else { + &[] + } + } + + /// ``` + /// use hg::utils::strings::SliceExt; + /// assert_eq!( + /// b" to trim ".trim(), + /// b"to trim" + /// ); + /// assert_eq!( + /// b"to trim ".trim(), + /// b"to trim" + /// ); + /// assert_eq!( + /// b" to trim".trim(), + /// b"to trim" + /// ); + /// ``` + fn trim(&self) -> &[u8] { + self.trim_start().trim_end() + } + + fn drop_prefix(&self, needle: &Self) -> Option<&Self> { + if self.starts_with(needle) { + Some(&self[needle.len()..]) + } else { + None + } + } + + fn split_2(&self, separator: u8) -> Option<(&[u8], &[u8])> { + let pos = memchr::memchr(separator, self)?; + Some((&self[..pos], &self[pos + 1..])) + } + + fn split_2_by_slice(&self, separator: &[u8]) -> Option<(&[u8], &[u8])> { + find_slice_in_slice(self, separator) + .map(|pos| (&self[..pos], &self[pos + separator.len()..])) + } +} + +pub trait Escaped { + /// Return bytes escaped for display to the user + fn escaped_bytes(&self) -> Vec; +} + +impl Escaped for u8 { + fn escaped_bytes(&self) -> Vec { + let mut acc = vec![]; + match self { + c @ b'\'' | c @ b'\\' => { + acc.push(b'\\'); + acc.push(*c); + } + b'\t' => { + acc.extend(br"\\t"); + } + b'\n' => { + acc.extend(br"\\n"); + } + b'\r' => { + acc.extend(br"\\r"); + } + c if (*c < b' ' || *c >= 127) => { + write!(acc, "\\x{:x}", self).unwrap(); + } + c => { + acc.push(*c); + } + } + acc + } +} + +impl<'a, T: Escaped> Escaped for &'a [T] { + fn escaped_bytes(&self) -> Vec { + self.iter().flat_map(Escaped::escaped_bytes).collect() + } +} + +impl Escaped for Vec { + fn escaped_bytes(&self) -> Vec { + self.deref().escaped_bytes() + } +} + +impl<'a> Escaped for &'a HgPath { + fn escaped_bytes(&self) -> Vec { + self.as_bytes().escaped_bytes() + } +} + +#[cfg(unix)] +pub fn shell_quote(value: &[u8]) -> Vec { + if value.iter().all(|&byte| { + matches!( + byte, + b'a'..=b'z' + | b'A'..=b'Z' + | b'0'..=b'9' + | b'.' + | b'_' + | b'/' + | b'+' + | b'-' + ) + }) { + value.to_owned() + } else { + let mut quoted = Vec::with_capacity(value.len() + 2); + quoted.push(b'\''); + for &byte in value { + if byte == b'\'' { + quoted.push(b'\\'); + } + quoted.push(byte); + } + quoted.push(b'\''); + quoted + } +} + +/// 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 = crate::utils::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(crate::utils::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() + }) + }) +} + +/// Join items of the iterable with the given separator, similar to Python’s +/// `separator.join(iter)`. +/// +/// Formatting the return value consumes the iterator. +/// Formatting it again will produce an empty string. +pub fn join_display( + iter: impl IntoIterator, + separator: impl fmt::Display, +) -> impl fmt::Display { + JoinDisplay { + iter: Cell::new(Some(iter.into_iter())), + separator, + } +} + +struct JoinDisplay { + iter: Cell>, + separator: S, +} + +impl fmt::Display for JoinDisplay +where + I: Iterator, + T: fmt::Display, + S: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(mut iter) = self.iter.take() { + if let Some(first) = iter.next() { + first.fmt(f)?; + } + for value in iter { + self.separator.fmt(f)?; + value.fmt(f)?; + } + } + Ok(()) + } +} + /// Returns a short representation of a user name or email address. pub fn short_user(user: &[u8]) -> &[u8] { let mut str = user; @@ -21,6 +304,23 @@ use super::*; #[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[..]); + } + + #[test] fn test_short_user() { assert_eq!(short_user(b""), b""); assert_eq!(short_user(b"Name"), b"Name"); diff -r 36d39726c0af -r 94e2547e6f3d rust/rhg/src/blackbox.rs --- a/rust/rhg/src/blackbox.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/rhg/src/blackbox.rs Thu Jan 16 13:15:02 2025 -0500 @@ -4,7 +4,7 @@ use format_bytes::format_bytes; use hg::errors::HgError; use hg::repo::Repo; -use hg::utils::{files::get_bytes_from_os_str, shell_quote}; +use hg::utils::{files::get_bytes_from_os_str, strings::shell_quote}; use std::ffi::OsString; // Python does not support %.3f, only %f diff -r 36d39726c0af -r 94e2547e6f3d rust/rhg/src/commands/config.rs --- a/rust/rhg/src/commands/config.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/rhg/src/commands/config.rs Thu Jan 16 13:15:02 2025 -0500 @@ -2,7 +2,7 @@ use clap::Arg; use format_bytes::format_bytes; use hg::errors::HgError; -use hg::utils::SliceExt; +use hg::utils::strings::SliceExt; pub const HELP_TEXT: &str = " With one argument of the form section.name, print just the value of that config item. diff -r 36d39726c0af -r 94e2547e6f3d rust/rhg/src/main.rs --- a/rust/rhg/src/main.rs Fri Jan 03 10:50:17 2025 -0500 +++ b/rust/rhg/src/main.rs Thu Jan 16 13:15:02 2025 -0500 @@ -6,7 +6,7 @@ use hg::config::{Config, ConfigSource, PlainInfo}; use hg::repo::{Repo, RepoError}; use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; -use hg::utils::SliceExt; +use hg::utils::strings::SliceExt; use hg::{exit_codes, requirements}; use std::borrow::Cow; use std::collections::{HashMap, HashSet};