mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
cp: add progress bar
Adds the `-g` and `--progress` flags to enable a progress bar via indicatif.
This commit is contained in:
parent
1172a7e781
commit
21e691c3b9
5 changed files with 149 additions and 11 deletions
|
@ -23,6 +23,7 @@ fsext
|
||||||
getopts
|
getopts
|
||||||
getrandom
|
getrandom
|
||||||
globset
|
globset
|
||||||
|
indicatif
|
||||||
itertools
|
itertools
|
||||||
lscolors
|
lscolors
|
||||||
mdbook
|
mdbook
|
||||||
|
|
48
Cargo.lock
generated
48
Cargo.lock
generated
|
@ -281,7 +281,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"strsim",
|
"strsim",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"terminal_size",
|
"terminal_size 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -327,6 +327,20 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3"
|
checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"terminal_size 0.1.17",
|
||||||
|
"unicode-width",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "constant_time_eq"
|
name = "constant_time_eq"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -791,6 +805,12 @@ version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
@ -1047,6 +1067,17 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indicatif"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fcc42b206e70d86ec03285b123e65a5458c92027d1fb2ae3555878b8113b3ddf"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"number_prefix",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inotify"
|
name = "inotify"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
@ -2033,6 +2064,16 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal_size"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "terminal_size"
|
name = "terminal_size"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -2050,7 +2091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
|
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"smawk",
|
"smawk",
|
||||||
"terminal_size",
|
"terminal_size 0.2.2",
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
@ -2293,6 +2334,7 @@ dependencies = [
|
||||||
"clap 4.0.22",
|
"clap 4.0.22",
|
||||||
"exacl",
|
"exacl",
|
||||||
"filetime",
|
"filetime",
|
||||||
|
"indicatif",
|
||||||
"libc",
|
"libc",
|
||||||
"quick-error",
|
"quick-error",
|
||||||
"selinux",
|
"selinux",
|
||||||
|
@ -2598,7 +2640,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"selinux",
|
"selinux",
|
||||||
"term_grid",
|
"term_grid",
|
||||||
"terminal_size",
|
"terminal_size 0.2.2",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"uucore",
|
"uucore",
|
||||||
]
|
]
|
||||||
|
|
|
@ -26,6 +26,7 @@ quick-error = "2.0.1"
|
||||||
selinux = { version="0.3", optional=true }
|
selinux = { version="0.3", optional=true }
|
||||||
uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["entries", "fs", "perms", "mode"] }
|
uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["entries", "fs", "perms", "mode"] }
|
||||||
walkdir = "2.2"
|
walkdir = "2.2"
|
||||||
|
indicatif = "0.17"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
xattr="0.2.3"
|
xattr="0.2.3"
|
||||||
|
|
|
@ -14,6 +14,7 @@ use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf, StripPrefixError};
|
use std::path::{Path, PathBuf, StripPrefixError};
|
||||||
|
|
||||||
|
use indicatif::ProgressBar;
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::UIoError;
|
use uucore::error::UIoError;
|
||||||
use uucore::fs::{canonicalize, FileInformation, MissingHandling, ResolveMode};
|
use uucore::fs::{canonicalize, FileInformation, MissingHandling, ResolveMode};
|
||||||
|
@ -170,6 +171,7 @@ impl Entry {
|
||||||
|
|
||||||
/// Copy a single entry during a directory traversal.
|
/// Copy a single entry during a directory traversal.
|
||||||
fn copy_direntry(
|
fn copy_direntry(
|
||||||
|
progress_bar: &Option<ProgressBar>,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
options: &Options,
|
options: &Options,
|
||||||
symlinked_files: &mut HashSet<FileInformation>,
|
symlinked_files: &mut HashSet<FileInformation>,
|
||||||
|
@ -213,6 +215,7 @@ fn copy_direntry(
|
||||||
preserve_hardlinks(hard_links, &source_absolute, &dest, &mut found_hard_link)?;
|
preserve_hardlinks(hard_links, &source_absolute, &dest, &mut found_hard_link)?;
|
||||||
if !found_hard_link {
|
if !found_hard_link {
|
||||||
match copy_file(
|
match copy_file(
|
||||||
|
progress_bar,
|
||||||
&source_absolute,
|
&source_absolute,
|
||||||
local_to_target.as_path(),
|
local_to_target.as_path(),
|
||||||
options,
|
options,
|
||||||
|
@ -240,6 +243,7 @@ fn copy_direntry(
|
||||||
// TODO What other kinds of errors, if any, should
|
// TODO What other kinds of errors, if any, should
|
||||||
// cause us to continue walking the directory?
|
// cause us to continue walking the directory?
|
||||||
match copy_file(
|
match copy_file(
|
||||||
|
progress_bar,
|
||||||
&source_absolute,
|
&source_absolute,
|
||||||
local_to_target.as_path(),
|
local_to_target.as_path(),
|
||||||
options,
|
options,
|
||||||
|
@ -272,6 +276,7 @@ fn copy_direntry(
|
||||||
/// Any errors encountered copying files in the tree will be logged but
|
/// Any errors encountered copying files in the tree will be logged but
|
||||||
/// will not cause a short-circuit.
|
/// will not cause a short-circuit.
|
||||||
pub(crate) fn copy_directory(
|
pub(crate) fn copy_directory(
|
||||||
|
progress_bar: &Option<ProgressBar>,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
target: &TargetSlice,
|
target: &TargetSlice,
|
||||||
options: &Options,
|
options: &Options,
|
||||||
|
@ -285,6 +290,7 @@ pub(crate) fn copy_directory(
|
||||||
// if no-dereference is enabled and this is a symlink, copy it as a file
|
// if no-dereference is enabled and this is a symlink, copy it as a file
|
||||||
if !options.dereference(source_in_command_line) && root.is_symlink() {
|
if !options.dereference(source_in_command_line) && root.is_symlink() {
|
||||||
return copy_file(
|
return copy_file(
|
||||||
|
progress_bar,
|
||||||
root,
|
root,
|
||||||
target,
|
target,
|
||||||
options,
|
options,
|
||||||
|
@ -344,6 +350,7 @@ pub(crate) fn copy_directory(
|
||||||
Ok(direntry) => {
|
Ok(direntry) => {
|
||||||
let entry = Entry::new(&context, &direntry)?;
|
let entry = Entry::new(&context, &direntry)?;
|
||||||
copy_direntry(
|
copy_direntry(
|
||||||
|
progress_bar,
|
||||||
entry,
|
entry,
|
||||||
options,
|
options,
|
||||||
symlinked_files,
|
symlinked_files,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
// For the full copyright and license information, please view the LICENSE file
|
// For the full copyright and license information, please view the LICENSE file
|
||||||
// that was distributed with this source code.
|
// that was distributed with this source code.
|
||||||
|
|
||||||
// spell-checker:ignore (ToDO) copydir ficlone ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked fiemap
|
// spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate quick_error;
|
extern crate quick_error;
|
||||||
|
@ -33,6 +33,7 @@ use std::string::ToString;
|
||||||
|
|
||||||
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
|
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use libc::mkfifo;
|
use libc::mkfifo;
|
||||||
use quick_error::ResultExt;
|
use quick_error::ResultExt;
|
||||||
|
@ -214,6 +215,7 @@ pub struct Options {
|
||||||
target_dir: Option<String>,
|
target_dir: Option<String>,
|
||||||
update: bool,
|
update: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
progress_bar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.";
|
static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.";
|
||||||
|
@ -244,6 +246,7 @@ mod options {
|
||||||
pub const PARENT: &str = "parent";
|
pub const PARENT: &str = "parent";
|
||||||
pub const PARENTS: &str = "parents";
|
pub const PARENTS: &str = "parents";
|
||||||
pub const PATHS: &str = "paths";
|
pub const PATHS: &str = "paths";
|
||||||
|
pub const PROGRESS_BAR: &str = "progress";
|
||||||
pub const PRESERVE: &str = "preserve";
|
pub const PRESERVE: &str = "preserve";
|
||||||
pub const PRESERVE_DEFAULT_ATTRIBUTES: &str = "preserve-default-attributes";
|
pub const PRESERVE_DEFAULT_ATTRIBUTES: &str = "preserve-default-attributes";
|
||||||
pub const RECURSIVE: &str = "recursive";
|
pub const RECURSIVE: &str = "recursive";
|
||||||
|
@ -538,6 +541,18 @@ pub fn uu_app() -> Command {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// END TODO
|
// END TODO
|
||||||
|
.arg(
|
||||||
|
// The 'g' short flag is modeled after advcpmv
|
||||||
|
// See this repo: https://github.com/jarun/advcpmv
|
||||||
|
Arg::new(options::PROGRESS_BAR)
|
||||||
|
.long(options::PROGRESS_BAR)
|
||||||
|
.short('g')
|
||||||
|
.action(clap::ArgAction::SetTrue)
|
||||||
|
.help(
|
||||||
|
"Display a progress bar. \n\
|
||||||
|
Note: this feature is not supported by GNU coreutils.",
|
||||||
|
),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(options::PATHS)
|
Arg::new(options::PATHS)
|
||||||
.action(ArgAction::Append)
|
.action(ArgAction::Append)
|
||||||
|
@ -817,6 +832,7 @@ impl Options {
|
||||||
preserve_attributes,
|
preserve_attributes,
|
||||||
recursive,
|
recursive,
|
||||||
target_dir,
|
target_dir,
|
||||||
|
progress_bar: matches.get_flag(options::PROGRESS_BAR),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(options)
|
Ok(options)
|
||||||
|
@ -935,7 +951,7 @@ fn preserve_hardlinks(
|
||||||
/// `Err(Error::NotAllFilesCopied)` if at least one non-fatal error was
|
/// `Err(Error::NotAllFilesCopied)` if at least one non-fatal error was
|
||||||
/// encountered.
|
/// encountered.
|
||||||
///
|
///
|
||||||
/// Behavior depends on `options`, see [`Options`] for details.
|
/// Behavior depends on path`options`, see [`Options`] for details.
|
||||||
///
|
///
|
||||||
/// [`Options`]: ./struct.Options.html
|
/// [`Options`]: ./struct.Options.html
|
||||||
fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResult<()> {
|
fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResult<()> {
|
||||||
|
@ -949,7 +965,23 @@ fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResu
|
||||||
let mut non_fatal_errors = false;
|
let mut non_fatal_errors = false;
|
||||||
let mut seen_sources = HashSet::with_capacity(sources.len());
|
let mut seen_sources = HashSet::with_capacity(sources.len());
|
||||||
let mut symlinked_files = HashSet::new();
|
let mut symlinked_files = HashSet::new();
|
||||||
for source in sources {
|
|
||||||
|
let progress_bar = if options.progress_bar {
|
||||||
|
Some(
|
||||||
|
ProgressBar::new(disk_usage(sources, options.recursive)?)
|
||||||
|
.with_style(
|
||||||
|
ProgressStyle::with_template(
|
||||||
|
"{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.with_message(uucore::util_name()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
for source in sources.iter() {
|
||||||
if seen_sources.contains(source) {
|
if seen_sources.contains(source) {
|
||||||
// FIXME: compare sources by the actual file they point to, not their path. (e.g. dir/file == dir/../dir/file in most cases)
|
// FIXME: compare sources by the actual file they point to, not their path. (e.g. dir/file == dir/../dir/file in most cases)
|
||||||
show_warning!("source {} specified more than once", source.quote());
|
show_warning!("source {} specified more than once", source.quote());
|
||||||
|
@ -960,9 +992,14 @@ fn copy(sources: &[Source], target: &TargetSlice, options: &Options) -> CopyResu
|
||||||
preserve_hardlinks(&mut hard_links, source, &dest, &mut found_hard_link)?;
|
preserve_hardlinks(&mut hard_links, source, &dest, &mut found_hard_link)?;
|
||||||
}
|
}
|
||||||
if !found_hard_link {
|
if !found_hard_link {
|
||||||
if let Err(error) =
|
if let Err(error) = copy_source(
|
||||||
copy_source(source, target, &target_type, options, &mut symlinked_files)
|
&progress_bar,
|
||||||
{
|
source,
|
||||||
|
target,
|
||||||
|
&target_type,
|
||||||
|
options,
|
||||||
|
&mut symlinked_files,
|
||||||
|
) {
|
||||||
match error {
|
match error {
|
||||||
// When using --no-clobber, we don't want to show
|
// When using --no-clobber, we don't want to show
|
||||||
// an error message
|
// an error message
|
||||||
|
@ -1017,6 +1054,7 @@ fn construct_dest_path(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_source(
|
fn copy_source(
|
||||||
|
progress_bar: &Option<ProgressBar>,
|
||||||
source: &SourceSlice,
|
source: &SourceSlice,
|
||||||
target: &TargetSlice,
|
target: &TargetSlice,
|
||||||
target_type: &TargetType,
|
target_type: &TargetType,
|
||||||
|
@ -1026,11 +1064,18 @@ fn copy_source(
|
||||||
let source_path = Path::new(&source);
|
let source_path = Path::new(&source);
|
||||||
if source_path.is_dir() {
|
if source_path.is_dir() {
|
||||||
// Copy as directory
|
// Copy as directory
|
||||||
copy_directory(source, target, options, symlinked_files, true)
|
copy_directory(progress_bar, source, target, options, symlinked_files, true)
|
||||||
} else {
|
} else {
|
||||||
// Copy as file
|
// Copy as file
|
||||||
let dest = construct_dest_path(source_path, target, target_type, options)?;
|
let dest = construct_dest_path(source_path, target, target_type, options)?;
|
||||||
copy_file(source_path, dest.as_path(), options, symlinked_files, true)
|
copy_file(
|
||||||
|
progress_bar,
|
||||||
|
source_path,
|
||||||
|
dest.as_path(),
|
||||||
|
options,
|
||||||
|
symlinked_files,
|
||||||
|
true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1277,6 +1322,7 @@ fn file_or_link_exists(path: &Path) -> bool {
|
||||||
/// The original permissions of `source` will be copied to `dest`
|
/// The original permissions of `source` will be copied to `dest`
|
||||||
/// after a successful copy.
|
/// after a successful copy.
|
||||||
fn copy_file(
|
fn copy_file(
|
||||||
|
progress_bar: &Option<ProgressBar>,
|
||||||
source: &Path,
|
source: &Path,
|
||||||
dest: &Path,
|
dest: &Path,
|
||||||
options: &Options,
|
options: &Options,
|
||||||
|
@ -1452,6 +1498,11 @@ fn copy_file(
|
||||||
fs::set_permissions(dest, dest_permissions).ok();
|
fs::set_permissions(dest, dest_permissions).ok();
|
||||||
}
|
}
|
||||||
copy_attributes(source, dest, &options.preserve_attributes)?;
|
copy_attributes(source, dest, &options.preserve_attributes)?;
|
||||||
|
|
||||||
|
if let Some(progress_bar) = progress_bar {
|
||||||
|
progress_bar.inc(fs::metadata(&source)?.len());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1571,6 +1622,42 @@ pub fn localize_to_target(root: &Path, source: &Path, target: &Path) -> CopyResu
|
||||||
Ok(target.join(local_to_root))
|
Ok(target.join(local_to_root))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the total size of a slice of files and directories.
|
||||||
|
///
|
||||||
|
/// This function is much like the `du` utility, by recursively getting the sizes of files in directories.
|
||||||
|
/// Files are not deduplicated when appearing in multiple sources. If `recursive` is set to `false`, the
|
||||||
|
/// directories in `paths` will be ignored.
|
||||||
|
fn disk_usage(paths: &[PathBuf], recursive: bool) -> io::Result<u64> {
|
||||||
|
let mut total = 0;
|
||||||
|
for p in paths {
|
||||||
|
let md = fs::metadata(p)?;
|
||||||
|
if md.file_type().is_dir() {
|
||||||
|
if recursive {
|
||||||
|
total += disk_usage_directory(p)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
total += md.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A helper for `disk_usage` specialized for directories.
|
||||||
|
fn disk_usage_directory(p: &Path) -> io::Result<u64> {
|
||||||
|
let mut total = 0;
|
||||||
|
|
||||||
|
for entry in fs::read_dir(p)? {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type()?.is_dir() {
|
||||||
|
total += disk_usage_directory(&entry.path())?;
|
||||||
|
} else {
|
||||||
|
total += entry.metadata()?.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cp_localize_to_target() {
|
fn test_cp_localize_to_target() {
|
||||||
assert!(
|
assert!(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue