Add 'from json'

This commit is contained in:
JT
2021-10-01 18:11:49 +13:00
parent d34e083976
commit 3e232a5db8
37 changed files with 4722 additions and 42 deletions

12
crates/nu-path/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
authors = ["The Nu Project Contributors"]
description = "Path handling library for Nushell"
edition = "2018"
license = "MIT"
name = "nu-path"
version = "0.37.1"
[dependencies]
dirs-next = "2.0.0"
dunce = "1.0.1"

3
crates/nu-path/README.md Normal file
View File

@ -0,0 +1,3 @@
# nu-path
This crate takes care of path handling in Nushell, such as canonicalization and component expansion, as well as other path-related utilities.

259
crates/nu-path/src/dots.rs Normal file
View 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.*[");
}
}
}

View File

@ -0,0 +1,75 @@
use std::io;
use std::path::{Path, PathBuf};
use super::dots::{expand_dots, expand_ndots};
use super::tilde::expand_tilde;
// Join a path relative to another path. Paths starting with tilde are considered as absolute.
fn join_path_relative<P, Q>(path: P, relative_to: Q) -> PathBuf
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let path = path.as_ref();
let relative_to = relative_to.as_ref();
if path == Path::new(".") {
// Joining a Path with '.' appends a '.' at the end, making the prompt
// more ugly - so we don't do anything, which should result in an equal
// path on all supported systems.
relative_to.into()
} else if path.starts_with("~") {
// do not end up with "/some/path/~"
path.into()
} else {
relative_to.join(path)
}
}
/// Resolve all symbolic links and all components (tilde, ., .., ...+) and return the path in its
/// absolute form.
///
/// Fails under the same conditions as
/// [std::fs::canonicalize](https://doc.rust-lang.org/std/fs/fn.canonicalize.html).
pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<PathBuf> {
let path = expand_tilde(path);
let path = expand_ndots(path);
dunce::canonicalize(path)
}
/// Same as canonicalize() but the input path is specified relative to another path
pub fn canonicalize_with<P, Q>(path: P, relative_to: Q) -> io::Result<PathBuf>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let path = join_path_relative(path, relative_to);
canonicalize(path)
}
/// Resolve only path components (tilde, ., .., ...+), if possible.
///
/// The function works in a "best effort" mode: It does not fail but rather returns the unexpanded
/// version if the expansion is not possible.
///
/// Furthermore, unlike canonicalize(), it does not use sys calls (such as readlink).
///
/// Does not convert to absolute form nor does it resolve symlinks.
pub fn expand_path(path: impl AsRef<Path>) -> PathBuf {
let path = expand_tilde(path);
let path = expand_ndots(path);
expand_dots(path)
}
/// Same as expand_path() but the input path is specified relative to another path
pub fn expand_path_with<P, Q>(path: P, relative_to: Q) -> PathBuf
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let path = join_path_relative(path, relative_to);
expand_path(path)
}

View File

@ -0,0 +1,8 @@
mod dots;
mod expansions;
mod tilde;
mod util;
pub use expansions::{canonicalize, canonicalize_with, expand_path, expand_path_with};
pub use tilde::expand_tilde;
pub use util::trim_trailing_slash;

View File

@ -0,0 +1,85 @@
use std::path::{Path, PathBuf};
fn expand_tilde_with(path: impl AsRef<Path>, home: Option<PathBuf>) -> PathBuf {
let path = path.as_ref();
if !path.starts_with("~") {
return path.into();
}
match home {
None => path.into(),
Some(mut h) => {
if h == Path::new("/") {
// Corner case: `h` is a root directory;
// don't prepend extra `/`, just drop the tilde.
path.strip_prefix("~").unwrap_or(path).into()
} else {
if let Ok(p) = path.strip_prefix("~/") {
h.push(p)
}
h
}
}
}
}
/// Expand tilde ("~") into a home directory if it is the first path component
pub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf {
// TODO: Extend this to work with "~user" style of home paths
expand_tilde_with(path, dirs_next::home_dir())
}
#[cfg(test)]
mod tests {
use super::*;
fn check_expanded(s: &str) {
let home = Path::new("/home");
let buf = Some(PathBuf::from(home));
assert!(expand_tilde_with(Path::new(s), buf).starts_with(&home));
// Tests the special case in expand_tilde for "/" as home
let home = Path::new("/");
let buf = Some(PathBuf::from(home));
assert!(!expand_tilde_with(Path::new(s), buf).starts_with("//"));
}
fn check_not_expanded(s: &str) {
let home = PathBuf::from("/home");
let expanded = expand_tilde_with(Path::new(s), Some(home));
assert!(expanded == Path::new(s));
}
#[test]
fn string_with_tilde() {
check_expanded("~");
}
#[test]
fn string_with_tilde_forward_slash() {
check_expanded("~/test/");
}
#[test]
fn string_with_tilde_double_forward_slash() {
check_expanded("~//test/");
}
#[test]
fn does_not_expand_tilde_if_tilde_is_not_first_character() {
check_not_expanded("1~1");
}
#[cfg(windows)]
#[test]
fn string_with_tilde_backslash() {
check_expanded("~\\test/test2/test3");
}
#[cfg(windows)]
#[test]
fn string_with_double_tilde_backslash() {
check_expanded("~\\\\test\\test2/test3");
}
}

View File

@ -0,0 +1,4 @@
/// Trim trailing path separator from a string
pub fn trim_trailing_slash(s: &str) -> &str {
s.trim_end_matches(std::path::is_separator)
}

View File

@ -0,0 +1 @@
mod util;

View File

@ -0,0 +1,45 @@
use nu_path::trim_trailing_slash;
use std::path::MAIN_SEPARATOR;
/// Helper function that joins string literals with '/' or '\', based on the host OS
fn join_path_sep(pieces: &[&str]) -> String {
let sep_string = String::from(MAIN_SEPARATOR);
pieces.join(&sep_string)
}
#[test]
fn trims_trailing_slash_without_trailing_slash() {
let path = join_path_sep(&["some", "path"]);
let actual = trim_trailing_slash(&path);
assert_eq!(actual, &path)
}
#[test]
fn trims_trailing_slash() {
let path = join_path_sep(&["some", "path", ""]);
let actual = trim_trailing_slash(&path);
let expected = join_path_sep(&["some", "path"]);
assert_eq!(actual, &expected)
}
#[test]
fn trims_many_trailing_slashes() {
let path = join_path_sep(&["some", "path", "", "", "", ""]);
let actual = trim_trailing_slash(&path);
let expected = join_path_sep(&["some", "path"]);
assert_eq!(actual, &expected)
}
#[test]
fn trims_trailing_slash_empty() {
let path = String::from(MAIN_SEPARATOR);
let actual = trim_trailing_slash(&path);
assert_eq!(actual, "")
}