1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 11:07:44 +00:00

mv: add progress bar (#4220)

* mv: add progress bar

Similarly to `cp`, adds `-g` and `--progress` flags to enable a progress
bar via indicatif.
This commit is contained in:
Christian 2022-12-13 11:46:54 +01:00 committed by GitHub
parent 7555527d07
commit 00bbe24639
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 125 additions and 22 deletions

1
Cargo.lock generated
View file

@ -2660,6 +2660,7 @@ version = "0.0.16"
dependencies = [ dependencies = [
"clap", "clap",
"fs_extra", "fs_extra",
"indicatif",
"uucore", "uucore",
] ]

View file

@ -17,6 +17,8 @@ path = "src/mv.rs"
[dependencies] [dependencies]
clap = { version = "4.0", features = ["wrap_help", "cargo"] } clap = { version = "4.0", features = ["wrap_help", "cargo"] }
fs_extra = "1.1.0" fs_extra = "1.1.0"
indicatif = "0.17"
uucore = { version=">=0.0.16", package="uucore", path="../../uucore" } uucore = { version=">=0.0.16", package="uucore", path="../../uucore" }
[[bin]] [[bin]]

View file

@ -12,6 +12,7 @@ mod error;
use clap::builder::ValueParser; use clap::builder::ValueParser;
use clap::{crate_version, error::ErrorKind, Arg, ArgAction, ArgMatches, Command}; use clap::{crate_version, error::ErrorKind, Arg, ArgAction, ArgMatches, Command};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::env; use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::fs; use std::fs;
@ -24,9 +25,12 @@ use std::path::{Path, PathBuf};
use uucore::backup_control::{self, BackupMode}; use uucore::backup_control::{self, BackupMode};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError}; use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError};
use uucore::{format_usage, prompt_yes, show, show_if_err}; use uucore::{format_usage, prompt_yes, show};
use fs_extra::dir::{move_dir, CopyOptions as DirCopyOptions}; use fs_extra::dir::{
get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions,
TransitProcess, TransitProcessResult,
};
use crate::error::MvError; use crate::error::MvError;
@ -39,6 +43,7 @@ pub struct Behavior {
no_target_dir: bool, no_target_dir: bool,
verbose: bool, verbose: bool,
strip_slashes: bool, strip_slashes: bool,
progress_bar: bool,
} }
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
@ -63,7 +68,7 @@ static OPT_TARGET_DIRECTORY: &str = "target-directory";
static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
static OPT_UPDATE: &str = "update"; static OPT_UPDATE: &str = "update";
static OPT_VERBOSE: &str = "verbose"; static OPT_VERBOSE: &str = "verbose";
static OPT_PROGRESS: &str = "progress";
static ARG_FILES: &str = "files"; static ARG_FILES: &str = "files";
#[uucore::main] #[uucore::main]
@ -122,6 +127,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY), no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY),
verbose: matches.get_flag(OPT_VERBOSE), verbose: matches.get_flag(OPT_VERBOSE),
strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES), strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES),
progress_bar: matches.get_flag(OPT_PROGRESS),
}; };
exec(&files[..], &behavior) exec(&files[..], &behavior)
@ -197,6 +203,16 @@ pub fn uu_app() -> Command {
.help("explain what is being done") .help("explain what is being done")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg(
Arg::new(OPT_PROGRESS)
.short('g')
.long(OPT_PROGRESS)
.help(
"Display a progress bar. \n\
Note: this feature is not supported by GNU coreutils.",
)
.action(ArgAction::SetTrue),
)
.arg( .arg(
Arg::new(ARG_FILES) Arg::new(ARG_FILES)
.action(ArgAction::Append) .action(ArgAction::Append)
@ -271,7 +287,7 @@ fn exec(files: &[OsString], b: &Behavior) -> UResult<()> {
if !source.is_dir() { if !source.is_dir() {
Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into())
} else { } else {
rename(source, target, b).map_err_context(|| { rename(source, target, b, None).map_err_context(|| {
format!("cannot move {} to {}", source.quote(), target.quote()) format!("cannot move {} to {}", source.quote(), target.quote())
}) })
} }
@ -294,7 +310,7 @@ fn exec(files: &[OsString], b: &Behavior) -> UResult<()> {
) )
.into()) .into())
} else { } else {
rename(source, target, b).map_err(|e| USimpleError::new(1, format!("{}", e))) rename(source, target, b, None).map_err(|e| USimpleError::new(1, format!("{}", e)))
} }
} }
_ => { _ => {
@ -321,7 +337,27 @@ 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 count_progress = if let Some(ref multi_progress) = multi_progress {
if files.len() > 1 {
Some(multi_progress.add(
ProgressBar::new(files.len().try_into().unwrap()).with_style(
ProgressStyle::with_template("moving {msg} {wide_bar} {pos}/{len}").unwrap(),
),
))
} else {
None
}
} else {
None
};
for sourcepath in files.iter() { for sourcepath in files.iter() {
if let Some(ref pb) = count_progress {
pb.set_message(sourcepath.to_string_lossy().to_string());
}
let targetpath = match sourcepath.file_name() { let targetpath = match sourcepath.file_name() {
Some(name) => target_dir.join(name), Some(name) => target_dir.join(name),
None => { None => {
@ -352,18 +388,35 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR
} }
} }
show_if_err!( let rename_result = rename(sourcepath, &targetpath, b, multi_progress.as_ref())
rename(sourcepath, &targetpath, b).map_err_context(|| format!( .map_err_context(|| {
"cannot move {} to {}", format!(
sourcepath.quote(), "cannot move {} to {}",
targetpath.quote() sourcepath.quote(),
)) targetpath.quote()
); )
});
if let Err(e) = rename_result {
match multi_progress {
Some(ref pb) => pb.suspend(|| show!(e)),
None => show!(e),
};
};
if let Some(ref pb) = count_progress {
pb.inc(1);
}
} }
Ok(()) Ok(())
} }
fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> { fn rename(
from: &Path,
to: &Path,
b: &Behavior,
multi_progress: Option<&MultiProgress>,
) -> io::Result<()> {
let mut backup_path = None; let mut backup_path = None;
if to.exists() { if to.exists() {
@ -385,7 +438,7 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> {
backup_path = backup_control::get_backup_path(b.backup, to, &b.suffix); backup_path = backup_control::get_backup_path(b.backup, to, &b.suffix);
if let Some(ref backup_path) = backup_path { if let Some(ref backup_path) = backup_path {
rename_with_fallback(to, backup_path)?; rename_with_fallback(to, backup_path, multi_progress)?;
} }
if b.update && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? { if b.update && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? {
@ -405,21 +458,36 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> {
} }
} }
rename_with_fallback(from, to)?; rename_with_fallback(from, to, multi_progress)?;
if b.verbose { if b.verbose {
print!("{} -> {}", from.quote(), to.quote()); let message = match backup_path {
match backup_path { Some(path) => format!(
Some(path) => println!(" (backup: {})", path.quote()), "{} -> {} (backup: {})",
None => println!(), from.quote(),
} to.quote(),
path.quote()
),
None => format!("{} -> {}", from.quote(), to.quote()),
};
match multi_progress {
Some(pb) => pb.suspend(|| {
println!("{}", message);
}),
None => println!("{}", message),
};
} }
Ok(()) Ok(())
} }
/// A wrapper around `fs::rename`, so that if it fails, we try falling back on /// A wrapper around `fs::rename`, so that if it fails, we try falling back on
/// copying and removing. /// copying and removing.
fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> { fn rename_with_fallback(
from: &Path,
to: &Path,
multi_progress: Option<&MultiProgress>,
) -> io::Result<()> {
if fs::rename(from, to).is_err() { if fs::rename(from, to).is_err() {
// Get metadata without following symlinks // Get metadata without following symlinks
let metadata = from.symlink_metadata()?; let metadata = from.symlink_metadata()?;
@ -441,7 +509,39 @@ fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> {
copy_inside: true, copy_inside: true,
..DirCopyOptions::new() ..DirCopyOptions::new()
}; };
if let Err(err) = move_dir(from, to, &options) {
// Calculate total size of directory
// Silently degrades:
// If finding the total size fails for whatever reason,
// the progress bar wont be shown for this file / dir.
// (Move will probably fail due to permission error later?)
let total_size = dir_get_size(from).ok();
let progress_bar =
if let (Some(multi_progress), Some(total_size)) = (multi_progress, total_size) {
let bar = ProgressBar::new(total_size).with_style(
ProgressStyle::with_template(
"{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}",
)
.unwrap(),
);
Some(multi_progress.add(bar))
} else {
None
};
let result = if let Some(ref pb) = progress_bar {
move_dir_with_progress(from, to, &options, |process_info: TransitProcess| {
pb.set_position(process_info.copied_bytes);
pb.set_message(process_info.file_name);
TransitProcessResult::ContinueOrAbort
})
} else {
move_dir(from, to, &options)
};
if let Err(err) = result {
return match err.kind { return match err.kind {
fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new( fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new(
io::ErrorKind::PermissionDenied, io::ErrorKind::PermissionDenied,