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:
parent
7555527d07
commit
00bbe24639
3 changed files with 125 additions and 22 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2660,6 +2660,7 @@ version = "0.0.16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
|
"indicatif",
|
||||||
"uucore",
|
"uucore",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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]]
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue