mirror of
https://github.com/nushell/nushell.git
synced 2025-06-01 07:35:49 +02: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 Prevents ndots from being expanded if they are prefixed with `./`, as the agreed resolution to #13303. Only applies to externals, mirroring the fix from #13218. I did [attempt](https://github.com/132ikl/nushell/tree/internal-ndots-attempt) to apply the fix for internal commands as well, but it seems like the path is expanded too aggressively and I haven't investigated it further yet. `./...` gets normalized into `<pwd>/./...`, which gets normalized into `<pwd>/...` before being handed to `expand_ndots`, and at that point it just looks like a normal n-dots so we can't tell we shouldn't expand. (Fixes #13303) # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> * N-dots are no longer expanded to external command calls when prefixed with `./`. # 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 toolkit.nu; toolkit test stdlib"` 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 > ``` --> Added tests to prevent regression. - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # 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. --> N/A
251 lines
7.1 KiB
Rust
251 lines
7.1 KiB
Rust
#[cfg(windows)]
|
|
use omnipath::WinPathExt;
|
|
use std::path::{Component, Path, PathBuf};
|
|
|
|
/// Normalize the path, expanding occurrences of n-dots.
|
|
///
|
|
/// It performs the same normalization as `nu_path::components()`, except it also expands n-dots,
|
|
/// such as "..." and "....", into multiple "..".
|
|
///
|
|
/// The resulting path will use platform-specific path separators, regardless of what path separators was used in the input.
|
|
pub fn expand_ndots(path: impl AsRef<Path>) -> PathBuf {
|
|
// Returns whether a path component is n-dots.
|
|
fn is_ndots(s: &std::ffi::OsStr) -> bool {
|
|
s.as_encoded_bytes().iter().all(|c| *c == b'.') && s.len() >= 3
|
|
}
|
|
|
|
let path = path.as_ref();
|
|
|
|
let mut result = PathBuf::with_capacity(path.as_os_str().len());
|
|
for component in crate::components(path) {
|
|
match component {
|
|
Component::Normal(s) if is_ndots(s) => {
|
|
let n = s.len();
|
|
// Push ".." to the path (n - 1) times.
|
|
for _ in 0..n - 1 {
|
|
result.push("..");
|
|
}
|
|
}
|
|
_ => result.push(component),
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Normalize the path, expanding occurrences of "." and "..".
|
|
///
|
|
/// It performs the same normalization as `nu_path::components()`, except it also expands ".."
|
|
/// when its preceding component is a normal component, ignoring the possibility of symlinks.
|
|
/// In other words, it operates on the lexical structure of the path.
|
|
///
|
|
/// This won't expand "/.." even though the parent directory of "/" is often
|
|
/// considered to be itself.
|
|
///
|
|
/// The resulting path will use platform-specific path separators, regardless of what path separators was used in the input.
|
|
pub fn expand_dots(path: impl AsRef<Path>) -> PathBuf {
|
|
// Check if the last component of the path is a normal component.
|
|
fn last_component_is_normal(path: &Path) -> bool {
|
|
matches!(path.components().last(), Some(Component::Normal(_)))
|
|
}
|
|
|
|
let path = path.as_ref();
|
|
|
|
let mut result = PathBuf::with_capacity(path.as_os_str().len());
|
|
for component in crate::components(path) {
|
|
match component {
|
|
Component::ParentDir if last_component_is_normal(&result) => {
|
|
result.pop();
|
|
}
|
|
Component::CurDir if last_component_is_normal(&result) => {
|
|
// no-op
|
|
}
|
|
_ => {
|
|
let prev_component = result.components().last();
|
|
if prev_component == Some(Component::RootDir) && component == Component::ParentDir {
|
|
continue;
|
|
}
|
|
result.push(component)
|
|
}
|
|
}
|
|
}
|
|
|
|
simiplified(&result)
|
|
}
|
|
|
|
/// Expand ndots, but only if it looks like it probably contains them, because there is some lossy
|
|
/// path normalization that happens.
|
|
pub fn expand_ndots_safe(path: impl AsRef<Path>) -> PathBuf {
|
|
let string = path.as_ref().to_string_lossy();
|
|
|
|
// Use ndots if it contains at least `...`, since that's the minimum trigger point.
|
|
// Don't use it if it contains ://, because that looks like a URL scheme and the path normalization
|
|
// will mess with that.
|
|
// Don't use it if it starts with `./`, as to not break golang wildcard syntax
|
|
// (since generally you're probably not using `./` with ndots)
|
|
if string.contains("...") && !string.contains("://") && !string.starts_with("./") {
|
|
expand_ndots(path)
|
|
} else {
|
|
path.as_ref().to_owned()
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn simiplified(path: &std::path::Path) -> PathBuf {
|
|
path.to_winuser_path()
|
|
.unwrap_or_else(|_| path.to_path_buf())
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
fn simiplified(path: &std::path::Path) -> PathBuf {
|
|
path.to_path_buf()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_expand_ndots {
|
|
use super::*;
|
|
use crate::assert_path_eq;
|
|
|
|
#[test]
|
|
fn empty_path() {
|
|
let path = Path::new("");
|
|
assert_path_eq!(expand_ndots(path), "");
|
|
}
|
|
|
|
#[test]
|
|
fn root_dir() {
|
|
let path = Path::new("/");
|
|
let expected = if cfg!(windows) { "\\" } else { "/" };
|
|
assert_path_eq!(expand_ndots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn two_dots() {
|
|
let path = Path::new("..");
|
|
assert_path_eq!(expand_ndots(path), "..");
|
|
}
|
|
|
|
#[test]
|
|
fn three_dots() {
|
|
let path = Path::new("...");
|
|
let expected = if cfg!(windows) { r"..\.." } else { "../.." };
|
|
assert_path_eq!(expand_ndots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn five_dots() {
|
|
let path = Path::new(".....");
|
|
let expected = if cfg!(windows) {
|
|
r"..\..\..\.."
|
|
} else {
|
|
"../../../.."
|
|
};
|
|
assert_path_eq!(expand_ndots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn three_dots_with_trailing_slash() {
|
|
let path = Path::new("/tmp/.../");
|
|
let expected = if cfg!(windows) {
|
|
r"\tmp\..\..\"
|
|
} else {
|
|
"/tmp/../../"
|
|
};
|
|
assert_path_eq!(expand_ndots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn filenames_with_dots() {
|
|
let path = Path::new("...foo.../");
|
|
let expected = if cfg!(windows) {
|
|
r"...foo...\"
|
|
} else {
|
|
"...foo.../"
|
|
};
|
|
assert_path_eq!(expand_ndots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_ndots() {
|
|
let path = Path::new("..././...");
|
|
let expected = if cfg!(windows) {
|
|
r"..\..\..\.."
|
|
} else {
|
|
"../../../.."
|
|
};
|
|
assert_path_eq!(expand_ndots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn trailing_dots() {
|
|
let path = Path::new("/foo/bar/..");
|
|
let expected = if cfg!(windows) {
|
|
r"\foo\bar\.."
|
|
} else {
|
|
"/foo/bar/.."
|
|
};
|
|
assert_path_eq!(expand_ndots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn leading_dot_slash() {
|
|
let path = Path::new("./...");
|
|
assert_path_eq!(expand_ndots_safe(path), "./...");
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_expand_dots {
|
|
use super::*;
|
|
use crate::assert_path_eq;
|
|
|
|
#[test]
|
|
fn empty_path() {
|
|
let path = Path::new("");
|
|
assert_path_eq!(expand_dots(path), "");
|
|
}
|
|
|
|
#[test]
|
|
fn single_dot() {
|
|
let path = Path::new("./");
|
|
let expected = if cfg!(windows) { r".\" } else { "./" };
|
|
assert_path_eq!(expand_dots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn more_single_dots() {
|
|
let path = Path::new("././.");
|
|
let expected = ".";
|
|
assert_path_eq!(expand_dots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn double_dots() {
|
|
let path = Path::new("../../..");
|
|
let expected = if cfg!(windows) {
|
|
r"..\..\.."
|
|
} else {
|
|
"../../.."
|
|
};
|
|
assert_path_eq!(expand_dots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn backtrack_once() {
|
|
let path = Path::new("/foo/bar/../baz/");
|
|
let expected = if cfg!(windows) {
|
|
r"\foo\baz\"
|
|
} else {
|
|
"/foo/baz/"
|
|
};
|
|
assert_path_eq!(expand_dots(path), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn backtrack_to_root() {
|
|
let path = Path::new("/foo/bar/../../../../baz");
|
|
let expected = if cfg!(windows) { r"\baz" } else { "/baz" };
|
|
assert_path_eq!(expand_dots(path), expected);
|
|
}
|
|
}
|