mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2026-01-19 11:41:10 +00:00
Merge pull request #4723 from sylvestre/deref
du: add support of --dereference-args & minor changes
This commit is contained in:
commit
63f608d249
2 changed files with 146 additions and 58 deletions
|
|
@ -33,7 +33,7 @@ use std::time::{Duration, UNIX_EPOCH};
|
|||
use std::{error::Error, fmt::Display};
|
||||
use uucore::display::{print_verbatim, Quotable};
|
||||
use uucore::error::FromIo;
|
||||
use uucore::error::{UError, UResult};
|
||||
use uucore::error::{set_exit_code, UError, UResult};
|
||||
use uucore::parse_glob;
|
||||
use uucore::parse_size::{parse_size, ParseSizeError};
|
||||
use uucore::{
|
||||
|
|
@ -68,6 +68,7 @@ mod options {
|
|||
pub const TIME_STYLE: &str = "time-style";
|
||||
pub const ONE_FILE_SYSTEM: &str = "one-file-system";
|
||||
pub const DEREFERENCE: &str = "dereference";
|
||||
pub const DEREFERENCE_ARGS: &str = "dereference-args";
|
||||
pub const INODES: &str = "inodes";
|
||||
pub const EXCLUDE: &str = "exclude";
|
||||
pub const EXCLUDE_FROM: &str = "exclude-from";
|
||||
|
|
@ -88,11 +89,18 @@ struct Options {
|
|||
total: bool,
|
||||
separate_dirs: bool,
|
||||
one_file_system: bool,
|
||||
dereference: bool,
|
||||
dereference: Deref,
|
||||
inodes: bool,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Deref {
|
||||
All,
|
||||
Args(Vec<PathBuf>),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
|
||||
struct FileInfo {
|
||||
file_id: u128,
|
||||
|
|
@ -112,13 +120,22 @@ struct Stat {
|
|||
}
|
||||
|
||||
impl Stat {
|
||||
fn new(path: PathBuf, options: &Options) -> Result<Self> {
|
||||
let metadata = if options.dereference {
|
||||
fs::metadata(&path)?
|
||||
} else {
|
||||
fs::symlink_metadata(&path)?
|
||||
fn new(path: &Path, options: &Options) -> Result<Self> {
|
||||
// Determine whether to dereference (follow) the symbolic link
|
||||
let should_dereference = match &options.dereference {
|
||||
Deref::All => true,
|
||||
Deref::Args(paths) => paths.contains(&path.to_path_buf()),
|
||||
Deref::None => false,
|
||||
};
|
||||
|
||||
let metadata = if should_dereference {
|
||||
// Get metadata, following symbolic links if necessary
|
||||
fs::metadata(path)
|
||||
} else {
|
||||
// Get metadata without following symbolic links
|
||||
fs::symlink_metadata(path)
|
||||
}?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let file_info = FileInfo {
|
||||
file_id: metadata.ino() as u128,
|
||||
|
|
@ -126,7 +143,7 @@ impl Stat {
|
|||
};
|
||||
#[cfg(not(windows))]
|
||||
return Ok(Self {
|
||||
path,
|
||||
path: path.to_path_buf(),
|
||||
is_dir: metadata.is_dir(),
|
||||
size: metadata.len(),
|
||||
blocks: metadata.blocks(),
|
||||
|
|
@ -138,12 +155,12 @@ impl Stat {
|
|||
});
|
||||
|
||||
#[cfg(windows)]
|
||||
let size_on_disk = get_size_on_disk(&path);
|
||||
let size_on_disk = get_size_on_disk(path);
|
||||
#[cfg(windows)]
|
||||
let file_info = get_file_info(&path);
|
||||
let file_info = get_file_info(path);
|
||||
#[cfg(windows)]
|
||||
Ok(Self {
|
||||
path,
|
||||
path: path.to_path_buf(),
|
||||
is_dir: metadata.is_dir(),
|
||||
size: metadata.len(),
|
||||
blocks: size_on_disk / 1024 * 2,
|
||||
|
|
@ -296,7 +313,7 @@ fn du(
|
|||
'file_loop: for f in read {
|
||||
match f {
|
||||
Ok(entry) => {
|
||||
match Stat::new(entry.path(), options) {
|
||||
match Stat::new(&entry.path(), options) {
|
||||
Ok(this_stat) => {
|
||||
// We have an exclude list
|
||||
for pattern in exclude {
|
||||
|
|
@ -397,6 +414,20 @@ fn convert_size_other(size: u64, _multiplier: u64, block_size: u64) -> String {
|
|||
format!("{}", ((size as f64) / (block_size as f64)).ceil())
|
||||
}
|
||||
|
||||
fn get_convert_size_fn(matches: &ArgMatches) -> Box<dyn Fn(u64, u64, u64) -> String> {
|
||||
if matches.get_flag(options::HUMAN_READABLE) || matches.get_flag(options::SI) {
|
||||
Box::new(convert_size_human)
|
||||
} else if matches.get_flag(options::BYTES) {
|
||||
Box::new(convert_size_b)
|
||||
} else if matches.get_flag(options::BLOCK_SIZE_1K) {
|
||||
Box::new(convert_size_k)
|
||||
} else if matches.get_flag(options::BLOCK_SIZE_1M) {
|
||||
Box::new(convert_size_m)
|
||||
} else {
|
||||
Box::new(convert_size_other)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DuError {
|
||||
InvalidMaxDepthArg(String),
|
||||
|
|
@ -490,7 +521,6 @@ fn build_exclude_patterns(matches: &ArgMatches) -> UResult<Vec<Pattern>> {
|
|||
}
|
||||
|
||||
#[uucore::main]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let args = args.collect_ignore();
|
||||
|
||||
|
|
@ -505,26 +535,33 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
summarize,
|
||||
)?;
|
||||
|
||||
let files = match matches.get_one::<String>(options::FILE) {
|
||||
Some(_) => matches
|
||||
.get_many::<String>(options::FILE)
|
||||
.unwrap()
|
||||
.map(PathBuf::from)
|
||||
.collect(),
|
||||
None => vec![PathBuf::from(".")],
|
||||
};
|
||||
|
||||
let options = Options {
|
||||
all: matches.get_flag(options::ALL),
|
||||
max_depth,
|
||||
total: matches.get_flag(options::TOTAL),
|
||||
separate_dirs: matches.get_flag(options::SEPARATE_DIRS),
|
||||
one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM),
|
||||
dereference: matches.get_flag(options::DEREFERENCE),
|
||||
dereference: if matches.get_flag(options::DEREFERENCE) {
|
||||
Deref::All
|
||||
} else if matches.get_flag(options::DEREFERENCE_ARGS) {
|
||||
// We don't care about the cost of cloning as it is rarely used
|
||||
Deref::Args(files.clone())
|
||||
} else {
|
||||
Deref::None
|
||||
},
|
||||
inodes: matches.get_flag(options::INODES),
|
||||
verbose: matches.get_flag(options::VERBOSE),
|
||||
};
|
||||
|
||||
let files = match matches.get_one::<String>(options::FILE) {
|
||||
Some(_) => matches
|
||||
.get_many::<String>(options::FILE)
|
||||
.unwrap()
|
||||
.map(|s| s.as_str())
|
||||
.collect(),
|
||||
None => vec!["."],
|
||||
};
|
||||
|
||||
if options.inodes
|
||||
&& (matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES))
|
||||
{
|
||||
|
|
@ -547,19 +584,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
} else {
|
||||
1024
|
||||
};
|
||||
let convert_size_fn = {
|
||||
if matches.get_flag(options::HUMAN_READABLE) || matches.get_flag(options::SI) {
|
||||
convert_size_human
|
||||
} else if matches.get_flag(options::BYTES) {
|
||||
convert_size_b
|
||||
} else if matches.get_flag(options::BLOCK_SIZE_1K) {
|
||||
convert_size_k
|
||||
} else if matches.get_flag(options::BLOCK_SIZE_1M) {
|
||||
convert_size_m
|
||||
} else {
|
||||
convert_size_other
|
||||
}
|
||||
};
|
||||
|
||||
let convert_size_fn = get_convert_size_fn(&matches);
|
||||
|
||||
let convert_size = |size: u64| {
|
||||
if options.inodes {
|
||||
size.to_string()
|
||||
|
|
@ -580,11 +607,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
let excludes = build_exclude_patterns(&matches)?;
|
||||
|
||||
let mut grand_total = 0;
|
||||
'loop_file: for path_string in files {
|
||||
'loop_file: for path in files {
|
||||
// Skip if we don't want to ignore anything
|
||||
if !&excludes.is_empty() {
|
||||
let path_string = path.to_string_lossy();
|
||||
for pattern in &excludes {
|
||||
if pattern.matches(path_string) {
|
||||
if pattern.matches(&path_string) {
|
||||
// if the directory is ignored, leave early
|
||||
if options.verbose {
|
||||
println!("{} ignored", path_string.quote());
|
||||
|
|
@ -594,9 +622,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
}
|
||||
}
|
||||
|
||||
let path = PathBuf::from(&path_string);
|
||||
// Check existence of path provided in argument
|
||||
if let Ok(stat) = Stat::new(path, &options) {
|
||||
if let Ok(stat) = Stat::new(&path, &options) {
|
||||
// Kick off the computation of disk usage from the initial path
|
||||
let mut inodes: HashSet<FileInfo> = HashSet::new();
|
||||
if let Some(inode) = stat.inode {
|
||||
|
|
@ -616,20 +643,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
|
||||
if matches.contains_id(options::TIME) {
|
||||
let tm = {
|
||||
let secs = {
|
||||
match matches.get_one::<String>(options::TIME) {
|
||||
Some(s) => match s.as_str() {
|
||||
"ctime" | "status" => stat.modified,
|
||||
"access" | "atime" | "use" => stat.accessed,
|
||||
"birth" | "creation" => stat
|
||||
.created
|
||||
.ok_or_else(|| DuError::InvalidTimeArg(s.into()))?,
|
||||
// below should never happen as clap already restricts the values.
|
||||
_ => unreachable!("Invalid field for --time"),
|
||||
},
|
||||
None => stat.modified,
|
||||
}
|
||||
};
|
||||
let secs = matches
|
||||
.get_one::<String>(options::TIME)
|
||||
.map(|s| get_time_secs(s, &stat))
|
||||
.transpose()?
|
||||
.unwrap_or(stat.modified);
|
||||
DateTime::<Local>::from(UNIX_EPOCH + Duration::from_secs(secs))
|
||||
};
|
||||
if !summarize || index == len - 1 {
|
||||
|
|
@ -652,9 +670,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
} else {
|
||||
show_error!(
|
||||
"{}: {}",
|
||||
path_string.maybe_quote(),
|
||||
path.to_string_lossy().maybe_quote(),
|
||||
"No such file or directory"
|
||||
);
|
||||
set_exit_code(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -666,6 +685,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn get_time_secs(s: &str, stat: &Stat) -> std::result::Result<u64, DuError> {
|
||||
let secs = match s {
|
||||
"ctime" | "status" => stat.modified,
|
||||
"access" | "atime" | "use" => stat.accessed,
|
||||
"birth" | "creation" => stat
|
||||
.created
|
||||
.ok_or_else(|| DuError::InvalidTimeArg(s.into()))?,
|
||||
// below should never happen as clap already restricts the values.
|
||||
_ => unreachable!("Invalid field for --time"),
|
||||
};
|
||||
Ok(secs)
|
||||
}
|
||||
|
||||
fn parse_time_style(s: Option<&str>) -> UResult<&str> {
|
||||
match s {
|
||||
Some(s) => match s {
|
||||
|
|
@ -788,6 +820,13 @@ pub fn uu_app() -> Command {
|
|||
.help("dereference all symbolic links")
|
||||
.action(ArgAction::SetTrue)
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::DEREFERENCE_ARGS)
|
||||
.short('D')
|
||||
.long(options::DEREFERENCE_ARGS)
|
||||
.help("dereference only symlinks that are listed on the command line")
|
||||
.action(ArgAction::SetTrue)
|
||||
)
|
||||
// .arg(
|
||||
// Arg::new("no-dereference")
|
||||
// .short('P')
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
// * For the full copyright and license information, please view the LICENSE
|
||||
// * file that was distributed with this source code.
|
||||
|
||||
// spell-checker:ignore (paths) sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty
|
||||
// spell-checker:ignore (paths) sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty tsublink
|
||||
#[cfg(not(windows))]
|
||||
use regex::Regex;
|
||||
#[cfg(not(windows))]
|
||||
use std::io::Write;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
|
|
@ -122,7 +121,7 @@ fn test_du_invalid_size() {
|
|||
fn test_du_basics_bad_name() {
|
||||
new_ucmd!()
|
||||
.arg("bad_name")
|
||||
.succeeds() // TODO: replace with ".fails()" once `du` is fixed
|
||||
.fails()
|
||||
.stderr_only("du: bad_name: No such file or directory\n");
|
||||
}
|
||||
|
||||
|
|
@ -286,6 +285,30 @@ fn test_du_dereference() {
|
|||
_du_dereference(result.stdout_str());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_du_dereference_args() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
at.mkdir_all("subdir");
|
||||
let mut file1 = at.make_file("subdir/file-ignore1");
|
||||
file1.write_all(b"azeaze").unwrap();
|
||||
let mut file2 = at.make_file("subdir/file-ignore1");
|
||||
file2.write_all(b"amaz?ng").unwrap();
|
||||
at.symlink_dir("subdir", "sublink");
|
||||
|
||||
let result = ts.ucmd().arg("-D").arg("-s").arg("sublink").succeeds();
|
||||
let stdout = result.stdout_str();
|
||||
|
||||
assert!(!stdout.starts_with("0"));
|
||||
assert!(stdout.contains("sublink"));
|
||||
|
||||
// Without the option
|
||||
let result = ts.ucmd().arg("-s").arg("sublink").succeeds();
|
||||
result.stdout_contains("0\tsublink\n");
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
fn _du_dereference(s: &str) {
|
||||
assert_eq!(s, "4\tsubdir/links/deeper_dir\n16\tsubdir/links\n");
|
||||
|
|
@ -851,3 +874,29 @@ fn test_du_exclude_invalid_syntax() {
|
|||
.fails()
|
||||
.stderr_contains("du: Invalid exclude syntax");
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_du_symlink_fail() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
at.symlink_file("non-existing.txt", "target.txt");
|
||||
|
||||
ts.ucmd().arg("-L").arg("target.txt").fails().code_is(1);
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_du_symlink_multiple_fail() {
|
||||
let ts = TestScenario::new(util_name!());
|
||||
let at = &ts.fixtures;
|
||||
|
||||
at.symlink_file("non-existing.txt", "target.txt");
|
||||
let mut file1 = at.make_file("file1");
|
||||
file1.write_all(b"azeaze").unwrap();
|
||||
|
||||
let result = ts.ucmd().arg("-L").arg("target.txt").arg("file1").fails();
|
||||
assert_eq!(result.code(), 1);
|
||||
result.stdout_contains("4\tfile1\n");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue