Implement PWD recovery (#12779)

This PR has two parts. The first part is the addition of the
`Stack::set_pwd()` API. It strips trailing slashes from paths for
convenience, but will reject otherwise bad paths, leaving PWD in a good
state. This should reduce the impact of faulty code incorrectly trying
to set PWD.
(https://github.com/nushell/nushell/pull/12760#issuecomment-2095393012)

The second part is implementing a PWD recovery mechanism. PWD can become
bad even when we did nothing wrong. For example, Unix allows you to
remove any directory when another process might still be using it, which
means PWD can just "disappear" under our nose. This PR makes it possible
to use `cd` to reset PWD into a good state. Here's a demonstration:

```sh
mkdir /tmp/foo
cd /tmp/foo

# delete "/tmp/foo" in a subshell, because Nushell is smart and refuse to delete PWD
nu -c 'cd /; rm -r /tmp/foo'

ls          # Error:   × $env.PWD points to a non-existent directory
            # help: Use `cd` to reset $env.PWD into a good state

cd /
pwd         # prints /
```

Also, auto-cd should be working again.
This commit is contained in:
YizhePKU
2024-05-11 00:06:33 +08:00
committed by GitHub
parent 70c01bbb26
commit b9a7faad5a
9 changed files with 214 additions and 33 deletions

View File

@ -1,3 +1,4 @@
use nu_cmd_base::util::get_init_cwd;
use nu_engine::command_prelude::*;
use nu_utils::filesystem::{have_permission, PermissionResult};
@ -39,7 +40,10 @@ impl Command for Cd {
) -> Result<PipelineData, ShellError> {
let physical = call.has_flag(engine_state, stack, "physical")?;
let path_val: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
let cwd = engine_state.cwd(Some(stack))?;
// If getting PWD failed, default to the initial directory. This way, the
// user can use `cd` to recover PWD to a good state.
let cwd = engine_state.cwd(Some(stack)).unwrap_or(get_init_cwd());
let path_val = {
if let Some(path) = path_val {
@ -52,13 +56,13 @@ impl Command for Cd {
}
};
let (path, span) = match path_val {
let path = match path_val {
Some(v) => {
if v.item == "-" {
if let Some(oldpwd) = stack.get_env_var(engine_state, "OLDPWD") {
(oldpwd.to_path()?, v.span)
oldpwd.to_path()?
} else {
(cwd, v.span)
cwd
}
} else {
// Trim whitespace from the end of path.
@ -66,7 +70,7 @@ impl Command for Cd {
&v.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
// If `--physical` is specified, canonicalize the path; otherwise expand the path.
let path = if physical {
if physical {
if let Ok(path) = nu_path::canonicalize_with(path_no_whitespace, &cwd) {
if !path.is_dir() {
return Err(ShellError::NotADirectory { span: v.span });
@ -90,19 +94,12 @@ impl Command for Cd {
return Err(ShellError::NotADirectory { span: v.span });
};
path
};
(path, v.span)
}
}
}
None => {
let path = nu_path::expand_tilde("~");
(path, call.head)
}
None => nu_path::expand_tilde("~"),
};
// Strip the trailing slash from the new path. This is required for PWD.
let path = nu_path::strip_trailing_slash(&path);
// Set OLDPWD.
// We're using `Stack::get_env_var()` instead of `EngineState::cwd()` to avoid a conversion roundtrip.
if let Some(oldpwd) = stack.get_env_var(engine_state, "PWD") {
@ -113,7 +110,7 @@ impl Command for Cd {
//FIXME: this only changes the current scope, but instead this environment variable
//should probably be a block that loads the information from the state in the overlay
PermissionResult::PermissionOk => {
stack.add_env_var("PWD".into(), Value::string(path.to_string_lossy(), span));
stack.set_cwd(path)?;
Ok(PipelineData::empty())
}
PermissionResult::PermissionDenied(reason) => Err(ShellError::IOError {