Refactor to create EnvMaintainer trait for interact with Stack/EngineState. 'ls' now PWD-per-drive ready.

This commit is contained in:
Zhenping Zhao 2024-12-12 05:25:54 -08:00
parent 2b9f6e64a1
commit 8c59cbf913
4 changed files with 286 additions and 263 deletions

View file

@ -4,8 +4,8 @@ use nu_engine::glob_from;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, env::current_dir};
use nu_glob::MatchOptions;
use nu_path::{expand_path_with, expand_to_real_path};
use nu_protocol::{DataSource, NuGlob, PipelineMetadata, Signals};
use nu_path::expand_to_real_path;
use nu_protocol::{engine::expand_path_with, DataSource, NuGlob, PipelineMetadata};
use pathdiff::diff_paths;
use rayon::prelude::*;
@ -98,8 +98,6 @@ impl Command for Ls {
let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?;
let use_threads = call.has_flag(engine_state, stack, "threads")?;
let call_span = call.head;
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?;
let args = Args {
all,
@ -120,26 +118,19 @@ impl Command for Ls {
Some(pattern_arg)
};
match input_pattern_arg {
None => Ok(
ls_for_one_pattern(None, args, engine_state.signals().clone(), cwd)?
.into_pipeline_data_with_metadata(
call_span,
engine_state.signals().clone(),
PipelineMetadata {
data_source: DataSource::Ls,
content_type: None,
},
),
),
None => Ok(ls_for_one_pattern(None, args, engine_state, stack)?
.into_pipeline_data_with_metadata(
call_span,
engine_state.signals().clone(),
PipelineMetadata {
data_source: DataSource::Ls,
content_type: None,
},
)),
Some(pattern) => {
let mut result_iters = vec![];
for pat in pattern {
result_iters.push(ls_for_one_pattern(
Some(pat),
args,
engine_state.signals().clone(),
cwd.clone(),
)?)
result_iters.push(ls_for_one_pattern(Some(pat), args, engine_state, stack)?)
}
// Here nushell needs to use
@ -221,8 +212,8 @@ impl Command for Ls {
fn ls_for_one_pattern(
pattern_arg: Option<Spanned<NuGlob>>,
args: Args,
signals: Signals,
cwd: PathBuf,
engine_state: &EngineState,
stack: &Stack,
) -> Result<PipelineData, ShellError> {
fn create_pool(num_threads: usize) -> Result<rayon::ThreadPool, ShellError> {
match rayon::ThreadPoolBuilder::new()
@ -240,6 +231,10 @@ fn ls_for_one_pattern(
}
}
let signals = engine_state.signals().clone();
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?;
let (tx, rx) = mpsc::channel();
let Args {
@ -282,8 +277,13 @@ fn ls_for_one_pattern(
let (pattern_arg, absolute_path) = match pattern_arg {
Some(pat) => {
// expand with cwd here is only used for checking
let tmp_expanded =
nu_path::expand_path_with(pat.item.as_ref(), &cwd, pat.item.is_expand());
let tmp_expanded = expand_path_with(
stack,
engine_state,
pat.item.as_ref(),
&cwd,
pat.item.is_expand(),
);
// Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true
if !directory && tmp_expanded.is_dir() {
if read_dir(&tmp_expanded, p_tag, use_threads)?
@ -319,7 +319,13 @@ fn ls_for_one_pattern(
let hidden_dir_specified = is_hidden_dir(pattern_arg.as_ref());
let path = pattern_arg.into_spanned(p_tag);
let (prefix, paths) = if just_read_dir {
let expanded = nu_path::expand_path_with(path.item.as_ref(), &cwd, path.item.is_expand());
let expanded = expand_path_with(
stack,
engine_state,
path.item.as_ref(),
&cwd,
path.item.is_expand(),
);
let paths = read_dir(&expanded, p_tag, use_threads)?;
// just need to read the directory, so prefix is path itself.
(Some(expanded), paths)
@ -349,8 +355,6 @@ fn ls_for_one_pattern(
let hidden_dirs = Arc::new(Mutex::new(Vec::new()));
let signals_clone = signals.clone();
let pool = if use_threads {
let count = std::thread::available_parallelism()?.get();
create_pool(count)?
@ -436,7 +440,8 @@ fn ls_for_one_pattern(
call_span,
long,
du,
&signals_clone,
engine_state,
stack,
use_mime_type,
args.full_paths,
);
@ -554,7 +559,8 @@ pub(crate) fn dir_entry_dict(
span: Span,
long: bool,
du: bool,
signals: &Signals,
engine_state: &EngineState,
stack: &Stack,
use_mime_type: bool,
full_symlink_target: bool,
) -> Result<Value, ShellError> {
@ -568,6 +574,8 @@ pub(crate) fn dir_entry_dict(
));
}
let signals = &engine_state.signals().clone();
let mut record = Record::new();
let mut file_type = "unknown".to_string();
@ -591,6 +599,8 @@ pub(crate) fn dir_entry_dict(
if full_symlink_target && filename.parent().is_some() {
Value::string(
expand_path_with(
stack,
engine_state,
path_to_link,
filename
.parent()

View file

@ -29,7 +29,9 @@ pub use engine_state::*;
pub use error_handler::*;
pub use overlay::*;
pub use pattern_match::*;
pub use pwd_per_drive::*;
pub use pwd_per_drive::expand_path_with;
#[cfg(windows)]
pub use pwd_per_drive::windows::{expand_pwd, set_pwd};
pub use sequence::*;
pub use stack::*;
pub use stack_out_dest::*;

View file

@ -1,6 +1,4 @@
use crate::engine::{EngineState, Stack};
#[cfg(windows)]
use crate::{Span, Value};
use std::path::{Path, PathBuf};
// For file system command usage
@ -26,256 +24,266 @@ where
Q: AsRef<Path>,
{
#[cfg(windows)]
if let Some(abs_path) = expand_pwd(_stack, _engine_state, path.as_ref()) {
if let Some(abs_path) = windows::expand_pwd(_stack, _engine_state, path.as_ref()) {
return abs_path;
}
nu_path::expand_path_with::<P, Q>(path, relative_to, expand_tilde)
}
/// For maintainer to notify current pwd
/// When user change current directory, maintainer notifies
/// PWD-per-drive by calling set_pwd() with current stack and path;
#[cfg(windows)]
pub fn set_pwd(stack: &mut Stack, path: &Path) {
if let Some(path_str) = path.to_str() {
if let Some(drive) = extract_drive_letter(path_str) {
stack.add_env_var(
env_var_for_drive(drive),
Value::string(path_str, Span::unknown()).clone(),
);
}
}
}
/// For file system command usage
/// File system command implementation can also directly use expand_pwd
/// to expand relate path for a drive and strip redundant double or
/// single quote like bash.
/// cd "''C:''nushell''"
/// C:\Users\nushell>
#[cfg(windows)]
pub fn expand_pwd(stack: &Stack, engine_state: &EngineState, path: &Path) -> Option<PathBuf> {
if let Some(path_str) = path.to_str() {
if let Some(drive_letter) = need_expand(path_str) {
let mut base = PathBuf::from(get_pwd_on_drive(stack, engine_state, drive_letter));
// need_expand() ensures path_str.len() >= 2
base.push(&path_str[2..]); // Join PWD with path parts after "C:"
return Some(base);
}
}
None
}
/// Implementation for maintainer and fs_client
/// Windows env var for drive
/// essential for integration with windows native shell CMD/PowerShell
/// and the core mechanism for supporting PWD-per-drive with nushell's
/// powerful layered environment system.
/// Returns uppercased "=X:".
#[cfg(windows)]
pub fn env_var_for_drive(drive_letter: char) -> String {
let drive_letter = drive_letter.to_ascii_uppercase();
format!("={}:", drive_letter)
}
/// get pwd for drive:
/// 1. From env_var, if no,
/// 2. From sys_absolute, if no,
/// 3. Construct root path to drives
#[cfg(windows)]
pub fn get_pwd_on_drive(stack: &Stack, engine_state: &EngineState, drive_letter: char) -> String {
let env_var_for_drive = env_var_for_drive(drive_letter);
let mut abs_pwd: Option<String> = None;
if let Some(pwd) = stack.get_env_var(engine_state, &env_var_for_drive) {
if let Ok(pwd_string) = pwd.clone().into_string() {
abs_pwd = Some(pwd_string);
}
}
if abs_pwd.is_none() {
if let Some(sys_pwd) = get_full_path_name_w(&format!("{}:", drive_letter)) {
abs_pwd = Some(sys_pwd);
}
}
if let Some(pwd) = abs_pwd {
ensure_trailing_delimiter(&pwd)
} else {
format!(r"{}:\", drive_letter)
}
}
/// Check if input path is relative path for drive letter,
/// which should be expanded with PWD-per-drive.
/// Returns Some(drive_letter) or None, drive_letter is upper case.
#[cfg(windows)]
pub fn need_expand(path: &str) -> Option<char> {
let chars: Vec<char> = path.chars().collect();
if chars.len() == 2 || (chars.len() > 2 && chars[2] != '/' && chars[2] != '\\') {
extract_drive_letter(path)
} else {
None
}
}
/// Extract the drive letter from a path, return uppercased
/// drive letter or None
#[cfg(windows)]
pub fn extract_drive_letter(path: &str) -> Option<char> {
let chars: Vec<char> = path.chars().collect();
if chars.len() >= 2 && chars[0].is_ascii_alphabetic() && chars[1] == ':' {
Some(chars[0].to_ascii_uppercase())
} else {
None
}
}
/// Ensure a path has a trailing `\\` or '/'
#[cfg(windows)]
pub fn ensure_trailing_delimiter(path: &str) -> String {
if !path.ends_with('\\') && !path.ends_with('/') {
format!(r"{}\", path)
} else {
path.to_string()
}
}
/// get_full_path_name_w
/// Call Windows system API (via omnipath crate) to expand
/// absolute path
#[cfg(windows)]
pub fn get_full_path_name_w(path_str: &str) -> Option<String> {
use omnipath::sys_absolute;
use std::path::Path;
if let Ok(path_sys_abs) = sys_absolute(Path::new(path_str)) {
Some(path_sys_abs.to_str()?.to_string())
} else {
None
}
}
#[cfg(all(windows, test))] // test only for windows
mod tests {
pub mod windows {
use super::*;
use crate::{FromValue, Value};
#[test]
fn test_expand_path_with() {
let mut stack = Stack::new();
let path_str = r"c:\users\nushell";
let path = Path::new(path_str);
set_pwd(&mut stack, path);
let engine_state = EngineState::new();
let rel_path = Path::new("c:.config");
let result = format!(r"{path_str}\.config");
assert_eq!(
Some(result.as_str()),
expand_path_with(&stack, &engine_state, rel_path, Path::new(path_str), false)
.as_path()
.to_str()
);
pub trait EnvMaintainer {
fn maintain(&mut self, key: String, value: Value);
}
#[test]
fn test_set_pwd() {
let mut stack = Stack::new();
let path_str = r"c:\users\nushell";
let path = Path::new(path_str);
set_pwd(&mut stack, path);
let engine_state = EngineState::new();
assert_eq!(
stack
.get_env_var(&engine_state, &env_var_for_drive('c'))
.unwrap()
.clone()
.into_string()
.unwrap(),
path_str.to_string()
);
macro_rules! impl_env_maintainer {
($type:ty) => {
impl EnvMaintainer for $type {
fn maintain(&mut self, key: String, value: Value) {
self.add_env_var(key, value);
}
}
};
}
#[test]
fn test_expand_pwd() {
let mut stack = Stack::new();
let path_str = r"c:\users\nushell";
let path = Path::new(path_str);
set_pwd(&mut stack, path);
let engine_state = EngineState::new();
impl_env_maintainer!(Stack);
impl_env_maintainer!(EngineState);
let rel_path = Path::new("c:.config");
let result = format!(r"{path_str}\.config");
assert_eq!(
Some(result.as_str()),
expand_pwd(&stack, &engine_state, rel_path)
.unwrap()
.as_path()
.to_str()
);
}
#[test]
fn test_env_var_for_drive() {
for drive_letter in 'A'..='Z' {
assert_eq!(env_var_for_drive(drive_letter), format!("={drive_letter}:"));
/// For maintainer to notify PWD
/// When user changes current directory, maintainer calls set_pwd()
/// and PWD-per-drive call maintainer back to store it for the
/// env_var_for_drive.
pub fn set_pwd<T: EnvMaintainer>(maintainer: &mut T, value: Value) {
if let Ok(path_string) = String::from_value(value.clone()) {
if let Some(drive) = extract_drive_letter(&path_string) {
maintainer.maintain(env_var_for_drive(drive), value);
}
}
for drive_letter in 'a'..='z' {
}
/// For file system command usage
/// File system command implementation can also directly use expand_pwd
/// to expand relate path for a drive and strip redundant double or
/// single quote like bash.
/// cd "''C:''nushell''"
/// C:\Users\nushell>
pub fn expand_pwd(stack: &Stack, engine_state: &EngineState, path: &Path) -> Option<PathBuf> {
if let Some(path_str) = path.to_str() {
if let Some(drive_letter) = need_expand(path_str) {
let mut base = PathBuf::from(get_pwd_on_drive(stack, engine_state, drive_letter));
// need_expand() ensures path_str.len() >= 2
base.push(&path_str[2..]); // Join PWD with path parts after "C:"
return Some(base);
}
}
None
}
/// Implementation for maintainer and fs_client
/// Windows env var for drive
/// essential for integration with windows native shell CMD/PowerShell
/// and the core mechanism for supporting PWD-per-drive with nushell's
/// powerful layered environment system.
/// Returns uppercased "=X:".
fn env_var_for_drive(drive_letter: char) -> String {
let drive_letter = drive_letter.to_ascii_uppercase();
format!("={}:", drive_letter)
}
/// get pwd for drive:
/// 1. From env_var, if no,
/// 2. From sys_absolute, if no,
/// 3. Construct root path to drives
fn get_pwd_on_drive(stack: &Stack, engine_state: &EngineState, drive_letter: char) -> String {
let env_var_for_drive = env_var_for_drive(drive_letter);
let mut abs_pwd: Option<String> = None;
if let Some(pwd) = stack.get_env_var(engine_state, &env_var_for_drive) {
if let Ok(pwd_string) = pwd.clone().into_string() {
abs_pwd = Some(pwd_string);
}
}
if abs_pwd.is_none() {
if let Some(sys_pwd) = get_full_path_name_w(&format!("{}:", drive_letter)) {
abs_pwd = Some(sys_pwd);
}
}
if let Some(pwd) = abs_pwd {
ensure_trailing_delimiter(&pwd)
} else {
format!(r"{}:\", drive_letter)
}
}
/// Check if input path is relative path for drive letter,
/// which should be expanded with PWD-per-drive.
/// Returns Some(drive_letter) or None, drive_letter is upper case.
fn need_expand(path: &str) -> Option<char> {
let chars: Vec<char> = path.chars().collect();
if chars.len() == 2 || (chars.len() > 2 && chars[2] != '/' && chars[2] != '\\') {
extract_drive_letter(path)
} else {
None
}
}
/// Extract the drive letter from a path, return uppercased
/// drive letter or None
fn extract_drive_letter(path: &str) -> Option<char> {
let chars: Vec<char> = path.chars().collect();
if chars.len() >= 2 && chars[0].is_ascii_alphabetic() && chars[1] == ':' {
Some(chars[0].to_ascii_uppercase())
} else {
None
}
}
/// Ensure a path has a trailing `\\` or '/'
fn ensure_trailing_delimiter(path: &str) -> String {
if !path.ends_with('\\') && !path.ends_with('/') {
format!(r"{}\", path)
} else {
path.to_string()
}
}
/// get_full_path_name_w
/// Call Windows system API (via omnipath crate) to expand
/// absolute path
fn get_full_path_name_w(path_str: &str) -> Option<String> {
use omnipath::sys_absolute;
use std::path::Path;
if let Ok(path_sys_abs) = sys_absolute(Path::new(path_str)) {
Some(path_sys_abs.to_str()?.to_string())
} else {
None
}
}
#[cfg(test)] // test only for windows
mod tests {
use super::*;
use crate::{IntoValue, Span};
#[test]
fn test_expand_path_with() {
let mut stack = Stack::new();
let path_str = r"c:\users\nushell";
set_pwd(&mut stack, path_str.into_value(Span::unknown()));
let engine_state = EngineState::new();
let rel_path = Path::new("c:.config");
let result = format!(r"{path_str}\.config");
assert_eq!(
env_var_for_drive(drive_letter),
format!("={}:", drive_letter.to_ascii_uppercase())
Some(result.as_str()),
expand_path_with(&stack, &engine_state, rel_path, Path::new(path_str), false)
.as_path()
.to_str()
);
}
}
#[test]
fn test_get_pwd_on_drive() {
let mut stack = Stack::new();
let path_str = r"c:\users\nushell";
let path = Path::new(path_str);
set_pwd(&mut stack, path);
let engine_state = EngineState::new();
let result = format!(r"{path_str}\");
assert_eq!(result, get_pwd_on_drive(&stack, &engine_state, 'c'));
}
#[test]
fn test_set_pwd() {
let mut stack = Stack::new();
let path_str = r"c:\users\nushell";
set_pwd(&mut stack, path_str.into_value(Span::unknown()));
let engine_state = EngineState::new();
assert_eq!(
stack
.get_env_var(&engine_state, &env_var_for_drive('c'))
.unwrap()
.clone()
.into_string()
.unwrap(),
path_str.to_string()
);
}
#[test]
fn test_need_expand() {
assert_eq!(need_expand(r"c:nushell\src"), Some('C'));
assert_eq!(need_expand("C:src/"), Some('C'));
assert_eq!(need_expand("a:"), Some('A'));
assert_eq!(need_expand("z:"), Some('Z'));
// Absolute path does not need expand
assert_eq!(need_expand(r"c:\"), None);
// Unix path does not need expand
assert_eq!(need_expand("/usr/bin"), None);
// Invalid path on drive
assert_eq!(need_expand("1:usr/bin"), None);
}
#[test]
fn test_expand_pwd() {
let mut stack = Stack::new();
let path_str = r"c:\users\nushell";
set_pwd(&mut stack, path_str.into_value(Span::unknown()));
let engine_state = EngineState::new();
#[test]
fn test_extract_drive_letter() {
assert_eq!(extract_drive_letter("C:test"), Some('C'));
assert_eq!(extract_drive_letter(r"d:\temp"), Some('D'));
assert_eq!(extract_drive_letter(r"1:temp"), None);
}
let rel_path = Path::new("c:.config");
let result = format!(r"{path_str}\.config");
assert_eq!(
Some(result.as_str()),
expand_pwd(&stack, &engine_state, rel_path)
.unwrap()
.as_path()
.to_str()
);
}
#[test]
fn test_ensure_trailing_delimiter() {
assert_eq!(ensure_trailing_delimiter("E:"), r"E:\");
assert_eq!(ensure_trailing_delimiter(r"e:\"), r"e:\");
assert_eq!(ensure_trailing_delimiter("c:/"), "c:/");
}
#[test]
fn test_env_var_for_drive() {
for drive_letter in 'A'..='Z' {
assert_eq!(env_var_for_drive(drive_letter), format!("={drive_letter}:"));
}
for drive_letter in 'a'..='z' {
assert_eq!(
env_var_for_drive(drive_letter),
format!("={}:", drive_letter.to_ascii_uppercase())
);
}
}
#[test]
fn test_get_full_path_name_w() {
let result = get_full_path_name_w("C:");
assert!(result.is_some());
let path = result.unwrap();
assert!(path.starts_with(r"C:\"));
#[test]
fn test_get_pwd_on_drive() {
let mut stack = Stack::new();
let path_str = r"c:\users\nushell";
set_pwd(&mut stack, path_str.into_value(Span::unknown()));
let engine_state = EngineState::new();
let result = format!(r"{path_str}\");
assert_eq!(result, get_pwd_on_drive(&stack, &engine_state, 'c'));
}
let result = get_full_path_name_w(r"c:nushell\src");
assert!(result.is_some());
let path = result.unwrap();
assert!(path.starts_with(r"C:\") || path.starts_with(r"c:\"));
assert!(path.ends_with(r"nushell\src"));
#[test]
fn test_need_expand() {
assert_eq!(need_expand(r"c:nushell\src"), Some('C'));
assert_eq!(need_expand("C:src/"), Some('C'));
assert_eq!(need_expand("a:"), Some('A'));
assert_eq!(need_expand("z:"), Some('Z'));
// Absolute path does not need expand
assert_eq!(need_expand(r"c:\"), None);
// Unix path does not need expand
assert_eq!(need_expand("/usr/bin"), None);
// Invalid path on drive
assert_eq!(need_expand("1:usr/bin"), None);
}
#[test]
fn test_extract_drive_letter() {
assert_eq!(extract_drive_letter("C:test"), Some('C'));
assert_eq!(extract_drive_letter(r"d:\temp"), Some('D'));
assert_eq!(extract_drive_letter(r"1:temp"), None);
}
#[test]
fn test_ensure_trailing_delimiter() {
assert_eq!(ensure_trailing_delimiter("E:"), r"E:\");
assert_eq!(ensure_trailing_delimiter(r"e:\"), r"e:\");
assert_eq!(ensure_trailing_delimiter("c:/"), "c:/");
}
#[test]
fn test_get_full_path_name_w() {
let result = get_full_path_name_w("C:");
assert!(result.is_some());
let path = result.unwrap();
assert!(path.starts_with(r"C:\"));
let result = get_full_path_name_w(r"c:nushell\src");
assert!(result.is_some());
let path = result.unwrap();
assert!(path.starts_with(r"C:\") || path.starts_with(r"c:\"));
assert!(path.ends_with(r"nushell\src"));
}
}
}

View file

@ -1,5 +1,5 @@
#[cfg(windows)]
use crate::engine::pwd_per_drive;
use crate::engine::set_pwd;
use crate::{
engine::{
ArgumentStack, EngineState, ErrorHandlerStack, Redirection, StackCallArgGuard,
@ -253,6 +253,11 @@ impl Stack {
}
pub fn add_env_var(&mut self, var: String, value: Value) {
#[cfg(windows)]
if var == "PWD" {
set_pwd(self, value.clone());
}
if let Some(last_overlay) = self.active_overlays.last() {
if let Some(env_hidden) = Arc::make_mut(&mut self.env_hidden).get_mut(last_overlay) {
// if the env var was hidden, let's activate it again
@ -763,8 +768,6 @@ impl Stack {
let path = nu_path::strip_trailing_slash(path);
let value = Value::string(path.to_string_lossy(), Span::unknown());
self.add_env_var("PWD".into(), value);
#[cfg(windows)] // Sync with PWD-per-drive
pwd_per_drive::set_pwd(self, &path);
Ok(())
}
}