rust: move code from utils to utils::strings
This moves string-related functions in hg::utils into the recently added
hg::utils::strings module.
--- 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<Item = impl AsRef<[u8]>>,
) -> Result<Option<Self>, ConfigError> {
fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
- 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.
--- 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<bool> {
match v.to_ascii_lowercase().as_slice() {
--- 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.
--- 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,
};
--- 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,
};
--- 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};
--- 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;
--- 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;
--- 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};
--- 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,
};
--- 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<T>(slice: &[T], needle: &[T]) -> Option<usize>
-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<T>(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<u8>;
-}
-
-impl Escaped for u8 {
- fn escaped_bytes(&self) -> Vec<u8> {
- 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<u8> {
- self.iter().flat_map(Escaped::escaped_bytes).collect()
- }
-}
-
-impl<T: Escaped> Escaped for Vec<T> {
- fn escaped_bytes(&self) -> Vec<u8> {
- self.deref().escaped_bytes()
- }
-}
-
-impl<'a> Escaped for &'a HgPath {
- fn escaped_bytes(&self) -> Vec<u8> {
- self.as_bytes().escaped_bytes()
- }
-}
-
-#[cfg(unix)]
-pub fn shell_quote(value: &[u8]) -> Vec<u8> {
- 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::path::PathBuf, HgError> {
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<V> {
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<Item = impl fmt::Display>,
- separator: impl fmt::Display,
-) -> impl fmt::Display {
- JoinDisplay {
- iter: Cell::new(Some(iter.into_iter())),
- separator,
- }
-}
-
-struct JoinDisplay<I, S> {
- iter: Cell<Option<I>>,
- separator: S,
-}
-
-impl<I, T, S> fmt::Display for JoinDisplay<I, S>
-where
- I: Iterator<Item = T>,
- 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
--- 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;
--- 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};
--- 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};
--- 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<T>(slice: &[T], needle: &[T]) -> Option<usize>
+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<T>(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<u8>;
+}
+
+impl Escaped for u8 {
+ fn escaped_bytes(&self) -> Vec<u8> {
+ 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<u8> {
+ self.iter().flat_map(Escaped::escaped_bytes).collect()
+ }
+}
+
+impl<T: Escaped> Escaped for Vec<T> {
+ fn escaped_bytes(&self) -> Vec<u8> {
+ self.deref().escaped_bytes()
+ }
+}
+
+impl<'a> Escaped for &'a HgPath {
+ fn escaped_bytes(&self) -> Vec<u8> {
+ self.as_bytes().escaped_bytes()
+ }
+}
+
+#[cfg(unix)]
+pub fn shell_quote(value: &[u8]) -> Vec<u8> {
+ 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<Item = impl fmt::Display>,
+ separator: impl fmt::Display,
+) -> impl fmt::Display {
+ JoinDisplay {
+ iter: Cell::new(Some(iter.into_iter())),
+ separator,
+ }
+}
+
+struct JoinDisplay<I, S> {
+ iter: Cell<Option<I>>,
+ separator: S,
+}
+
+impl<I, T, S> fmt::Display for JoinDisplay<I, S>
+where
+ I: Iterator<Item = T>,
+ 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");
--- 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
--- 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.
--- 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};