1use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue};
8use glob::Pattern;
9use std::collections::HashSet;
10use std::env;
11use std::ffi::OsStr;
12use std::ffi::OsString;
13use std::fs::Metadata;
14use std::fs::{self, DirEntry, File};
15use std::io::{BufRead, BufReader, stdout};
16#[cfg(not(windows))]
17use std::os::unix::fs::MetadataExt;
18#[cfg(windows)]
19use std::os::windows::io::AsRawHandle;
20use std::path::{Path, PathBuf};
21use std::str::FromStr;
22use std::sync::mpsc;
23use std::thread;
24use thiserror::Error;
25use uucore::display::{Quotable, print_verbatim};
26use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code};
27use uucore::fsext::{MetadataTimeField, metadata_get_time};
28use uucore::line_ending::LineEnding;
29#[cfg(target_os = "linux")]
30use uucore::safe_traversal::DirFd;
31use uucore::translate;
32
33use uucore::parser::parse_glob;
34use uucore::parser::parse_size::{ParseSizeError, parse_size_non_zero_u64, parse_size_u64};
35use uucore::parser::shortcut_value_parser::ShortcutValueParser;
36use uucore::time::{FormatSystemTimeFallback, format, format_system_time};
37use uucore::{format_usage, show, show_error, show_warning};
38#[cfg(windows)]
39use windows_sys::Win32::Foundation::HANDLE;
40#[cfg(windows)]
41use windows_sys::Win32::Storage::FileSystem::{
42 FILE_ID_128, FILE_ID_INFO, FILE_STANDARD_INFO, FileIdInfo, FileStandardInfo,
43 GetFileInformationByHandleEx,
44};
45
46mod options {
47 pub const HELP: &str = "help";
48 pub const NULL: &str = "0";
49 pub const ALL: &str = "all";
50 pub const APPARENT_SIZE: &str = "apparent-size";
51 pub const BLOCK_SIZE: &str = "block-size";
52 pub const BYTES: &str = "b";
53 pub const TOTAL: &str = "c";
54 pub const MAX_DEPTH: &str = "d";
55 pub const HUMAN_READABLE: &str = "h";
56 pub const BLOCK_SIZE_1K: &str = "k";
57 pub const COUNT_LINKS: &str = "l";
58 pub const BLOCK_SIZE_1M: &str = "m";
59 pub const SEPARATE_DIRS: &str = "S";
60 pub const SUMMARIZE: &str = "s";
61 pub const THRESHOLD: &str = "threshold";
62 pub const SI: &str = "si";
63 pub const TIME: &str = "time";
64 pub const TIME_STYLE: &str = "time-style";
65 pub const ONE_FILE_SYSTEM: &str = "one-file-system";
66 pub const DEREFERENCE: &str = "dereference";
67 pub const DEREFERENCE_ARGS: &str = "dereference-args";
68 pub const NO_DEREFERENCE: &str = "no-dereference";
69 pub const INODES: &str = "inodes";
70 pub const EXCLUDE: &str = "exclude";
71 pub const EXCLUDE_FROM: &str = "exclude-from";
72 pub const FILES0_FROM: &str = "files0-from";
73 pub const VERBOSE: &str = "verbose";
74 pub const FILE: &str = "FILE";
75}
76
77struct TraversalOptions {
78 all: bool,
79 separate_dirs: bool,
80 one_file_system: bool,
81 dereference: Deref,
82 count_links: bool,
83 verbose: bool,
84 excludes: Vec<Pattern>,
85}
86
87struct StatPrinter {
88 total: bool,
89 inodes: bool,
90 max_depth: Option<usize>,
91 threshold: Option<Threshold>,
92 apparent_size: bool,
93 size_format: SizeFormat,
94 time: Option<MetadataTimeField>,
95 time_format: String,
96 line_ending: LineEnding,
97 summarize: bool,
98 total_text: String,
99}
100
101#[derive(PartialEq, Clone)]
102enum Deref {
103 All,
104 Args(Vec<PathBuf>),
105 None,
106}
107
108#[derive(Clone)]
109enum SizeFormat {
110 HumanDecimal,
111 HumanBinary,
112 BlockSize(u64),
113}
114
115#[derive(PartialEq, Eq, Hash, Clone, Copy)]
116struct FileInfo {
117 file_id: u128,
118 dev_id: u64,
119}
120
121struct Stat {
122 path: PathBuf,
123 size: u64,
124 blocks: u64,
125 inodes: u64,
126 inode: Option<FileInfo>,
127 metadata: Metadata,
128}
129
130impl Stat {
131 fn new(
132 path: &Path,
133 dir_entry: Option<&DirEntry>,
134 options: &TraversalOptions,
135 ) -> std::io::Result<Self> {
136 let should_dereference = match &options.dereference {
138 Deref::All => true,
139 Deref::Args(paths) => paths.contains(&path.to_path_buf()),
140 Deref::None => false,
141 };
142
143 let metadata = if should_dereference {
144 fs::metadata(path)
146 } else if let Some(dir_entry) = dir_entry {
147 dir_entry.metadata()
149 } else {
150 fs::symlink_metadata(path)
152 }?;
153
154 let file_info = get_file_info(path, &metadata);
155 let blocks = get_blocks(path, &metadata);
156
157 Ok(Self {
158 path: path.to_path_buf(),
159 size: if metadata.is_dir() { 0 } else { metadata.len() },
160 blocks,
161 inodes: 1,
162 inode: file_info,
163 metadata,
164 })
165 }
166
167 #[cfg(target_os = "linux")]
169 fn new_from_dirfd(dir_fd: &DirFd, full_path: &Path) -> std::io::Result<Self> {
170 let safe_metadata = dir_fd.metadata()?;
172
173 let file_info = safe_metadata.file_info();
175 let file_info_option = Some(FileInfo {
176 file_id: file_info.inode() as u128,
177 dev_id: file_info.device(),
178 });
179
180 let blocks = safe_metadata.blocks();
181
182 let std_metadata = fs::symlink_metadata(full_path)?;
186
187 Ok(Self {
188 path: full_path.to_path_buf(),
189 size: if safe_metadata.is_dir() {
190 0
191 } else {
192 safe_metadata.len()
193 },
194 blocks,
195 inodes: 1,
196 inode: file_info_option,
197 metadata: std_metadata,
198 })
199 }
200}
201
202#[cfg(not(windows))]
203fn get_blocks(_path: &Path, metadata: &Metadata) -> u64 {
204 metadata.blocks()
205}
206
207#[cfg(windows)]
208fn get_blocks(path: &Path, _metadata: &Metadata) -> u64 {
209 let mut size_on_disk = 0;
210
211 let Ok(file) = File::open(path) else {
214 return size_on_disk; };
216
217 unsafe {
218 let mut file_info: FILE_STANDARD_INFO = core::mem::zeroed();
219 let file_info_ptr: *mut FILE_STANDARD_INFO = &raw mut file_info;
220
221 let success = GetFileInformationByHandleEx(
222 file.as_raw_handle() as HANDLE,
223 FileStandardInfo,
224 file_info_ptr.cast(),
225 size_of::<FILE_STANDARD_INFO>() as u32,
226 );
227
228 if success != 0 {
229 size_on_disk = file_info.AllocationSize as u64;
230 }
231 }
232
233 size_on_disk / 1024 * 2
234}
235
236#[cfg(not(windows))]
237fn get_file_info(_path: &Path, metadata: &Metadata) -> Option<FileInfo> {
238 Some(FileInfo {
239 file_id: metadata.ino() as u128,
240 dev_id: metadata.dev(),
241 })
242}
243
244#[cfg(windows)]
245fn get_file_info(path: &Path, _metadata: &Metadata) -> Option<FileInfo> {
246 let mut result = None;
247
248 let Ok(file) = File::open(path) else {
249 return result;
250 };
251
252 unsafe {
253 let mut file_info: FILE_ID_INFO = core::mem::zeroed();
254 let file_info_ptr: *mut FILE_ID_INFO = &raw mut file_info;
255
256 let success = GetFileInformationByHandleEx(
257 file.as_raw_handle() as HANDLE,
258 FileIdInfo,
259 file_info_ptr.cast(),
260 size_of::<FILE_ID_INFO>() as u32,
261 );
262
263 if success != 0 {
264 result = Some(FileInfo {
265 file_id: std::mem::transmute::<FILE_ID_128, u128>(file_info.FileId),
266 dev_id: file_info.VolumeSerialNumber,
267 });
268 }
269 }
270
271 result
272}
273
274fn block_size_from_env() -> Option<u64> {
275 for env_var in ["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] {
276 if let Ok(env_size) = env::var(env_var) {
277 return parse_size_non_zero_u64(&env_size).ok();
278 }
279 }
280
281 None
282}
283
284fn read_block_size(s: Option<&str>) -> UResult<u64> {
285 if let Some(s) = s {
286 parse_size_u64(s)
287 .map_err(|e| USimpleError::new(1, format_error_message(&e, s, options::BLOCK_SIZE)))
288 } else if let Some(bytes) = block_size_from_env() {
289 Ok(bytes)
290 } else if env::var("POSIXLY_CORRECT").is_ok() {
291 Ok(512)
292 } else {
293 Ok(1024)
294 }
295}
296
297#[cfg(target_os = "linux")]
298fn safe_du(
301 path: &Path,
302 options: &TraversalOptions,
303 depth: usize,
304 seen_inodes: &mut HashSet<FileInfo>,
305 print_tx: &mpsc::Sender<UResult<StatPrintInfo>>,
306 parent_fd: Option<&DirFd>,
307) -> Result<Stat, Box<mpsc::SendError<UResult<StatPrintInfo>>>> {
308 let mut my_stat = if let Some(parent_fd) = parent_fd {
310 let dir_name = path.file_name().unwrap_or(path.as_os_str());
312 match parent_fd.metadata_at(dir_name, false) {
313 Ok(safe_metadata) => {
314 let file_info = safe_metadata.file_info();
316 let file_info_option = Some(FileInfo {
317 file_id: file_info.inode() as u128,
318 dev_id: file_info.device(),
319 });
320 let blocks = safe_metadata.blocks();
321
322 let std_metadata = fs::symlink_metadata(path).unwrap_or_else(|_| {
325 fs::symlink_metadata("/").expect("root should be accessible")
328 });
329
330 Stat {
331 path: path.to_path_buf(),
332 size: if safe_metadata.is_dir() {
333 0
334 } else {
335 safe_metadata.len()
336 },
337 blocks,
338 inodes: 1,
339 inode: file_info_option,
340 metadata: std_metadata,
341 }
342 }
343 Err(e) => {
344 let error = e.map_err_context(
345 || translate!("du-error-cannot-access", "path" => path.quote()),
346 );
347 if let Err(send_error) = print_tx.send(Err(error)) {
348 return Err(Box::new(send_error));
349 }
350 return Err(Box::new(mpsc::SendError(Err(USimpleError::new(
351 0,
352 "Error already handled",
353 )))));
354 }
355 }
356 } else {
357 match Stat::new(path, None, options) {
359 Ok(s) => s,
360 Err(_e) => {
361 match DirFd::open(path) {
363 Ok(dir_fd) => match Stat::new_from_dirfd(&dir_fd, path) {
364 Ok(s) => s,
365 Err(e) => {
366 let error = e.map_err_context(
367 || translate!("du-error-cannot-access", "path" => path.quote()),
368 );
369 if let Err(send_error) = print_tx.send(Err(error)) {
370 return Err(Box::new(send_error));
371 }
372 return Err(Box::new(mpsc::SendError(Err(USimpleError::new(
373 0,
374 "Error already handled",
375 )))));
376 }
377 },
378 Err(e) => {
379 let error = e.map_err_context(
380 || translate!("du-error-cannot-access", "path" => path.quote()),
381 );
382 if let Err(send_error) = print_tx.send(Err(error)) {
383 return Err(Box::new(send_error));
384 }
385 return Err(Box::new(mpsc::SendError(Err(USimpleError::new(
386 0,
387 "Error already handled",
388 )))));
389 }
390 }
391 }
392 }
393 };
394 if !my_stat.metadata.is_dir() {
395 return Ok(my_stat);
396 }
397
398 let open_result = match parent_fd {
400 Some(parent) => parent.open_subdir(path.file_name().unwrap_or(path.as_os_str())),
401 None => DirFd::open(path),
402 };
403
404 let dir_fd = match open_result {
405 Ok(fd) => fd,
406 Err(e) => {
407 print_tx.send(Err(e.map_err_context(
408 || translate!("du-error-cannot-read-directory", "path" => path.quote()),
409 )))?;
410 return Ok(my_stat);
411 }
412 };
413
414 let entries = match dir_fd.read_dir() {
416 Ok(entries) => entries,
417 Err(e) => {
418 print_tx.send(Err(e.map_err_context(
419 || translate!("du-error-cannot-read-directory", "path" => path.quote()),
420 )))?;
421 return Ok(my_stat);
422 }
423 };
424
425 'file_loop: for entry_name in entries {
426 let entry_path = path.join(&entry_name);
427
428 let lstat = match dir_fd.stat_at(&entry_name, false) {
430 Ok(stat) => stat,
431 Err(e) => {
432 print_tx.send(Err(e.map_err_context(
433 || translate!("du-error-cannot-access", "path" => entry_path.quote()),
434 )))?;
435 continue;
436 }
437 };
438
439 const S_IFMT: u32 = 0o170_000;
441 const S_IFDIR: u32 = 0o040_000;
442 const S_IFLNK: u32 = 0o120_000;
443 let is_symlink = (lstat.st_mode & S_IFMT) == S_IFLNK;
444
445 if is_symlink && options.dereference == Deref::All {
449 continue;
452 }
453
454 let is_dir = (lstat.st_mode & S_IFMT) == S_IFDIR;
455 let entry_stat = lstat;
456
457 let file_info = (entry_stat.st_ino != 0).then_some(FileInfo {
458 file_id: entry_stat.st_ino as u128,
459 dev_id: entry_stat.st_dev,
460 });
461
462 let this_stat = if is_dir {
465 Stat {
467 path: entry_path.clone(),
468 size: 0,
469 blocks: entry_stat.st_blocks as u64,
470 inodes: 1,
471 inode: file_info,
472 metadata: my_stat.metadata.clone(),
475 }
476 } else {
477 Stat {
479 path: entry_path.clone(),
480 size: entry_stat.st_size as u64,
481 blocks: entry_stat.st_blocks as u64,
482 inodes: 1,
483 inode: file_info,
484 metadata: my_stat.metadata.clone(),
485 }
486 };
487
488 for pattern in &options.excludes {
490 if pattern.matches(&this_stat.path.to_string_lossy())
491 || pattern.matches(&entry_name.to_string_lossy())
492 {
493 if options.verbose {
494 println!(
495 "{}",
496 translate!("du-verbose-ignored", "path" => this_stat.path.quote())
497 );
498 }
499 continue 'file_loop;
500 }
501 }
502
503 if let Some(inode) = this_stat.inode {
505 if seen_inodes.contains(&inode) && (!options.count_links || !options.all) {
506 if options.count_links && !options.all {
507 my_stat.inodes += 1;
508 }
509 continue;
510 }
511 seen_inodes.insert(inode);
512 }
513
514 if is_dir {
516 if options.one_file_system {
517 if let (Some(this_inode), Some(my_inode)) = (this_stat.inode, my_stat.inode) {
518 if this_inode.dev_id != my_inode.dev_id {
519 continue;
520 }
521 }
522 }
523
524 let this_stat = safe_du(
525 &entry_path,
526 options,
527 depth + 1,
528 seen_inodes,
529 print_tx,
530 Some(&dir_fd),
531 )?;
532
533 if !options.separate_dirs {
534 my_stat.size += this_stat.size;
535 my_stat.blocks += this_stat.blocks;
536 my_stat.inodes += this_stat.inodes;
537 }
538 print_tx.send(Ok(StatPrintInfo {
539 stat: this_stat,
540 depth: depth + 1,
541 }))?;
542 } else {
543 my_stat.size += this_stat.size;
544 my_stat.blocks += this_stat.blocks;
545 my_stat.inodes += 1;
546 if options.all {
547 print_tx.send(Ok(StatPrintInfo {
548 stat: this_stat,
549 depth: depth + 1,
550 }))?;
551 }
552 }
553 }
554
555 Ok(my_stat)
556}
557
558#[allow(clippy::cognitive_complexity)]
563fn du_regular(
564 mut my_stat: Stat,
565 options: &TraversalOptions,
566 depth: usize,
567 seen_inodes: &mut HashSet<FileInfo>,
568 print_tx: &mpsc::Sender<UResult<StatPrintInfo>>,
569 ancestors: Option<&mut HashSet<FileInfo>>,
570 symlink_depth: Option<usize>,
571) -> Result<Stat, Box<mpsc::SendError<UResult<StatPrintInfo>>>> {
572 let mut default_ancestors = HashSet::new();
573 let ancestors = ancestors.unwrap_or(&mut default_ancestors);
574 let symlink_depth = symlink_depth.unwrap_or(0);
575 const MAX_SYMLINK_DEPTH: usize = 40;
577
578 let my_inode = if my_stat.metadata.is_dir() {
580 my_stat.inode
581 } else {
582 None
583 };
584
585 if let Some(inode) = my_inode {
586 ancestors.insert(inode);
587 }
588 if my_stat.metadata.is_dir() {
589 let read = match fs::read_dir(&my_stat.path) {
590 Ok(read) => read,
591 Err(e) => {
592 print_tx.send(Err(e.map_err_context(
593 || translate!("du-error-cannot-read-directory", "path" => my_stat.path.quote()),
594 )))?;
595 return Ok(my_stat);
596 }
597 };
598
599 'file_loop: for f in read {
600 match f {
601 Ok(entry) => {
602 let entry_path = entry.path();
603
604 let mut current_symlink_depth = symlink_depth;
606 let is_symlink = match entry.file_type() {
607 Ok(ft) => ft.is_symlink(),
608 Err(_) => false,
609 };
610
611 if is_symlink && options.dereference == Deref::All {
612 current_symlink_depth += 1;
614
615 if current_symlink_depth > MAX_SYMLINK_DEPTH {
617 print_tx.send(Err(std::io::Error::new(
618 std::io::ErrorKind::InvalidData,
619 "Too many levels of symbolic links",
620 ).map_err_context(
621 || translate!("du-error-cannot-access", "path" => entry_path.quote()),
622 )))?;
623 continue 'file_loop;
624 }
625 }
626
627 match Stat::new(&entry_path, Some(&entry), options) {
628 Ok(this_stat) => {
629 if is_symlink
631 && options.dereference == Deref::All
632 && this_stat.metadata.is_dir()
633 {
634 if let Some(inode) = this_stat.inode {
635 if ancestors.contains(&inode) {
636 continue 'file_loop;
638 }
639 }
640 }
641
642 for pattern in &options.excludes {
644 if pattern.matches(&this_stat.path.to_string_lossy())
648 || pattern.matches(&entry.file_name().into_string().unwrap())
649 {
650 if options.verbose {
652 println!(
653 "{}",
654 translate!("du-verbose-ignored", "path" => this_stat.path.quote())
655 );
656 }
657 continue 'file_loop;
659 }
660 }
661
662 if let Some(inode) = this_stat.inode {
663 if seen_inodes.contains(&inode)
665 && (!options.count_links || !options.all)
666 {
667 if options.count_links && !options.all {
669 my_stat.inodes += 1;
670 }
671 continue;
673 }
674 seen_inodes.insert(inode);
676 }
677
678 if this_stat.metadata.is_dir() {
679 if options.one_file_system {
680 if let (Some(this_inode), Some(my_inode)) =
681 (this_stat.inode, my_stat.inode)
682 {
683 if this_inode.dev_id != my_inode.dev_id {
684 continue;
685 }
686 }
687 }
688
689 let this_stat = du_regular(
690 this_stat,
691 options,
692 depth + 1,
693 seen_inodes,
694 print_tx,
695 Some(ancestors),
696 Some(current_symlink_depth),
697 )?;
698
699 if !options.separate_dirs {
700 my_stat.size += this_stat.size;
701 my_stat.blocks += this_stat.blocks;
702 my_stat.inodes += this_stat.inodes;
703 }
704 print_tx.send(Ok(StatPrintInfo {
705 stat: this_stat,
706 depth: depth + 1,
707 }))?;
708 } else {
709 my_stat.size += this_stat.size;
710 my_stat.blocks += this_stat.blocks;
711 my_stat.inodes += 1;
712 if options.all {
713 print_tx.send(Ok(StatPrintInfo {
714 stat: this_stat,
715 depth: depth + 1,
716 }))?;
717 }
718 }
719 }
720 Err(e) => {
721 print_tx.send(Err(e.map_err_context(
722 || translate!("du-error-cannot-access", "path" => entry_path.quote()),
723 )))?;
724 }
725 }
726 }
727 Err(error) => print_tx.send(Err(error.into()))?,
728 }
729 }
730 }
731
732 if let Some(inode) = my_inode {
734 ancestors.remove(&inode);
735 }
736
737 Ok(my_stat)
738}
739
740#[derive(Debug, Error)]
741enum DuError {
742 #[error("{}", translate!("du-error-invalid-max-depth", "depth" => _0.quote()))]
743 InvalidMaxDepthArg(String),
744
745 #[error("{}", translate!("du-error-summarize-depth-conflict", "depth" => _0.maybe_quote()))]
746 SummarizeDepthConflict(String),
747
748 #[error("{}", translate!("du-error-invalid-time-style", "style" => _0.quote(), "help" => uucore::execution_phrase()))]
749 InvalidTimeStyleArg(String),
750
751 #[error("{}", translate!("du-error-invalid-glob", "error" => _0))]
752 InvalidGlob(String),
753}
754
755impl UError for DuError {
756 fn code(&self) -> i32 {
757 match self {
758 Self::InvalidMaxDepthArg(_)
759 | Self::SummarizeDepthConflict(_)
760 | Self::InvalidTimeStyleArg(_)
761 | Self::InvalidGlob(_) => 1,
762 }
763 }
764}
765
766fn file_as_vec(filename: impl AsRef<Path>) -> Vec<String> {
768 let file = File::open(filename).expect("no such file");
769 let buf = BufReader::new(file);
770
771 buf.lines()
772 .map(|l| l.expect("Could not parse line"))
773 .collect()
774}
775
776fn build_exclude_patterns(matches: &ArgMatches) -> UResult<Vec<Pattern>> {
779 let exclude_from_iterator = matches
780 .get_many::<String>(options::EXCLUDE_FROM)
781 .unwrap_or_default()
782 .flat_map(file_as_vec);
783
784 let excludes_iterator = matches
785 .get_many::<String>(options::EXCLUDE)
786 .unwrap_or_default()
787 .cloned();
788
789 let mut exclude_patterns = Vec::new();
790 for f in excludes_iterator.chain(exclude_from_iterator) {
791 if matches.get_flag(options::VERBOSE) {
792 println!(
793 "{}",
794 translate!("du-verbose-adding-to-exclude-list", "pattern" => f.clone())
795 );
796 }
797 match parse_glob::from_str(&f) {
798 Ok(glob) => exclude_patterns.push(glob),
799 Err(err) => return Err(DuError::InvalidGlob(err.to_string()).into()),
800 }
801 }
802 Ok(exclude_patterns)
803}
804
805struct StatPrintInfo {
806 stat: Stat,
807 depth: usize,
808}
809
810impl StatPrinter {
811 fn choose_size(&self, stat: &Stat) -> u64 {
812 if self.inodes {
813 stat.inodes
814 } else if self.apparent_size {
815 stat.size
816 } else {
817 stat.blocks * 512
820 }
821 }
822
823 fn print_stats(&self, rx: &mpsc::Receiver<UResult<StatPrintInfo>>) -> UResult<()> {
824 let mut grand_total = 0;
825 loop {
826 let received = rx.recv();
827
828 match received {
829 Ok(message) => match message {
830 Ok(stat_info) => {
831 let size = self.choose_size(&stat_info.stat);
832
833 if stat_info.depth == 0 {
834 grand_total += size;
835 }
836
837 if !self
838 .threshold
839 .is_some_and(|threshold| threshold.should_exclude(size))
840 && self
841 .max_depth
842 .is_none_or(|max_depth| stat_info.depth <= max_depth)
843 && (!self.summarize || stat_info.depth == 0)
844 {
845 self.print_stat(&stat_info.stat, size)?;
846 }
847 }
848 Err(e) => show!(e),
849 },
850 Err(_) => break,
851 }
852 }
853
854 if self.total {
855 print!("{}\t{}", self.convert_size(grand_total), self.total_text);
856 print!("{}", self.line_ending);
857 }
858
859 Ok(())
860 }
861
862 fn convert_size(&self, size: u64) -> String {
863 match self.size_format {
864 SizeFormat::HumanDecimal => uucore::format::human::human_readable(
865 size,
866 uucore::format::human::SizeFormat::Decimal,
867 ),
868 SizeFormat::HumanBinary => uucore::format::human::human_readable(
869 size,
870 uucore::format::human::SizeFormat::Binary,
871 ),
872 SizeFormat::BlockSize(block_size) => {
873 if self.inodes {
874 size.to_string()
876 } else {
877 size.div_ceil(block_size).to_string()
878 }
879 }
880 }
881 }
882
883 fn print_stat(&self, stat: &Stat, size: u64) -> UResult<()> {
884 print!("{}\t", self.convert_size(size));
885
886 if let Some(md_time) = &self.time {
887 if let Some(time) = metadata_get_time(&stat.metadata, *md_time) {
888 format_system_time(
889 &mut stdout(),
890 time,
891 &self.time_format,
892 FormatSystemTimeFallback::IntegerError,
893 )?;
894 print!("\t");
895 } else {
896 print!("???\t");
897 }
898 }
899
900 print_verbatim(&stat.path).unwrap();
901 print!("{}", self.line_ending);
902
903 Ok(())
904 }
905}
906
907fn read_files_from(file_name: &OsStr) -> Result<Vec<PathBuf>, std::io::Error> {
909 let reader: Box<dyn BufRead> = if file_name == "-" {
910 Box::new(BufReader::new(std::io::stdin()))
912 } else {
913 let path = PathBuf::from(file_name);
915 if path.is_dir() {
916 return Err(std::io::Error::other(
917 translate!("du-error-read-error-is-directory", "file" => file_name.to_string_lossy()),
918 ));
919 }
920
921 match File::open(file_name) {
923 Ok(file) => Box::new(BufReader::new(file)),
924 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
925 return Err(std::io::Error::other(
926 translate!("du-error-cannot-open-for-reading", "file" => file_name.to_string_lossy()),
927 ));
928 }
929 Err(e) => return Err(e),
930 }
931 };
932
933 let mut paths = Vec::new();
934
935 for (i, line) in reader.split(b'\0').enumerate() {
936 let path = line?;
937
938 if path.is_empty() {
939 let line_number = i + 1;
940 show_error!(
941 "{}",
942 translate!("du-error-invalid-zero-length-file-name", "file" => file_name.to_string_lossy(), "line" => line_number)
943 );
944 set_exit_code(1);
945 } else {
946 let p = PathBuf::from(&*uucore::os_str_from_bytes(&path).unwrap());
947 if !paths.contains(&p) {
948 paths.push(p);
949 }
950 }
951 }
952
953 Ok(paths)
954}
955
956#[uucore::main]
957#[allow(clippy::cognitive_complexity)]
958pub fn uumain(args: impl uucore::Args) -> UResult<()> {
959 let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
960
961 let summarize = matches.get_flag(options::SUMMARIZE);
962
963 let count_links = matches.get_flag(options::COUNT_LINKS);
964
965 let max_depth = parse_depth(
966 matches
967 .get_one::<String>(options::MAX_DEPTH)
968 .map(|s| s.as_str()),
969 summarize,
970 )?;
971
972 let files = if let Some(file_from) = matches.get_one::<OsString>(options::FILES0_FROM) {
973 if file_from == "-" && matches.get_one::<OsString>(options::FILE).is_some() {
974 return Err(std::io::Error::other(
975 translate!("du-error-extra-operand-with-files0-from",
976 "file" => matches
977 .get_one::<OsString>(options::FILE)
978 .unwrap()
979 .to_string_lossy()
980 .quote()
981 ),
982 )
983 .into());
984 }
985
986 read_files_from(file_from)?
987 } else if let Some(files) = matches.get_many::<OsString>(options::FILE) {
988 let files = files.map(PathBuf::from);
989 if count_links {
990 files.collect()
991 } else {
992 let mut seen = HashSet::new();
994 files
995 .filter(|path| seen.insert(path.clone()))
996 .collect::<Vec<_>>()
997 }
998 } else {
999 vec![PathBuf::from(".")]
1000 };
1001
1002 let time = matches.contains_id(options::TIME).then(|| {
1003 matches
1004 .get_one::<String>(options::TIME)
1005 .map_or(MetadataTimeField::Modification, |s| s.as_str().into())
1006 });
1007
1008 let size_format = if matches.get_flag(options::HUMAN_READABLE) {
1009 SizeFormat::HumanBinary
1010 } else if matches.get_flag(options::SI) {
1011 SizeFormat::HumanDecimal
1012 } else if matches.get_flag(options::BYTES) {
1013 SizeFormat::BlockSize(1)
1014 } else if matches.get_flag(options::BLOCK_SIZE_1K) {
1015 SizeFormat::BlockSize(1024)
1016 } else if matches.get_flag(options::BLOCK_SIZE_1M) {
1017 SizeFormat::BlockSize(1024 * 1024)
1018 } else {
1019 let block_size_str = matches.get_one::<String>(options::BLOCK_SIZE);
1020 let block_size = read_block_size(block_size_str.map(AsRef::as_ref))?;
1021 if block_size == 0 {
1022 return Err(std::io::Error::other(translate!("du-error-invalid-block-size-argument", "option" => options::BLOCK_SIZE, "value" => block_size_str.map_or("???BUG", |v| v).quote()))
1023 .into());
1024 }
1025 SizeFormat::BlockSize(block_size)
1026 };
1027
1028 let traversal_options = TraversalOptions {
1029 all: matches.get_flag(options::ALL),
1030 separate_dirs: matches.get_flag(options::SEPARATE_DIRS),
1031 one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM),
1032 dereference: if matches.get_flag(options::DEREFERENCE) {
1033 Deref::All
1034 } else if matches.get_flag(options::DEREFERENCE_ARGS) {
1035 Deref::Args(files.clone())
1037 } else {
1038 Deref::None
1039 },
1040 count_links,
1041 verbose: matches.get_flag(options::VERBOSE),
1042 excludes: build_exclude_patterns(&matches)?,
1043 };
1044
1045 let time_format = if time.is_some() {
1046 parse_time_style(matches.get_one::<String>("time-style"))?
1047 } else {
1048 format::LONG_ISO.to_string()
1049 };
1050
1051 let stat_printer = StatPrinter {
1052 max_depth,
1053 size_format,
1054 summarize,
1055 total: matches.get_flag(options::TOTAL),
1056 inodes: matches.get_flag(options::INODES),
1057 threshold: matches
1058 .get_one::<String>(options::THRESHOLD)
1059 .map(|s| {
1060 Threshold::from_str(s).map_err(|e| {
1061 USimpleError::new(1, format_error_message(&e, s, options::THRESHOLD))
1062 })
1063 })
1064 .transpose()?,
1065 apparent_size: matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES),
1066 time,
1067 time_format,
1068 line_ending: LineEnding::from_zero_flag(matches.get_flag(options::NULL)),
1069 total_text: translate!("du-total"),
1070 };
1071
1072 if stat_printer.inodes
1073 && (matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES))
1074 {
1075 show_warning!(
1076 "{}",
1077 translate!("du-warning-apparent-size-ineffective-with-inodes")
1078 );
1079 }
1080
1081 let (print_tx, rx) = mpsc::channel::<UResult<StatPrintInfo>>();
1083 let printing_thread = thread::spawn(move || stat_printer.print_stats(&rx));
1084
1085 'loop_file: for path in files {
1086 if !&traversal_options.excludes.is_empty() {
1088 let path_string = path.to_string_lossy();
1089 for pattern in &traversal_options.excludes {
1090 if pattern.matches(&path_string) {
1091 if traversal_options.verbose {
1093 println!(
1094 "{}",
1095 translate!("du-verbose-ignored", "path" => path_string.quote())
1096 );
1097 }
1098 continue 'loop_file;
1099 }
1100 }
1101 }
1102
1103 let mut seen_inodes: HashSet<FileInfo> = HashSet::new();
1105
1106 #[cfg(target_os = "linux")]
1108 let use_safe_traversal = traversal_options.dereference != Deref::All;
1109 #[cfg(not(target_os = "linux"))]
1110 let use_safe_traversal = false;
1111
1112 if use_safe_traversal {
1113 #[cfg(target_os = "linux")]
1115 {
1116 if let Ok(stat) = Stat::new(&path, None, &traversal_options) {
1118 if let Some(inode) = stat.inode {
1119 seen_inodes.insert(inode);
1120 }
1121 }
1122
1123 match safe_du(
1124 &path,
1125 &traversal_options,
1126 0,
1127 &mut seen_inodes,
1128 &print_tx,
1129 None,
1130 ) {
1131 Ok(stat) => {
1132 print_tx
1133 .send(Ok(StatPrintInfo { stat, depth: 0 }))
1134 .map_err(|e| USimpleError::new(1, e.to_string()))?;
1135 }
1136 Err(e) => {
1137 if let mpsc::SendError(Err(simple_error)) = e.as_ref() {
1139 if simple_error.code() == 0 {
1140 continue 'loop_file;
1142 }
1143 }
1144 return Err(USimpleError::new(1, e.to_string()));
1145 }
1146 }
1147 }
1148 } else {
1149 if let Ok(stat) = Stat::new(&path, None, &traversal_options) {
1151 if let Some(inode) = stat.inode {
1152 seen_inodes.insert(inode);
1153 }
1154 let stat = du_regular(
1155 stat,
1156 &traversal_options,
1157 0,
1158 &mut seen_inodes,
1159 &print_tx,
1160 None,
1161 None,
1162 )
1163 .map_err(|e| USimpleError::new(1, e.to_string()))?;
1164
1165 print_tx
1166 .send(Ok(StatPrintInfo { stat, depth: 0 }))
1167 .map_err(|e| USimpleError::new(1, e.to_string()))?;
1168 } else {
1169 #[cfg(target_os = "linux")]
1170 let error_msg = translate!("du-error-cannot-access", "path" => path.quote());
1171 #[cfg(not(target_os = "linux"))]
1172 let error_msg = translate!("du-error-cannot-access-no-such-file", "path" => path.to_string_lossy().quote());
1173
1174 print_tx
1175 .send(Err(USimpleError::new(1, error_msg)))
1176 .map_err(|e| USimpleError::new(1, e.to_string()))?;
1177 }
1178 }
1179 }
1180
1181 drop(print_tx);
1182
1183 printing_thread
1184 .join()
1185 .map_err(|_| USimpleError::new(1, translate!("du-error-printing-thread-panicked")))??;
1186
1187 Ok(())
1188}
1189
1190fn parse_time_style(s: Option<&String>) -> UResult<String> {
1192 let s = match s {
1193 Some(s) => Some(s.into()),
1194 None => {
1195 match env::var("TIME_STYLE") {
1196 Ok(s) => {
1199 let s = s.strip_prefix("posix-").unwrap_or(s.as_str());
1200 let s = match s.chars().next().unwrap() {
1201 '+' => s.split('\n').next().unwrap(),
1202 _ => s,
1203 };
1204 match s {
1205 "locale" => None,
1206 _ => Some(s.to_string()),
1207 }
1208 }
1209 Err(_) => None,
1210 }
1211 }
1212 };
1213 match s {
1214 Some(s) => match s.as_ref() {
1215 "full-iso" => Ok(format::FULL_ISO.to_string()),
1216 "long-iso" => Ok(format::LONG_ISO.to_string()),
1217 "iso" => Ok(format::ISO.to_string()),
1218 _ => match s.chars().next().unwrap() {
1219 '+' => Ok(s[1..].to_string()),
1220 _ => Err(DuError::InvalidTimeStyleArg(s).into()),
1221 },
1222 },
1223 None => Ok(format::LONG_ISO.to_string()),
1224 }
1225}
1226
1227fn parse_depth(max_depth_str: Option<&str>, summarize: bool) -> UResult<Option<usize>> {
1228 let max_depth = max_depth_str.as_ref().and_then(|s| s.parse::<usize>().ok());
1229 match (max_depth_str, max_depth) {
1230 (Some(s), _) if summarize => Err(DuError::SummarizeDepthConflict(s.into()).into()),
1231 (Some(s), None) => Err(DuError::InvalidMaxDepthArg(s.into()).into()),
1232 (Some(_), Some(_)) | (None, _) => Ok(max_depth),
1233 }
1234}
1235
1236pub fn uu_app() -> Command {
1237 Command::new(uucore::util_name())
1238 .version(uucore::crate_version!())
1239 .help_template(uucore::localized_help_template(uucore::util_name()))
1240 .about(translate!("du-about"))
1241 .after_help(translate!("du-after-help"))
1242 .override_usage(format_usage(&translate!("du-usage")))
1243 .infer_long_args(true)
1244 .disable_help_flag(true)
1245 .arg(
1246 Arg::new(options::HELP)
1247 .long(options::HELP)
1248 .help(translate!("du-help-print-help"))
1249 .action(ArgAction::Help),
1250 )
1251 .arg(
1252 Arg::new(options::ALL)
1253 .short('a')
1254 .long(options::ALL)
1255 .help(translate!("du-help-all"))
1256 .action(ArgAction::SetTrue),
1257 )
1258 .arg(
1259 Arg::new(options::APPARENT_SIZE)
1260 .long(options::APPARENT_SIZE)
1261 .help(translate!("du-help-apparent-size"))
1262 .action(ArgAction::SetTrue),
1263 )
1264 .arg(
1265 Arg::new(options::BLOCK_SIZE)
1266 .short('B')
1267 .long(options::BLOCK_SIZE)
1268 .value_name("SIZE")
1269 .help(translate!("du-help-block-size")),
1270 )
1271 .arg(
1272 Arg::new(options::BYTES)
1273 .short('b')
1274 .long("bytes")
1275 .help(translate!("du-help-bytes"))
1276 .action(ArgAction::SetTrue),
1277 )
1278 .arg(
1279 Arg::new(options::TOTAL)
1280 .long("total")
1281 .short('c')
1282 .help(translate!("du-help-total"))
1283 .action(ArgAction::SetTrue),
1284 )
1285 .arg(
1286 Arg::new(options::MAX_DEPTH)
1287 .short('d')
1288 .long("max-depth")
1289 .value_name("N")
1290 .help(translate!("du-help-max-depth")),
1291 )
1292 .arg(
1293 Arg::new(options::HUMAN_READABLE)
1294 .long("human-readable")
1295 .short('h')
1296 .help(translate!("du-help-human-readable"))
1297 .action(ArgAction::SetTrue),
1298 )
1299 .arg(
1300 Arg::new(options::INODES)
1301 .long(options::INODES)
1302 .help(translate!("du-help-inodes"))
1303 .action(ArgAction::SetTrue),
1304 )
1305 .arg(
1306 Arg::new(options::BLOCK_SIZE_1K)
1307 .short('k')
1308 .help(translate!("du-help-block-size-1k"))
1309 .action(ArgAction::SetTrue),
1310 )
1311 .arg(
1312 Arg::new(options::COUNT_LINKS)
1313 .short('l')
1314 .long("count-links")
1315 .help(translate!("du-help-count-links"))
1316 .action(ArgAction::SetTrue),
1317 )
1318 .arg(
1319 Arg::new(options::DEREFERENCE)
1320 .short('L')
1321 .long(options::DEREFERENCE)
1322 .help(translate!("du-help-dereference"))
1323 .action(ArgAction::SetTrue),
1324 )
1325 .arg(
1326 Arg::new(options::DEREFERENCE_ARGS)
1327 .short('D')
1328 .visible_short_alias('H')
1329 .long(options::DEREFERENCE_ARGS)
1330 .help(translate!("du-help-dereference-args"))
1331 .action(ArgAction::SetTrue),
1332 )
1333 .arg(
1334 Arg::new(options::NO_DEREFERENCE)
1335 .short('P')
1336 .long(options::NO_DEREFERENCE)
1337 .help(translate!("du-help-no-dereference"))
1338 .overrides_with(options::DEREFERENCE)
1339 .action(ArgAction::SetTrue),
1340 )
1341 .arg(
1342 Arg::new(options::BLOCK_SIZE_1M)
1343 .short('m')
1344 .help(translate!("du-help-block-size-1m"))
1345 .action(ArgAction::SetTrue),
1346 )
1347 .arg(
1348 Arg::new(options::NULL)
1349 .short('0')
1350 .long("null")
1351 .help(translate!("du-help-null"))
1352 .action(ArgAction::SetTrue),
1353 )
1354 .arg(
1355 Arg::new(options::SEPARATE_DIRS)
1356 .short('S')
1357 .long("separate-dirs")
1358 .help(translate!("du-help-separate-dirs"))
1359 .action(ArgAction::SetTrue),
1360 )
1361 .arg(
1362 Arg::new(options::SUMMARIZE)
1363 .short('s')
1364 .long("summarize")
1365 .help(translate!("du-help-summarize"))
1366 .action(ArgAction::SetTrue),
1367 )
1368 .arg(
1369 Arg::new(options::SI)
1370 .long(options::SI)
1371 .help(translate!("du-help-si"))
1372 .action(ArgAction::SetTrue),
1373 )
1374 .arg(
1375 Arg::new(options::ONE_FILE_SYSTEM)
1376 .short('x')
1377 .long(options::ONE_FILE_SYSTEM)
1378 .help(translate!("du-help-one-file-system"))
1379 .action(ArgAction::SetTrue),
1380 )
1381 .arg(
1382 Arg::new(options::THRESHOLD)
1383 .short('t')
1384 .long(options::THRESHOLD)
1385 .value_name("SIZE")
1386 .num_args(1)
1387 .allow_hyphen_values(true)
1388 .help(translate!("du-help-threshold")),
1389 )
1390 .arg(
1391 Arg::new(options::VERBOSE)
1392 .short('v')
1393 .long("verbose")
1394 .help(translate!("du-help-verbose"))
1395 .action(ArgAction::SetTrue),
1396 )
1397 .arg(
1398 Arg::new(options::EXCLUDE)
1399 .long(options::EXCLUDE)
1400 .value_name("PATTERN")
1401 .help(translate!("du-help-exclude"))
1402 .action(ArgAction::Append),
1403 )
1404 .arg(
1405 Arg::new(options::EXCLUDE_FROM)
1406 .short('X')
1407 .long("exclude-from")
1408 .value_name("FILE")
1409 .value_hint(clap::ValueHint::FilePath)
1410 .help(translate!("du-help-exclude-from"))
1411 .action(ArgAction::Append),
1412 )
1413 .arg(
1414 Arg::new(options::FILES0_FROM)
1415 .long("files0-from")
1416 .value_name("FILE")
1417 .value_hint(clap::ValueHint::FilePath)
1418 .value_parser(clap::value_parser!(OsString))
1419 .help(translate!("du-help-files0-from"))
1420 .action(ArgAction::Append),
1421 )
1422 .arg(
1423 Arg::new(options::TIME)
1424 .long(options::TIME)
1425 .value_name("WORD")
1426 .require_equals(true)
1427 .num_args(0..)
1428 .value_parser(ShortcutValueParser::new([
1429 PossibleValue::new("atime").alias("access").alias("use"),
1430 PossibleValue::new("ctime").alias("status"),
1431 PossibleValue::new("creation").alias("birth"),
1432 ]))
1433 .help(translate!("du-help-time")),
1434 )
1435 .arg(
1436 Arg::new(options::TIME_STYLE)
1437 .long(options::TIME_STYLE)
1438 .value_name("STYLE")
1439 .help(translate!("du-help-time-style")),
1440 )
1441 .arg(
1442 Arg::new(options::FILE)
1443 .hide(true)
1444 .value_hint(clap::ValueHint::AnyPath)
1445 .value_parser(clap::value_parser!(OsString))
1446 .action(ArgAction::Append),
1447 )
1448}
1449
1450#[derive(Clone, Copy)]
1451enum Threshold {
1452 Lower(u64),
1453 Upper(u64),
1454}
1455
1456impl FromStr for Threshold {
1457 type Err = ParseSizeError;
1458
1459 fn from_str(s: &str) -> Result<Self, Self::Err> {
1460 let offset = usize::from(s.starts_with(&['-', '+'][..]));
1461
1462 let size = parse_size_u64(&s[offset..])?;
1463
1464 if s.starts_with('-') {
1465 if size == 0 {
1468 return Err(ParseSizeError::ParseFailure(s.to_string()));
1469 }
1470 Ok(Self::Upper(size))
1471 } else {
1472 Ok(Self::Lower(size))
1473 }
1474 }
1475}
1476
1477impl Threshold {
1478 fn should_exclude(&self, size: u64) -> bool {
1479 match *self {
1480 Self::Upper(threshold) => size > threshold,
1481 Self::Lower(threshold) => size < threshold,
1482 }
1483 }
1484}
1485
1486fn format_error_message(error: &ParseSizeError, s: &str, option: &str) -> String {
1487 match error {
1490 ParseSizeError::InvalidSuffix(_) => {
1491 translate!("du-error-invalid-suffix", "option" => option, "value" => s.quote())
1492 }
1493 ParseSizeError::ParseFailure(_) | ParseSizeError::PhysicalMem(_) => {
1494 translate!("du-error-invalid-argument", "option" => option, "value" => s.quote())
1495 }
1496 ParseSizeError::SizeTooBig(_) => {
1497 translate!("du-error-argument-too-large", "option" => option, "value" => s.quote())
1498 }
1499 }
1500}
1501
1502#[cfg(test)]
1503mod test_du {
1504 #[allow(unused_imports)]
1505 use super::*;
1506
1507 #[test]
1508 fn test_read_block_size() {
1509 let test_data = [Some("1024".to_string()), Some("K".to_string()), None];
1510 for it in &test_data {
1511 assert!(matches!(read_block_size(it.as_deref()), Ok(1024)));
1512 }
1513 }
1514}