mirror of
https://github.com/nushell/nushell
synced 2025-01-05 01:39:02 +00:00
8e2917b9ae
# Description This makes assignment operations and `const` behave the same way `let` and `mut` do, absorbing the rest of the pipeline. Changes the lexer to be able to recognize assignment operators as a separate token, and then makes the lite parser continue to push spans into the same command regardless of any redirections or pipes if an assignment operator is encountered. Because the pipeline is no longer split up by the lite parser at this point, it's trivial to just parse the right hand side as if it were a subexpression not contained within parentheses. # User-Facing Changes Big breaking change. These are all now possible: ```nushell const path = 'a' | path join 'b' mut x = 2 $x = random int $x = [1 2 3] | math sum $env.FOO = random chars ``` In the past, these would have led to (an attempt at) bare word string parsing. So while `$env.FOO = bar` would have previously set the environment variable `FOO` to the string `"bar"`, it now tries to run the command named `bar`, hence the major breaking change. However, this is desirable because it is very consistent - if you see the `=`, you can just assume it absorbs everything else to the right of it. # Tests + Formatting Added tests for the new behaviour. Adjusted some existing tests that depended on the right hand side of assignments being parsed as barewords. # After Submitting - [ ] release notes (breaking change!)
572 lines
15 KiB
Rust
572 lines
15 KiB
Rust
use nu_path::AbsolutePath;
|
|
use nu_test_support::fs::{files_exist_at, Stub::EmptyFile};
|
|
use nu_test_support::nu;
|
|
use nu_test_support::playground::Playground;
|
|
use rstest::rstest;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn removes_a_file() {
|
|
Playground::setup("rm_test_1", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("i_will_be_deleted.txt")]);
|
|
|
|
nu!(
|
|
cwd: dirs.root(),
|
|
"rm rm_test_1/i_will_be_deleted.txt"
|
|
);
|
|
|
|
let path = dirs.test().join("i_will_be_deleted.txt");
|
|
|
|
assert!(!path.exists());
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn removes_files_with_wildcard() {
|
|
Playground::setup("rm_test_2", |dirs, sandbox| {
|
|
sandbox
|
|
.within("src")
|
|
.with_files(&[
|
|
EmptyFile("cli.rs"),
|
|
EmptyFile("lib.rs"),
|
|
EmptyFile("prelude.rs"),
|
|
])
|
|
.within("src/parser")
|
|
.with_files(&[EmptyFile("parse.rs"), EmptyFile("parser.rs")])
|
|
.within("src/parser/parse")
|
|
.with_files(&[EmptyFile("token_tree.rs")])
|
|
.within("src/parser/hir")
|
|
.with_files(&[
|
|
EmptyFile("baseline_parse.rs"),
|
|
EmptyFile("baseline_parse_tokens.rs"),
|
|
]);
|
|
|
|
nu!(
|
|
cwd: dirs.test(),
|
|
r#"rm src/*/*/*.rs"#
|
|
);
|
|
|
|
assert!(!files_exist_at(
|
|
vec![
|
|
"src/parser/parse/token_tree.rs",
|
|
"src/parser/hir/baseline_parse.rs",
|
|
"src/parser/hir/baseline_parse_tokens.rs"
|
|
],
|
|
dirs.test()
|
|
));
|
|
|
|
assert_eq!(
|
|
Playground::glob_vec(&format!("{}/src/*/*/*.rs", dirs.test().display())),
|
|
Vec::<std::path::PathBuf>::new()
|
|
);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn removes_deeply_nested_directories_with_wildcard_and_recursive_flag() {
|
|
Playground::setup("rm_test_3", |dirs, sandbox| {
|
|
sandbox
|
|
.within("src")
|
|
.with_files(&[
|
|
EmptyFile("cli.rs"),
|
|
EmptyFile("lib.rs"),
|
|
EmptyFile("prelude.rs"),
|
|
])
|
|
.within("src/parser")
|
|
.with_files(&[EmptyFile("parse.rs"), EmptyFile("parser.rs")])
|
|
.within("src/parser/parse")
|
|
.with_files(&[EmptyFile("token_tree.rs")])
|
|
.within("src/parser/hir")
|
|
.with_files(&[
|
|
EmptyFile("baseline_parse.rs"),
|
|
EmptyFile("baseline_parse_tokens.rs"),
|
|
]);
|
|
|
|
nu!(
|
|
cwd: dirs.test(),
|
|
"rm -r src/*"
|
|
);
|
|
|
|
assert!(!files_exist_at(
|
|
vec!["src/parser/parse", "src/parser/hir"],
|
|
dirs.test()
|
|
));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn removes_directory_contents_without_recursive_flag_if_empty() {
|
|
Playground::setup("rm_test_4", |dirs, _| {
|
|
nu!(
|
|
cwd: dirs.root(),
|
|
"rm rm_test_4"
|
|
);
|
|
|
|
assert!(!dirs.test().exists());
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn removes_directory_contents_with_recursive_flag() {
|
|
Playground::setup("rm_test_5", |dirs, sandbox| {
|
|
sandbox.with_files(&[
|
|
EmptyFile("yehuda.txt"),
|
|
EmptyFile("jttxt"),
|
|
EmptyFile("andres.txt"),
|
|
]);
|
|
|
|
nu!(
|
|
cwd: dirs.root(),
|
|
"rm rm_test_5 --recursive"
|
|
);
|
|
|
|
assert!(!dirs.test().exists());
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn errors_if_attempting_to_delete_a_directory_with_content_without_recursive_flag() {
|
|
Playground::setup("rm_test_6", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("some_empty_file.txt")]);
|
|
let actual = nu!(
|
|
cwd: dirs.root(),
|
|
"rm rm_test_6"
|
|
);
|
|
|
|
assert!(dirs.test().exists());
|
|
assert!(actual.err.contains("cannot remove non-empty directory"));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn errors_if_attempting_to_delete_home() {
|
|
Playground::setup("rm_test_8", |dirs, _| {
|
|
let actual = nu!(
|
|
cwd: dirs.root(),
|
|
"$env.HOME = 'myhome' ; rm -rf ~"
|
|
);
|
|
|
|
assert!(actual.err.contains("please use -I or -i"));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn errors_if_attempting_to_delete_single_dot_as_argument() {
|
|
Playground::setup("rm_test_7", |dirs, _| {
|
|
let actual = nu!(
|
|
cwd: dirs.root(),
|
|
"rm ."
|
|
);
|
|
|
|
assert!(actual.err.contains("cannot remove any parent directory"));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn errors_if_attempting_to_delete_two_dot_as_argument() {
|
|
Playground::setup("rm_test_8", |dirs, _| {
|
|
let actual = nu!(
|
|
cwd: dirs.root(),
|
|
"rm .."
|
|
);
|
|
|
|
assert!(actual.err.contains("cannot"));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn removes_multiple_directories() {
|
|
Playground::setup("rm_test_9", |dirs, sandbox| {
|
|
sandbox
|
|
.within("src")
|
|
.with_files(&[EmptyFile("a.rs"), EmptyFile("b.rs")])
|
|
.within("src/cli")
|
|
.with_files(&[EmptyFile("c.rs"), EmptyFile("d.rs")])
|
|
.within("test")
|
|
.with_files(&[EmptyFile("a_test.rs"), EmptyFile("b_test.rs")]);
|
|
|
|
nu!(
|
|
cwd: dirs.test(),
|
|
"rm src test --recursive"
|
|
);
|
|
|
|
assert_eq!(
|
|
Playground::glob_vec(&format!("{}/*", dirs.test().display())),
|
|
Vec::<std::path::PathBuf>::new()
|
|
);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn removes_multiple_files() {
|
|
Playground::setup("rm_test_10", |dirs, sandbox| {
|
|
sandbox.with_files(&[
|
|
EmptyFile("yehuda.txt"),
|
|
EmptyFile("jttxt"),
|
|
EmptyFile("andres.txt"),
|
|
]);
|
|
|
|
nu!(
|
|
cwd: dirs.test(),
|
|
"rm yehuda.txt jttxt andres.txt"
|
|
);
|
|
|
|
assert_eq!(
|
|
Playground::glob_vec(&format!("{}/*", dirs.test().display())),
|
|
Vec::<std::path::PathBuf>::new()
|
|
);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn removes_multiple_files_with_asterisks() {
|
|
Playground::setup("rm_test_11", |dirs, sandbox| {
|
|
sandbox.with_files(&[
|
|
EmptyFile("yehuda.txt"),
|
|
EmptyFile("jt.txt"),
|
|
EmptyFile("andres.toml"),
|
|
]);
|
|
|
|
nu!(
|
|
cwd: dirs.test(),
|
|
"rm *.txt *.toml"
|
|
);
|
|
|
|
assert_eq!(
|
|
Playground::glob_vec(&format!("{}/*", dirs.test().display())),
|
|
Vec::<std::path::PathBuf>::new()
|
|
);
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn allows_doubly_specified_file() {
|
|
Playground::setup("rm_test_12", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("yehuda.txt"), EmptyFile("jt.toml")]);
|
|
|
|
let actual = nu!(
|
|
cwd: dirs.test(),
|
|
"rm *.txt yehuda* *.toml"
|
|
);
|
|
|
|
assert_eq!(
|
|
Playground::glob_vec(&format!("{}/*", dirs.test().display())),
|
|
Vec::<std::path::PathBuf>::new()
|
|
);
|
|
assert!(!actual.out.contains("error"))
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn remove_files_from_two_parents_up_using_multiple_dots_and_glob() {
|
|
Playground::setup("rm_test_13", |dirs, sandbox| {
|
|
sandbox.with_files(&[
|
|
EmptyFile("yehuda.txt"),
|
|
EmptyFile("jt.txt"),
|
|
EmptyFile("kevin.txt"),
|
|
]);
|
|
|
|
sandbox.within("foo").mkdir("bar");
|
|
|
|
nu!(
|
|
cwd: dirs.test().join("foo/bar"),
|
|
"rm .../*.txt"
|
|
);
|
|
|
|
assert!(!files_exist_at(
|
|
vec!["yehuda.txt", "jttxt", "kevin.txt"],
|
|
dirs.test()
|
|
));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn no_errors_if_attempting_to_delete_non_existent_file_with_f_flag() {
|
|
Playground::setup("rm_test_14", |dirs, _| {
|
|
let actual = nu!(
|
|
cwd: dirs.root(),
|
|
"rm -f non_existent_file.txt"
|
|
);
|
|
|
|
assert!(!actual.err.contains("no valid path"));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn rm_wildcard_keeps_dotfiles() {
|
|
Playground::setup("rm_test_15", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("foo"), EmptyFile(".bar")]);
|
|
|
|
nu!(
|
|
cwd: dirs.test(),
|
|
r#"rm *"#
|
|
);
|
|
|
|
assert!(!files_exist_at(vec!["foo"], dirs.test()));
|
|
assert!(files_exist_at(vec![".bar"], dirs.test()));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn rm_wildcard_leading_dot_deletes_dotfiles() {
|
|
Playground::setup("rm_test_16", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("foo"), EmptyFile(".bar")]);
|
|
|
|
nu!(
|
|
cwd: dirs.test(),
|
|
"rm .*"
|
|
);
|
|
|
|
assert!(files_exist_at(vec!["foo"], dirs.test()));
|
|
assert!(!files_exist_at(vec![".bar"], dirs.test()));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn removes_files_with_case_sensitive_glob_matches_by_default() {
|
|
Playground::setup("glob_test", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("A0"), EmptyFile("a1")]);
|
|
|
|
nu!(
|
|
cwd: dirs.root(),
|
|
"rm glob_test/A*"
|
|
);
|
|
|
|
let deleted_path = dirs.test().join("A0");
|
|
let skipped_path = dirs.test().join("a1");
|
|
|
|
assert!(!deleted_path.exists());
|
|
assert!(skipped_path.exists());
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn remove_ignores_ansi() {
|
|
Playground::setup("rm_test_ansi", |_dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("test.txt")]);
|
|
|
|
let actual = nu!(
|
|
cwd: sandbox.cwd(),
|
|
"ls | find test | get name | rm $in.0; ls | is-empty",
|
|
);
|
|
assert_eq!(actual.out, "true");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn removes_symlink() {
|
|
let symlink_target = "symlink_target";
|
|
let symlink = "symlink";
|
|
Playground::setup("rm_test_symlink", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile(symlink_target)]);
|
|
|
|
#[cfg(not(windows))]
|
|
std::os::unix::fs::symlink(dirs.test().join(symlink_target), dirs.test().join(symlink))
|
|
.unwrap();
|
|
#[cfg(windows)]
|
|
std::os::windows::fs::symlink_file(
|
|
dirs.test().join(symlink_target),
|
|
dirs.test().join(symlink),
|
|
)
|
|
.unwrap();
|
|
|
|
let _ = nu!(cwd: sandbox.cwd(), "rm symlink");
|
|
|
|
assert!(!dirs.test().join(symlink).exists());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn removes_symlink_pointing_to_directory() {
|
|
Playground::setup("rm_symlink_to_directory", |dirs, sandbox| {
|
|
sandbox.mkdir("test").symlink("test", "test_link");
|
|
|
|
nu!(cwd: sandbox.cwd(), "rm test_link");
|
|
|
|
assert!(!dirs.test().join("test_link").exists());
|
|
// The pointed directory should not be deleted.
|
|
assert!(dirs.test().join("test").exists());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn removes_file_after_cd() {
|
|
Playground::setup("rm_after_cd", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("delete.txt")]);
|
|
|
|
nu!(
|
|
cwd: dirs.root(),
|
|
"let file = 'delete.txt'; cd rm_after_cd; rm $file",
|
|
);
|
|
|
|
let path = dirs.test().join("delete.txt");
|
|
assert!(!path.exists());
|
|
})
|
|
}
|
|
|
|
struct Cleanup<'a> {
|
|
dir_to_clean: &'a AbsolutePath,
|
|
}
|
|
|
|
fn set_dir_read_only(directory: &AbsolutePath, read_only: bool) {
|
|
let mut permissions = fs::metadata(directory).unwrap().permissions();
|
|
permissions.set_readonly(read_only);
|
|
fs::set_permissions(directory, permissions).expect("failed to set directory permissions");
|
|
}
|
|
|
|
impl<'a> Drop for Cleanup<'a> {
|
|
/// Restores write permissions to the given directory so that the Playground can be successfully
|
|
/// cleaned up.
|
|
fn drop(&mut self) {
|
|
set_dir_read_only(self.dir_to_clean, false);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
// This test is only about verifying file names are included in rm error messages. It is easier
|
|
// to only have this work on non-windows systems (i.e., unix-like) than to try to get the
|
|
// permissions to work on all platforms.
|
|
#[cfg(not(windows))]
|
|
fn rm_prints_filenames_on_error() {
|
|
Playground::setup("rm_prints_filenames_on_error", |dirs, sandbox| {
|
|
let file_names = vec!["test1.txt", "test2.txt"];
|
|
|
|
let with_files: Vec<_> = file_names
|
|
.iter()
|
|
.map(|file_name| EmptyFile(file_name))
|
|
.collect();
|
|
sandbox.with_files(&with_files);
|
|
|
|
let test_dir = dirs.test();
|
|
|
|
set_dir_read_only(test_dir, true);
|
|
let _cleanup = Cleanup {
|
|
dir_to_clean: test_dir,
|
|
};
|
|
|
|
// This rm is expected to fail, and stderr output indicating so is also expected.
|
|
let actual = nu!(cwd: test_dir, "rm test*.txt");
|
|
|
|
assert!(files_exist_at(file_names.clone(), test_dir));
|
|
for file_name in file_names {
|
|
let path = test_dir.join(file_name);
|
|
let substr = format!("Could not delete {}", path.to_string_lossy());
|
|
assert!(
|
|
actual.err.contains(&substr),
|
|
"Matching: {}\n=== Command stderr:\n{}\n=== End stderr",
|
|
substr,
|
|
actual.err
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn rm_files_inside_glob_metachars_dir() {
|
|
Playground::setup("rm_files_inside_glob_metachars_dir", |dirs, sandbox| {
|
|
let sub_dir = "test[]";
|
|
sandbox
|
|
.within(sub_dir)
|
|
.with_files(&[EmptyFile("test_file.txt")]);
|
|
|
|
let actual = nu!(
|
|
cwd: dirs.test().join(sub_dir),
|
|
"rm test_file.txt",
|
|
);
|
|
|
|
assert!(actual.err.is_empty());
|
|
assert!(!files_exist_at(
|
|
vec!["test_file.txt"],
|
|
dirs.test().join(sub_dir)
|
|
));
|
|
});
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("a]c")]
|
|
#[case("a[c")]
|
|
#[case("a[bc]d")]
|
|
#[case("a][c")]
|
|
fn rm_files_with_glob_metachars(#[case] src_name: &str) {
|
|
Playground::setup("rm_files_with_glob_metachars", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile(src_name)]);
|
|
|
|
let src = dirs.test().join(src_name);
|
|
|
|
let actual = nu!(
|
|
cwd: dirs.test(),
|
|
"rm '{}'",
|
|
src.display(),
|
|
);
|
|
|
|
assert!(actual.err.is_empty());
|
|
assert!(!src.exists());
|
|
|
|
// test with variables
|
|
sandbox.with_files(&[EmptyFile(src_name)]);
|
|
let actual = nu!(
|
|
cwd: dirs.test(),
|
|
"let f = '{}'; rm $f",
|
|
src.display(),
|
|
);
|
|
|
|
assert!(actual.err.is_empty());
|
|
assert!(!src.exists());
|
|
});
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
#[rstest]
|
|
#[case("a]?c")]
|
|
#[case("a*.?c")]
|
|
// windows doesn't allow filename with `*`.
|
|
fn rm_files_with_glob_metachars_nw(#[case] src_name: &str) {
|
|
rm_files_with_glob_metachars(src_name);
|
|
}
|
|
|
|
#[test]
|
|
fn force_rm_suppress_error() {
|
|
Playground::setup("force_rm_suppress_error", |dirs, sandbox| {
|
|
sandbox.with_files(&[EmptyFile("test_file.txt")]);
|
|
|
|
// the second rm should suppress error.
|
|
let actual = nu!(
|
|
cwd: dirs.test(),
|
|
"rm test_file.txt; rm -f test_file.txt",
|
|
);
|
|
|
|
assert!(actual.err.is_empty());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn rm_with_tilde() {
|
|
Playground::setup("rm_tilde", |dirs, sandbox| {
|
|
sandbox.within("~tilde").with_files(&[
|
|
EmptyFile("f1.txt"),
|
|
EmptyFile("f2.txt"),
|
|
EmptyFile("f3.txt"),
|
|
]);
|
|
|
|
let actual = nu!(cwd: dirs.test(), "rm '~tilde/f1.txt'");
|
|
assert!(actual.err.is_empty());
|
|
assert!(!files_exist_at(
|
|
vec![Path::new("f1.txt")],
|
|
dirs.test().join("~tilde")
|
|
));
|
|
|
|
// pass variable
|
|
let actual = nu!(cwd: dirs.test(), "let f = '~tilde/f2.txt'; rm $f");
|
|
assert!(actual.err.is_empty());
|
|
assert!(!files_exist_at(
|
|
vec![Path::new("f2.txt")],
|
|
dirs.test().join("~tilde")
|
|
));
|
|
|
|
// remove directory
|
|
let actual = nu!(cwd: dirs.test(), "let f = '~tilde'; rm -r $f");
|
|
assert!(actual.err.is_empty());
|
|
assert!(!files_exist_at(vec![Path::new("~tilde")], dirs.test()));
|
|
})
|
|
}
|