mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 22:50:14 +02:00
nu-path crate refactor (#3730)
* Resolve rebase artifacts * Remove leftover dependencies on removed feature * Remove unnecessary 'pub' * Start taking notes and fooling around * Split canonicalize to two versions; Add TODOs One that takes `relative_to` and one that doesn't. More TODO notes. * Merge absolutize to and rename resolve_dots * Add custom absolutize fn and use it in path expand * Convert a couple of dunce::canonicalize to ours * Update nu-path description * Replace all canonicalize with nu-path version * Remove leftover dunce dependencies * Fix broken autocd with trailing slash Trailing slash is preserved *only* in paths that do not contain "." or "..". This should be fixed in the future to cover all paths but for now it at least covers basic cases. * Use dunce::canonicalize for canonicalizing * Alow cd recovery from non-existent cwd * Disable removed canonicalize functionality tests Remove unused import * Break down nu-path into separate modules * Remove unused public imports * Remove abundant cow mapping * Fix clippy warning * Reformulate old canonicalize tests to expand_path They wouldn't work with the new canonicalize. * Canonicalize also ~ and ndots; Unify path joining Also, add doc comments in nu_path::expansions. * Add comment * Avoid expanding ndots if path is not valid UTF-8 With this change, no lossy path->string conversion should happen in the nu-path crate. * Fmt * Slight expand_tilde refactor; Add doc comments * Start nu-path integration tests * Add tests TODO * Fix docstring typo * Fix some doc strings * Add README for nu-path crate * Add a couple of canonicalize tests * Add nu-path integration tests * Add trim trailing slashes tests * Update nu-path dependency * Remove unused import * Regenerate lockfile
This commit is contained in:
259
crates/nu-path/src/dots.rs
Normal file
259
crates/nu-path/src/dots.rs
Normal file
@ -0,0 +1,259 @@
|
||||
use std::path::{is_separator, Component, Path, PathBuf};
|
||||
|
||||
const EXPAND_STR: &str = if cfg!(windows) { r"..\" } else { "../" };
|
||||
|
||||
fn handle_dots_push(string: &mut String, count: u8) {
|
||||
if count < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
if count == 1 {
|
||||
string.push('.');
|
||||
return;
|
||||
}
|
||||
|
||||
for _ in 0..(count - 1) {
|
||||
string.push_str(EXPAND_STR);
|
||||
}
|
||||
|
||||
string.pop(); // remove last '/'
|
||||
}
|
||||
|
||||
/// Expands any occurence of more than two dots into a sequence of ../ (or ..\ on windows), e.g.,
|
||||
/// "..." into "../..", "...." into "../../../", etc.
|
||||
pub fn expand_ndots(path: impl AsRef<Path>) -> PathBuf {
|
||||
// Check if path is valid UTF-8 and if not, return it as it is to avoid breaking it via string
|
||||
// conversion.
|
||||
let path_str = match path.as_ref().to_str() {
|
||||
Some(s) => s,
|
||||
None => return path.as_ref().into(),
|
||||
};
|
||||
|
||||
// find if we need to expand any >2 dot paths and early exit if not
|
||||
let mut dots_count = 0u8;
|
||||
let ndots_present = {
|
||||
for chr in path_str.chars() {
|
||||
if chr == '.' {
|
||||
dots_count += 1;
|
||||
} else {
|
||||
if is_separator(chr) && (dots_count > 2) {
|
||||
// this path component had >2 dots
|
||||
break;
|
||||
}
|
||||
|
||||
dots_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
dots_count > 2
|
||||
};
|
||||
|
||||
if !ndots_present {
|
||||
return path.as_ref().into();
|
||||
}
|
||||
|
||||
let mut dots_count = 0u8;
|
||||
let mut expanded = String::new();
|
||||
for chr in path_str.chars() {
|
||||
if chr == '.' {
|
||||
dots_count += 1;
|
||||
} else {
|
||||
if is_separator(chr) {
|
||||
// check for dots expansion only at path component boundaries
|
||||
handle_dots_push(&mut expanded, dots_count);
|
||||
dots_count = 0;
|
||||
} else {
|
||||
// got non-dot within path component => do not expand any dots
|
||||
while dots_count > 0 {
|
||||
expanded.push('.');
|
||||
dots_count -= 1;
|
||||
}
|
||||
}
|
||||
expanded.push(chr);
|
||||
}
|
||||
}
|
||||
|
||||
handle_dots_push(&mut expanded, dots_count);
|
||||
|
||||
expanded.into()
|
||||
}
|
||||
|
||||
/// Expand "." and ".." into nothing and parent directory, respectively.
|
||||
pub fn expand_dots(path: impl AsRef<Path>) -> PathBuf {
|
||||
let path = path.as_ref();
|
||||
|
||||
// Early-exit if path does not contain '.' or '..'
|
||||
if !path
|
||||
.components()
|
||||
.any(|c| std::matches!(c, Component::CurDir | Component::ParentDir))
|
||||
{
|
||||
return path.into();
|
||||
}
|
||||
|
||||
let mut result = PathBuf::with_capacity(path.as_os_str().len());
|
||||
|
||||
// Only pop/skip path elements if the previous one was an actual path element
|
||||
let prev_is_normal = |p: &Path| -> bool {
|
||||
p.components()
|
||||
.next_back()
|
||||
.map(|c| std::matches!(c, Component::Normal(_)))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
path.components().for_each(|component| match component {
|
||||
Component::ParentDir if prev_is_normal(&result) => {
|
||||
result.pop();
|
||||
}
|
||||
Component::CurDir if prev_is_normal(&result) => {}
|
||||
_ => result.push(component),
|
||||
});
|
||||
|
||||
dunce::simplified(&result).to_path_buf()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn expand_two_dots() {
|
||||
let path = Path::new("/foo/bar/..");
|
||||
|
||||
assert_eq!(
|
||||
PathBuf::from("/foo"), // missing path
|
||||
expand_dots(path)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_dots_with_curdir() {
|
||||
let path = Path::new("/foo/./bar/./baz");
|
||||
|
||||
assert_eq!(PathBuf::from("/foo/bar/baz"), expand_dots(path));
|
||||
}
|
||||
|
||||
fn check_ndots_expansion(expected: &str, s: &str) {
|
||||
let expanded = expand_ndots(Path::new(s));
|
||||
assert_eq!(Path::new(expected), &expanded);
|
||||
}
|
||||
|
||||
// common tests
|
||||
#[test]
|
||||
fn string_without_ndots() {
|
||||
check_ndots_expansion("../hola", "../hola");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_three_ndots_and_chars() {
|
||||
check_ndots_expansion("a...b", "a...b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_two_ndots_and_chars() {
|
||||
check_ndots_expansion("a..b", "a..b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_one_dot_and_chars() {
|
||||
check_ndots_expansion("a.b", "a.b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_dots_double_dots_no_change() {
|
||||
// Can't resolve this as we don't know our parent dir
|
||||
assert_eq!(Path::new(".."), expand_dots(Path::new("..")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_dots_single_dot_no_change() {
|
||||
// Can't resolve this as we don't know our current dir
|
||||
assert_eq!(Path::new("."), expand_dots(Path::new(".")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_dots_multi_single_dots_no_change() {
|
||||
assert_eq!(Path::new("././."), expand_dots(Path::new("././.")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_multi_double_dots_no_change() {
|
||||
assert_eq!(Path::new("../../../"), expand_dots(Path::new("../../../")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_dots_no_change_with_dirs() {
|
||||
// Can't resolve this as we don't know our parent dir
|
||||
assert_eq!(
|
||||
Path::new("../../../dir1/dir2/"),
|
||||
expand_dots(Path::new("../../../dir1/dir2"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_dots_simple() {
|
||||
assert_eq!(Path::new("/foo"), expand_dots(Path::new("/foo/bar/..")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_dots_complex() {
|
||||
assert_eq!(
|
||||
Path::new("/test"),
|
||||
expand_dots(Path::new("/foo/./bar/../../test/././test2/../"))
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn string_with_three_ndots() {
|
||||
check_ndots_expansion(r"..\..", "...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_mixed_ndots_and_chars() {
|
||||
check_ndots_expansion(
|
||||
r"a...b/./c..d/../e.f/..\..\..//.",
|
||||
"a...b/./c..d/../e.f/....//.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_three_ndots_and_final_slash() {
|
||||
check_ndots_expansion(r"..\../", ".../");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_three_ndots_and_garbage() {
|
||||
check_ndots_expansion(r"ls ..\../ garbage.*[", "ls .../ garbage.*[");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod non_windows {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn string_with_three_ndots() {
|
||||
check_ndots_expansion(r"../..", "...");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_mixed_ndots_and_chars() {
|
||||
check_ndots_expansion(
|
||||
"a...b/./c..d/../e.f/../../..//.",
|
||||
"a...b/./c..d/../e.f/....//.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_three_ndots_and_final_slash() {
|
||||
check_ndots_expansion("../../", ".../");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_with_three_ndots_and_garbage() {
|
||||
check_ndots_expansion("ls ../../ garbage.*[", "ls .../ garbage.*[");
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user