hb/src/args.rs

204 lines
5.5 KiB
Rust

use clap::{Parser, ArgAction};
use glob::Pattern;
use crate::unit::Unit;
use std::fs::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, default_value_t = false,
help = "keep going if an error occurs",
long_help = "keep going if an error occurs (ex. unreadable subdirectories in a readable directory)"
)]
persistant: 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",
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",
default_values_t = once_with(|| Pattern::new("").unwrap()),
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 {
pub fn post_process(mut self) -> Self {
if self.base_two {
self.unit = Unit::Kibi;
} else if self.si {
self.unit = Unit::Kilo;
}
if self.show_hidden {
self.exclude_print = Vec::new();
}
if self.path.is_empty() {
self.path = vec![ ".".to_owned() ];
}
self
}
pub fn should_exclude(&self, path: &Path, file: &Metadata) -> bool {
if !self.follow_links && file.is_symlink() {
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)
// TODO: this exists because when a file matches an exclude pattern
// is it still returned, just with no size or children, so in order
// to not accidentally print things that we said we were excluding,
// we also have to check that it's not excluded by search.
// `self.exclude_print.extend(&self.exclude_search)` is wasteful,
// but until I find a better way this is what it's gotta be`
&& ! any_pattern_matches_any_component(&self.exclude_search, 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()
}
}
fn validate_path(s: &str) -> Result<String, String> {
// try to access it's metadata, since that is what is used
// to get its length
std::fs::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(|_| format!("invalid glob: {s}"))
}
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 { continue };
let Some(s) = cmp.to_str() else { continue };
if pat.matches(s) {
return true
}
}
}
false
}