Migrate to a new PWD API (#12603)

This is the first PR towards migrating to a new `$env.PWD` API that
returns potentially un-canonicalized paths. Refer to PR #12515 for
motivations.

## New API: `EngineState::cwd()`

The goal of the new API is to cover both parse-time and runtime use
case, and avoid unintentional misuse. It takes an `Option<Stack>` as
argument, which if supplied, will search for `$env.PWD` on the stack in
additional to the engine state. I think with this design, there's less
confusion over parse-time and runtime environments. If you have access
to a stack, just supply it; otherwise supply `None`.

## Deprecation of other PWD-related APIs

Other APIs are re-implemented using `EngineState::cwd()` and properly
documented. They're marked deprecated, but their behavior is unchanged.
Unused APIs are deleted, and code that accesses `$env.PWD` directly
without using an API is rewritten.

Deprecated APIs:

* `EngineState::current_work_dir()`
* `StateWorkingSet::get_cwd()`
* `env::current_dir()`
* `env::current_dir_str()`
* `env::current_dir_const()`
* `env::current_dir_str_const()`

Other changes:

* `EngineState::get_cwd()` (deleted)
* `StateWorkingSet::list_env()` (deleted)
* `repl::do_run_cmd()` (rewritten with `env::current_dir_str()`)

## `cd` and `pwd` now use logical paths by default

This pulls the changes from PR #12515. It's currently somewhat broken
because using non-canonicalized paths exposed a bug in our path
normalization logic (Issue #12602). Once that is fixed, this should
work.

## Future plans

This PR needs some tests. Which test helpers should I use, and where
should I put those tests?

I noticed that unquoted paths are expanded within `eval_filepath()` and
`eval_directory()` before they even reach the `cd` command. This means
every paths is expanded twice. Is this intended?

Once this PR lands, the plan is to review all usages of the deprecated
APIs and migrate them to `EngineState::cwd()`. In the meantime, these
usages are annotated with `#[allow(deprecated)]` to avoid breaking CI.

---------

Co-authored-by: Jakub Žádník <kubouch@gmail.com>
This commit is contained in:
YizhePKU
2024-05-03 19:33:09 +08:00
committed by GitHub
parent f32ecc641f
commit bdb6daa4b5
44 changed files with 551 additions and 275 deletions

View File

@ -46,6 +46,7 @@ strum_macros = "0.26"
nu-test-support = { path = "../nu-test-support", version = "0.93.1" }
pretty_assertions = { workspace = true }
rstest = { workspace = true }
tempfile = { workspace = true }
[package.metadata.docs.rs]
all-features = true

View File

@ -26,8 +26,6 @@ type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
#[cfg(feature = "plugin")]
use crate::{PluginRegistryFile, PluginRegistryItem, RegisteredPlugin};
pub static PWD_ENV: &str = "PWD";
#[derive(Clone, Debug)]
pub enum VirtualPath {
File(FileId),
@ -893,14 +891,6 @@ impl EngineState {
self.num_files() - 1
}
pub fn get_cwd(&self) -> Option<String> {
if let Some(pwd_value) = self.get_env_var(PWD_ENV) {
pwd_value.coerce_string().ok()
} else {
None
}
}
pub fn set_config_path(&mut self, key: &str, val: PathBuf) {
self.config_path.insert(key.to_string(), val);
}
@ -922,12 +912,71 @@ impl EngineState {
.map(|comment_spans| self.build_usage(comment_spans))
}
/// Returns the current working directory, which is guaranteed to be canonicalized.
///
/// Returns an empty String if $env.PWD doesn't exist.
#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")]
pub fn current_work_dir(&self) -> String {
self.get_env_var("PWD")
.map(|d| d.coerce_string().unwrap_or_default())
self.cwd(None)
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_default()
}
/// Returns the current working directory, which is guaranteed to be an
/// absolute path without trailing slashes, but might contain symlink
/// components.
///
/// If `stack` is supplied, also considers modifications to the working
/// directory on the stack that have yet to be merged into the engine state.
pub fn cwd(&self, stack: Option<&Stack>) -> Result<PathBuf, ShellError> {
// Helper function to create a simple generic error.
// Its messages are not especially helpful, but these errors don't occur often, so it's probably fine.
fn error(msg: &str) -> Result<PathBuf, ShellError> {
Err(ShellError::GenericError {
error: msg.into(),
msg: "".into(),
span: None,
help: None,
inner: vec![],
})
}
// Helper function to check if a path has trailing slashes.
fn has_trailing_slash(path: &Path) -> bool {
nu_path::components(path).last()
== Some(std::path::Component::Normal(std::ffi::OsStr::new("")))
}
// Retrieve $env.PWD from the stack or the engine state.
let pwd = if let Some(stack) = stack {
stack.get_env_var(self, "PWD")
} else {
self.get_env_var("PWD").map(ToOwned::to_owned)
};
if let Some(pwd) = pwd {
if let Value::String { val, .. } = pwd {
let path = PathBuf::from(val);
if has_trailing_slash(&path) {
error("$env.PWD contains trailing slashes")
} else if !path.is_absolute() {
error("$env.PWD is not an absolute path")
} else if !path.exists() {
error("$env.PWD points to a non-existent directory")
} else if !path.is_dir() {
error("$env.PWD points to a non-directory")
} else {
Ok(path)
}
} else {
error("$env.PWD is not a string")
}
} else {
error("$env.PWD not found")
}
}
// TODO: see if we can completely get rid of this
pub fn get_file_contents(&self) -> &[CachedFile] {
&self.files
@ -1077,3 +1126,213 @@ mod engine_state_tests {
);
}
}
#[cfg(test)]
mod test_cwd {
//! Here're the test cases we need to cover:
//!
//! `EngineState::cwd()` computes the result from `self.env_vars["PWD"]` and
//! optionally `stack.env_vars["PWD"]`.
//!
//! PWD may be unset in either `env_vars`.
//! PWD should NOT be an empty string.
//! PWD should NOT be a non-string value.
//! PWD should NOT be a relative path.
//! PWD should NOT contain trailing slashes.
//! PWD may point to a directory or a symlink to directory.
//! PWD should NOT point to a file or a symlink to file.
//! PWD should NOT point to non-existent entities in the filesystem.
use crate::{
engine::{EngineState, Stack},
Span, Value,
};
use nu_path::assert_path_eq;
use std::path::Path;
use tempfile::{NamedTempFile, TempDir};
/// Creates a symlink. Works on both Unix and Windows.
#[cfg(any(unix, windows))]
fn symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> std::io::Result<()> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(original, link)
}
#[cfg(windows)]
{
if original.as_ref().is_dir() {
std::os::windows::fs::symlink_dir(original, link)
} else {
std::os::windows::fs::symlink_file(original, link)
}
}
}
/// Create an engine state initialized with the given PWD.
fn engine_state_with_pwd(path: impl AsRef<Path>) -> EngineState {
let mut engine_state = EngineState::new();
engine_state.add_env_var(
"PWD".into(),
Value::String {
val: path.as_ref().to_string_lossy().to_string(),
internal_span: Span::unknown(),
},
);
engine_state
}
/// Create a stack initialized with the given PWD.
fn stack_with_pwd(path: impl AsRef<Path>) -> Stack {
let mut stack = Stack::new();
stack.add_env_var(
"PWD".into(),
Value::String {
val: path.as_ref().to_string_lossy().to_string(),
internal_span: Span::unknown(),
},
);
stack
}
#[test]
fn pwd_not_set() {
let engine_state = EngineState::new();
engine_state.cwd(None).unwrap_err();
}
#[test]
fn pwd_is_empty_string() {
let engine_state = engine_state_with_pwd("");
engine_state.cwd(None).unwrap_err();
}
#[test]
fn pwd_is_non_string_value() {
let mut engine_state = EngineState::new();
engine_state.add_env_var(
"PWD".into(),
Value::Glob {
val: "*".into(),
no_expand: false,
internal_span: Span::unknown(),
},
);
engine_state.cwd(None).unwrap_err();
}
#[test]
fn pwd_is_relative_path() {
let engine_state = engine_state_with_pwd("./foo");
engine_state.cwd(None).unwrap_err();
}
#[test]
fn pwd_has_trailing_slash() {
let dir = TempDir::new().unwrap();
let engine_state = engine_state_with_pwd(dir.path().join(""));
engine_state.cwd(None).unwrap_err();
}
#[test]
fn pwd_points_to_normal_file() {
let file = NamedTempFile::new().unwrap();
let engine_state = engine_state_with_pwd(file.path());
engine_state.cwd(None).unwrap_err();
}
#[test]
fn pwd_points_to_normal_directory() {
let dir = TempDir::new().unwrap();
let engine_state = engine_state_with_pwd(dir.path());
let cwd = engine_state.cwd(None).unwrap();
assert_path_eq!(cwd, dir.path());
}
#[test]
fn pwd_points_to_symlink_to_file() {
let file = NamedTempFile::new().unwrap();
let dir = TempDir::new().unwrap();
let link = dir.path().join("link");
symlink(file.path(), &link).unwrap();
let engine_state = engine_state_with_pwd(&link);
engine_state.cwd(None).unwrap_err();
}
#[test]
fn pwd_points_to_symlink_to_directory() {
let dir = TempDir::new().unwrap();
let link = dir.path().join("link");
symlink(dir.path(), &link).unwrap();
let engine_state = engine_state_with_pwd(&link);
let cwd = engine_state.cwd(None).unwrap();
assert_path_eq!(cwd, link);
}
#[test]
fn pwd_points_to_broken_symlink() {
let dir = TempDir::new().unwrap();
let link = dir.path().join("link");
symlink(TempDir::new().unwrap().path(), &link).unwrap();
let engine_state = engine_state_with_pwd(&link);
engine_state.cwd(None).unwrap_err();
}
#[test]
fn pwd_points_to_nonexistent_entity() {
let engine_state = engine_state_with_pwd(TempDir::new().unwrap().path());
engine_state.cwd(None).unwrap_err();
}
#[test]
fn stack_pwd_not_set() {
let dir = TempDir::new().unwrap();
let engine_state = engine_state_with_pwd(dir.path());
let stack = Stack::new();
let cwd = engine_state.cwd(Some(&stack)).unwrap();
assert_eq!(cwd, dir.path());
}
#[test]
fn stack_pwd_is_empty_string() {
let dir = TempDir::new().unwrap();
let engine_state = engine_state_with_pwd(dir.path());
let stack = stack_with_pwd("");
engine_state.cwd(Some(&stack)).unwrap_err();
}
#[test]
fn stack_pwd_points_to_normal_directory() {
let dir1 = TempDir::new().unwrap();
let dir2 = TempDir::new().unwrap();
let engine_state = engine_state_with_pwd(dir1.path());
let stack = stack_with_pwd(dir2.path());
let cwd = engine_state.cwd(Some(&stack)).unwrap();
assert_path_eq!(cwd, dir2.path());
}
#[test]
fn stack_pwd_points_to_normal_directory_with_symlink_components() {
// `/tmp/dir/link` points to `/tmp/dir`, then we set PWD to `/tmp/dir/link/foo`
let dir = TempDir::new().unwrap();
let link = dir.path().join("link");
symlink(dir.path(), &link).unwrap();
let foo = link.join("foo");
std::fs::create_dir(dir.path().join("foo")).unwrap();
let engine_state = EngineState::new();
let stack = stack_with_pwd(&foo);
let cwd = engine_state.cwd(Some(&stack)).unwrap();
assert_path_eq!(cwd, foo);
}
}

View File

@ -2,7 +2,7 @@ use crate::{
ast::Block,
engine::{
usage::build_usage, CachedFile, Command, CommandType, EngineState, OverlayFrame,
StateDelta, Variable, VirtualPath, Visibility, PWD_ENV,
StateDelta, Variable, VirtualPath, Visibility,
},
BlockId, Category, Config, DeclId, FileId, Module, ModuleId, ParseError, ParseWarning, Span,
Type, Value, VarId, VirtualPathId,
@ -601,13 +601,16 @@ impl<'a> StateWorkingSet<'a> {
next_id
}
/// Returns the current working directory as a String, which is guaranteed to be canonicalized.
/// Returns an empty string if $env.PWD doesn't exist, is not a String, or is not an absolute path.
///
/// It does NOT consider modifications to the working directory made on a stack.
#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")]
pub fn get_cwd(&self) -> String {
let pwd = self
.permanent_state
.get_env_var(PWD_ENV)
.expect("internal error: can't find PWD");
pwd.coerce_string()
.expect("internal error: PWD not a string")
self.permanent_state
.cwd(None)
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_default()
}
pub fn get_env_var(&self, name: &str) -> Option<&Value> {
@ -622,16 +625,6 @@ impl<'a> StateWorkingSet<'a> {
&self.permanent_state.config
}
pub fn list_env(&self) -> Vec<String> {
let mut env_vars = vec![];
for env_var in self.permanent_state.env_vars.iter() {
env_vars.push(env_var.0.clone());
}
env_vars
}
pub fn set_variable_type(&mut self, var_id: VarId, ty: Type) {
let num_permanent_vars = self.permanent_state.num_vars();
if var_id < num_permanent_vars {

View File

@ -14,6 +14,7 @@ use std::{
/// Create a Value for `$nu`.
pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Value, ShellError> {
fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf {
#[allow(deprecated)]
let cwd = engine_state.current_work_dir();
if path.exists() {