use crate::commands::cd::CdArgs; use crate::commands::command::EvaluatedWholeStreamCommandArgs; use crate::commands::cp::CopyArgs; use crate::commands::ls::LsArgs; use crate::commands::mkdir::MkdirArgs; use crate::commands::mv::MoveArgs; use crate::commands::rm::RemoveArgs; use crate::data::dir_entry_dict; use crate::path::canonicalize; use crate::prelude::*; use crate::shell::completer::NuCompleter; use crate::shell::shell::Shell; use crate::utils::FileStructure; use rustyline::completion::FilenameCompleter; use rustyline::hint::{Hinter, HistoryHinter}; use std::collections::HashMap; use std::path::{Path, PathBuf}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use nu_errors::ShellError; use nu_parser::expand_ndots; use nu_protocol::{Primitive, ReturnSuccess, UntaggedValue}; use nu_source::Tagged; pub struct FilesystemShell { pub(crate) path: String, pub(crate) last_path: String, completer: NuCompleter, hinter: HistoryHinter, } impl std::fmt::Debug for FilesystemShell { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "FilesystemShell @ {}", self.path) } } impl Clone for FilesystemShell { fn clone(&self) -> Self { FilesystemShell { path: self.path.clone(), last_path: self.path.clone(), completer: NuCompleter { file_completer: FilenameCompleter::new(), commands: self.completer.commands.clone(), homedir: self.homedir(), }, hinter: HistoryHinter {}, } } } impl FilesystemShell { pub fn basic(commands: CommandRegistry) -> Result { let path = std::env::current_dir()?; Ok(FilesystemShell { path: path.to_string_lossy().to_string(), last_path: path.to_string_lossy().to_string(), completer: NuCompleter { file_completer: FilenameCompleter::new(), commands, homedir: dirs::home_dir(), }, hinter: HistoryHinter {}, }) } pub fn with_location( path: String, commands: CommandRegistry, ) -> Result { let path = canonicalize(std::env::current_dir()?, &path)?; let path = path.display().to_string(); let last_path = path.clone(); Ok(FilesystemShell { path, last_path, completer: NuCompleter { file_completer: FilenameCompleter::new(), commands, homedir: dirs::home_dir(), }, hinter: HistoryHinter {}, }) } } impl Shell for FilesystemShell { fn name(&self) -> String { "filesystem".to_string() } fn homedir(&self) -> Option { dirs::home_dir() } fn ls( &self, LsArgs { path, all, full, short_names, with_symlink_targets, du, }: LsArgs, name_tag: Tag, ctrl_c: Arc, ) -> Result { let ctrl_c_copy = ctrl_c.clone(); let (path, p_tag) = match path { Some(p) => { let p_tag = p.tag; let mut p = p.item; if p.is_dir() { if is_empty_dir(&p) { return Ok(OutputStream::empty()); } p.push("*"); } (p, p_tag) } None => { if is_empty_dir(&self.path()) { return Ok(OutputStream::empty()); } else { (PathBuf::from("./*"), name_tag.clone()) } } }; let mut paths = glob::glob(&path.to_string_lossy()) .map_err(|e| ShellError::labeled_error(e.to_string(), "invalid pattern", &p_tag))? .peekable(); if paths.peek().is_none() { return Err(ShellError::labeled_error( "No matches found", "no matches found", &p_tag, )); } // Generated stream: impl Stream Ok(futures::stream::iter(paths.filter_map(move |path| { let path = match path.map_err(|e| ShellError::from(e.into_error())) { Ok(path) => path, Err(err) => return Some(Err(err)), }; if !all && is_hidden_dir(&path) { return None; } let metadata = match std::fs::symlink_metadata(&path) { Ok(metadata) => Some(metadata), Err(e) => { if e.kind() == std::io::ErrorKind::PermissionDenied { None } else { return Some(Err(e.into())); } } }; let entry = dir_entry_dict( &path, metadata.as_ref(), name_tag.clone(), full, short_names, with_symlink_targets, du, ctrl_c.clone(), ) .map(ReturnSuccess::Value); Some(entry) })) .interruptible(ctrl_c_copy) .to_output_stream()) } fn cd(&self, args: CdArgs, name: Tag) -> Result { let path = match args.path { None => match dirs::home_dir() { Some(o) => o, _ => { return Err(ShellError::labeled_error( "Cannot change to home directory", "cannot go to home", &name, )) } }, Some(v) => { let Tagged { item: target, tag } = v; if target == Path::new("-") { PathBuf::from(&self.last_path) } else { let path = canonicalize(self.path(), target).map_err(|_| { ShellError::labeled_error( "Cannot change to directory", "directory not found", &tag, ) })?; if !path.is_dir() { return Err(ShellError::labeled_error( "Cannot change to directory", "is not a directory", &tag, )); } #[cfg(unix)] { let has_exec = path .metadata() .map(|m| { umask::Mode::from(m.permissions().mode()).has(umask::USER_READ) }) .map_err(|e| { ShellError::labeled_error( "Cannot change to directory", format!("cannot stat ({})", e), &tag, ) })?; if !has_exec { return Err(ShellError::labeled_error( "Cannot change to directory", "permission denied", &tag, )); } } path } } }; let mut stream = VecDeque::new(); stream.push_back(ReturnSuccess::change_cwd( path.to_string_lossy().to_string(), )); Ok(stream.into()) } fn cp( &self, CopyArgs { src, dst, recursive, }: CopyArgs, name: Tag, path: &str, ) -> Result { let name_tag = name; let path = Path::new(path); let source = path.join(&src.item); let destination = path.join(&dst.item); let sources: Vec<_> = match glob::glob(&source.to_string_lossy()) { Ok(files) => files.collect(), Err(e) => { return Err(ShellError::labeled_error( e.to_string(), "invalid pattern", src.tag, )) } }; if sources.is_empty() { return Err(ShellError::labeled_error( "No matches found", "no matches found", src.tag, )); } if sources.len() > 1 && !destination.is_dir() { return Err(ShellError::labeled_error( "Destination must be a directory when copying multiple files", "is not a directory", dst.tag, )); } let any_source_is_dir = sources.iter().any(|f| match f { Ok(f) => f.is_dir(), Err(_) => false, }); if any_source_is_dir && !recursive.item { return Err(ShellError::labeled_error( "Directories must be copied using \"--recursive\"", "resolves to a directory (not copied)", src.tag, )); } for entry in sources { if let Ok(entry) = entry { let mut sources = FileStructure::new(); sources.walk_decorate(&entry)?; if entry.is_file() { let sources = sources.paths_applying_with(|(source_file, _depth_level)| { if destination.is_dir() { let mut dest = canonicalize(&path, &dst.item)?; if let Some(name) = entry.file_name() { dest.push(name); } Ok((source_file, dest)) } else { Ok((source_file, destination.clone())) } })?; for (src, dst) in sources { if src.is_file() { std::fs::copy(src, dst).map_err(|e| { ShellError::labeled_error(e.to_string(), e.to_string(), &name_tag) })?; } } } else if entry.is_dir() { let destination = if !destination.exists() { destination.clone() } else { match entry.file_name() { Some(name) => destination.join(name), None => { return Err(ShellError::labeled_error( "Copy aborted. Not a valid path", "not a valid path", dst.tag, )) } } }; std::fs::create_dir_all(&destination).map_err(|e| { ShellError::labeled_error(e.to_string(), e.to_string(), &dst.tag) })?; let sources = sources.paths_applying_with(|(source_file, depth_level)| { let mut dest = destination.clone(); let path = canonicalize(&path, &source_file)?; let comps: Vec<_> = path .components() .map(|fragment| fragment.as_os_str()) .rev() .take(1 + depth_level) .collect(); for fragment in comps.into_iter().rev() { dest.push(fragment); } Ok((PathBuf::from(&source_file), dest)) })?; let dst_tag = &dst.tag; for (src, dst) in sources { if src.is_dir() && !dst.exists() { std::fs::create_dir_all(&dst).map_err(|e| { ShellError::labeled_error(e.to_string(), e.to_string(), dst_tag) })?; } if src.is_file() { std::fs::copy(&src, &dst).map_err(|e| { ShellError::labeled_error(e.to_string(), e.to_string(), &name_tag) })?; } } } } } Ok(OutputStream::empty()) } fn mkdir( &self, MkdirArgs { rest: directories, show_created_paths, }: MkdirArgs, name: Tag, path: &str, ) -> Result { let path = Path::new(path); let mut stream = VecDeque::new(); if directories.is_empty() { return Err(ShellError::labeled_error( "mkdir requires directory paths", "needs parameter", name, )); } for dir in directories.iter() { let create_at = path.join(&dir.item); let dir_res = std::fs::create_dir_all(&create_at); if let Err(reason) = dir_res { return Err(ShellError::labeled_error( reason.to_string(), reason.to_string(), dir.tag(), )); } if show_created_paths { let val = format!("{:}", create_at.to_string_lossy()).into(); stream.push_back(Ok(ReturnSuccess::Value(val))); } } Ok(stream.into()) } fn mv( &self, MoveArgs { src, dst }: MoveArgs, _name: Tag, path: &str, ) -> Result { let path = Path::new(path); let source = path.join(&src.item); let destination = path.join(&dst.item); let sources = glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect); if sources.is_empty() { return Err(ShellError::labeled_error( "Invalid file or pattern", "invalid file or pattern", src.tag, )); } // We have two possibilities. // // First, the destination exists. // - If a directory, move everything into that directory, otherwise // - if only a single source, overwrite the file, otherwise // - error. // // Second, the destination doesn't exist, so we can only rename a single source. Otherwise // it's an error. if (destination.exists() && !destination.is_dir() && sources.len() > 1) || (!destination.exists() && sources.len() > 1) { return Err(ShellError::labeled_error( "Can only move multiple sources if destination is a directory", "destination must be a directory when multiple sources", dst.tag, )); } for entry in sources { if let Ok(entry) = entry { move_file( TaggedPathBuf(&entry, &src.tag), TaggedPathBuf(&destination, &dst.tag), )? } } Ok(OutputStream::empty()) } fn rm( &self, RemoveArgs { rest: targets, recursive, trash: _trash, permanent: _permanent, }: RemoveArgs, name: Tag, path: &str, ) -> Result { let name_tag = name; if targets.is_empty() { return Err(ShellError::labeled_error( "rm requires target paths", "needs parameter", name_tag, )); } let path = Path::new(path); let mut all_targets: HashMap = HashMap::new(); for target in targets { let all_dots = target .item .to_str() .map_or(false, |v| v.chars().all(|c| c == '.')); if all_dots { return Err(ShellError::labeled_error( "Cannot remove any parent directory", "cannot remove any parent directory", target.tag, )); } let path = path.join(&target.item); match glob::glob(&path.to_string_lossy()) { Ok(files) => { for file in files { match file { Ok(ref f) => { all_targets .entry(f.clone()) .or_insert_with(|| target.tag.clone()); } Err(e) => { return Err(ShellError::labeled_error( format!("Could not remove {:}", path.to_string_lossy()), e.to_string(), &target.tag, )); } } } } Err(e) => { return Err(ShellError::labeled_error( e.to_string(), e.to_string(), &name_tag, )) } }; } if all_targets.is_empty() { return Err(ShellError::labeled_error( "No valid paths", "no valid paths", name_tag, )); } Ok( futures::stream::iter(all_targets.into_iter().map(move |(f, tag)| { let is_empty = || match f.read_dir() { Ok(mut p) => p.next().is_none(), Err(_) => false, }; if let Ok(metadata) = f.symlink_metadata() { if metadata.is_file() || metadata.file_type().is_symlink() || recursive.item || is_empty() { let result; #[cfg(feature = "trash-support")] { let rm_always_trash = config::config(Tag::unknown())? .get("rm_always_trash") .map(|val| val.is_true()) .unwrap_or(false); result = if _trash.item || (rm_always_trash && !_permanent.item) { trash::remove(&f).map_err(|_| f.to_string_lossy()) } else if metadata.is_file() { std::fs::remove_file(&f).map_err(|_| f.to_string_lossy()) } else { std::fs::remove_dir_all(&f).map_err(|_| f.to_string_lossy()) }; } #[cfg(not(feature = "trash-support"))] { result = if metadata.is_file() { std::fs::remove_file(&f).map_err(|_| f.to_string_lossy()) } else { std::fs::remove_dir_all(&f).map_err(|_| f.to_string_lossy()) }; } if let Err(e) = result { let msg = format!("Could not delete {:}", e); Err(ShellError::labeled_error(msg, e, tag)) } else { let val = format!("deleted {:}", f.to_string_lossy()).into(); Ok(ReturnSuccess::Value(val)) } } else { let msg = format!("Cannot remove {:}. try --recursive", f.to_string_lossy()); Err(ShellError::labeled_error( msg, "cannot remove non-empty directory", tag, )) } } else { let msg = format!("no such file or directory: {:}", f.to_string_lossy()); Err(ShellError::labeled_error( msg, "no such file or directory", tag, )) } })) .to_output_stream(), ) } fn path(&self) -> String { self.path.clone() } fn pwd(&self, args: EvaluatedWholeStreamCommandArgs) -> Result { let path = PathBuf::from(self.path()); let p = match dunce::canonicalize(path.as_path()) { Ok(p) => p, Err(_) => { return Err(ShellError::labeled_error( "unable to show current directory", "pwd command failed", &args.call_info.name_tag, )); } }; let mut stream = VecDeque::new(); stream.push_back(ReturnSuccess::value( UntaggedValue::Primitive(Primitive::String(p.to_string_lossy().to_string())) .into_value(&args.call_info.name_tag), )); Ok(stream.into()) } fn set_path(&mut self, path: String) { let pathbuf = PathBuf::from(&path); let path = match canonicalize(self.path(), pathbuf.as_path()) { Ok(path) => { let _ = std::env::set_current_dir(&path); std::env::set_var("PWD", &path); path } _ => { // TODO: handle the case where the path cannot be canonicalized pathbuf } }; self.last_path = self.path.clone(); self.path = path.to_string_lossy().to_string(); } fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>, ) -> Result<(usize, Vec), rustyline::error::ReadlineError> { let expanded = expand_ndots(&line); // Find the first not-matching char position, if there is one let differ_pos = line .chars() .zip(expanded.chars()) .enumerate() .find(|(_index, (a, b))| a != b) .map(|(differ_pos, _)| differ_pos); let pos = if let Some(differ_pos) = differ_pos { if differ_pos < pos { pos + (expanded.len() - line.len()) } else { pos } } else { pos }; self.completer.complete(&expanded, pos, ctx) } fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option { self.hinter.hint(line, pos, ctx) } } struct TaggedPathBuf<'a>(&'a PathBuf, &'a Tag); fn move_file(from: TaggedPathBuf, to: TaggedPathBuf) -> Result<(), ShellError> { let TaggedPathBuf(from, from_tag) = from; let TaggedPathBuf(to, to_tag) = to; if to.exists() && from.is_dir() && to.is_file() { return Err(ShellError::labeled_error( "Cannot rename a directory to a file", "invalid destination", to_tag, )); } let destination_dir_exists = if to.is_dir() { true } else { to.parent().map(Path::exists).unwrap_or(true) }; if !destination_dir_exists { return Err(ShellError::labeled_error( "Destination directory does not exist", "destination does not exist", to_tag, )); } let mut to = to.clone(); if to.is_dir() { let from_file_name = match from.file_name() { Some(name) => name, None => { return Err(ShellError::labeled_error( "Not a valid entry name", "not a valid entry name", from_tag, )) } }; to.push(from_file_name); } // We first try a rename, which is a quick operation. If that doesn't work, we'll try a copy // and remove the old file. This is necessary if we're moving across filesystems. std::fs::rename(&from, &to) .or_else(|_| std::fs::copy(&from, &to).and_then(|_| std::fs::remove_file(&from))) .map_err(|e| { ShellError::labeled_error( format!("Could not move {:?} to {:?}. {:}", from, to, e.to_string()), "could not move", from_tag, ) }) } fn is_empty_dir(dir: impl AsRef) -> bool { match dir.as_ref().read_dir() { Err(_) => true, Ok(mut s) => s.next().is_none(), } } fn is_hidden_dir(dir: impl AsRef) -> bool { cfg_if::cfg_if! { if #[cfg(windows)] { use std::os::windows::fs::MetadataExt; if let Ok(metadata) = dir.as_ref().metadata() { let attributes = metadata.file_attributes(); // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants (attributes & 0x2) != 0 } else { false } } else { dir.as_ref() .file_name() .map(|name| name.to_string_lossy().starts_with('.')) .unwrap_or(false) } } }