233 lines
6.2 KiB
Rust
233 lines
6.2 KiB
Rust
use clap::{Parser, ArgAction};
|
|
use glob::Pattern;
|
|
|
|
use crate::unit::Unit;
|
|
|
|
use std::fs::{symlink_metadata, Metadata};
|
|
use std::path::{Component, Path};
|
|
use std::slice::Iter;
|
|
use std::iter::once_with;
|
|
|
|
#[allow(clippy::struct_excessive_bools)]
|
|
#[derive(Parser, Debug, Clone)]
|
|
pub struct Args {
|
|
#[arg(
|
|
short, long,
|
|
help = "keep going if an error occurs",
|
|
long_help = "keep going if an error occurs, silencing them in the process",
|
|
default_value_t = false,
|
|
)]
|
|
persistant: bool,
|
|
|
|
#[arg(
|
|
short, long,
|
|
help = "suppress error messages",
|
|
long_help = "suppress error messages, but still quit if an error occurs",
|
|
default_value_t = false,
|
|
)]
|
|
quiet: bool,
|
|
|
|
#[arg(
|
|
short, long,
|
|
help = "minimize output",
|
|
long_help = "print nothing but the total size for all directories, without a newline. Also supresses all error messages",
|
|
conflicts_with = "total",
|
|
default_value_t = false,
|
|
)]
|
|
minimal: bool,
|
|
|
|
#[arg(
|
|
short='T', long,
|
|
help = "display in tree",
|
|
default_value_t = false,
|
|
conflicts_with = "minimal",
|
|
)]
|
|
tree: bool,
|
|
|
|
#[arg(
|
|
short, long,
|
|
help = "display the total size",
|
|
conflicts_with = "minimal",
|
|
default_value_t = false,
|
|
)]
|
|
total: bool,
|
|
|
|
#[arg(
|
|
short='2', long,
|
|
help = "alias for --unit 1024",
|
|
default_value_t = false,
|
|
conflicts_with_all = ["si","unit"],
|
|
)]
|
|
base_two: bool,
|
|
|
|
#[arg(
|
|
short='k', long,
|
|
help = "alias for --unit 1000",
|
|
default_value_t = false,
|
|
conflicts_with_all = ["base_two","unit"],
|
|
)]
|
|
si: bool,
|
|
|
|
#[arg(
|
|
short, long,
|
|
help = "unit to print in",
|
|
long_help = "printing unit (case insensitive): b = bytes, kb = kilobytes, ki = kibibytes, gb = gigabytes, gi = gibibytes, tb = terabytes, ti = tibibytes",
|
|
value_parser = Unit::parse,
|
|
default_value_t = Unit::Byte,
|
|
conflicts_with_all = ["base_two","si"],
|
|
)]
|
|
unit: Unit,
|
|
|
|
#[arg(
|
|
short='s', long,
|
|
help = "follow symlinks",
|
|
default_value_t = false,
|
|
)]
|
|
follow_links: bool,
|
|
|
|
#[arg(
|
|
short='x', long = "exclude",
|
|
help = "include in search, but exclude from printing",
|
|
long_help = "include in search, but exclude from printing. accepts glob syntax. separate rules by comma",
|
|
default_values_t = once_with(|| Pattern::new(".*").unwrap()),
|
|
value_parser = parse_glob,
|
|
value_delimiter = ',',
|
|
action = ArgAction::Append,
|
|
)]
|
|
exclude_print: Vec<Pattern>,
|
|
|
|
#[arg(
|
|
short='X', long,
|
|
help = "exclude from search and printing",
|
|
long_help = "exclude from search and printin. accepts glob syntax. separate rules by comma",
|
|
value_parser = parse_glob,
|
|
value_delimiter = ',',
|
|
action = ArgAction::Append,
|
|
)]
|
|
exclude_search: Vec<Pattern>,
|
|
|
|
#[arg(
|
|
short='H', long,
|
|
help = "disable implicit hiding of results",
|
|
long_help = "don't implicitly hide dotfiles and dot directories",
|
|
conflicts_with = "exclude_print",
|
|
default_value_t = false,
|
|
)]
|
|
show_hidden: bool,
|
|
|
|
#[arg(
|
|
value_parser = validate_path,
|
|
help = "items to summate",
|
|
action = ArgAction::Append,
|
|
num_args = 1..
|
|
)]
|
|
path: Vec<String>,
|
|
}
|
|
impl Args {
|
|
/// utility method to chuck default values on the end.
|
|
/// it feels like I should be able to do this with
|
|
/// clever `clap` macros but I don't know how
|
|
pub fn parse_and_process() -> Self {
|
|
let mut this = Self::parse();
|
|
|
|
if this.base_two {
|
|
this.unit = Unit::Kibi;
|
|
} else if this.si {
|
|
this.unit = Unit::Kilo;
|
|
}
|
|
|
|
if this.show_hidden {
|
|
this.exclude_print = Vec::new();
|
|
}
|
|
|
|
if this.path.is_empty() {
|
|
this.path = vec![ ".".to_owned() ];
|
|
}
|
|
|
|
this
|
|
}
|
|
|
|
pub fn should_exclude(&self, path: &Path, file: &Metadata) -> bool {
|
|
if !self.follow_links
|
|
&& file.is_symlink()
|
|
// `.` counts as a symlink. excluding . is usually not
|
|
// useful, so even if this is a symlink when we're not
|
|
// supposed to be following them, if this opens with a
|
|
// CurDir component then we shouldn't exclude
|
|
&& !matches!(path.components().nth(0), Some(Component::CurDir))
|
|
{
|
|
return true
|
|
}
|
|
|
|
any_pattern_matches_any_component(&self.exclude_search, path)
|
|
}
|
|
|
|
pub fn should_print(&self, path: &Path) -> bool {
|
|
! any_pattern_matches_any_component(&self.exclude_print, path)
|
|
}
|
|
|
|
pub const fn persistant(&self) -> bool {
|
|
self.persistant
|
|
}
|
|
|
|
pub const fn minimal(&self) -> bool {
|
|
self.minimal
|
|
}
|
|
|
|
pub const fn tree(&self) -> bool {
|
|
self.tree
|
|
}
|
|
|
|
pub const fn total(&self) -> bool {
|
|
self.total
|
|
}
|
|
|
|
pub const fn unit(&self) -> Unit {
|
|
self.unit
|
|
}
|
|
|
|
pub fn iter(&self) -> Iter<'_, String> {
|
|
self.path.iter()
|
|
}
|
|
|
|
pub fn quiet(&self) -> bool {
|
|
self.quiet
|
|
}
|
|
}
|
|
|
|
fn validate_path(s: &str) -> Result<String, String> {
|
|
// try to access it's metadata, since that is what is used
|
|
// to get its length. using symlink because that's what's
|
|
// used in the actual program
|
|
symlink_metadata(s)
|
|
.map(|_| s.to_string())
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
fn parse_glob(s: &str) -> Result<Pattern, String> {
|
|
Pattern::new(s).map_err(|globerr| format!("invalid glob \"{s}\": {}", globerr.msg))
|
|
}
|
|
|
|
fn any_pattern_matches_any_component(patterns: &[Pattern], path: &Path) -> bool {
|
|
for pat in patterns {
|
|
for cmp in path.components() {
|
|
let Component::Normal(cmp) = cmp else {
|
|
// this seems wacky, but only normal components
|
|
// are useful
|
|
continue
|
|
};
|
|
let Some(s) = cmp.to_str() else {
|
|
// this is a code smell
|
|
// I don't believe it, but I can't think
|
|
// of anything worthwhile to do when
|
|
// you can't get a usable &str
|
|
continue
|
|
};
|
|
if pat.matches(s) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
false
|
|
} |