1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-31 21:17:46 +00:00

feat(mv): expose functionality, document for nushell

This commit is contained in:
PThorpe92 2023-09-25 18:24:20 -04:00
parent c595948dbc
commit 6091bafe08
No known key found for this signature in database
GPG key ID: 66DB3FBACBDD05CC

View file

@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
// spell-checker:ignore (ToDO) sourcepath targetpath // spell-checker:ignore (ToDO) sourcepath targetpath nushell
mod error; mod error;
@ -23,6 +23,7 @@ use uucore::backup_control::{self, source_is_target_backup, BackupMode};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError, UUsageError}; use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError, UUsageError};
use uucore::fs::{are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file}; use uucore::fs::{are_hardlinks_or_one_way_symlink_to_same_file, are_hardlinks_to_same_file};
use uucore::libc::ENOTEMPTY;
use uucore::update_control::{self, UpdateMode}; use uucore::update_control::{self, UpdateMode};
use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show}; use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show};
@ -32,23 +33,56 @@ use fs_extra::dir::{
}; };
use crate::error::MvError; use crate::error::MvError;
/// Options contains all the possible behaviors and flags for mv.
///
/// All options are public so that the options can be programmatically
/// constructed by other crates, such as nushell. That means that this struct
/// is part of our public API. It should therefore not be changed without good
/// reason.
/// The fields are documented with the arguments that determine their value.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Options {
/// specifies overwrite behavior
/// '-n' '--no-clobber'
/// '-i' '--interactive'
/// '-f' '--force'
pub overwrite: OverwriteMode,
pub struct Behavior { /// `--backup[=CONTROL]`, `-b`
overwrite: OverwriteMode, pub backup: BackupMode,
backup: BackupMode,
suffix: String, /// '-S' --suffix' backup suffix
update: UpdateMode, pub suffix: String,
target_dir: Option<OsString>,
no_target_dir: bool, /// Available update mode "--update-mode=all|none|older"
verbose: bool, pub update: UpdateMode,
strip_slashes: bool,
progress_bar: bool, /// Specifies target directory
/// '-t, --target-directory=DIRECTORY'
pub target_dir: Option<OsString>,
/// Treat destination as a normal file
/// '-T, --no-target-directory
pub no_target_dir: bool,
/// '-v, --verbose'
pub verbose: bool,
/// '--strip-trailing-slashes'
pub strip_slashes: bool,
/// '-g, --progress'
pub progress_bar: bool,
} }
#[derive(Clone, Eq, PartialEq)] /// specifies behavior of the overwrite flag
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OverwriteMode { pub enum OverwriteMode {
/// '-n' '--no-clobber' do not overwrite
NoClobber, NoClobber,
/// '-i' '--interactive' prompt before overwrite
Interactive, Interactive,
///'-f' '--force' overwrite without prompt
Force, Force,
} }
@ -116,7 +150,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
} }
} }
let behavior = Behavior { let opts = Options {
overwrite: overwrite_mode, overwrite: overwrite_mode,
backup: backup_mode, backup: backup_mode,
suffix: backup_suffix, suffix: backup_suffix,
@ -128,7 +162,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
progress_bar: matches.get_flag(OPT_PROGRESS), progress_bar: matches.get_flag(OPT_PROGRESS),
}; };
exec(&files[..], &behavior) exec_mv(&files[..], &opts)
} }
pub fn uu_app() -> Command { pub fn uu_app() -> Command {
@ -220,7 +254,7 @@ pub fn uu_app() -> Command {
) )
} }
fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode { pub fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode {
// This does not exactly match the GNU implementation: // This does not exactly match the GNU implementation:
// The GNU mv defaults to Force, but if more than one of the // The GNU mv defaults to Force, but if more than one of the
// overwrite options are supplied, only the last takes effect. // overwrite options are supplied, only the last takes effect.
@ -235,10 +269,10 @@ fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode {
} }
} }
fn parse_paths(files: &[OsString], b: &Behavior) -> Vec<PathBuf> { fn parse_paths(files: &[OsString], opts: &Options) -> Vec<PathBuf> {
let paths = files.iter().map(Path::new); let paths = files.iter().map(Path::new);
if b.strip_slashes { if opts.strip_slashes {
paths paths
.map(|p| p.components().as_path().to_owned()) .map(|p| p.components().as_path().to_owned())
.collect::<Vec<PathBuf>>() .collect::<Vec<PathBuf>>()
@ -247,8 +281,10 @@ fn parse_paths(files: &[OsString], b: &Behavior) -> Vec<PathBuf> {
} }
} }
fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> { fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> {
if b.backup == BackupMode::SimpleBackup && source_is_target_backup(source, target, &b.suffix) { if opts.backup == BackupMode::SimpleBackup
&& source_is_target_backup(source, target, &opts.suffix)
{
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::NotFound, io::ErrorKind::NotFound,
format!( format!(
@ -266,7 +302,7 @@ fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> {
if (source.eq(target) if (source.eq(target)
|| are_hardlinks_to_same_file(source, target) || are_hardlinks_to_same_file(source, target)
|| are_hardlinks_or_one_way_symlink_to_same_file(source, target)) || are_hardlinks_or_one_way_symlink_to_same_file(source, target))
&& b.backup == BackupMode::NoBackup && opts.backup == BackupMode::NoBackup
{ {
if source.eq(Path::new(".")) || source.ends_with("/.") || source.is_file() { if source.eq(Path::new(".")) || source.ends_with("/.") || source.is_file() {
return Err( return Err(
@ -278,19 +314,19 @@ fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> {
} }
if target.is_dir() { if target.is_dir() {
if b.no_target_dir { if opts.no_target_dir {
if source.is_dir() { if source.is_dir() {
rename(source, target, b, None).map_err_context(|| { rename(source, target, opts, None).map_err_context(|| {
format!("cannot move {} to {}", source.quote(), target.quote()) format!("cannot move {} to {}", source.quote(), target.quote())
}) })
} else { } else {
Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into())
} }
} else { } else {
move_files_into_dir(&[source.to_path_buf()], target, b) move_files_into_dir(&[source.to_path_buf()], target, opts)
} }
} else if target.exists() && source.is_dir() { } else if target.exists() && source.is_dir() {
match b.overwrite { match opts.overwrite {
OverwriteMode::NoClobber => return Ok(()), OverwriteMode::NoClobber => return Ok(()),
OverwriteMode::Interactive => { OverwriteMode::Interactive => {
if !prompt_yes!("overwrite {}? ", target.quote()) { if !prompt_yes!("overwrite {}? ", target.quote()) {
@ -305,12 +341,12 @@ fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> {
) )
.into()) .into())
} else { } else {
rename(source, target, b, None).map_err(|e| USimpleError::new(1, format!("{e}"))) rename(source, target, opts, None).map_err(|e| USimpleError::new(1, format!("{e}")))
} }
} }
fn handle_multiple_paths(paths: &[PathBuf], b: &Behavior) -> UResult<()> { fn handle_multiple_paths(paths: &[PathBuf], opts: &Options) -> UResult<()> {
if b.no_target_dir { if opts.no_target_dir {
return Err(UUsageError::new( return Err(UUsageError::new(
1, 1,
format!("mv: extra operand {}", paths[2].quote()), format!("mv: extra operand {}", paths[2].quote()),
@ -319,24 +355,29 @@ fn handle_multiple_paths(paths: &[PathBuf], b: &Behavior) -> UResult<()> {
let target_dir = paths.last().unwrap(); let target_dir = paths.last().unwrap();
let sources = &paths[..paths.len() - 1]; let sources = &paths[..paths.len() - 1];
move_files_into_dir(sources, target_dir, b) move_files_into_dir(sources, target_dir, opts)
} }
fn exec(files: &[OsString], b: &Behavior) -> UResult<()> { /// Execute mv command, moving 'source' to 'target', where
let paths = parse_paths(files, b); /// 'target' is a directory. If 'target' does not exist, and source is a single
/// file or directory, then 'source' will be renamed to 'target'.
///
/// returns MvError | UError
pub fn exec_mv(files: &[OsString], opts: &Options) -> UResult<()> {
let paths = parse_paths(files, opts);
if let Some(ref name) = b.target_dir { if let Some(ref name) = opts.target_dir {
return move_files_into_dir(&paths, &PathBuf::from(name), b); return move_files_into_dir(&paths, &PathBuf::from(name), opts);
} }
match paths.len() { match paths.len() {
2 => handle_two_paths(&paths[0], &paths[1], b), 2 => handle_two_paths(&paths[0], &paths[1], opts),
_ => handle_multiple_paths(&paths, b), _ => handle_multiple_paths(&paths, opts),
} }
} }
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UResult<()> { fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, opts: &Options) -> UResult<()> {
if !target_dir.is_dir() { if !target_dir.is_dir() {
return Err(MvError::NotADirectory(target_dir.quote().to_string()).into()); return Err(MvError::NotADirectory(target_dir.quote().to_string()).into());
} }
@ -345,7 +386,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
.canonicalize() .canonicalize()
.unwrap_or_else(|_| target_dir.to_path_buf()); .unwrap_or_else(|_| target_dir.to_path_buf());
let multi_progress = b.progress_bar.then(MultiProgress::new); let multi_progress = opts.progress_bar.then(MultiProgress::new);
let count_progress = if let Some(ref multi_progress) = multi_progress { let count_progress = if let Some(ref multi_progress) = multi_progress {
if files.len() > 1 { if files.len() > 1 {
@ -396,24 +437,37 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
} }
} }
match rename(sourcepath, &targetpath, b, multi_progress.as_ref()) { match rename(sourcepath, &targetpath, opts, multi_progress.as_ref()) {
Err(e) if e.to_string().is_empty() => set_exit_code(1), Err(e) if e.to_string().is_empty() => set_exit_code(1),
Err(e) => { Err(e) => {
let e = e.map_err_context(|| { match e.raw_os_error() {
format!( Some(ENOTEMPTY) => {
"cannot move {} to {}", // The error message was changed to match GNU's decision
sourcepath.quote(), // when an issue was filed. These will match when merged upstream.
targetpath.quote() let e = e
) .map_err_context(|| format!("cannot overwrite {}", targetpath.quote()));
}); match multi_progress {
match multi_progress { Some(ref pb) => pb.suspend(|| show!(e)),
Some(ref pb) => pb.suspend(|| show!(e)), None => show!(e),
None => show!(e), };
}; }
_ => {
let e = e.map_err_context(|| {
format!(
"cannot move {} to {}",
sourcepath.quote(),
targetpath.quote()
)
});
match multi_progress {
Some(ref pb) => pb.suspend(|| show!(e)),
None => show!(e),
};
}
}
} }
Ok(()) => (), Ok(()) => (),
} }
if let Some(ref pb) = count_progress { if let Some(ref pb) = count_progress {
pb.inc(1); pb.inc(1);
} }
@ -424,29 +478,30 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
fn rename( fn rename(
from: &Path, from: &Path,
to: &Path, to: &Path,
b: &Behavior, opts: &Options,
multi_progress: Option<&MultiProgress>, multi_progress: Option<&MultiProgress>,
) -> io::Result<()> { ) -> io::Result<()> {
let mut backup_path = None; let mut backup_path = None;
if to.exists() { if to.exists() {
if b.update == UpdateMode::ReplaceIfOlder && b.overwrite == OverwriteMode::Interactive { if opts.update == UpdateMode::ReplaceIfOlder && opts.overwrite == OverwriteMode::Interactive
{
// `mv -i --update old new` when `new` exists doesn't move anything // `mv -i --update old new` when `new` exists doesn't move anything
// and exit with 0 // and exit with 0
return Ok(()); return Ok(());
} }
if b.update == UpdateMode::ReplaceNone { if opts.update == UpdateMode::ReplaceNone {
return Ok(()); return Ok(());
} }
if (b.update == UpdateMode::ReplaceIfOlder) if (opts.update == UpdateMode::ReplaceIfOlder)
&& fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()?
{ {
return Ok(()); return Ok(());
} }
match b.overwrite { match opts.overwrite {
OverwriteMode::NoClobber => { OverwriteMode::NoClobber => {
let err_msg = format!("not replacing {}", to.quote()); let err_msg = format!("not replacing {}", to.quote());
return Err(io::Error::new(io::ErrorKind::Other, err_msg)); return Err(io::Error::new(io::ErrorKind::Other, err_msg));
@ -459,7 +514,7 @@ fn rename(
OverwriteMode::Force => {} OverwriteMode::Force => {}
}; };
backup_path = backup_control::get_backup_path(b.backup, to, &b.suffix); backup_path = backup_control::get_backup_path(opts.backup, to, &opts.suffix);
if let Some(ref backup_path) = backup_path { if let Some(ref backup_path) = backup_path {
rename_with_fallback(to, backup_path, multi_progress)?; rename_with_fallback(to, backup_path, multi_progress)?;
} }
@ -472,14 +527,14 @@ fn rename(
if is_empty_dir(to) { if is_empty_dir(to) {
fs::remove_dir(to)?; fs::remove_dir(to)?;
} else { } else {
return Err(io::Error::new(io::ErrorKind::Other, "Directory not empty")); return Err(io::Error::from_raw_os_error(ENOTEMPTY));
} }
} }
} }
rename_with_fallback(from, to, multi_progress)?; rename_with_fallback(from, to, multi_progress)?;
if b.verbose { if opts.verbose {
let message = match backup_path { let message = match backup_path {
Some(path) => format!( Some(path) => format!(
"renamed {} -> {} (backup: {})", "renamed {} -> {} (backup: {})",