use clap::{Parser, crate_description, crate_version};
use ignore::gitignore::Gitignore;
use rayon::iter::*;
use std::{fmt::Debug, fs, path::Path};
#[derive(Parser, Debug)]
#[command(version = crate_version!(), about = crate_description!(), long_about = None, color = clap::ColorChoice::Always)]
struct Args {
#[arg(value_name = "path", default_value = ".")]
files: Vec<String>,
#[arg(short, default_value = None)]
depth: Option<usize>,
#[arg(short, default_value_t = false)]
bytes: bool,
#[arg(long, short, default_value_t = false)]
ignore: bool,
#[arg(long, short = 'c', default_value_t = false)]
no_color: bool,
}
struct Options {
max_depth: Option<usize>,
bytes: bool,
ignore: bool,
no_color: bool,
}
fn main() {
let args = Args::parse();
let options = Options {
max_depth: args.depth,
bytes: args.bytes,
ignore: args.ignore,
no_color: args.no_color,
};
args.files.iter().for_each(|path| {
let (gitignore, _) = Gitignore::new(Path::new(path).join(".gitignore"));
print_path(path, 0, &options, &gitignore);
});
}
fn print_path<P: AsRef<Path>>(
path: P,
depth: usize,
options: &Options,
gitignore: &Gitignore,
) -> u64 {
let path = path.as_ref();
let meta = match fs::symlink_metadata(path) {
Ok(meta) => meta,
Err(e) => {
eprintln!("{}: {}", path.display(), e);
return 0;
}
};
if options.ignore {
#[cfg(windows)]
let hidden = gitignore.matched(path, meta.is_dir()).is_ignore() || is_hidden(&meta);
#[cfg(not(windows))]
let hidden = gitignore.matched(path, meta.is_dir()).is_ignore() || is_hidden(path);
if hidden {
return 0;
}
};
if meta.is_dir() {
match fs::read_dir(path) {
Ok(entries) => {
let size = entries
.par_bridge()
.map(|entry| match entry {
Ok(entry) => print_path(entry.path(), depth + 1, options, gitignore),
Err(e) => {
eprintln!("{}", e);
0
}
})
.sum();
if options.max_depth.is_none_or(|d| d >= depth) {
print_size(size, path.display(), options.bytes, options.no_color);
}
return size;
}
Err(e) => {
match e.kind() {
std::io::ErrorKind::PermissionDenied => {
eprintln!("{}: Permission denied", path.display())
}
std::io::ErrorKind::NotFound => {
eprintln!("{}: Not found", path.display())
}
_ => {
eprintln!("{}: {}", path.display(), e)
}
}
return 0;
}
}
}
let size = get_file_size(&meta, path);
if depth == 0 {
std::hint::cold_path();
print_size(size, path.display(), options.bytes, options.no_color);
}
size
}
fn get_file_size(meta: &fs::Metadata, path: &Path) -> u64 {
let size;
#[cfg(not(windows))]
{
_ = path;
use std::os::unix::fs::MetadataExt;
size = meta.blocks() * 512;
}
#[cfg(windows)]
{
size = get_size_on_disk(path);
}
size
}
#[cfg(windows)]
fn get_size_on_disk(path: &Path) -> u64 {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::{
Foundation::HANDLE,
Storage::FileSystem::{FILE_STANDARD_INFO, FileStandardInfo, GetFileInformationByHandleEx},
};
let mut size_on_disk = 0;
let Ok(file) = std::fs::File::open(path) else {
return size_on_disk; };
unsafe {
let mut file_info: FILE_STANDARD_INFO = core::mem::zeroed();
let file_info_ptr: *mut FILE_STANDARD_INFO = &mut file_info;
let success = GetFileInformationByHandleEx(
file.as_raw_handle() as HANDLE,
FileStandardInfo,
file_info_ptr.cast(),
core::mem::size_of::<FILE_STANDARD_INFO>() as u32,
);
if success != 0 {
size_on_disk = file_info.AllocationSize as u64;
}
}
size_on_disk
}
#[cfg(windows)]
fn is_hidden(meta: &std::fs::Metadata) -> bool {
use std::os::windows::fs::MetadataExt;
meta.file_attributes() & 0x2 != 0
}
#[cfg(not(windows))]
fn is_hidden(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.starts_with("."))
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
const HUMAN_SIZE: humansize::FormatSizeOptions = humansize::BINARY;
#[cfg(not(target_os = "linux"))]
const HUMAN_SIZE: humansize::FormatSizeOptions = humansize::DECIMAL;
fn print_size<T: std::fmt::Display>(size: u64, path: T, print_bytes: bool, print_no_color: bool) {
if print_bytes {
if !print_no_color {
println!("\x1b[1;33m{:<10}\x1b[0m \x1b[1;36m{}\x1b[0m", size, path);
} else {
println!("{:<10} {}", size, path);
}
} else {
let humansize = humansize::format_size(size, HUMAN_SIZE.space_after_value(false));
if !print_no_color {
println!(
"\x1b[1;33m{:<10}\x1b[0m \x1b[1;36m{}\x1b[0m",
humansize, path
);
} else {
println!("{:<10} {}", humansize, path);
}
}
}