Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mv: expose main functionality for nushell #5335

Merged
merged 2 commits into from
Oct 7, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
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 @@
#[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 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)]

Check warning on line 44 in src/uu/mv/src/mv.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/mv.rs#L44

Added line #L44 was not covered by tests
pub struct Options {
/// specifies overwrite behavior
/// '-n' '--no-clobber'
/// '-i' '--interactive'
/// '-f' '--force'
pub overwrite: OverwriteMode,

/// `--backup[=CONTROL]`, `-b`
pub backup: BackupMode,
PThorpe92 marked this conversation as resolved.
Show resolved Hide resolved

/// '-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,

Check warning on line 76 in src/uu/mv/src/mv.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/mv.rs#L76

Added line #L76 was not covered by tests
}

#[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 @@
}
}

let behavior = Behavior {
let opts = Options {
overwrite: overwrite_mode,
backup: backup_mode,
suffix: backup_suffix,
Expand All @@ -128,7 +163,7 @@
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 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 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)
{

Check warning on line 288 in src/uu/mv/src/mv.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/mv.rs#L288

Added line #L288 was not covered by tests
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
Expand All @@ -266,7 +303,7 @@
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 @@
}

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 @@
)
.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 @@
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 @@
.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 @@
}
}

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 @@
}
Ok(()) => (),
}

if let Some(ref pb) = count_progress {
pb.inc(1);
}
Expand All @@ -424,29 +463,30 @@
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
{

Check warning on line 473 in src/uu/mv/src/mv.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/mv/src/mv.rs#L473

Added line #L473 was not covered by tests
// `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 @@
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 @@

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