mirror of
https://github.com/nushell/nushell.git
synced 2025-03-26 07:19:55 +01:00
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> Fix typos in comments # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --> Signed-off-by: geekvest <cuimoman@sohu.com>
342 lines
10 KiB
Rust
342 lines
10 KiB
Rust
use std::path::{is_separator, Component, Path, PathBuf};
|
|
|
|
use super::helpers;
|
|
|
|
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 occurrence 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 mut not_separator_before_dot = false;
|
|
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;
|
|
}
|
|
not_separator_before_dot = !(is_separator(chr) || chr.is_whitespace());
|
|
dots_count = 0;
|
|
}
|
|
}
|
|
|
|
dots_count > 2
|
|
};
|
|
|
|
if !ndots_present || not_separator_before_dot {
|
|
return path.as_ref().into();
|
|
}
|
|
|
|
enum Segment {
|
|
Empty,
|
|
OnlyDots,
|
|
OtherChars,
|
|
}
|
|
let mut dots_count = 0u8;
|
|
let mut path_segment = Segment::Empty;
|
|
let mut expanded = String::with_capacity(path_str.len() + 10);
|
|
for chr in path_str.chars() {
|
|
if chr == '.' {
|
|
if matches!(path_segment, Segment::Empty) {
|
|
path_segment = Segment::OnlyDots;
|
|
}
|
|
dots_count += 1;
|
|
} else {
|
|
if is_separator(chr) {
|
|
if matches!(path_segment, Segment::OnlyDots) {
|
|
// check for dots expansion only at path component boundaries
|
|
handle_dots_push(&mut expanded, dots_count);
|
|
dots_count = 0;
|
|
} else {
|
|
// if at a path component boundary a secment consists of not only dots
|
|
// don't expand the dots and only append the appropriate number of .
|
|
while dots_count > 0 {
|
|
expanded.push('.');
|
|
dots_count -= 1;
|
|
}
|
|
}
|
|
path_segment = Segment::Empty;
|
|
} else {
|
|
// got non-dot within path component => do not expand any dots
|
|
path_segment = Segment::OtherChars;
|
|
while dots_count > 0 {
|
|
expanded.push('.');
|
|
dots_count -= 1;
|
|
}
|
|
}
|
|
expanded.push(chr);
|
|
}
|
|
}
|
|
|
|
// Here only the final dots without any following characters are handled
|
|
if matches!(path_segment, Segment::OnlyDots) {
|
|
handle_dots_push(&mut expanded, dots_count);
|
|
} else {
|
|
for _ in 0..dots_count {
|
|
expanded.push('.');
|
|
}
|
|
}
|
|
|
|
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),
|
|
});
|
|
|
|
helpers::simiplified(&result)
|
|
}
|
|
|
|
#[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));
|
|
}
|
|
|
|
// track_caller refers, in the panic-message, to the line of the function call and not
|
|
// inside of the function, which is nice for a test-helper-function
|
|
#[track_caller]
|
|
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 string_starts_with_dots() {
|
|
check_ndots_expansion(".file", ".file");
|
|
check_ndots_expansion("..file", "..file");
|
|
check_ndots_expansion("...file", "...file");
|
|
check_ndots_expansion("....file", "....file");
|
|
check_ndots_expansion(".....file", ".....file");
|
|
}
|
|
|
|
#[test]
|
|
fn string_ends_with_dots() {
|
|
check_ndots_expansion("file.", "file.");
|
|
check_ndots_expansion("file..", "file..");
|
|
check_ndots_expansion("file...", "file...");
|
|
check_ndots_expansion("file....", "file....");
|
|
check_ndots_expansion("file.....", "file.....");
|
|
}
|
|
|
|
#[test]
|
|
fn string_starts_and_ends_with_dots() {
|
|
check_ndots_expansion(".file.", ".file.");
|
|
check_ndots_expansion("..file..", "..file..");
|
|
check_ndots_expansion("...file...", "...file...");
|
|
check_ndots_expansion("....file....", "....file....");
|
|
check_ndots_expansion(".....file.....", ".....file.....");
|
|
}
|
|
#[test]
|
|
fn expand_multiple_dots() {
|
|
check_ndots_expansion("../..", "...");
|
|
check_ndots_expansion("../../..", "....");
|
|
check_ndots_expansion("../../../..", ".....");
|
|
check_ndots_expansion("../../../../", ".../...");
|
|
check_ndots_expansion("../../file name/../../", ".../file name/...");
|
|
check_ndots_expansion("../../../file name/../../../", "..../file name/....");
|
|
}
|
|
|
|
#[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"not_a_cmd.../ garbage.*[", "not_a_cmd.../ 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() {
|
|
// filenames can contain spaces, in these cases the ... .... etc.
|
|
// that are part of a filepath should not be expanded
|
|
check_ndots_expansion("not_a_cmd.../ garbage.*[", "not_a_cmd.../ garbage.*[");
|
|
check_ndots_expansion("/not_a_cmd.../ garbage.*[", "/not_a_cmd.../ garbage.*[");
|
|
check_ndots_expansion("./not_a_cmd.../ garbage.*[", "./not_a_cmd.../ garbage.*[");
|
|
check_ndots_expansion(
|
|
"../../not a cmd.../ garbage.*[",
|
|
".../not a cmd.../ garbage.*[",
|
|
);
|
|
check_ndots_expansion(
|
|
"../../not a cmd.../ garbage.*[...",
|
|
".../not a cmd.../ garbage.*[...",
|
|
);
|
|
check_ndots_expansion("../../ not a cmd garbage.*[", ".../ not a cmd garbage.*[");
|
|
}
|
|
}
|
|
}
|