diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index cab9da227..f78e0d4ae 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -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), + 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 { - let metadata = if options.dereference { - fs::metadata(&path)? - } else { - fs::symlink_metadata(&path)? + fn new(path: &Path, options: &Options) -> Result { + // 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 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> { } #[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::(options::FILE) { + Some(_) => matches + .get_many::(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::(options::FILE) { - Some(_) => matches - .get_many::(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 = 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::(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::(options::TIME) + .map(|s| get_time_secs(s, &stat)) + .transpose()? + .unwrap_or(stat.modified); DateTime::::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 { + 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') diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index d69eaaf99..a965d3fc6 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -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"); +}