Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 85 additions & 45 deletions src/uu/mv/src/mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

// spell-checker:ignore (ToDO) sourcepath targetpath
// spell-checker:ignore (ToDO) sourcepath targetpath nushell

mod error;

Expand All @@ -19,7 +19,8 @@ use std::os::unix;
#[cfg(windows)]
use std::os::windows;
use std::path::{Path, PathBuf};
use uucore::backup_control::{self, source_is_target_backup, BackupMode};
pub use uucore::backup_control::BackupMode;
use uucore::backup_control::{self, source_is_target_backup};
use uucore::display::Quotable;
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};
Expand All @@ -33,22 +34,56 @@ use fs_extra::dir::{

use crate::error::MvError;

pub struct Behavior {
overwrite: OverwriteMode,
backup: BackupMode,
suffix: String,
update: UpdateMode,
target_dir: Option<OsString>,
no_target_dir: bool,
verbose: bool,
strip_slashes: bool,
progress_bar: bool,
/// 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,

/// `--backup[=CONTROL]`, `-b`
pub backup: BackupMode,

/// '-S' --suffix' backup suffix
pub suffix: String,

/// Available update mode "--update-mode=all|none|older"
pub update: UpdateMode,

/// 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 {
/// '-n' '--no-clobber' do not overwrite
NoClobber,
/// '-i' '--interactive' prompt before overwrite
Interactive,
///'-f' '--force' overwrite without prompt
Force,
}

Expand Down Expand Up @@ -116,7 +151,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
}

let behavior = Behavior {
let opts = Options {
overwrite: overwrite_mode,
backup: backup_mode,
suffix: backup_suffix,
Expand All @@ -128,7 +163,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
progress_bar: matches.get_flag(OPT_PROGRESS),
};

exec(&files[..], &behavior)
mv(&files[..], &opts)
}

pub fn uu_app() -> Command {
Expand Down Expand Up @@ -235,10 +270,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);

if b.strip_slashes {
if opts.strip_slashes {
paths
.map(|p| p.components().as_path().to_owned())
.collect::<Vec<PathBuf>>()
Expand All @@ -247,8 +282,10 @@ fn parse_paths(files: &[OsString], b: &Behavior) -> Vec<PathBuf> {
}
}

fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> {
if b.backup == BackupMode::SimpleBackup && source_is_target_backup(source, target, &b.suffix) {
fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> {
if opts.backup == BackupMode::SimpleBackup
&& source_is_target_backup(source, target, &opts.suffix)
{
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
Expand All @@ -266,7 +303,7 @@ fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> {
if (source.eq(target)
|| are_hardlinks_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() {
return Err(
Expand All @@ -278,19 +315,19 @@ fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> {
}

if target.is_dir() {
if b.no_target_dir {
if opts.no_target_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())
})
} else {
Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into())
}
} 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() {
match b.overwrite {
match opts.overwrite {
OverwriteMode::NoClobber => return Ok(()),
OverwriteMode::Interactive => {
if !prompt_yes!("overwrite {}? ", target.quote()) {
Expand All @@ -305,12 +342,12 @@ fn handle_two_paths(source: &Path, target: &Path, b: &Behavior) -> UResult<()> {
)
.into())
} 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<()> {
if b.no_target_dir {
fn handle_multiple_paths(paths: &[PathBuf], opts: &Options) -> UResult<()> {
if opts.no_target_dir {
return Err(UUsageError::new(
1,
format!("mv: extra operand {}", paths[2].quote()),
Expand All @@ -319,24 +356,27 @@ fn handle_multiple_paths(paths: &[PathBuf], b: &Behavior) -> UResult<()> {
let target_dir = paths.last().unwrap();
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<()> {
let paths = parse_paths(files, b);
/// Execute the mv command. This moves 'source' to 'target', where
/// 'target' is a directory. If 'target' does not exist, and source is a single
/// file or directory, then 'source' will be renamed to 'target'.
pub fn mv(files: &[OsString], opts: &Options) -> UResult<()> {
let paths = parse_paths(files, opts);

if let Some(ref name) = b.target_dir {
return move_files_into_dir(&paths, &PathBuf::from(name), b);
if let Some(ref name) = opts.target_dir {
return move_files_into_dir(&paths, &PathBuf::from(name), opts);
}

match paths.len() {
2 => handle_two_paths(&paths[0], &paths[1], b),
_ => handle_multiple_paths(&paths, b),
2 => handle_two_paths(&paths[0], &paths[1], opts),
_ => handle_multiple_paths(&paths, opts),
}
}

#[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() {
return Err(MvError::NotADirectory(target_dir.quote().to_string()).into());
}
Expand All @@ -345,7 +385,7 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
.canonicalize()
.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 {
if files.len() > 1 {
Expand Down Expand Up @@ -396,7 +436,7 @@ 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) => {
let e = e.map_err_context(|| {
Expand All @@ -413,7 +453,6 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
}
Ok(()) => (),
}

if let Some(ref pb) = count_progress {
pb.inc(1);
}
Expand All @@ -424,29 +463,30 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
fn rename(
from: &Path,
to: &Path,
b: &Behavior,
opts: &Options,
multi_progress: Option<&MultiProgress>,
) -> io::Result<()> {
let mut backup_path = None;

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
// and exit with 0
return Ok(());
}

if b.update == UpdateMode::ReplaceNone {
if opts.update == UpdateMode::ReplaceNone {
return Ok(());
}

if (b.update == UpdateMode::ReplaceIfOlder)
if (opts.update == UpdateMode::ReplaceIfOlder)
&& fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()?
{
return Ok(());
}

match b.overwrite {
match opts.overwrite {
OverwriteMode::NoClobber => {
let err_msg = format!("not replacing {}", to.quote());
return Err(io::Error::new(io::ErrorKind::Other, err_msg));
Expand All @@ -459,7 +499,7 @@ fn rename(
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 {
rename_with_fallback(to, backup_path, multi_progress)?;
}
Expand All @@ -479,7 +519,7 @@ fn rename(

rename_with_fallback(from, to, multi_progress)?;

if b.verbose {
if opts.verbose {
let message = match backup_path {
Some(path) => format!(
"renamed {} -> {} (backup: {})",
Expand Down