mirror of
https://github.com/nushell/nushell
synced 2024-12-27 05:23:11 +00:00
Re-port filesystem commands (#4387)
* Re-port the filesystem commands * Remove commented out section
This commit is contained in:
parent
94ab981235
commit
43850bf20e
9 changed files with 494 additions and 391 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -1062,6 +1062,12 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs_extra"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futf"
|
name = "futf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -2180,6 +2186,7 @@ dependencies = [
|
||||||
"eml-parser",
|
"eml-parser",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"filesize",
|
"filesize",
|
||||||
|
"fs_extra",
|
||||||
"glob",
|
"glob",
|
||||||
"hamcrest2",
|
"hamcrest2",
|
||||||
"htmlescape",
|
"htmlescape",
|
||||||
|
|
|
@ -38,6 +38,7 @@ dtparse = "1.2.0"
|
||||||
eml-parser = "0.1.0"
|
eml-parser = "0.1.0"
|
||||||
encoding_rs = "0.8.30"
|
encoding_rs = "0.8.30"
|
||||||
filesize = "0.2.0"
|
filesize = "0.2.0"
|
||||||
|
fs_extra = "1.2.0"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
htmlescape = "0.3.1"
|
htmlescape = "0.3.1"
|
||||||
ical = "0.7.0"
|
ical = "0.7.0"
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::util::get_interactive_confirmation;
|
|
||||||
use nu_engine::env::current_dir;
|
use nu_engine::env::current_dir;
|
||||||
use nu_engine::CallExt;
|
use nu_engine::CallExt;
|
||||||
use nu_path::canonicalize_with;
|
use nu_path::canonicalize_with;
|
||||||
use nu_protocol::ast::Call;
|
use nu_protocol::ast::Call;
|
||||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||||
use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape};
|
use nu_protocol::{Category, PipelineData, ShellError, Signature, Spanned, SyntaxShape};
|
||||||
|
|
||||||
use crate::filesystem::util::FileStructure;
|
use crate::filesystem::util::FileStructure;
|
||||||
|
|
||||||
|
const GLOB_PARAMS: glob::MatchOptions = glob::MatchOptions {
|
||||||
|
case_sensitive: true,
|
||||||
|
require_literal_separator: false,
|
||||||
|
require_literal_leading_dot: false,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Cp;
|
pub struct Cp;
|
||||||
|
|
||||||
|
@ -32,8 +37,9 @@ impl Command for Cp {
|
||||||
"copy recursively through subdirectories",
|
"copy recursively through subdirectories",
|
||||||
Some('r'),
|
Some('r'),
|
||||||
)
|
)
|
||||||
.switch("force", "suppress error when no file", Some('f'))
|
// TODO: add back in additional features
|
||||||
.switch("interactive", "ask user to confirm action", Some('i'))
|
// .switch("force", "suppress error when no file", Some('f'))
|
||||||
|
// .switch("interactive", "ask user to confirm action", Some('i'))
|
||||||
.category(Category::FileSystem)
|
.category(Category::FileSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,93 +50,49 @@ impl Command for Cp {
|
||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let source: String = call.req(engine_state, stack, 0)?;
|
let src: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||||
let destination: String = call.req(engine_state, stack, 1)?;
|
let dst: Spanned<String> = call.req(engine_state, stack, 1)?;
|
||||||
let interactive = call.has_flag("interactive");
|
let recursive = call.has_flag("recursive");
|
||||||
let force = call.has_flag("force");
|
|
||||||
|
|
||||||
let path = current_dir(engine_state, stack)?;
|
let path = current_dir(engine_state, stack)?;
|
||||||
let source = path.join(source.as_str());
|
let source = path.join(src.item.as_str());
|
||||||
let destination = path.join(destination.as_str());
|
let destination = path.join(dst.item.as_str());
|
||||||
|
|
||||||
|
let sources: Vec<_> = match glob::glob_with(&source.to_string_lossy(), GLOB_PARAMS) {
|
||||||
|
Ok(files) => files.collect(),
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
e.to_string(),
|
||||||
|
"invalid pattern".to_string(),
|
||||||
|
src.span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut sources =
|
|
||||||
glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect);
|
|
||||||
if sources.is_empty() {
|
if sources.is_empty() {
|
||||||
return Err(ShellError::FileNotFound(call.positional[0].span));
|
return Err(ShellError::SpannedLabeledError(
|
||||||
}
|
"No matches found".into(),
|
||||||
|
"no matches found".into(),
|
||||||
if sources.len() > 1 && !destination.is_dir() {
|
src.span,
|
||||||
return Err(ShellError::MoveNotPossible {
|
|
||||||
source_message: "Can't move many files".to_string(),
|
|
||||||
source_span: call.positional[0].span,
|
|
||||||
destination_message: "into single file".to_string(),
|
|
||||||
destination_span: call.positional[1].span,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir()));
|
|
||||||
let recursive: bool = call.has_flag("recursive");
|
|
||||||
if any_source_is_dir && !recursive {
|
|
||||||
return Err(ShellError::MoveNotPossibleSingle(
|
|
||||||
"Directories must be copied using \"--recursive\"".to_string(),
|
|
||||||
call.positional[0].span,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if interactive && !force {
|
if sources.len() > 1 && !destination.is_dir() {
|
||||||
let mut remove: Vec<usize> = vec![];
|
return Err(ShellError::SpannedLabeledError(
|
||||||
for (index, file) in sources.iter().enumerate() {
|
"Destination must be a directory when copying multiple files".into(),
|
||||||
let prompt = format!(
|
"is not a directory".into(),
|
||||||
"Are you shure that you want to copy {} to {}?",
|
dst.span,
|
||||||
file.as_ref()
|
));
|
||||||
.map_err(|err| ShellError::SpannedLabeledError(
|
}
|
||||||
"Reference error".into(),
|
|
||||||
err.to_string(),
|
|
||||||
call.head
|
|
||||||
))?
|
|
||||||
.file_name()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"File name error".into(),
|
|
||||||
"Unable to get file name".into(),
|
|
||||||
call.head
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"Unable to get str error".into(),
|
|
||||||
"Unable to convert to str file name".into(),
|
|
||||||
call.head
|
|
||||||
))?,
|
|
||||||
destination
|
|
||||||
.file_name()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"File name error".into(),
|
|
||||||
"Unable to get file name".into(),
|
|
||||||
call.head
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"Unable to get str error".into(),
|
|
||||||
"Unable to convert to str file name".into(),
|
|
||||||
call.head
|
|
||||||
))?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let input = get_interactive_confirmation(prompt)?;
|
let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir()));
|
||||||
|
|
||||||
if !input {
|
if any_source_is_dir && !recursive {
|
||||||
remove.push(index);
|
return Err(ShellError::SpannedLabeledError(
|
||||||
}
|
"Directories must be copied using \"--recursive\"".into(),
|
||||||
}
|
"resolves to a directory (not copied)".into(),
|
||||||
|
src.span,
|
||||||
remove.reverse();
|
));
|
||||||
|
|
||||||
for index in remove {
|
|
||||||
sources.remove(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if sources.is_empty() {
|
|
||||||
return Err(ShellError::NoFileToBeCopied());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in sources.into_iter().flatten() {
|
for entry in sources.into_iter().flatten() {
|
||||||
|
@ -140,7 +102,7 @@ impl Command for Cp {
|
||||||
if entry.is_file() {
|
if entry.is_file() {
|
||||||
let sources = sources.paths_applying_with(|(source_file, _depth_level)| {
|
let sources = sources.paths_applying_with(|(source_file, _depth_level)| {
|
||||||
if destination.is_dir() {
|
if destination.is_dir() {
|
||||||
let mut dest = canonicalize_with(&destination, &path)?;
|
let mut dest = canonicalize_with(&dst.item, &path)?;
|
||||||
if let Some(name) = entry.file_name() {
|
if let Some(name) = entry.file_name() {
|
||||||
dest.push(name);
|
dest.push(name);
|
||||||
}
|
}
|
||||||
|
@ -152,15 +114,8 @@ impl Command for Cp {
|
||||||
|
|
||||||
for (src, dst) in sources {
|
for (src, dst) in sources {
|
||||||
if src.is_file() {
|
if src.is_file() {
|
||||||
std::fs::copy(&src, dst).map_err(|e| {
|
std::fs::copy(src, dst).map_err(|e| {
|
||||||
ShellError::MoveNotPossibleSingle(
|
ShellError::SpannedLabeledError(e.to_string(), e.to_string(), call.head)
|
||||||
format!(
|
|
||||||
"failed to move containing file \"{}\": {}",
|
|
||||||
src.to_string_lossy(),
|
|
||||||
e
|
|
||||||
),
|
|
||||||
call.positional[0].span,
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,58 +126,48 @@ impl Command for Cp {
|
||||||
match entry.file_name() {
|
match entry.file_name() {
|
||||||
Some(name) => destination.join(name),
|
Some(name) => destination.join(name),
|
||||||
None => {
|
None => {
|
||||||
return Err(ShellError::FileNotFoundCustom(
|
return Err(ShellError::SpannedLabeledError(
|
||||||
format!("containing \"{:?}\" is not a valid path", entry),
|
"Copy aborted. Not a valid path".into(),
|
||||||
call.positional[0].span,
|
"not a valid path".into(),
|
||||||
|
dst.span,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::fs::create_dir_all(&destination).map_err(|e| {
|
std::fs::create_dir_all(&destination).map_err(|e| {
|
||||||
ShellError::MoveNotPossibleSingle(
|
ShellError::SpannedLabeledError(e.to_string(), e.to_string(), dst.span)
|
||||||
format!("failed to recursively fill destination: {}", e),
|
|
||||||
call.positional[1].span,
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let sources = sources.paths_applying_with(|(source_file, depth_level)| {
|
let sources = sources.paths_applying_with(|(source_file, depth_level)| {
|
||||||
let mut dest = destination.clone();
|
let mut dest = destination.clone();
|
||||||
let path = canonicalize_with(&source_file, &path)?;
|
let path = canonicalize_with(&source_file, &path)?;
|
||||||
let components = path
|
|
||||||
|
#[allow(clippy::needless_collect)]
|
||||||
|
let comps: Vec<_> = path
|
||||||
.components()
|
.components()
|
||||||
.map(|fragment| fragment.as_os_str())
|
.map(|fragment| fragment.as_os_str())
|
||||||
.rev()
|
.rev()
|
||||||
.take(1 + depth_level);
|
.take(1 + depth_level)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for fragment in comps.into_iter().rev() {
|
||||||
|
dest.push(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
components.for_each(|fragment| dest.push(fragment));
|
|
||||||
Ok((PathBuf::from(&source_file), dest))
|
Ok((PathBuf::from(&source_file), dest))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
for (src, dst) in sources {
|
for (s, d) in sources {
|
||||||
if src.is_dir() && !dst.exists() {
|
if s.is_dir() && !d.exists() {
|
||||||
std::fs::create_dir_all(&dst).map_err(|e| {
|
std::fs::create_dir_all(&d).map_err(|e| {
|
||||||
ShellError::MoveNotPossibleSingle(
|
ShellError::SpannedLabeledError(e.to_string(), e.to_string(), dst.span)
|
||||||
format!(
|
|
||||||
"failed to create containing directory \"{}\": {}",
|
|
||||||
dst.to_string_lossy(),
|
|
||||||
e
|
|
||||||
),
|
|
||||||
call.positional[1].span,
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if src.is_file() {
|
if s.is_file() {
|
||||||
std::fs::copy(&src, &dst).map_err(|e| {
|
std::fs::copy(&s, &d).map_err(|e| {
|
||||||
ShellError::MoveNotPossibleSingle(
|
ShellError::SpannedLabeledError(e.to_string(), e.to_string(), call.head)
|
||||||
format!(
|
|
||||||
"failed to move containing file \"{}\": {}",
|
|
||||||
src.to_string_lossy(),
|
|
||||||
e
|
|
||||||
),
|
|
||||||
call.positional[0].span,
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,4 +176,183 @@ impl Command for Cp {
|
||||||
|
|
||||||
Ok(PipelineData::new(call.head))
|
Ok(PipelineData::new(call.head))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let mut sources =
|
||||||
|
// glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect);
|
||||||
|
// if sources.is_empty() {
|
||||||
|
// return Err(ShellError::FileNotFound(call.positional[0].span));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if sources.len() > 1 && !destination.is_dir() {
|
||||||
|
// return Err(ShellError::MoveNotPossible {
|
||||||
|
// source_message: "Can't move many files".to_string(),
|
||||||
|
// source_span: call.positional[0].span,
|
||||||
|
// destination_message: "into single file".to_string(),
|
||||||
|
// destination_span: call.positional[1].span,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let any_source_is_dir = sources.iter().any(|f| matches!(f, Ok(f) if f.is_dir()));
|
||||||
|
// let recursive: bool = call.has_flag("recursive");
|
||||||
|
// if any_source_is_dir && !recursive {
|
||||||
|
// return Err(ShellError::MoveNotPossibleSingle(
|
||||||
|
// "Directories must be copied using \"--recursive\"".to_string(),
|
||||||
|
// call.positional[0].span,
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if interactive && !force {
|
||||||
|
// let mut remove: Vec<usize> = vec![];
|
||||||
|
// for (index, file) in sources.iter().enumerate() {
|
||||||
|
// let prompt = format!(
|
||||||
|
// "Are you shure that you want to copy {} to {}?",
|
||||||
|
// file.as_ref()
|
||||||
|
// .map_err(|err| ShellError::SpannedLabeledError(
|
||||||
|
// "Reference error".into(),
|
||||||
|
// err.to_string(),
|
||||||
|
// call.head
|
||||||
|
// ))?
|
||||||
|
// .file_name()
|
||||||
|
// .ok_or_else(|| ShellError::SpannedLabeledError(
|
||||||
|
// "File name error".into(),
|
||||||
|
// "Unable to get file name".into(),
|
||||||
|
// call.head
|
||||||
|
// ))?
|
||||||
|
// .to_str()
|
||||||
|
// .ok_or_else(|| ShellError::SpannedLabeledError(
|
||||||
|
// "Unable to get str error".into(),
|
||||||
|
// "Unable to convert to str file name".into(),
|
||||||
|
// call.head
|
||||||
|
// ))?,
|
||||||
|
// destination
|
||||||
|
// .file_name()
|
||||||
|
// .ok_or_else(|| ShellError::SpannedLabeledError(
|
||||||
|
// "File name error".into(),
|
||||||
|
// "Unable to get file name".into(),
|
||||||
|
// call.head
|
||||||
|
// ))?
|
||||||
|
// .to_str()
|
||||||
|
// .ok_or_else(|| ShellError::SpannedLabeledError(
|
||||||
|
// "Unable to get str error".into(),
|
||||||
|
// "Unable to convert to str file name".into(),
|
||||||
|
// call.head
|
||||||
|
// ))?,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let input = get_interactive_confirmation(prompt)?;
|
||||||
|
|
||||||
|
// if !input {
|
||||||
|
// remove.push(index);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// remove.reverse();
|
||||||
|
|
||||||
|
// for index in remove {
|
||||||
|
// sources.remove(index);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if sources.is_empty() {
|
||||||
|
// return Err(ShellError::NoFileToBeCopied());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for entry in sources.into_iter().flatten() {
|
||||||
|
// let mut sources = FileStructure::new();
|
||||||
|
// sources.walk_decorate(&entry, engine_state, stack)?;
|
||||||
|
|
||||||
|
// if entry.is_file() {
|
||||||
|
// let sources = sources.paths_applying_with(|(source_file, _depth_level)| {
|
||||||
|
// if destination.is_dir() {
|
||||||
|
// let mut dest = canonicalize_with(&destination, &path)?;
|
||||||
|
// 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::MoveNotPossibleSingle(
|
||||||
|
// format!(
|
||||||
|
// "failed to move containing file \"{}\": {}",
|
||||||
|
// src.to_string_lossy(),
|
||||||
|
// e
|
||||||
|
// ),
|
||||||
|
// call.positional[0].span,
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } 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::FileNotFoundCustom(
|
||||||
|
// format!("containing \"{:?}\" is not a valid path", entry),
|
||||||
|
// call.positional[0].span,
|
||||||
|
// ))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// std::fs::create_dir_all(&destination).map_err(|e| {
|
||||||
|
// ShellError::MoveNotPossibleSingle(
|
||||||
|
// format!("failed to recursively fill destination: {}", e),
|
||||||
|
// call.positional[1].span,
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// let sources = sources.paths_applying_with(|(source_file, depth_level)| {
|
||||||
|
// let mut dest = destination.clone();
|
||||||
|
// let path = canonicalize_with(&source_file, &path)?;
|
||||||
|
// let components = path
|
||||||
|
// .components()
|
||||||
|
// .map(|fragment| fragment.as_os_str())
|
||||||
|
// .rev()
|
||||||
|
// .take(1 + depth_level);
|
||||||
|
|
||||||
|
// components.for_each(|fragment| dest.push(fragment));
|
||||||
|
// Ok((PathBuf::from(&source_file), dest))
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// for (src, dst) in sources {
|
||||||
|
// if src.is_dir() && !dst.exists() {
|
||||||
|
// std::fs::create_dir_all(&dst).map_err(|e| {
|
||||||
|
// ShellError::MoveNotPossibleSingle(
|
||||||
|
// format!(
|
||||||
|
// "failed to create containing directory \"{}\": {}",
|
||||||
|
// dst.to_string_lossy(),
|
||||||
|
// e
|
||||||
|
// ),
|
||||||
|
// call.positional[1].span,
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if src.is_file() {
|
||||||
|
// std::fs::copy(&src, &dst).map_err(|e| {
|
||||||
|
// ShellError::MoveNotPossibleSingle(
|
||||||
|
// format!(
|
||||||
|
// "failed to move containing file \"{}\": {}",
|
||||||
|
// src.to_string_lossy(),
|
||||||
|
// e
|
||||||
|
// ),
|
||||||
|
// call.positional[0].span,
|
||||||
|
// )
|
||||||
|
// })?;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ok(PipelineData::new(call.head))
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::util::get_interactive_confirmation;
|
// use super::util::get_interactive_confirmation;
|
||||||
use nu_engine::env::current_dir;
|
use nu_engine::env::current_dir;
|
||||||
use nu_engine::CallExt;
|
use nu_engine::CallExt;
|
||||||
use nu_protocol::ast::Call;
|
use nu_protocol::ast::Call;
|
||||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||||
use nu_protocol::{Category, PipelineData, ShellError, Signature, Spanned, SyntaxShape};
|
use nu_protocol::{Category, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape};
|
||||||
|
|
||||||
|
const GLOB_PARAMS: glob::MatchOptions = glob::MatchOptions {
|
||||||
|
case_sensitive: true,
|
||||||
|
require_literal_separator: false,
|
||||||
|
require_literal_leading_dot: false,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Mv;
|
pub struct Mv;
|
||||||
|
@ -32,8 +38,8 @@ impl Command for Mv {
|
||||||
SyntaxShape::Filepath,
|
SyntaxShape::Filepath,
|
||||||
"the location to move files/directories to",
|
"the location to move files/directories to",
|
||||||
)
|
)
|
||||||
.switch("interactive", "ask user to confirm action", Some('i'))
|
// .switch("interactive", "ask user to confirm action", Some('i'))
|
||||||
.switch("force", "suppress error when no file", Some('f'))
|
// .switch("force", "suppress error when no file", Some('f'))
|
||||||
.category(Category::FileSystem)
|
.category(Category::FileSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,99 +52,58 @@ impl Command for Mv {
|
||||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||||
// TODO: handle invalid directory or insufficient permissions when moving
|
// TODO: handle invalid directory or insufficient permissions when moving
|
||||||
let spanned_source: Spanned<String> = call.req(engine_state, stack, 0)?;
|
let spanned_source: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||||
let destination: String = call.req(engine_state, stack, 1)?;
|
let spanned_destination: Spanned<String> = call.req(engine_state, stack, 1)?;
|
||||||
let interactive = call.has_flag("interactive");
|
// let interactive = call.has_flag("interactive");
|
||||||
let force = call.has_flag("force");
|
// let force = call.has_flag("force");
|
||||||
|
|
||||||
let path = current_dir(engine_state, stack)?;
|
let path = current_dir(engine_state, stack)?;
|
||||||
let source = path.join(spanned_source.item.as_str());
|
let source = path.join(spanned_source.item.as_str());
|
||||||
let destination = path.join(destination.as_str());
|
let destination = path.join(spanned_destination.item.as_str());
|
||||||
|
|
||||||
let mut sources =
|
let mut sources = glob::glob_with(&source.to_string_lossy(), GLOB_PARAMS)
|
||||||
glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect);
|
.map_or_else(|_| Vec::new(), Iterator::collect);
|
||||||
|
|
||||||
if sources.is_empty() {
|
if sources.is_empty() {
|
||||||
return Err(ShellError::FileNotFound(spanned_source.span));
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
"Invalid file or pattern".into(),
|
||||||
|
"invalid file or pattern".into(),
|
||||||
|
spanned_source.span,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if interactive && !force {
|
// We have two possibilities.
|
||||||
let mut remove: Vec<usize> = vec![];
|
//
|
||||||
for (index, file) in sources.iter().enumerate() {
|
// First, the destination exists.
|
||||||
let prompt = format!(
|
// - If a directory, move everything into that directory, otherwise
|
||||||
"Are you shure that you want to move {} to {}?",
|
// - if only a single source, overwrite the file, otherwise
|
||||||
file.as_ref()
|
// - error.
|
||||||
.map_err(|err| ShellError::SpannedLabeledError(
|
//
|
||||||
"Reference error".into(),
|
// Second, the destination doesn't exist, so we can only rename a single source. Otherwise
|
||||||
err.to_string(),
|
// it's an error.
|
||||||
call.head
|
|
||||||
))?
|
|
||||||
.file_name()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"File name error".into(),
|
|
||||||
"Unable to get file name".into(),
|
|
||||||
call.head
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"Unable to get str error".into(),
|
|
||||||
"Unable to convert to str file name".into(),
|
|
||||||
call.head
|
|
||||||
))?,
|
|
||||||
destination
|
|
||||||
.file_name()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"File name error".into(),
|
|
||||||
"Unable to get file name".into(),
|
|
||||||
call.head
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"Unable to get str error".into(),
|
|
||||||
"Unable to convert to str file name".into(),
|
|
||||||
call.head
|
|
||||||
))?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let input = get_interactive_confirmation(prompt)?;
|
|
||||||
|
|
||||||
if !input {
|
|
||||||
remove.push(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remove.reverse();
|
|
||||||
|
|
||||||
for index in remove {
|
|
||||||
sources.remove(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if sources.is_empty() {
|
|
||||||
return Err(ShellError::NoFileToBeMoved());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (destination.exists() && !destination.is_dir() && sources.len() > 1)
|
if (destination.exists() && !destination.is_dir() && sources.len() > 1)
|
||||||
|| (!destination.exists() && sources.len() > 1)
|
|| (!destination.exists() && sources.len() > 1)
|
||||||
{
|
{
|
||||||
return Err(ShellError::MoveNotPossible {
|
return Err(ShellError::SpannedLabeledError(
|
||||||
source_message: "Can't move many files".to_string(),
|
"Can only move multiple sources if destination is a directory".into(),
|
||||||
source_span: call.positional[0].span,
|
"destination must be a directory when multiple sources".into(),
|
||||||
destination_message: "into single file".to_string(),
|
spanned_destination.span,
|
||||||
destination_span: call.positional[1].span,
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let some_if_source_is_destination = sources
|
let some_if_source_is_destination = sources
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| matches!(f, Ok(f) if destination.starts_with(f)));
|
.find(|f| matches!(f, Ok(f) if destination.starts_with(f)));
|
||||||
if destination.exists() && destination.is_dir() && sources.len() == 1 {
|
if destination.exists() && destination.is_dir() && sources.len() == 1 {
|
||||||
if let Some(Ok(_filename)) = some_if_source_is_destination {
|
if let Some(Ok(filename)) = some_if_source_is_destination {
|
||||||
return Err(ShellError::MoveNotPossible {
|
return Err(ShellError::SpannedLabeledError(
|
||||||
source_message: "Can't move directory".to_string(),
|
format!(
|
||||||
source_span: call.positional[0].span,
|
"Not possible to move {:?} to itself",
|
||||||
destination_message: "into itself".to_string(),
|
filename.file_name().expect("Invalid file name")
|
||||||
destination_span: call.positional[1].span,
|
),
|
||||||
});
|
"cannot move to itself".into(),
|
||||||
|
spanned_destination.span,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,20 +115,41 @@ impl Command for Mv {
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in sources.into_iter().flatten() {
|
for entry in sources.into_iter().flatten() {
|
||||||
move_file(call, &entry, &destination)?
|
move_file(
|
||||||
|
Spanned {
|
||||||
|
item: entry,
|
||||||
|
span: spanned_source.span,
|
||||||
|
},
|
||||||
|
Spanned {
|
||||||
|
item: destination.clone(),
|
||||||
|
span: spanned_destination.span,
|
||||||
|
},
|
||||||
|
)?
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PipelineData::new(call.head))
|
Ok(PipelineData::new(call.head))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_file(call: &Call, from: &Path, to: &Path) -> Result<(), ShellError> {
|
fn move_file(
|
||||||
|
spanned_from: Spanned<PathBuf>,
|
||||||
|
spanned_to: Spanned<PathBuf>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
let Spanned {
|
||||||
|
item: from,
|
||||||
|
span: from_span,
|
||||||
|
} = spanned_from;
|
||||||
|
let Spanned {
|
||||||
|
item: to,
|
||||||
|
span: to_span,
|
||||||
|
} = spanned_to;
|
||||||
|
|
||||||
if to.exists() && from.is_dir() && to.is_file() {
|
if to.exists() && from.is_dir() && to.is_file() {
|
||||||
return Err(ShellError::MoveNotPossible {
|
return Err(ShellError::MoveNotPossible {
|
||||||
source_message: "Can't move a directory".to_string(),
|
source_message: "Can't move a directory".to_string(),
|
||||||
source_span: call.positional[0].span,
|
source_span: spanned_from.span,
|
||||||
destination_message: "to a file".to_string(),
|
destination_message: "to a file".to_string(),
|
||||||
destination_span: call.positional[1].span,
|
destination_span: spanned_to.span,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,29 +160,42 @@ fn move_file(call: &Call, from: &Path, to: &Path) -> Result<(), ShellError> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if !destination_dir_exists {
|
if !destination_dir_exists {
|
||||||
return Err(ShellError::DirectoryNotFound(call.positional[1].span));
|
return Err(ShellError::DirectoryNotFound(to_span));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut to = to.to_path_buf();
|
let mut to = to;
|
||||||
if to.is_dir() {
|
if to.is_dir() {
|
||||||
let from_file_name = match from.file_name() {
|
let from_file_name = match from.file_name() {
|
||||||
Some(name) => name,
|
Some(name) => name,
|
||||||
None => return Err(ShellError::DirectoryNotFound(call.positional[1].span)),
|
None => return Err(ShellError::DirectoryNotFound(to_span)),
|
||||||
};
|
};
|
||||||
|
|
||||||
to.push(from_file_name);
|
to.push(from_file_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
move_item(call, from, &to)
|
move_item(&from, from_span, &to)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_item(call: &Call, from: &Path, to: &Path) -> Result<(), ShellError> {
|
fn move_item(from: &Path, from_span: Span, to: &Path) -> Result<(), ShellError> {
|
||||||
// We first try a rename, which is a quick operation. If that doesn't work, we'll try a copy
|
// 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/folder. This is necessary if we're moving across filesystems or devices.
|
// and remove the old file/folder. This is necessary if we're moving across filesystems or devices.
|
||||||
std::fs::rename(&from, &to).map_err(|_| ShellError::MoveNotPossible {
|
std::fs::rename(&from, &to).or_else(|_| {
|
||||||
source_message: "failed to move".to_string(),
|
match if from.is_file() {
|
||||||
source_span: call.positional[0].span,
|
let mut options = fs_extra::file::CopyOptions::new();
|
||||||
destination_message: "into".to_string(),
|
options.overwrite = true;
|
||||||
destination_span: call.positional[1].span,
|
fs_extra::file::move_file(from, to, &options)
|
||||||
|
} else {
|
||||||
|
let mut options = fs_extra::dir::CopyOptions::new();
|
||||||
|
options.overwrite = true;
|
||||||
|
options.copy_inside = true;
|
||||||
|
fs_extra::dir::move_dir(from, to, &options)
|
||||||
|
} {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(ShellError::SpannedLabeledError(
|
||||||
|
format!("Could not move {:?} to {:?}. {:}", from, to, e),
|
||||||
|
"could not move".into(),
|
||||||
|
from_span,
|
||||||
|
)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,29 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::ErrorKind;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::prelude::FileTypeExt;
|
use std::os::unix::prelude::FileTypeExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::util::get_interactive_confirmation;
|
// use super::util::get_interactive_confirmation;
|
||||||
|
|
||||||
use nu_engine::env::current_dir;
|
use nu_engine::env::current_dir;
|
||||||
use nu_engine::CallExt;
|
use nu_engine::CallExt;
|
||||||
use nu_protocol::ast::Call;
|
use nu_protocol::ast::Call;
|
||||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, SyntaxShape,
|
Category, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, Spanned,
|
||||||
Value,
|
SyntaxShape, Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const GLOB_PARAMS: glob::MatchOptions = glob::MatchOptions {
|
||||||
|
case_sensitive: true,
|
||||||
|
require_literal_separator: false,
|
||||||
|
require_literal_leading_dot: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Rm;
|
pub struct Rm;
|
||||||
|
|
||||||
// Where self.0 is the unexpanded target's positional index (i.e. call.positional[self.0].span)
|
|
||||||
struct Target(usize, PathBuf);
|
|
||||||
|
|
||||||
struct RmArgs {
|
|
||||||
targets: Vec<Target>,
|
|
||||||
recursive: bool,
|
|
||||||
trash: bool,
|
|
||||||
permanent: bool,
|
|
||||||
force: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command for Rm {
|
impl Command for Rm {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"rm"
|
"rm"
|
||||||
|
@ -50,7 +47,7 @@ impl Command for Rm {
|
||||||
)
|
)
|
||||||
.switch("recursive", "delete subdirectories recursively", Some('r'))
|
.switch("recursive", "delete subdirectories recursively", Some('r'))
|
||||||
.switch("force", "suppress error when no file", Some('f'))
|
.switch("force", "suppress error when no file", Some('f'))
|
||||||
.switch("interactive", "ask user to confirm action", Some('i'))
|
// .switch("interactive", "ask user to confirm action", Some('i'))
|
||||||
.rest(
|
.rest(
|
||||||
"rest",
|
"rest",
|
||||||
SyntaxShape::GlobPattern,
|
SyntaxShape::GlobPattern,
|
||||||
|
@ -77,142 +74,120 @@ fn rm(
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let trash = call.has_flag("trash");
|
let trash = call.has_flag("trash");
|
||||||
let permanent = call.has_flag("permanent");
|
let permanent = call.has_flag("permanent");
|
||||||
let interactive = call.has_flag("interactive");
|
let recursive = call.has_flag("recursive");
|
||||||
|
let force = call.has_flag("force");
|
||||||
|
// let interactive = call.has_flag("interactive");
|
||||||
|
|
||||||
if trash && permanent {
|
let ctrlc = engine_state.ctrlc.clone();
|
||||||
return Err(ShellError::IncompatibleParametersSingle(
|
|
||||||
"Can't use \"--trash\" with \"--permanent\"".to_string(),
|
let targets: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
||||||
|
let span = call.head;
|
||||||
|
|
||||||
|
let config = stack.get_config()?;
|
||||||
|
|
||||||
|
let rm_always_trash = config.rm_always_trash;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "trash-support"))]
|
||||||
|
{
|
||||||
|
if rm_always_trash {
|
||||||
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
"Cannot execute `rm`; the current configuration specifies \
|
||||||
|
`rm_always_trash = true`, but the current nu executable was not \
|
||||||
|
built with feature `trash_support`."
|
||||||
|
.into(),
|
||||||
|
"trash required to be true but not supported".into(),
|
||||||
|
span,
|
||||||
|
));
|
||||||
|
} else if trash {
|
||||||
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
"Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled"
|
||||||
|
.into(),
|
||||||
|
"this option is only available if nu is built with the `trash-support` feature"
|
||||||
|
.into(),
|
||||||
|
span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targets.is_empty() {
|
||||||
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
"rm requires target paths".into(),
|
||||||
|
"needs parameter".into(),
|
||||||
|
span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = current_dir(engine_state, stack)?;
|
||||||
|
|
||||||
|
let mut all_targets: HashMap<PathBuf, Span> = HashMap::new();
|
||||||
|
for target in targets {
|
||||||
|
if path.to_string_lossy() == target.item
|
||||||
|
|| path.as_os_str().to_string_lossy().starts_with(&format!(
|
||||||
|
"{}{}",
|
||||||
|
target.item,
|
||||||
|
std::path::MAIN_SEPARATOR
|
||||||
|
))
|
||||||
|
{
|
||||||
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
"Cannot remove any parent directory".into(),
|
||||||
|
"cannot remove any parent directory".into(),
|
||||||
|
target.span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = path.join(&target.item);
|
||||||
|
match glob::glob_with(
|
||||||
|
&path.to_string_lossy(),
|
||||||
|
glob::MatchOptions {
|
||||||
|
require_literal_leading_dot: true,
|
||||||
|
..GLOB_PARAMS
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(files) => {
|
||||||
|
for file in files {
|
||||||
|
match file {
|
||||||
|
Ok(ref f) => {
|
||||||
|
// It is not appropriate to try and remove the
|
||||||
|
// current directory or its parent when using
|
||||||
|
// glob patterns.
|
||||||
|
let name = f.display().to_string();
|
||||||
|
if name.ends_with("/.") || name.ends_with("/..") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
all_targets.entry(f.clone()).or_insert_with(|| target.span);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
format!("Could not remove {:}", path.to_string_lossy()),
|
||||||
|
e.to_string(),
|
||||||
|
target.span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
e.to_string(),
|
||||||
|
e.to_string(),
|
||||||
|
call.head,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_targets.is_empty() && !force {
|
||||||
|
return Err(ShellError::SpannedLabeledError(
|
||||||
|
"No valid paths".into(),
|
||||||
|
"no valid paths".into(),
|
||||||
call.head,
|
call.head,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_path = current_dir(engine_state, stack)?;
|
Ok(all_targets
|
||||||
let mut paths = call
|
|
||||||
.rest::<String>(engine_state, stack, 0)?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|path| current_path.join(path))
|
.map(move |(f, _)| {
|
||||||
.peekable();
|
|
||||||
|
|
||||||
if paths.peek().is_none() {
|
|
||||||
return Err(ShellError::FileNotFound(call.positional[0].span));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand and flatten files
|
|
||||||
let resolve_path = |i: usize, path: PathBuf| {
|
|
||||||
glob::glob(&path.to_string_lossy()).map_or_else(
|
|
||||||
|_| Vec::new(),
|
|
||||||
|path_iter| path_iter.flatten().map(|f| Target(i, f)).collect(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut targets: Vec<Target> = vec![];
|
|
||||||
for (i, path) in paths.enumerate() {
|
|
||||||
let mut paths: Vec<Target> = resolve_path(i, path);
|
|
||||||
|
|
||||||
if paths.is_empty() {
|
|
||||||
return Err(ShellError::FileNotFound(call.positional[i].span));
|
|
||||||
}
|
|
||||||
|
|
||||||
targets.append(paths.as_mut());
|
|
||||||
}
|
|
||||||
|
|
||||||
let recursive = call.has_flag("recursive");
|
|
||||||
let force = call.has_flag("force");
|
|
||||||
|
|
||||||
if interactive && !force {
|
|
||||||
let mut remove: Vec<usize> = vec![];
|
|
||||||
for (index, file) in targets.iter().enumerate() {
|
|
||||||
let prompt: String = format!(
|
|
||||||
"Are you sure that you what to delete {}?",
|
|
||||||
file.1
|
|
||||||
.file_name()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"File name error".into(),
|
|
||||||
"Unable to get file name".into(),
|
|
||||||
call.head
|
|
||||||
))?
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| ShellError::SpannedLabeledError(
|
|
||||||
"Unable to get str error".into(),
|
|
||||||
"Unable to convert to str file name".into(),
|
|
||||||
call.head
|
|
||||||
))?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let input = get_interactive_confirmation(prompt)?;
|
|
||||||
|
|
||||||
if !input {
|
|
||||||
remove.push(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remove.reverse();
|
|
||||||
|
|
||||||
for index in remove {
|
|
||||||
targets.remove(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if targets.is_empty() {
|
|
||||||
return Err(ShellError::NoFileToBeRemoved());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = RmArgs {
|
|
||||||
targets,
|
|
||||||
recursive,
|
|
||||||
trash,
|
|
||||||
permanent,
|
|
||||||
force,
|
|
||||||
};
|
|
||||||
let response = rm_helper(call, args);
|
|
||||||
|
|
||||||
// let temp = rm_helper(call, args).flatten();
|
|
||||||
// let temp = input.flatten(call.head, move |_| rm_helper(call, args));
|
|
||||||
|
|
||||||
Ok(response
|
|
||||||
.into_iter()
|
|
||||||
.into_pipeline_data(engine_state.ctrlc.clone()))
|
|
||||||
// Ok(Value::Nothing { span })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rm_helper(call: &Call, args: RmArgs) -> Vec<Value> {
|
|
||||||
let (targets, recursive, trash, _permanent, force) = (
|
|
||||||
args.targets,
|
|
||||||
args.recursive,
|
|
||||||
args.trash,
|
|
||||||
args.permanent,
|
|
||||||
args.force,
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(not(feature = "trash-support"))]
|
|
||||||
{
|
|
||||||
if trash {
|
|
||||||
let error = match call.get_flag_expr("trash").ok_or_else(|| {
|
|
||||||
ShellError::SpannedLabeledError(
|
|
||||||
"Flag not found".into(),
|
|
||||||
"trash flag not found".into(),
|
|
||||||
call.head,
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
Ok(expr) => ShellError::FeatureNotEnabled(expr.span),
|
|
||||||
Err(err) => err,
|
|
||||||
};
|
|
||||||
|
|
||||||
return vec![Value::Error { error }];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if targets.is_empty() && !force {
|
|
||||||
return vec![Value::Error {
|
|
||||||
error: ShellError::FileNotFound(call.head),
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
targets
|
|
||||||
.into_iter()
|
|
||||||
.map(move |target| {
|
|
||||||
let (i, f) = (target.0, target.1);
|
|
||||||
|
|
||||||
let is_empty = || match f.read_dir() {
|
let is_empty = || match f.read_dir() {
|
||||||
Ok(mut p) => p.next().is_none(),
|
Ok(mut p) => p.next().is_none(),
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
|
@ -240,9 +215,8 @@ fn rm_helper(call: &Call, args: RmArgs) -> Vec<Value> {
|
||||||
#[cfg(feature = "trash-support")]
|
#[cfg(feature = "trash-support")]
|
||||||
{
|
{
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
result = if trash {
|
result = if trash || (rm_always_trash && !permanent) {
|
||||||
trash::delete(&f).map_err(|e: trash::Error| {
|
trash::delete(&f).map_err(|e: trash::Error| {
|
||||||
use std::io::ErrorKind;
|
|
||||||
Error::new(ErrorKind::Other, format!("{:?}", e))
|
Error::new(ErrorKind::Other, format!("{:?}", e))
|
||||||
})
|
})
|
||||||
} else if metadata.is_file() {
|
} else if metadata.is_file() {
|
||||||
|
@ -261,34 +235,34 @@ fn rm_helper(call: &Call, args: RmArgs) -> Vec<Value> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
|
let msg = format!("Could not delete because: {:}\nTry '--trash' flag", e);
|
||||||
Value::Error {
|
Value::Error {
|
||||||
error: ShellError::RemoveNotPossible(
|
error: ShellError::SpannedLabeledError(msg, e.to_string(), span),
|
||||||
format!("Could not delete because: {:}\nTry '--trash' flag", e),
|
|
||||||
call.head,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Value::String {
|
let val = format!("deleted {:}", f.to_string_lossy());
|
||||||
val: format!("deleted {:}", f.to_string_lossy()),
|
Value::String { val, span }
|
||||||
span: call.positional[i].span,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let msg = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
|
||||||
Value::Error {
|
Value::Error {
|
||||||
error: ShellError::RemoveNotPossible(
|
error: ShellError::SpannedLabeledError(
|
||||||
"Cannot remove. try --recursive".to_string(),
|
msg,
|
||||||
call.positional[i].span,
|
"cannot remove non-empty directory".into(),
|
||||||
|
span,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let msg = format!("no such file or directory: {:}", f.to_string_lossy());
|
||||||
Value::Error {
|
Value::Error {
|
||||||
error: ShellError::RemoveNotPossible(
|
error: ShellError::SpannedLabeledError(
|
||||||
"no such file or directory".to_string(),
|
msg,
|
||||||
call.positional[i].span,
|
"no such file or directory".into(),
|
||||||
|
span,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.into_pipeline_data(ctrlc))
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,7 @@ pub struct Resource {
|
||||||
|
|
||||||
impl Resource {}
|
impl Resource {}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn get_interactive_confirmation(prompt: String) -> Result<bool, Box<dyn Error>> {
|
pub fn get_interactive_confirmation(prompt: String) -> Result<bool, Box<dyn Error>> {
|
||||||
let input = Input::new()
|
let input = Input::new()
|
||||||
.with_prompt(prompt)
|
.with_prompt(prompt)
|
||||||
|
|
|
@ -31,8 +31,6 @@ fn copies_the_file_inside_directory_if_path_to_copy_is_directory() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: jt: needs more work
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_if_attempting_to_copy_a_directory_to_another_directory() {
|
fn error_if_attempting_to_copy_a_directory_to_another_directory() {
|
||||||
Playground::setup("cp_test_3", |dirs, _| {
|
Playground::setup("cp_test_3", |dirs, _| {
|
||||||
|
@ -77,8 +75,6 @@ fn copies_the_directory_inside_directory_if_path_to_copy_is_directory_and_with_r
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: jt: needs more work
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deep_copies_with_recursive_flag() {
|
fn deep_copies_with_recursive_flag() {
|
||||||
Playground::setup("cp_test_5", |dirs, sandbox| {
|
Playground::setup("cp_test_5", |dirs, sandbox| {
|
||||||
|
|
|
@ -121,8 +121,6 @@ fn removes_directory_contents_with_recursive_flag() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: jt: needs more work
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn errors_if_attempting_to_delete_a_directory_with_content_without_recursive_flag() {
|
fn errors_if_attempting_to_delete_a_directory_with_content_without_recursive_flag() {
|
||||||
Playground::setup("rm_test_6", |dirs, sandbox| {
|
Playground::setup("rm_test_6", |dirs, sandbox| {
|
||||||
|
@ -137,8 +135,6 @@ fn errors_if_attempting_to_delete_a_directory_with_content_without_recursive_fla
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: jt: needs more work
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn errors_if_attempting_to_delete_single_dot_as_argument() {
|
fn errors_if_attempting_to_delete_single_dot_as_argument() {
|
||||||
Playground::setup("rm_test_7", |dirs, _| {
|
Playground::setup("rm_test_7", |dirs, _| {
|
||||||
|
@ -151,8 +147,6 @@ fn errors_if_attempting_to_delete_single_dot_as_argument() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: jt: needs more work
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn errors_if_attempting_to_delete_two_dot_as_argument() {
|
fn errors_if_attempting_to_delete_two_dot_as_argument() {
|
||||||
Playground::setup("rm_test_8", |dirs, _| {
|
Playground::setup("rm_test_8", |dirs, _| {
|
||||||
|
@ -283,8 +277,6 @@ fn no_errors_if_attempting_to_delete_non_existent_file_with_f_flag() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: jt: needs more work
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rm_wildcard_keeps_dotfiles() {
|
fn rm_wildcard_keeps_dotfiles() {
|
||||||
Playground::setup("rm_test_15", |dirs, sandbox| {
|
Playground::setup("rm_test_15", |dirs, sandbox| {
|
||||||
|
|
|
@ -67,6 +67,7 @@ pub struct Config {
|
||||||
pub menu_config: HashMap<String, Value>,
|
pub menu_config: HashMap<String, Value>,
|
||||||
pub keybindings: Vec<ParsedKeybinding>,
|
pub keybindings: Vec<ParsedKeybinding>,
|
||||||
pub history_config: HashMap<String, Value>,
|
pub history_config: HashMap<String, Value>,
|
||||||
|
pub rm_always_trash: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
@ -90,6 +91,7 @@ impl Default for Config {
|
||||||
menu_config: HashMap::new(),
|
menu_config: HashMap::new(),
|
||||||
keybindings: Vec::new(),
|
keybindings: Vec::new(),
|
||||||
history_config: HashMap::new(),
|
history_config: HashMap::new(),
|
||||||
|
rm_always_trash: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,6 +196,13 @@ impl Value {
|
||||||
eprintln!("$config.quick_completions is not a bool")
|
eprintln!("$config.quick_completions is not a bool")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"rm_always_trash" => {
|
||||||
|
if let Ok(b) = value.as_bool() {
|
||||||
|
config.rm_always_trash = b;
|
||||||
|
} else {
|
||||||
|
eprintln!("$config.rm_always_trash is not a bool")
|
||||||
|
}
|
||||||
|
}
|
||||||
"filesize_format" => {
|
"filesize_format" => {
|
||||||
if let Ok(v) = value.as_string() {
|
if let Ok(v) = value.as_string() {
|
||||||
config.filesize_format = v.to_lowercase();
|
config.filesize_format = v.to_lowercase();
|
||||||
|
|
Loading…
Reference in a new issue