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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 551 additions and 275 deletions

1
Cargo.lock generated
View file

@ -3227,6 +3227,7 @@ dependencies = [
"serde_json", "serde_json",
"strum", "strum",
"strum_macros 0.26.2", "strum_macros 0.26.2",
"tempfile",
"thiserror", "thiserror",
"typetag", "typetag",
] ]

View file

@ -21,6 +21,7 @@ fn load_bench_commands() -> EngineState {
} }
fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf { fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf {
#[allow(deprecated)]
let cwd = engine_state.current_work_dir(); let cwd = engine_state.current_work_dir();
if path.exists() { if path.exists() {

View file

@ -43,6 +43,7 @@ impl Completer for DirectoryCompletion {
let AdjustView { prefix, span, .. } = adjust_if_intermediate(&prefix, working_set, span); let AdjustView { prefix, span, .. } = adjust_if_intermediate(&prefix, working_set, span);
// Filter only the folders // Filter only the folders
#[allow(deprecated)]
let output: Vec<_> = directory_completion( let output: Vec<_> = directory_completion(
span, span,
&prefix, &prefix,

View file

@ -84,6 +84,7 @@ impl Completer for DotNuCompletion {
partial = base_dir_partial; partial = base_dir_partial;
} else { } else {
// Fetch the current folder // Fetch the current folder
#[allow(deprecated)]
let current_folder = self.engine_state.current_work_dir(); let current_folder = self.engine_state.current_work_dir();
is_current_folder = true; is_current_folder = true;

View file

@ -47,6 +47,7 @@ impl Completer for FileCompletion {
readjusted, readjusted,
} = adjust_if_intermediate(&prefix, working_set, span); } = adjust_if_intermediate(&prefix, working_set, span);
#[allow(deprecated)]
let output: Vec<_> = complete_item( let output: Vec<_> = complete_item(
readjusted, readjusted,
span, span,

View file

@ -177,6 +177,7 @@ pub fn add_plugin_file(
use std::path::Path; use std::path::Path;
let working_set = StateWorkingSet::new(engine_state); let working_set = StateWorkingSet::new(engine_state);
#[allow(deprecated)]
let cwd = working_set.get_cwd(); let cwd = working_set.get_cwd();
if let Some(plugin_file) = plugin_file { if let Some(plugin_file) = plugin_file {
@ -235,6 +236,7 @@ pub fn eval_config_contents(
engine_state.file = prev_file; engine_state.file = prev_file;
// Merge the environment in case env vars changed in the config // Merge the environment in case env vars changed in the config
#[allow(deprecated)]
match nu_engine::env::current_dir(engine_state, stack) { match nu_engine::env::current_dir(engine_state, stack) {
Ok(cwd) => { Ok(cwd) => {
if let Err(e) = engine_state.merge_env(stack, cwd) { if let Err(e) = engine_state.merge_env(stack, cwd) {
@ -272,6 +274,7 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
#[allow(deprecated)]
let cwd = engine_state.current_work_dir(); let cwd = engine_state.current_work_dir();
let Some(config_dir) = nu_path::config_dir().and_then(|mut dir| { let Some(config_dir) = nu_path::config_dir().and_then(|mut dir| {

View file

@ -1,6 +1,7 @@
use crate::util::eval_source; use crate::util::eval_source;
use log::{info, trace}; use log::{info, trace};
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
#[allow(deprecated)]
use nu_engine::{convert_env_values, current_dir, eval_block}; use nu_engine::{convert_env_values, current_dir, eval_block};
use nu_parser::parse; use nu_parser::parse;
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
@ -29,6 +30,7 @@ pub fn evaluate_file(
std::process::exit(1); std::process::exit(1);
} }
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let file_path = canonicalize_with(&path, cwd).unwrap_or_else(|e| { let file_path = canonicalize_with(&path, cwd).unwrap_or_else(|e| {

View file

@ -20,7 +20,8 @@ use nu_cmd_base::{
util::{get_editor, get_guaranteed_cwd}, util::{get_editor, get_guaranteed_cwd},
}; };
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_engine::{convert_env_values, env_to_strings}; #[allow(deprecated)]
use nu_engine::{convert_env_values, current_dir_str, env_to_strings};
use nu_parser::{lex, parse, trim_quotes_str}; use nu_parser::{lex, parse, trim_quotes_str};
use nu_protocol::{ use nu_protocol::{
config::NuCursorShape, config::NuCursorShape,
@ -799,6 +800,7 @@ fn prepare_history_metadata(
line_editor: &mut Reedline, line_editor: &mut Reedline,
) { ) {
if !s.is_empty() && line_editor.has_last_command_context() { if !s.is_empty() && line_editor.has_last_command_context() {
#[allow(deprecated)]
let result = line_editor let result = line_editor
.update_last_command_context(&|mut c| { .update_last_command_context(&|mut c| {
c.start_timestamp = Some(chrono::Utc::now()); c.start_timestamp = Some(chrono::Utc::now());
@ -869,6 +871,7 @@ fn parse_operation(
) -> Result<ReplOperation, ErrReport> { ) -> Result<ReplOperation, ErrReport> {
let tokens = lex(s.as_bytes(), 0, &[], &[], false); let tokens = lex(s.as_bytes(), 0, &[], &[], false);
// Check if this is a single call to a directory, if so auto-cd // Check if this is a single call to a directory, if so auto-cd
#[allow(deprecated)]
let cwd = nu_engine::env::current_dir_str(engine_state, stack)?; let cwd = nu_engine::env::current_dir_str(engine_state, stack)?;
let mut orig = s.clone(); let mut orig = s.clone();
if orig.starts_with('`') { if orig.starts_with('`') {
@ -909,8 +912,6 @@ fn do_auto_cd(
}, },
); );
} }
let path = nu_path::canonicalize_with(path, &cwd)
.expect("internal error: cannot canonicalize known path");
path.to_string_lossy().to_string() path.to_string_lossy().to_string()
}; };
@ -1027,9 +1028,8 @@ fn run_shell_integration_osc2(
stack: &mut Stack, stack: &mut Stack,
use_color: bool, use_color: bool,
) { ) {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { #[allow(deprecated)]
match cwd.coerce_into_string() { if let Ok(path) = current_dir_str(engine_state, stack) {
Ok(path) => {
let start_time = Instant::now(); let start_time = Instant::now();
// Try to abbreviate string for windows title // Try to abbreviate string for windows title
@ -1067,11 +1067,6 @@ fn run_shell_integration_osc2(
use_color, use_color,
); );
} }
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
} }
fn run_shell_integration_osc7( fn run_shell_integration_osc7(
@ -1080,9 +1075,8 @@ fn run_shell_integration_osc7(
stack: &mut Stack, stack: &mut Stack,
use_color: bool, use_color: bool,
) { ) {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { #[allow(deprecated)]
match cwd.coerce_into_string() { if let Ok(path) = current_dir_str(engine_state, stack) {
Ok(path) => {
let start_time = Instant::now(); let start_time = Instant::now();
// Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir) // Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir)
@ -1105,17 +1099,11 @@ fn run_shell_integration_osc7(
use_color, use_color,
); );
} }
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
} }
fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) { fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { #[allow(deprecated)]
match cwd.coerce_into_string() { if let Ok(path) = current_dir_str(engine_state, stack) {
Ok(path) => {
let start_time = Instant::now(); let start_time = Instant::now();
// Otherwise, communicate the path as OSC 9;9 from ConEmu (often used for spawning new tabs in the same dir) // Otherwise, communicate the path as OSC 9;9 from ConEmu (often used for spawning new tabs in the same dir)
@ -1134,22 +1122,14 @@ fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, u
use_color, use_color,
); );
} }
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
} }
fn run_shell_integration_osc633(engine_state: &EngineState, stack: &mut Stack, use_color: bool) { fn run_shell_integration_osc633(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { #[allow(deprecated)]
match cwd.coerce_into_string() { if let Ok(path) = current_dir_str(engine_state, stack) {
Ok(path) => {
// Supported escape sequences of Microsoft's Visual Studio Code (vscode) // Supported escape sequences of Microsoft's Visual Studio Code (vscode)
// https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences // https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
if stack.get_env_var(engine_state, "TERM_PROGRAM") if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) {
== Some(Value::test_string("vscode"))
{
let start_time = Instant::now(); let start_time = Instant::now();
// If we're in vscode, run their specific ansi escape sequence. // If we're in vscode, run their specific ansi escape sequence.
@ -1169,11 +1149,6 @@ fn run_shell_integration_osc633(engine_state: &EngineState, stack: &mut Stack, u
); );
} }
} }
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
}
}
} }
fn run_shell_integration_reset_application_mode() { fn run_shell_integration_reset_application_mode() {

View file

@ -37,6 +37,7 @@ impl Highlighter for NuHighlighter {
let str_word = String::from_utf8_lossy(str_contents).to_string(); let str_word = String::from_utf8_lossy(str_contents).to_string();
let paths = env::path_str(&self.engine_state, &self.stack, *span).ok(); let paths = env::path_str(&self.engine_state, &self.stack, *span).ok();
#[allow(deprecated)]
let res = if let Ok(cwd) = let res = if let Ok(cwd) =
env::current_dir_str(&self.engine_state, &self.stack) env::current_dir_str(&self.engine_state, &self.stack)
{ {

View file

@ -6,7 +6,7 @@ use nu_parser::parse;
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData}; use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData};
use reedline::{Completer, Suggestion}; use reedline::{Completer, Suggestion};
use rstest::{fixture, rstest}; use rstest::{fixture, rstest};
use std::path::PathBuf; use std::path::{PathBuf, MAIN_SEPARATOR};
use support::{ use support::{
completions_helpers::{new_partial_engine, new_quote_engine}, completions_helpers::{new_partial_engine, new_quote_engine},
file, folder, match_suggestions, new_engine, file, folder, match_suggestions, new_engine,
@ -220,7 +220,7 @@ fn file_completions() {
let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Test completions for the current folder // Test completions for the current folder
let target_dir = format!("cp {dir_str}"); let target_dir = format!("cp {dir_str}{MAIN_SEPARATOR}");
let suggestions = completer.complete(&target_dir, target_dir.len()); let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values // Create the expected values
@ -664,7 +664,7 @@ fn folder_with_directorycompletions() {
let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Test completions for the current folder // Test completions for the current folder
let target_dir = format!("cd {dir_str}"); let target_dir = format!("cd {dir_str}{MAIN_SEPARATOR}");
let suggestions = completer.complete(&target_dir, target_dir.len()); let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values // Create the expected values

View file

@ -8,9 +8,7 @@ use nu_protocol::{
}; };
use nu_test_support::fs; use nu_test_support::fs;
use reedline::Suggestion; use reedline::Suggestion;
use std::path::PathBuf; use std::path::{PathBuf, MAIN_SEPARATOR};
const SEP: char = std::path::MAIN_SEPARATOR;
fn create_default_context() -> EngineState { fn create_default_context() -> EngineState {
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()) nu_command::add_shell_command_context(nu_cmd_lang::create_default_context())
@ -20,12 +18,11 @@ fn create_default_context() -> EngineState {
pub fn new_engine() -> (PathBuf, String, EngineState, Stack) { pub fn new_engine() -> (PathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("completions"); let dir = fs::fixtures().join("completions");
let mut dir_str = dir let dir_str = dir
.clone() .clone()
.into_os_string() .into_os_string()
.into_string() .into_string()
.unwrap_or_default(); .unwrap_or_default();
dir_str.push(SEP);
// Create a new engine with default context // Create a new engine with default context
let mut engine_state = create_default_context(); let mut engine_state = create_default_context();
@ -77,12 +74,11 @@ pub fn new_engine() -> (PathBuf, String, EngineState, Stack) {
pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) { pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("quoted_completions"); let dir = fs::fixtures().join("quoted_completions");
let mut dir_str = dir let dir_str = dir
.clone() .clone()
.into_os_string() .into_os_string()
.into_string() .into_string()
.unwrap_or_default(); .unwrap_or_default();
dir_str.push(SEP);
// Create a new engine with default context // Create a new engine with default context
let mut engine_state = create_default_context(); let mut engine_state = create_default_context();
@ -113,12 +109,11 @@ pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) {
pub fn new_partial_engine() -> (PathBuf, String, EngineState, Stack) { pub fn new_partial_engine() -> (PathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("partial_completions"); let dir = fs::fixtures().join("partial_completions");
let mut dir_str = dir let dir_str = dir
.clone() .clone()
.into_os_string() .into_os_string()
.into_string() .into_string()
.unwrap_or_default(); .unwrap_or_default();
dir_str.push(SEP);
// Create a new engine with default context // Create a new engine with default context
let mut engine_state = create_default_context(); let mut engine_state = create_default_context();
@ -165,7 +160,7 @@ pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
// append the separator to the converted path // append the separator to the converted path
pub fn folder(path: PathBuf) -> String { pub fn folder(path: PathBuf) -> String {
let mut converted_path = file(path); let mut converted_path = file(path);
converted_path.push(SEP); converted_path.push(MAIN_SEPARATOR);
converted_path converted_path
} }

View file

@ -13,6 +13,7 @@ pub fn get_init_cwd() -> PathBuf {
} }
pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf { pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf {
#[allow(deprecated)]
nu_engine::env::current_dir(engine_state, stack).unwrap_or_else(|e| { nu_engine::env::current_dir(engine_state, stack).unwrap_or_else(|e| {
let working_set = StateWorkingSet::new(engine_state); let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e); report_error(&working_set, &e);

View file

@ -1,8 +1,8 @@
use std::sync::Arc; #[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::{command_prelude::*, current_dir};
use nu_plugin_engine::{GetPlugin, PersistentPlugin}; use nu_plugin_engine::{GetPlugin, PersistentPlugin};
use nu_protocol::{PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin}; use nu_protocol::{PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin};
use std::sync::Arc;
use crate::util::{get_plugin_dirs, modify_plugin_file}; use crate::util::{get_plugin_dirs, modify_plugin_file};
@ -82,6 +82,7 @@ apparent the next time `nu` is next launched with that plugin registry file.
let filename: Spanned<String> = call.req(engine_state, stack, 0)?; let filename: Spanned<String> = call.req(engine_state, stack, 0)?;
let shell: Option<Spanned<String>> = call.get_flag(engine_state, stack, "shell")?; let shell: Option<Spanned<String>> = call.get_flag(engine_state, stack, "shell")?;
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
// Check the current directory, or fall back to NU_PLUGIN_DIRS // Check the current directory, or fall back to NU_PLUGIN_DIRS

View file

@ -1,11 +1,11 @@
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir};
use nu_protocol::{engine::StateWorkingSet, PluginRegistryFile};
use std::{ use std::{
fs::{self, File}, fs::{self, File},
path::PathBuf, path::PathBuf,
}; };
use nu_engine::{command_prelude::*, current_dir};
use nu_protocol::{engine::StateWorkingSet, PluginRegistryFile};
pub(crate) fn modify_plugin_file( pub(crate) fn modify_plugin_file(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
@ -13,6 +13,7 @@ pub(crate) fn modify_plugin_file(
custom_path: Option<Spanned<String>>, custom_path: Option<Spanned<String>>,
operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>, operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>,
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let plugin_registry_file_path = if let Some(ref custom_path) = custom_path { let plugin_registry_file_path = if let Some(ref custom_path) = custom_path {
@ -58,6 +59,7 @@ pub(crate) fn canonicalize_possible_filename_arg(
arg: &str, arg: &str,
) -> PathBuf { ) -> PathBuf {
// This results in the best possible chance of a match with the plugin item // This results in the best possible chance of a match with the plugin item
#[allow(deprecated)]
if let Ok(cwd) = nu_engine::current_dir(engine_state, stack) { if let Ok(cwd) = nu_engine::current_dir(engine_state, stack) {
let path = nu_path::expand_path_with(arg, &cwd, true); let path = nu_path::expand_path_with(arg, &cwd, true);
// Try to canonicalize // Try to canonicalize

View file

@ -1,4 +1,4 @@
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::command_prelude::*;
use nu_utils::filesystem::{have_permission, PermissionResult}; use nu_utils::filesystem::{have_permission, PermissionResult};
#[derive(Clone)] #[derive(Clone)]
@ -20,6 +20,7 @@ impl Command for Cd {
fn signature(&self) -> nu_protocol::Signature { fn signature(&self) -> nu_protocol::Signature {
Signature::build("cd") Signature::build("cd")
.input_output_types(vec![(Type::Nothing, Type::Nothing)]) .input_output_types(vec![(Type::Nothing, Type::Nothing)])
.switch("physical", "use the physical directory structure; resolve symbolic links before processing instances of ..", Some('P'))
.optional("path", SyntaxShape::Directory, "The path to change to.") .optional("path", SyntaxShape::Directory, "The path to change to.")
.input_output_types(vec![ .input_output_types(vec![
(Type::Nothing, Type::Nothing), (Type::Nothing, Type::Nothing),
@ -36,8 +37,9 @@ impl Command for Cd {
call: &Call, call: &Call,
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> 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 path_val: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
let cwd = current_dir(engine_state, stack)?; let cwd = engine_state.cwd(Some(stack))?;
let path_val = { let path_val = {
if let Some(path) = path_val { if let Some(path) = path_val {
@ -53,54 +55,53 @@ impl Command for Cd {
let (path, span) = match path_val { let (path, span) = match path_val {
Some(v) => { Some(v) => {
if v.item == "-" { if v.item == "-" {
let oldpwd = stack.get_env_var(engine_state, "OLDPWD"); if let Some(oldpwd) = stack.get_env_var(engine_state, "OLDPWD") {
(oldpwd.to_path()?, v.span)
if let Some(oldpwd) = oldpwd {
let path = oldpwd.to_path()?;
let path = match nu_path::canonicalize_with(path.clone(), &cwd) {
Ok(p) => p,
Err(_) => {
return Err(ShellError::DirectoryNotFound {
dir: path.to_string_lossy().to_string(),
span: v.span,
});
}
};
(path.to_string_lossy().to_string(), v.span)
} else { } else {
(cwd.to_string_lossy().to_string(), v.span) (cwd, v.span)
} }
} else { } else {
// Trim whitespace from the end of path.
let path_no_whitespace = let path_no_whitespace =
&v.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d')); &v.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
let path = match nu_path::canonicalize_with(path_no_whitespace, &cwd) { // If `--physical` is specified, canonicalize the path; otherwise expand the path.
Ok(p) => { let path = if physical {
if !p.is_dir() { if let Ok(path) = nu_path::canonicalize_with(path_no_whitespace, &cwd) {
if !path.is_dir() {
return Err(ShellError::NotADirectory { span: v.span }); return Err(ShellError::NotADirectory { span: v.span });
}; };
p path
} } else {
// if canonicalize failed, let's check to see if it's abbreviated
Err(_) => {
return Err(ShellError::DirectoryNotFound { return Err(ShellError::DirectoryNotFound {
dir: path_no_whitespace.to_string(), dir: path_no_whitespace.to_string(),
span: v.span, span: v.span,
}); });
} }
} else {
let path = nu_path::expand_path_with(path_no_whitespace, &cwd, true);
if !path.exists() {
return Err(ShellError::DirectoryNotFound {
dir: path_no_whitespace.to_string(),
span: v.span,
});
}; };
(path.to_string_lossy().to_string(), v.span) if !path.is_dir() {
return Err(ShellError::NotADirectory { span: v.span });
};
path
};
(path, v.span)
} }
} }
None => { None => {
let path = nu_path::expand_tilde("~"); let path = nu_path::expand_tilde("~");
(path.to_string_lossy().to_string(), call.head) (path, call.head)
} }
}; };
let path_value = Value::string(path.clone(), span); // 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") { if let Some(oldpwd) = stack.get_env_var(engine_state, "PWD") {
stack.add_env_var("OLDPWD".into(), oldpwd) stack.add_env_var("OLDPWD".into(), oldpwd)
} }
@ -109,11 +110,15 @@ impl Command for Cd {
//FIXME: this only changes the current scope, but instead this environment variable //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 //should probably be a block that loads the information from the state in the overlay
PermissionResult::PermissionOk => { PermissionResult::PermissionOk => {
stack.add_env_var("PWD".into(), path_value); stack.add_env_var("PWD".into(), Value::string(path.to_string_lossy(), span));
Ok(PipelineData::empty()) Ok(PipelineData::empty())
} }
PermissionResult::PermissionDenied(reason) => Err(ShellError::IOError { PermissionResult::PermissionDenied(reason) => Err(ShellError::IOError {
msg: format!("Cannot change directory to {path}: {reason}"), msg: format!(
"Cannot change directory to {}: {}",
path.to_string_lossy(),
reason
),
}), }),
} }
} }

View file

@ -1,5 +1,6 @@
use super::util::get_rest_for_glob_pattern; use super::util::get_rest_for_glob_pattern;
use crate::{DirBuilder, DirInfo, FileInfo}; use crate::{DirBuilder, DirInfo, FileInfo};
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::{command_prelude::*, current_dir};
use nu_glob::Pattern; use nu_glob::Pattern;
use nu_protocol::NuGlob; use nu_protocol::NuGlob;
@ -98,6 +99,7 @@ impl Command for Du {
let all = call.has_flag(engine_state, stack, "all")?; let all = call.has_flag(engine_state, stack, "all")?;
let deref = call.has_flag(engine_state, stack, "deref")?; let deref = call.has_flag(engine_state, stack, "deref")?;
let exclude = call.get_flag(engine_state, stack, "exclude")?; let exclude = call.get_flag(engine_state, stack, "exclude")?;
#[allow(deprecated)]
let current_dir = current_dir(engine_state, stack)?; let current_dir = current_dir(engine_state, stack)?;
let paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; let paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;

View file

@ -1,3 +1,4 @@
#[allow(deprecated)]
use nu_engine::{command_prelude::*, env::current_dir}; use nu_engine::{command_prelude::*, env::current_dir};
use std::sync::{atomic::AtomicBool, Arc}; use std::sync::{atomic::AtomicBool, Arc};
use wax::{Glob as WaxGlob, WalkBehavior, WalkEntry}; use wax::{Glob as WaxGlob, WalkBehavior, WalkEntry};
@ -178,6 +179,7 @@ impl Command for Glob {
} }
}; };
#[allow(deprecated)]
let path = current_dir(engine_state, stack)?; let path = current_dir(engine_state, stack)?;
let path = match nu_path::canonicalize_with(prefix, path) { let path = match nu_path::canonicalize_with(prefix, path) {
Ok(path) => path, Ok(path) => path,

View file

@ -1,6 +1,7 @@
use super::util::get_rest_for_glob_pattern; use super::util::get_rest_for_glob_pattern;
use crate::{DirBuilder, DirInfo}; use crate::{DirBuilder, DirInfo};
use chrono::{DateTime, Local, LocalResult, TimeZone, Utc}; use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
#[allow(deprecated)]
use nu_engine::{command_prelude::*, env::current_dir}; use nu_engine::{command_prelude::*, env::current_dir};
use nu_glob::{MatchOptions, Pattern}; use nu_glob::{MatchOptions, Pattern};
use nu_path::expand_to_real_path; use nu_path::expand_to_real_path;
@ -91,6 +92,7 @@ impl Command for Ls {
let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?; let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?;
let ctrl_c = engine_state.ctrlc.clone(); let ctrl_c = engine_state.ctrlc.clone();
let call_span = call.head; let call_span = call.head;
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let args = Args { let args = Args {

View file

@ -1,3 +1,4 @@
#[allow(deprecated)]
use nu_engine::{command_prelude::*, env::current_dir}; use nu_engine::{command_prelude::*, env::current_dir};
use std::path::PathBuf; use std::path::PathBuf;
@ -90,6 +91,7 @@ impl Command for Mktemp {
} else if directory || tmpdir { } else if directory || tmpdir {
Some(std::env::temp_dir()) Some(std::env::temp_dir())
} else { } else {
#[allow(deprecated)]
Some(current_dir(engine_state, stack)?) Some(current_dir(engine_state, stack)?)
}; };

View file

@ -1,4 +1,5 @@
use super::util::get_rest_for_glob_pattern; use super::util::get_rest_for_glob_pattern;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir, get_eval_block}; use nu_engine::{command_prelude::*, current_dir, get_eval_block};
use nu_protocol::{BufferedReader, DataSource, NuGlob, PipelineMetadata, RawStream}; use nu_protocol::{BufferedReader, DataSource, NuGlob, PipelineMetadata, RawStream};
use std::{io::BufReader, path::Path}; use std::{io::BufReader, path::Path};
@ -51,6 +52,7 @@ impl Command for Open {
let raw = call.has_flag(engine_state, stack, "raw")?; let raw = call.has_flag(engine_state, stack, "raw")?;
let call_span = call.head; let call_span = call.head;
let ctrlc = engine_state.ctrlc.clone(); let ctrlc = engine_state.ctrlc.clone();
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
let eval_block = get_eval_block(engine_state); let eval_block = get_eval_block(engine_state);

View file

@ -1,4 +1,5 @@
use super::util::{get_rest_for_glob_pattern, try_interaction}; use super::util::{get_rest_for_glob_pattern, try_interaction};
#[allow(deprecated)]
use nu_engine::{command_prelude::*, env::current_dir}; use nu_engine::{command_prelude::*, env::current_dir};
use nu_glob::MatchOptions; use nu_glob::MatchOptions;
use nu_path::expand_path_with; use nu_path::expand_path_with;
@ -130,6 +131,7 @@ fn rm(
let mut unique_argument_check = None; let mut unique_argument_check = None;
#[allow(deprecated)]
let currentdir_path = current_dir(engine_state, stack)?; let currentdir_path = current_dir(engine_state, stack)?;
let home: Option<String> = nu_path::home_dir().map(|path| { let home: Option<String> = nu_path::home_dir().map(|path| {

View file

@ -1,4 +1,5 @@
use crate::progress_bar; use crate::progress_bar;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::{command_prelude::*, current_dir};
use nu_path::expand_path_with; use nu_path::expand_path_with;
use nu_protocol::{ use nu_protocol::{
@ -85,6 +86,7 @@ impl Command for Save {
}; };
let span = call.head; let span = call.head;
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let path_arg = call.req::<Spanned<PathBuf>>(engine_state, stack, 0)?; let path_arg = call.req::<Spanned<PathBuf>>(engine_state, stack, 0)?;

View file

@ -1,4 +1,5 @@
use filetime::FileTime; use filetime::FileTime;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::{command_prelude::*, current_dir};
use nu_path::expand_path_with; use nu_path::expand_path_with;
use nu_protocol::NuGlob; use nu_protocol::NuGlob;
@ -113,6 +114,7 @@ impl Command for Touch {
})?; })?;
} }
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
for (index, glob) in files.into_iter().enumerate() { for (index, glob) in files.into_iter().enumerate() {

View file

@ -1,4 +1,5 @@
use super::util::get_rest_for_glob_pattern; use super::util::get_rest_for_glob_pattern;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::{command_prelude::*, current_dir};
use std::path::PathBuf; use std::path::PathBuf;
use uu_cp::{BackupMode, CopyMode, UpdateMode}; use uu_cp::{BackupMode, CopyMode, UpdateMode};
@ -177,6 +178,7 @@ impl Command for UCp {
let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely( let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
target.item.to_string(), target.item.to_string(),
)); ));
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand()); let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand());
if target.item.as_ref().ends_with(PATH_SEPARATOR) && !target_path.is_dir() { if target.item.as_ref().ends_with(PATH_SEPARATOR) && !target_path.is_dir() {

View file

@ -1,3 +1,4 @@
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::{command_prelude::*, current_dir};
use uu_mkdir::mkdir; use uu_mkdir::mkdir;
@ -58,6 +59,7 @@ impl Command for UMkdir {
call: &Call, call: &Call,
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let mut directories = get_rest_for_glob_pattern(engine_state, stack, call, 0)? let mut directories = get_rest_for_glob_pattern(engine_state, stack, call, 0)?
.into_iter() .into_iter()

View file

@ -1,4 +1,5 @@
use super::util::get_rest_for_glob_pattern; use super::util::get_rest_for_glob_pattern;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::{command_prelude::*, current_dir};
use nu_path::expand_path_with; use nu_path::expand_path_with;
use nu_protocol::NuGlob; use nu_protocol::NuGlob;
@ -77,6 +78,7 @@ impl Command for UMv {
uu_mv::OverwriteMode::Force uu_mv::OverwriteMode::Force
}; };
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
if paths.is_empty() { if paths.is_empty() {

View file

@ -5,6 +5,7 @@ use notify_debouncer_full::{
EventKind, RecursiveMode, Watcher, EventKind, RecursiveMode, Watcher,
}, },
}; };
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir, ClosureEval}; use nu_engine::{command_prelude::*, current_dir, ClosureEval};
use nu_protocol::{ use nu_protocol::{
engine::{Closure, StateWorkingSet}, engine::{Closure, StateWorkingSet},
@ -73,6 +74,7 @@ impl Command for Watch {
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let path_arg: Spanned<String> = call.req(engine_state, stack, 0)?; let path_arg: Spanned<String> = call.req(engine_state, stack, 0)?;

View file

@ -1,4 +1,5 @@
use super::PathSubcommandArguments; use super::PathSubcommandArguments;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir, current_dir_const}; use nu_engine::{command_prelude::*, current_dir, current_dir_const};
use nu_path::expand_path_with; use nu_path::expand_path_with;
use nu_protocol::engine::StateWorkingSet; use nu_protocol::engine::StateWorkingSet;
@ -53,6 +54,7 @@ If you need to distinguish dirs and files, please use `path type`."#
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
#[allow(deprecated)]
let args = Arguments { let args = Arguments {
pwd: current_dir(engine_state, stack)?, pwd: current_dir(engine_state, stack)?,
not_follow_symlink: call.has_flag(engine_state, stack, "no-symlink")?, not_follow_symlink: call.has_flag(engine_state, stack, "no-symlink")?,
@ -74,6 +76,7 @@ If you need to distinguish dirs and files, please use `path type`."#
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
#[allow(deprecated)]
let args = Arguments { let args = Arguments {
pwd: current_dir_const(working_set)?, pwd: current_dir_const(working_set)?,
not_follow_symlink: call.has_flag_const(working_set, "no-symlink")?, not_follow_symlink: call.has_flag_const(working_set, "no-symlink")?,

View file

@ -1,4 +1,5 @@
use super::PathSubcommandArguments; use super::PathSubcommandArguments;
#[allow(deprecated)]
use nu_engine::{ use nu_engine::{
command_prelude::*, command_prelude::*,
env::{current_dir_str, current_dir_str_const}, env::{current_dir_str, current_dir_str_const},
@ -57,6 +58,7 @@ impl Command for SubCommand {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
#[allow(deprecated)]
let args = Arguments { let args = Arguments {
strict: call.has_flag(engine_state, stack, "strict")?, strict: call.has_flag(engine_state, stack, "strict")?,
cwd: current_dir_str(engine_state, stack)?, cwd: current_dir_str(engine_state, stack)?,
@ -79,6 +81,7 @@ impl Command for SubCommand {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
#[allow(deprecated)]
let args = Arguments { let args = Arguments {
strict: call.has_flag_const(working_set, "strict")?, strict: call.has_flag_const(working_set, "strict")?,
cwd: current_dir_str_const(working_set)?, cwd: current_dir_str_const(working_set)?,

View file

@ -1,4 +1,5 @@
use super::run_external::create_external_command; use super::run_external::create_external_command;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir}; use nu_engine::{command_prelude::*, current_dir};
use nu_protocol::OutDest; use nu_protocol::OutDest;
@ -62,6 +63,7 @@ fn exec(
external_command.out = OutDest::Inherit; external_command.out = OutDest::Inherit;
external_command.err = OutDest::Inherit; external_command.err = OutDest::Inherit;
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let mut command = external_command.spawn_simple_command(&cwd.to_string_lossy())?; let mut command = external_command.spawn_simple_command(&cwd.to_string_lossy())?;
command.current_dir(cwd); command.current_dir(cwd);

View file

@ -229,6 +229,7 @@ fn which(
let mut output = vec![]; let mut output = vec![];
#[allow(deprecated)]
let cwd = env::current_dir_str(engine_state, stack)?; let cwd = env::current_dir_str(engine_state, stack)?;
let paths = env::path_str(engine_state, stack, call.head)?; let paths = env::path_str(engine_state, stack, call.head)?;

View file

@ -207,7 +207,15 @@ fn filesystem_change_directory_to_symlink_relative() {
$env.PWD $env.PWD
" "
); );
assert_eq!(PathBuf::from(actual.out), dirs.test().join("foo_link"));
let actual = nu!(
cwd: dirs.test().join("boo"),
"
cd -P ../foo_link
$env.PWD
"
);
assert_eq!(PathBuf::from(actual.out), dirs.test().join("foo")); assert_eq!(PathBuf::from(actual.out), dirs.test().join("foo"));
}) })
} }

View file

@ -2,7 +2,7 @@ use crate::ClosureEvalOnce;
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
use nu_protocol::{ use nu_protocol::{
ast::{Call, Expr}, ast::{Call, Expr},
engine::{EngineState, Stack, StateWorkingSet, PWD_ENV}, engine::{EngineState, Stack, StateWorkingSet},
Config, ShellError, Span, Value, VarId, Config, ShellError, Span, Value, VarId,
}; };
use std::{ use std::{
@ -156,85 +156,56 @@ pub fn env_to_strings(
Ok(env_vars_str) Ok(env_vars_str)
} }
/// Shorthand for env_to_string() for PWD with custom error /// Returns the current working directory as a String, which is guaranteed to be canonicalized.
/// Unlike `current_dir_str_const()`, this also considers modifications to the current working directory made on the stack.
///
/// Returns an error if $env.PWD doesn't exist, is not a String, or is not an absolute path.
#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")]
pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result<String, ShellError> { pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result<String, ShellError> {
if let Some(pwd) = stack.get_env_var(engine_state, PWD_ENV) { #[allow(deprecated)]
// TODO: PWD should be string by default, we don't need to run ENV_CONVERSIONS on it current_dir(engine_state, stack).map(|path| path.to_string_lossy().to_string())
match env_to_string(PWD_ENV, &pwd, engine_state, stack) {
Ok(cwd) => {
if Path::new(&cwd).is_absolute() {
Ok(cwd)
} else {
Err(ShellError::GenericError {
error: "Invalid current directory".into(),
msg: format!("The 'PWD' environment variable must be set to an absolute path. Found: '{cwd}'"),
span: Some(pwd.span()),
help: None,
inner: vec![]
})
}
}
Err(e) => Err(e),
}
} else {
Err(ShellError::GenericError {
error: "Current directory not found".into(),
msg: "".into(),
span: None,
help: Some("The environment variable 'PWD' was not found. It is required to define the current directory.".into()),
inner: vec![],
})
}
} }
/// Simplified version of current_dir_str() for constant evaluation /// Returns the current working directory as a String, which is guaranteed to be canonicalized.
///
/// Returns an error if $env.PWD doesn't exist, is not a String, or is not an absolute path.
#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")]
pub fn current_dir_str_const(working_set: &StateWorkingSet) -> Result<String, ShellError> { pub fn current_dir_str_const(working_set: &StateWorkingSet) -> Result<String, ShellError> {
if let Some(pwd) = working_set.get_env_var(PWD_ENV) { #[allow(deprecated)]
let span = pwd.span(); current_dir_const(working_set).map(|path| path.to_string_lossy().to_string())
match pwd {
Value::String { val, .. } => {
if Path::new(val).is_absolute() {
Ok(val.clone())
} else {
Err(ShellError::GenericError {
error: "Invalid current directory".into(),
msg: format!("The 'PWD' environment variable must be set to an absolute path. Found: '{val}'"),
span: Some(span),
help: None,
inner: vec![]
})
}
}
_ => Err(ShellError::GenericError {
error: "PWD is not a string".into(),
msg: "".into(),
span: None,
help: Some(
"Cusrrent working directory environment variable 'PWD' must be a string."
.into(),
),
inner: vec![],
}),
}
} else {
Err(ShellError::GenericError{
error: "Current directory not found".into(),
msg: "".into(),
span: None,
help: Some("The environment variable 'PWD' was not found. It is required to define the current directory.".into()),
inner: vec![],
})
}
} }
/// Calls current_dir_str() and returns the current directory as a PathBuf /// Returns the current working directory, which is guaranteed to be canonicalized.
/// Unlike `current_dir_const()`, this also considers modifications to the current working directory made on the stack.
///
/// Returns an error if $env.PWD doesn't exist, is not a String, or is not an absolute path.
#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")]
pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result<PathBuf, ShellError> { pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result<PathBuf, ShellError> {
current_dir_str(engine_state, stack).map(PathBuf::from) let cwd = engine_state.cwd(Some(stack))?;
// `EngineState::cwd()` always returns absolute path.
// We're using `canonicalize_with` instead of `fs::canonicalize()` because
// we still need to simplify Windows paths. "." is safe because `cwd` should
// be an absolute path already.
canonicalize_with(&cwd, ".").map_err(|_| ShellError::DirectoryNotFound {
dir: cwd.to_string_lossy().to_string(),
span: Span::unknown(),
})
} }
/// Version of current_dir() for constant evaluation /// Returns the current working directory, which is guaranteed to be canonicalized.
///
/// Returns an error if $env.PWD doesn't exist, is not a String, or is not an absolute path.
#[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")]
pub fn current_dir_const(working_set: &StateWorkingSet) -> Result<PathBuf, ShellError> { pub fn current_dir_const(working_set: &StateWorkingSet) -> Result<PathBuf, ShellError> {
current_dir_str_const(working_set).map(PathBuf::from) let cwd = working_set.permanent_state.cwd(None)?;
// `EngineState::cwd()` always returns absolute path.
// We're using `canonicalize_with` instead of `fs::canonicalize()` because
// we still need to simplify Windows paths. "." is safe because `cwd` should
// be an absolute path already.
canonicalize_with(&cwd, ".").map_err(|_| ShellError::DirectoryNotFound {
dir: cwd.to_string_lossy().to_string(),
span: Span::unknown(),
})
} }
/// Get the contents of path environment variable as a list of strings /// Get the contents of path environment variable as a list of strings
@ -315,6 +286,7 @@ pub fn find_in_dirs_env(
Err(e) => return Err(e), Err(e) => return Err(e),
} }
} else { } else {
#[allow(deprecated)]
current_dir_str(engine_state, stack)? current_dir_str(engine_state, stack)?
}; };

View file

@ -1,4 +1,5 @@
use crate::{current_dir, current_dir_str, get_config, get_full_help}; #[allow(deprecated)]
use crate::{current_dir, get_config, get_full_help};
use nu_path::expand_path_with; use nu_path::expand_path_with;
use nu_protocol::{ use nu_protocol::{
ast::{ ast::{
@ -325,6 +326,7 @@ fn eval_redirection<D: DebugContext>(
) -> Result<Redirection, ShellError> { ) -> Result<Redirection, ShellError> {
match target { match target {
RedirectionTarget::File { expr, append, .. } => { RedirectionTarget::File { expr, append, .. } => {
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?; let cwd = current_dir(engine_state, stack)?;
let value = eval_expression::<D>(engine_state, stack, expr)?; let value = eval_expression::<D>(engine_state, stack, expr)?;
let path = Spanned::<PathBuf>::from_value(value)?.item; let path = Spanned::<PathBuf>::from_value(value)?.item;
@ -633,7 +635,7 @@ impl Eval for EvalRuntime {
if quoted { if quoted {
Ok(Value::string(path, span)) Ok(Value::string(path, span))
} else { } else {
let cwd = current_dir_str(engine_state, stack)?; let cwd = engine_state.cwd(Some(stack))?;
let path = expand_path_with(path, cwd, true); let path = expand_path_with(path, cwd, true);
Ok(Value::string(path.to_string_lossy(), span)) Ok(Value::string(path.to_string_lossy(), span))
@ -652,7 +654,7 @@ impl Eval for EvalRuntime {
} else if quoted { } else if quoted {
Ok(Value::string(path, span)) Ok(Value::string(path, span))
} else { } else {
let cwd = current_dir_str(engine_state, stack)?; let cwd = engine_state.cwd(Some(stack))?;
let path = expand_path_with(path, cwd, true); let path = expand_path_with(path, cwd, true);
Ok(Value::string(path.to_string_lossy(), span)) Ok(Value::string(path.to_string_lossy(), span))

View file

@ -1949,6 +1949,7 @@ pub fn parse_module_file_or_dir(
return None; return None;
} }
#[allow(deprecated)]
let cwd = working_set.get_cwd(); let cwd = working_set.get_cwd();
let module_path = let module_path =
@ -3359,6 +3360,7 @@ pub fn parse_source(working_set: &mut StateWorkingSet, lite_command: &LiteComman
let scoped = name == b"source-env"; let scoped = name == b"source-env";
if let Some(decl_id) = working_set.find_decl(name) { if let Some(decl_id) = working_set.find_decl(name) {
#[allow(deprecated)]
let cwd = working_set.get_cwd(); let cwd = working_set.get_cwd();
// Is this the right call to be using here? // Is this the right call to be using here?
@ -3564,6 +3566,7 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
let spans = &lite_command.parts; let spans = &lite_command.parts;
#[allow(deprecated)]
let cwd = working_set.get_cwd(); let cwd = working_set.get_cwd();
// Checking that the function is used with the correct name // Checking that the function is used with the correct name
@ -3785,6 +3788,7 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
pub fn parse_plugin_use(working_set: &mut StateWorkingSet, call: Box<Call>) -> Pipeline { pub fn parse_plugin_use(working_set: &mut StateWorkingSet, call: Box<Call>) -> Pipeline {
use nu_protocol::{FromValue, PluginRegistryFile}; use nu_protocol::{FromValue, PluginRegistryFile};
#[allow(deprecated)]
let cwd = working_set.get_cwd(); let cwd = working_set.get_cwd();
if let Err(err) = (|| { if let Err(err) = (|| {

View file

@ -125,6 +125,7 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
} }
fn get_current_dir(&self) -> Result<Spanned<String>, ShellError> { fn get_current_dir(&self) -> Result<Spanned<String>, ShellError> {
#[allow(deprecated)]
let cwd = nu_engine::env::current_dir_str(&self.engine_state, &self.stack)?; let cwd = nu_engine::env::current_dir_str(&self.engine_state, &self.stack)?;
// The span is not really used, so just give it call.head // The span is not really used, so just give it call.head
Ok(cwd.into_spanned(self.call.head)) Ok(cwd.into_spanned(self.call.head))

View file

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

View file

@ -26,8 +26,6 @@ type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
use crate::{PluginRegistryFile, PluginRegistryItem, RegisteredPlugin}; use crate::{PluginRegistryFile, PluginRegistryItem, RegisteredPlugin};
pub static PWD_ENV: &str = "PWD";
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum VirtualPath { pub enum VirtualPath {
File(FileId), File(FileId),
@ -893,14 +891,6 @@ impl EngineState {
self.num_files() - 1 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) { pub fn set_config_path(&mut self, key: &str, val: PathBuf) {
self.config_path.insert(key.to_string(), val); self.config_path.insert(key.to_string(), val);
} }
@ -922,12 +912,71 @@ impl EngineState {
.map(|comment_spans| self.build_usage(comment_spans)) .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 { pub fn current_work_dir(&self) -> String {
self.get_env_var("PWD") self.cwd(None)
.map(|d| d.coerce_string().unwrap_or_default()) .map(|path| path.to_string_lossy().to_string())
.unwrap_or_default() .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 // TODO: see if we can completely get rid of this
pub fn get_file_contents(&self) -> &[CachedFile] { pub fn get_file_contents(&self) -> &[CachedFile] {
&self.files &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, ast::Block,
engine::{ engine::{
usage::build_usage, CachedFile, Command, CommandType, EngineState, OverlayFrame, 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, BlockId, Category, Config, DeclId, FileId, Module, ModuleId, ParseError, ParseWarning, Span,
Type, Value, VarId, VirtualPathId, Type, Value, VarId, VirtualPathId,
@ -601,13 +601,16 @@ impl<'a> StateWorkingSet<'a> {
next_id 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 { pub fn get_cwd(&self) -> String {
let pwd = self self.permanent_state
.permanent_state .cwd(None)
.get_env_var(PWD_ENV) .map(|path| path.to_string_lossy().to_string())
.expect("internal error: can't find PWD"); .unwrap_or_default()
pwd.coerce_string()
.expect("internal error: PWD not a string")
} }
pub fn get_env_var(&self, name: &str) -> Option<&Value> { pub fn get_env_var(&self, name: &str) -> Option<&Value> {
@ -622,16 +625,6 @@ impl<'a> StateWorkingSet<'a> {
&self.permanent_state.config &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) { pub fn set_variable_type(&mut self, var_id: VarId, ty: Type) {
let num_permanent_vars = self.permanent_state.num_vars(); let num_permanent_vars = self.permanent_state.num_vars();
if var_id < num_permanent_vars { if var_id < num_permanent_vars {

View file

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

View file

@ -1,4 +1,5 @@
use log::trace; use log::trace;
#[allow(deprecated)]
use nu_engine::{env::current_dir, eval_block}; use nu_engine::{env::current_dir, eval_block};
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::{ use nu_protocol::{
@ -98,6 +99,7 @@ use std pwd
eval_block::<WithoutDebug>(engine_state, &mut stack, &block, pipeline_data)?; eval_block::<WithoutDebug>(engine_state, &mut stack, &block, pipeline_data)?;
#[allow(deprecated)]
let cwd = current_dir(engine_state, &stack)?; let cwd = current_dir(engine_state, &stack)?;
engine_state.merge_env(&mut stack, cwd)?; engine_state.merge_env(&mut stack, cwd)?;

View file

@ -196,9 +196,15 @@ export def ellie [] {
} }
# Return the current working directory # Return the current working directory
export def pwd [] { export def pwd [
--physical (-P) # resolve symbolic links
] {
if $physical {
$env.PWD | path expand
} else {
$env.PWD $env.PWD
} }
}
# repeat anything a bunch of times, yielding a list of *n* times the input # repeat anything a bunch of times, yielding a list of *n* times the input
# #

View file

@ -31,6 +31,7 @@ pub(crate) fn read_config_file(
// Load config startup file // Load config startup file
if let Some(file) = config_file { if let Some(file) = config_file {
let working_set = StateWorkingSet::new(engine_state); let working_set = StateWorkingSet::new(engine_state);
#[allow(deprecated)]
let cwd = working_set.get_cwd(); let cwd = working_set.get_cwd();
if let Ok(path) = canonicalize_with(&file.item, cwd) { if let Ok(path) = canonicalize_with(&file.item, cwd) {
@ -143,6 +144,7 @@ pub(crate) fn read_default_env_file(engine_state: &mut EngineState, stack: &mut
info!("read_config_file {}:{}:{}", file!(), line!(), column!()); info!("read_config_file {}:{}:{}", file!(), line!(), column!());
// Merge the environment in case env vars changed in the config // Merge the environment in case env vars changed in the config
#[allow(deprecated)]
match nu_engine::env::current_dir(engine_state, stack) { match nu_engine::env::current_dir(engine_state, stack) {
Ok(cwd) => { Ok(cwd) => {
if let Err(e) = engine_state.merge_env(stack, cwd) { if let Err(e) = engine_state.merge_env(stack, cwd) {
@ -184,6 +186,7 @@ fn eval_default_config(
); );
// Merge the environment in case env vars changed in the config // Merge the environment in case env vars changed in the config
#[allow(deprecated)]
match nu_engine::env::current_dir(engine_state, stack) { match nu_engine::env::current_dir(engine_state, stack) {
Ok(cwd) => { Ok(cwd) => {
if let Err(e) = engine_state.merge_env(stack, cwd) { if let Err(e) = engine_state.merge_env(stack, cwd) {

View file

@ -249,6 +249,7 @@ pub fn nu_repl() {
for (i, line) in source_lines.iter().enumerate() { for (i, line) in source_lines.iter().enumerate() {
let mut stack = Stack::with_parent(top_stack.clone()); let mut stack = Stack::with_parent(top_stack.clone());
#[allow(deprecated)]
let cwd = nu_engine::env::current_dir(&engine_state, &stack) let cwd = nu_engine::env::current_dir(&engine_state, &stack)
.unwrap_or_else(|err| outcome_err(&engine_state, &err)); .unwrap_or_else(|err| outcome_err(&engine_state, &err));