8000 du: add support for --exclude and --exclude-from by sylvestre · Pull Request #3381 · uutils/coreutils · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

du: add support for --exclude and --exclude-from #3381

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/uu/du/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ path = "src/du.rs"

[dependencies]
chrono = "^0.4.11"
# For the --exclude & --exclude-from options
glob = "0.3.0"
clap = { version = "3.1", features = ["wrap_help", "cargo"] }
uucore = { version=">=0.0.11", package="uucore", path="../../uucore" }

Expand Down
230 changes: 178 additions & 52 deletions src/uu/du/src/du.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ extern crate uucore;
use chrono::prelude::DateTime;
use chrono::Local;
use clap::{crate_version, Arg, ArgMatches, Command};
use glob::Pattern;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::fs::File;
#[cfg(not(windows))]
use std::fs::Metadata;
use std::io::BufRead;
use std::io::BufReader;
use std::io::{ErrorKind, Result};
use std::iter;
#[cfg(not(windows))]
Expand All @@ -24,7 +28,6 @@ use std::os::unix::fs::MetadataExt;
use std::os::windows::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::io::AsRawHandle;
#[cfg(windows)]
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
Expand Down Expand Up @@ -68,6 +71,9 @@ mod options {
pub const ONE_FILE_SYSTEM: &str = "one-file-system";
pub const DEREFERENCE: &str = "dereference";
pub const INODES: &str = "inodes";
pub const EXCLUDE: &str = "exclude";
pub const EXCLUDE_FROM: &str = "exclude-from";
pub const VERBOSE: &str = "verbose";
pub const FILE: &str = "FILE";
}

Expand All @@ -80,6 +86,12 @@ Otherwise, units default to 1024 bytes (or 512 if POSIXLY_CORRECT is set).
SIZE is an integer and optional unit (example: 10M is 10*1024*1024).
Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers
of 1000).

PATTERN allows some advanced exclusions. For example, the following syntaxes
are supported:
? will match only one character
* will match zero or more characters
{a,b} will match a or b
";
const USAGE: &str = "\
{} [OPTION]... [FILE]...
Expand All @@ -97,6 +109,7 @@ struct Options {
one_file_system: bool,
dereference: bool,
inodes: bool,
verbose: bool,
}

#[derive(PartialEq, Eq, Hash, Clone, Copy)]
Expand Down Expand Up @@ -287,6 +300,7 @@ fn du(
options: &Options,
depth: usize,
inodes: &mut HashSet<FileInfo>,
exclude: &[Pattern],
) -> Box<dyn DoubleEndedIterator<Item = Stat>> {
let mut stats = vec![];
let mut futures = vec![];
Expand All @@ -306,49 +320,68 @@ fn du(
}
};

for f in read {
'file_loop: for f in read {
match f {
Ok(entry) => match Stat::new(entry.path(), options) {
Ok(this_stat) => {
if let Some(inode) = this_stat.inode {
if inodes.contains(&inode) {
continue;
}
inodes.insert(inode);
}
if this_stat.is_dir {
if options.one_file_system {
if let (Some(this_inode), Some(my_inode)) =
(this_stat.inode, my_stat.inode)
Ok(entry) => {
match Stat::new(entry.path(), options) {
Ok(this_stat) => {
// We have an exclude list
for pattern in exclude {
// Look at all patterns with both short and long paths
// if we have 'du foo' but search to exclude 'foo/bar'
// we need the full path
if pattern.matches(&this_stat.path.to_string_lossy())
|| pattern.matches(&entry.file_name().into_string().unwrap())
{
if this_inode.dev_id != my_inode.dev_id {
continue;
// if the directory is ignored, leave early
if options.verbose {
println!("{} ignored", &this_stat.path.quote());
}
// Go to the next file
continue 'file_loop;
}
}

if let Some(inode) = this_stat.inode {
if inodes.contains(&inode) {
continue;
}
inodes.insert(inode);
}
futures.push(du(this_stat, options, depth + 1, inodes));
} else {
my_stat.size += this_stat.size;
my_stat.blocks += this_stat.blocks;
my_stat.inodes += 1;
if options.all {
stats.push(this_stat);
if this_stat.is_dir {
if options.one_file_system {
if let (Some(this_inode), Some(my_inode)) =
(this_stat.inode, my_stat.inode)
{
if this_inode.dev_id != my_inode.dev_id {
continue;
}
}
}
futures.push(du(this_stat, options, depth + 1, inodes, exclude));
} else {
my_stat.size += this_stat.size;
my_stat.blocks += this_stat.blocks;
my_stat.inodes += 1;
if options.all {
stats.push(this_stat);
}
}
}
Err(error) => match error.kind() {
ErrorKind::PermissionDenied => {
let description = format!("cannot access {}", entry.path().quote());
let error_message = "Permission denied";
show_error_custom_description!(description, "{}", error_message);
set_exit_code(1);
}
_ => {
set_exit_code(1);
show_error!("cannot access {}: {}", entry.path().quote(), error);
}
},
}
Err(error) => match error.kind() {
ErrorKind::PermissionDenied => {
let description = format!("cannot access {}", entry.path().quote());
let error_message = "Permission denied";
show_error_custom_description!(description, "{}", error_message);
set_exit_code(1);
}
_ => {
set_exit_code(1);
show_error!("cannot access {}: {}", entry.path().quote(), error);
}
},
},
}
Err(error) => show_error!("{}", error),
}
}
Expand Down Expand Up @@ -406,6 +439,7 @@ enum DuError {
SummarizeDepthConflict(String),
InvalidTimeStyleArg(String),
InvalidTimeArg(String),
InvalidGlob(String),
}

impl Display for DuError {
Expand Down Expand Up @@ -436,6 +470,7 @@ Try '{} --help' for more information.",
'birth' and 'creation' arguments are not supported on this platform.",
s.quote()
),
DuError::InvalidGlob(s) => write!(f, "Invalid exclude syntax: {}", s),
}
}
}
Expand All @@ -448,11 +483,75 @@ impl UError for DuError {
Self::InvalidMaxDepthArg(_)
| Self::SummarizeDepthConflict(_)
| Self::InvalidTimeStyleArg(_)
| Self::InvalidTimeArg(_) => 1,
| Self::InvalidTimeArg(_)
| Self::InvalidGlob(_) => 1,
}
}
}

// Read a file and return each line in a vector of String
fn file_as_vec(filename: impl AsRef<Path>) -> Vec<String> {
let file = File::open(filename).expect("no such file");
let buf = BufReader::new(file);

buf.lines()
.map(|l| l.expect("Could not parse line"))
.collect()
}

// Given the --exclude-from and/or --exclude arguments, returns the globset lists
// to ignore the files
fn get_glob_ignore(matches: &ArgMatches) -> UResult<Vec<Pattern>> {
let mut excludes_from = if matches.is_present(options::EXCLUDE_FROM) {
match matches.values_of(options::EXCLUDE_FROM) {
Some(all_files) => {
let mut exclusion = Vec::<String>::new();
// Read the exclude lists from all the files
// and add them into a vector of string
let files: Vec<String> = all_files.clone().map(|v| v.to_owned()).collect();
for f in files {
exclusion.extend(file_as_vec(&f));
}
exclusion
}
None => Vec::<String>::new(),
}
} else {
Vec::<String>::new()
};

let mut excludes = if matches.is_present(options::EXCLUDE) {
match matches.values_of(options::EXCLUDE) {
Some(v) => {
// Read the various arguments
v.clone().map(|v| v.to_owned()).collect()
}
None => Vec::<String>::new(),
}
} else {
Vec::<String>::new()
};

// Merge the two lines
excludes.append(&mut excludes_from);
if !&excludes.is_empty() {
let mut builder = Vec::new();
// Create the `Vec` of excludes
for f in excludes {
if matches.is_present(options::VERBOSE) {
println!("adding {:?} to the exclude list ", &f);
}
match Pattern::new(&f) {
Ok(glob) => builder.push(glob),
Err(err) => return Err(DuError::InvalidGlob(err.to_string()).into()),
};
}
Ok(builder)
} else {
Ok(Vec::new())
}
}

#[uucore::main]
#[allow(clippy::cognitive_complexity)]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Expand All @@ -475,6 +574,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
one_file_system: matches.is_present(options::ONE_FILE_SYSTEM),
dereference: matches.is_present(options::DEREFERENCE),
inodes: matches.is_present(options::INODES),
verbose: matches.is_present(options::VERBOSE),
};

let files = match matches.value_of(options::FILE) {
Expand Down Expand Up @@ -529,16 +629,33 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
"\n"
};

let excludes = get_glob_ignore(&matches)?;

let mut grand_total = 0;
for path_string in files {
'loop_file: for path_string in files {
// Skip if we don't want to ignore anything
if !&excludes.is_empty() {
for pattern in &excludes {
{
if pattern.matches(path_string) {
// if the directory is ignored, leave early
if options.verbose {
println!("{} ignored", path_string.quote());
}
continue 'loop_file;
}
}
}
}

let path = PathBuf::from(&path_string);
match Stat::new(path, &options) {
Ok(stat) => {
let mut inodes: HashSet<FileInfo> = HashSet::new();
if let Some(inode) = stat.inode {
inodes.insert(inode);
}
let iter = du(stat, &options, 0, &mut inodes);
let iter = du(stat, &options, 0, &mut inodes, &excludes);
let (_, len) = iter.size_hint();
let len = len.unwrap();
for (index, stat) in iter.enumerate() {
Expand Down Expand Up @@ -763,19 +880,28 @@ pub fn uu_app<'a>() -> Command<'a> {
.help("exclude entries smaller than SIZE if positive, \
or entries greater than SIZE if negative")
)
// .arg(
// Arg::new("")
// .short('x')
// .long("exclude-from")
// .value_name("FILE")
// .help("exclude files that match any pattern in FILE")
// )
// .arg(
// Arg::new("exclude")
// .long("exclude")
// .value_name("PATTERN")
// .help("exclude files that match PATTERN")
// )
.arg(
Arg::new(options::VERBOSE)
.short('v')
.long("verbose")
.help("verbose mode (option not present in GNU/Coreutils)")
)
.arg(
Arg::new(options::EXCLUDE)
.long(options::EXCLUDE)
.value_name("PATTERN")
.help("exclude files that match PATTERN")
.multiple_occurrences(true)
)
.arg(
Arg::new(options::EXCLUDE_FROM)
.short('X')
.long("exclude-from")
.value_name("FILE")
.help("exclude files that match any pattern in FILE")
.multiple_occurrences(true)

)
.arg(
Arg::new(options::TIME)
.long(options::TIME)
Expand Down
Loading
0