pijul_core/path.rs
1//! Treating strings as paths. For portability reasons, paths must
2//! internally be treated as strings, and converted to paths only by
3//! the backend, if required (in-memory backends will typically not
4//! need that conversion).
5
6/// Returns the parent of the path, if it exists. This function tries
7/// to replicate the behaviour of `std::path::Path::parent`, but with
8/// `&str` instead of `Path`.
9///
10/// ```ignore
11/// use pijul_core::path::parent;
12/// assert_eq!(parent("/foo/bar"), Some("/foo"));
13/// assert_eq!(parent("foo"), Some(""));
14/// assert_eq!(parent("/"), None);
15/// assert_eq!(parent(""), None);
16/// ```
17pub fn parent(mut path: &str) -> Option<&str> {
18 loop {
19 if path == "/" || path.is_empty() {
20 return None;
21 } else if let Some(i) = path.rfind('/') {
22 let (a, b) = path.split_at(i);
23 if b == "/" {
24 path = a
25 } else {
26 return Some(a);
27 }
28 } else {
29 return Some("");
30 }
31 }
32}
33
34/// Returns the file name of the path. if it exists. This function
35/// tries to replicate the behaviour of `std::path::Path::file_name`,
36/// but with `&str` instead of `Path`.
37///
38/// Like the original, returns `None` if the path terminates in `..`.
39///
40/// ```ignore
41/// use pijul_core::path::file_name;
42/// assert_eq!(file_name("/usr/bin/"), Some("bin"));
43/// assert_eq!(file_name("tmp/foo.txt"), Some("foo.txt"));
44/// assert_eq!(file_name("foo.txt/."), Some("foo.txt"));
45/// assert_eq!(file_name("foo.txt/.//"), Some("foo.txt"));
46/// assert_eq!(file_name("foo.txt/.."), None);
47/// assert_eq!(file_name("/"), None);
48/// ```
49pub fn file_name(mut path: &str) -> Option<&str> {
50 if path == "/" || path.is_empty() {
51 None
52 } else {
53 while let Some(i) = path.rfind('/') {
54 let (_, f) = path.split_at(i + 1);
55 if f == ".." {
56 return None;
57 } else if f.is_empty() || f == "." {
58 path = path.split_at(i).0
59 } else {
60 return Some(f);
61 }
62 }
63 Some(path)
64 }
65}
66
67#[test]
68fn test_file_name() {
69 assert_eq!(file_name("/usr/bin/"), Some("bin"));
70 assert_eq!(file_name("tmp/foo.txt"), Some("foo.txt"));
71 assert_eq!(file_name("foo.txt/."), Some("foo.txt"));
72 assert_eq!(file_name("foo.txt/.//"), Some("foo.txt"));
73 assert_eq!(file_name("foo.txt/.."), None);
74 assert_eq!(file_name("/"), None);
75}
76
77/// Returns an iterator of the non-empty components of a path,
78/// delimited by `/`. Note that `.` and `..` are treated as
79/// components.
80#[cfg(not(windows))]
81pub fn components<'a>(path: &'a str) -> Components<'a> {
82 Components(path.split(&['/'][..]))
83}
84
85#[cfg(windows)]
86pub fn components<'a>(path: &'a str) -> Components<'a> {
87 Components(path.split(&['/', '\\'][..]))
88}
89
90#[derive(Clone)]
91pub struct Components<'a>(std::str::Split<'a, &'static [char]>);
92
93impl<'a> std::fmt::Debug for Components<'a> {
94 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
95 write!(fmt, "Components {{ .. }}")
96 }
97}
98
99impl<'a> Iterator for Components<'a> {
100 type Item = &'a str;
101 fn next(&mut self) -> Option<Self::Item> {
102 loop {
103 if let Some(n) = self.0.next() {
104 if !n.is_empty() {
105 return Some(n);
106 }
107 } else {
108 return None;
109 }
110 }
111 }
112}
113
114/// Push a path component on an existing path. Only works if `extra`
115/// is a relative path.
116/// ```ignore
117/// use pijul_core::path::push;
118/// let mut s = "a".to_string();
119/// push(&mut s, "b");
120/// assert_eq!(s, "a/b");
121/// push(&mut s, "c");
122/// assert_eq!(s, "a/b/c");
123/// ```
124pub fn push(path: &mut String, extra: &str) {
125 assert!(!extra.starts_with('/')); // Make sure the extra path is relative.
126 if !path.ends_with('/') && !path.is_empty() {
127 path.push('/');
128 }
129 path.push_str(extra)
130}
131
132/// Pop the last component off an existing path.
133/// ```ignore
134/// use pijul_core::path::pop;
135/// let mut s = "a/b/c".to_string();
136/// pop(&mut s);
137/// assert_eq!(s, "a/b");
138/// pop(&mut s);
139/// assert_eq!(s, "a");
140/// pop(&mut s);
141/// assert_eq!(s, "");
142/// ```
143pub fn pop(path: &mut String) {
144 if let Some(i) = path.rfind('/') {
145 path.truncate(i)
146 } else {
147 path.clear()
148 }
149}