Skip to content

Commit f2e3481

Browse files
committed
Add Path::has_trailing_sep and related methods
1 parent 7827d55 commit f2e3481

File tree

1 file changed

+169
-3
lines changed

1 file changed

+169
-3
lines changed

library/std/src/path.rs

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,88 @@ impl PathBuf {
14031403
}
14041404
}
14051405

1406+
/// Sets whether the path has a trailing [separator](MAIN_SEPARATOR).
1407+
///
1408+
/// The value returned by [`has_trailing_sep`](Self::has_trailing_sep) will be equivalent to
1409+
/// the provided value.
1410+
///
1411+
/// # Examples
1412+
///
1413+
/// ```
1414+
/// #![feature(path_trailing_sep)]
1415+
/// use std::ffi::OsStr;
1416+
/// use std::path::{PathBuf, Path};
1417+
///
1418+
/// let mut p = PathBuf::from("dir");
1419+
///
1420+
/// assert!(!p.has_trailing_sep());
1421+
/// p.set_trailing_sep(false);
1422+
/// assert!(!p.has_trailing_sep());
1423+
/// p.set_trailing_sep(true);
1424+
/// assert!(p.has_trailing_sep());
1425+
/// assert_eq!(p.file_name(), None);
1426+
/// p.set_trailing_sep(false);
1427+
/// assert!(!p.has_trailing_sep());
1428+
/// assert_eq!(p.file_name(), Some(OsStr::new("dir")));
1429+
/// assert_eq!(p, Path::new("dir"));
1430+
/// ```
1431+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
1432+
pub fn set_trailing_sep(&mut self, trailing_sep: bool) {
1433+
if trailing_sep { self.push_trailing_sep() } else { self.pop_trailing_sep() }
1434+
}
1435+
1436+
/// Adds a trailing [separator](MAIN_SEPARATOR) to the path.
1437+
///
1438+
/// This acts similarly to [`Path::with_trailing_sep`], but mutates the underlying `PathBuf`.
1439+
///
1440+
/// # Examples
1441+
///
1442+
/// ```
1443+
/// #![feature(path_trailing_sep)]
1444+
/// use std::ffi::OsStr;
1445+
/// use std::path::{Path, PathBuf};
1446+
///
1447+
/// let mut p = PathBuf::from("dir");
1448+
///
1449+
/// assert_eq!(p.file_name(), Some(OsStr::new("dir")));
1450+
/// p.push_trailing_sep();
1451+
/// assert_eq!(p.file_name(), None);
1452+
/// p.push_trailing_sep();
1453+
/// assert_eq!(p.file_name(), None);
1454+
/// assert_eq!(p, Path::new("dir/"));
1455+
/// ```
1456+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
1457+
pub fn push_trailing_sep(&mut self) {
1458+
if !self.has_trailing_sep() {
1459+
self.push("");
1460+
}
1461+
}
1462+
1463+
/// Removes a trailing [separator](MAIN_SEPARATOR) from the path, if possible.
1464+
///
1465+
/// This acts similarly to [`Path::trim_trailing_sep`], but mutates the underlying `PathBuf`.
1466+
///
1467+
/// # Examples
1468+
///
1469+
/// ```
1470+
/// #![feature(path_trailing_sep)]
1471+
/// use std::ffi::OsStr;
1472+
/// use std::path::{Path, PathBuf};
1473+
///
1474+
/// let mut p = PathBuf::from("dir/");
1475+
///
1476+
/// assert_eq!(p.file_name(), None);
1477+
/// p.pop_trailing_sep();
1478+
/// assert_eq!(p.file_name(), Some(OsStr::new("dir")));
1479+
/// p.pop_trailing_sep();
1480+
/// assert_eq!(p.file_name(), Some(OsStr::new("dir")));
1481+
/// assert_eq!(p, Path::new("/dir"));
1482+
/// ```
1483+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
1484+
pub fn pop_trailing_sep(&mut self) {
1485+
self.inner.truncate(self.trim_trailing_sep().as_os_str().len());
1486+
}
1487+
14061488
/// Updates [`self.file_name`] to `file_name`.
14071489
///
14081490
/// If [`self.file_name`] was [`None`], this is equivalent to pushing
@@ -1603,7 +1685,7 @@ impl PathBuf {
16031685
let new = extension.as_encoded_bytes();
16041686
if !new.is_empty() {
16051687
// truncate until right after the file name
1606-
// this is necessary for trimming the trailing slash
1688+
// this is necessary for trimming the trailing separator
16071689
let end_file_name = file_name[file_name.len()..].as_ptr().addr();
16081690
let start = self.inner.as_encoded_bytes().as_ptr().addr();
16091691
self.inner.truncate(end_file_name.wrapping_sub(start));
@@ -2686,6 +2768,90 @@ impl Path {
26862768
self.file_name().map(rsplit_file_at_dot).and_then(|(before, after)| before.and(after))
26872769
}
26882770

2771+
/// Checks whether the path ends in a trailing [separator](MAIN_SEPARATOR).
2772+
///
2773+
/// This is generally done to ensure that a path is treated as a directory, not a file,
2774+
/// although it does not actually guarantee that such a path is a directory on the underlying
2775+
/// file system.
2776+
///
2777+
/// Despite this behavior, two paths are still considered the same in Rust whether they have a
2778+
/// trailing separator or not.
2779+
///
2780+
/// # Examples
2781+
///
2782+
/// ```
2783+
/// #![feature(path_trailing_sep)]
2784+
/// use std::path::Path;
2785+
///
2786+
/// assert!(Path::new("dir/").has_trailing_sep());
2787+
/// assert!(!Path::new("file.rs").has_trailing_sep());
2788+
/// ```
2789+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
2790+
#[must_use]
2791+
#[inline]
2792+
pub fn has_trailing_sep(&self) -> bool {
2793+
self.as_os_str().as_encoded_bytes().last().copied().is_some_and(is_sep_byte)
2794+
}
2795+
2796+
/// Ensures that a path has a trailing [separator](MAIN_SEPARATOR),
2797+
/// allocating a [`PathBuf`] if necessary.
2798+
///
2799+
/// The resulting path will return true for [`has_trailing_sep`](Self::has_trailing_sep) and
2800+
/// `None` for [`file_name`](Self::file_name).
2801+
///
2802+
/// # Examples
2803+
///
2804+
/// ```
2805+
/// #![feature(path_trailing_sep)]
2806+
/// use std::ffi::OsStr;
2807+
/// use std::path::Path;
2808+
///
2809+
/// assert_eq!(Path::new("dir//").with_trailing_sep().file_name(), None);
2810+
/// assert_eq!(Path::new("dir/").with_trailing_sep().file_name(), None);
2811+
/// assert_eq!(Path::new("dir").with_trailing_sep().file_name(), None);
2812+
/// assert_eq!(Path::new("dir").file_name(), Some(OsStr::new("dir")));
2813+
/// ```
2814+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
2815+
#[must_use]
2816+
#[inline]
2817+
pub fn with_trailing_sep(&self) -> Cow<'_, Path> {
2818+
if self.has_trailing_sep() { Cow::Borrowed(self) } else { Cow::Owned(self.join("")) }
2819+
}
2820+
2821+
/// Trims a trailing [separator](MAIN_SEPARATOR) from a path, if possible.
2822+
///
2823+
/// The resulting path will return false for [`has_trailing_sep`](Self::has_trailing_sep).
2824+
///
2825+
/// # Examples
2826+
///
2827+
/// ```
2828+
/// #![feature(path_trailing_sep)]
2829+
/// use std::ffi::OsStr;
2830+
/// use std::path::Path;
2831+
///
2832+
/// assert_eq!(Path::new("dir//").trim_trailing_sep().file_name(), Some(OsStr::new("dir")));
2833+
/// assert_eq!(Path::new("dir/").trim_trailing_sep().file_name(), Some(OsStr::new("dir")));
2834+
/// assert_eq!(Path::new("dir").trim_trailing_sep().file_name(), Some(OsStr::new("dir")));
2835+
/// ```
2836+
#[unstable(feature = "path_trailing_sep", issue = "142503")]
2837+
#[must_use]
2838+
#[inline]
2839+
pub fn trim_trailing_sep(&self) -> &Path {
2840+
if self.has_trailing_sep() && (!self.has_root() || self.parent().is_some()) {
2841+
let mut bytes = self.inner.as_encoded_bytes();
2842+
while let Some((last, init)) = bytes.split_last()
2843+
&& is_sep_byte(*last)
2844+
{
2845+
bytes = init;
2846+
}
2847+
2848+
// SAFETY: Trimming trailing ASCII bytes will retain the validity of the string.
2849+
Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
2850+
} else {
2851+
self
2852+
}
2853+
}
2854+
26892855
/// Creates an owned [`PathBuf`] with `path` adjoined to `self`.
26902856
///
26912857
/// If `path` is absolute, it replaces the current path.
@@ -2840,7 +3006,7 @@ impl Path {
28403006
/// `a/b` all have `a` and `b` as components, but `./a/b` starts with
28413007
/// an additional [`CurDir`] component.
28423008
///
2843-
/// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent.
3009+
/// * Trailing separators are normalized away, so `/a/b` and `/a/b/` are equivalent.
28443010
///
28453011
/// Note that no other normalization takes place; in particular, `a/c`
28463012
/// and `a/b/../c` are distinct, to account for the possibility that `b`
@@ -3610,7 +3776,7 @@ impl Error for NormalizeError {}
36103776
///
36113777
/// On POSIX platforms, the path is resolved using [POSIX semantics][posix-semantics],
36123778
/// except that it stops short of resolving symlinks. This means it will keep `..`
3613-
/// components and trailing slashes.
3779+
/// components and trailing separators.
36143780
///
36153781
/// On Windows, for verbatim paths, this will simply return the path as given. For other
36163782
/// paths, this is currently equivalent to calling

0 commit comments

Comments
 (0)