use clap::{Arg, ArgAction, Command};
use color_eyre::Result;
use color_eyre::eyre::eyre;
use std::env;
#[cfg(unix)]
use std::fs::File;
#[cfg(unix)]
use std::io::{BufRead, BufReader};
use std::path::Path;
#[cfg(unix)]
use crate::posix::{posix_statfs, PosixStatFs, PosixError};
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct DfFsStat {
f_bsize: u64,
f_blocks: u64,
f_bfree: u64,
f_bavail: u64,
f_files: u64,
f_ffree: u64,
}
#[cfg(unix)]
fn statfs_to_df(mut s: PosixStatFs, inodes: bool) -> DfFsStat {
if inodes {
s.f_blocks = s.f_files;
s.f_bfree = s.f_ffree;
s.f_bavail = s.f_ffree;
s.f_bsize = 1;
}
DfFsStat {
f_bsize: s.f_bsize,
f_blocks: s.f_blocks,
f_bfree: s.f_bfree,
f_bavail: s.f_bavail,
f_files: s.f_files,
f_ffree: s.f_ffree,
}
}
#[cfg(windows)]
fn statfs_to_df_windows(
total: u64,
free_total: u64,
avail: u64,
bsize: u64,
_inodes: bool,
) -> DfFsStat {
let blocks = (total + bsize - 1) / bsize;
let bfree = (free_total + bsize - 1) / bsize;
let bavail = (avail + bsize - 1) / bsize;
DfFsStat {
f_bsize: bsize,
f_blocks: blocks,
f_bfree: bfree,
f_bavail: bavail,
f_files: 0,
f_ffree: 0,
}
}
#[derive(Debug)]
pub struct DfOptions {
pub posix_format: bool,
pub human_readable: bool,
pub human_decimal: bool,
pub mega_bytes: bool,
pub kilo_bytes: bool,
pub block_size: Option<u64>,
pub show_fs_type: bool,
pub inodes: bool,
pub all_filesystems: bool,
pub fs_type_filter: Option<String>,
pub filesystems: Vec<String>,
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<DfOptions> {
let posix_format = matches.get_flag("posix");
let human_readable = matches.get_flag("human-readable");
let human_decimal = matches.get_flag("human-decimal");
let mega_bytes = matches.get_flag("mega-bytes");
let kilo_bytes = matches.get_flag("kilo-bytes");
let block_size = matches.get_one::<String>("block-size").map(|s| s.to_string());
let show_fs_type = matches.get_flag("print-type");
let inodes = matches.get_flag("inodes");
let all_filesystems = matches.get_flag("all");
let fs_type_filter = matches.get_one::<String>("type").map(|s| s.to_string());
let filesystems: Vec<String> = matches.get_many::<String>("filesystem")
.map(|vals| vals.cloned().collect())
.unwrap_or_default();
let block_size_parsed = if let Some(bs) = block_size {
Some(parse_block_size(&bs)?)
} else {
None
};
Ok(DfOptions {
posix_format,
human_readable,
human_decimal,
mega_bytes,
kilo_bytes,
block_size: block_size_parsed,
show_fs_type,
inodes,
all_filesystems,
fs_type_filter,
filesystems,
})
}
fn parse_block_size(s: &str) -> Result<u64> {
if s.is_empty() {
return Err(eyre!("empty block size"));
}
let suffix = s.chars().last().unwrap();
let (num_str, multiplier) = match suffix {
'0'..='9' => (s, 1),
'K' | 'k' => (&s[..s.len()-1], 1024),
'M' | 'm' => (&s[..s.len()-1], 1024 * 1024),
'G' | 'g' => (&s[..s.len()-1], 1024 * 1024 * 1024),
'T' | 't' => (&s[..s.len()-1], 1024 * 1024 * 1024 * 1024),
'P' | 'p' => (&s[..s.len()-1], 1024 * 1024 * 1024 * 1024 * 1024),
'E' | 'e' => (&s[..s.len()-1], 1024 * 1024 * 1024 * 1024 * 1024 * 1024),
_ => (s, 1),
};
let num: u64 = num_str.parse()
.map_err(|_| eyre!("invalid block size: '{}'", s))?;
Ok(num * multiplier)
}
pub fn command() -> Command {
Command::new("df")
.about("Report filesystem disk space usage")
.disable_help_flag(true)
.arg(
Arg::new("posix")
.short('P')
.long("portability")
.help("Use POSIX output format")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("human-readable")
.short('h')
.help("Print sizes in human readable format (e.g., 1K 234M 2G)")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("human-decimal")
.short('H')
.help("Same as -h, but use powers of 1000 instead of 1024")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("mega-bytes")
.short('m')
.help("Display values in 1MB blocks")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("kilo-bytes")
.short('k')
.help("Display values in 1KB blocks (default)")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("block-size")
.short('B')
.long("block-size")
.help("Scale sizes by SIZE before printing them (e.g., '-BM' prints sizes in megabytes)")
.value_name("SIZE")
)
.arg(
Arg::new("print-type")
.short('T')
.help("Print filesystem type")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("inodes")
.short('i')
.help("List inode information instead of block usage (Unix only; not supported on Windows)")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("all")
.short('a')
.long("all")
.help("Include dummy, duplicate or inaccessible filesystems")
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("type")
.short('t')
.help("Limit listing to filesystems of type TYPE")
.value_name("TYPE")
)
.arg(
Arg::new("filesystem")
.num_args(0..)
.help("Filesystem or mount point to show (default: all mounted filesystems)")
)
.arg(Arg::new("help").long("help").action(clap::ArgAction::Help))
}
fn format_blocks(blocks: u64, block_size: u64, display_block_size: u64, human_readable: bool, human_decimal: bool, _posix_format: bool) -> String {
if human_readable || human_decimal {
format_size_human(blocks * block_size, human_decimal)
} else if display_block_size == 1 {
blocks.to_string()
} else {
let total_bytes = blocks as u128 * block_size as u128;
let display_blocks = (total_bytes + (display_block_size as u128 / 2)) / display_block_size as u128;
display_blocks.to_string()
}
}
fn format_size_human(size: u64, decimal: bool) -> String {
const UNITS_BINARY: &[&str] = &["B", "K", "M", "G", "T", "P", "E"];
const UNITS_DECIMAL: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB", "EB"];
let units = if decimal { UNITS_DECIMAL } else { UNITS_BINARY };
let base = if decimal { 1000.0 } else { 1024.0 };
let mut size_f = size as f64;
let mut unit_idx = 0;
while size_f >= base && unit_idx < units.len() - 1 {
size_f /= base;
unit_idx += 1;
}
if unit_idx == 0 {
format!("{}", size as u64)
} else {
format!("{:.1}{}", size_f, units[unit_idx])
}
}
#[cfg(unix)]
fn read_mount_table() -> Result<Vec<(String, String, String)>> {
let file = File::open("/proc/mounts").or_else(|_| File::open("/etc/mtab"))?;
let reader = BufReader::new(file);
let mut mounts = Vec::new();
for line in reader.lines() {
let line = line?;
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let device = parts[0].to_string();
let mount_point = parts[1].to_string();
let fs_type = parts[2].to_string();
mounts.push((device, mount_point, fs_type));
}
}
Ok(mounts)
}
#[cfg(windows)]
mod win_df {
use super::*;
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::PathBuf;
#[link(name = "kernel32")]
extern "system" {
fn GetLogicalDriveStringsW(n_buffer_length: u32, lp_buffer: *mut u16) -> u32;
fn GetDiskFreeSpaceExW(
lp_directory_name: *const u16,
lp_free_bytes_available: *mut u64,
lp_total_number_of_bytes: *mut u64,
lp_total_free_bytes: *mut u64,
) -> i32;
fn GetVolumeInformationW(
lp_root_path_name: *const u16,
lp_volume_name_buffer: *mut u16,
n_volume_name_size: u32,
lp_volume_serial_number: *mut u32,
lp_max_component_len: *mut u32,
lp_file_system_flags: *mut u32,
lp_file_system_name_buffer: *mut u16,
n_file_system_name_size: u32,
) -> i32;
}
fn to_wide_nul(path: &OsStr) -> Vec<u16> {
path.encode_wide().chain(std::iter::once(0)).collect()
}
pub fn read_mount_table() -> Result<Vec<(String, String, String)>> {
let mut buf = vec![0u16; 512];
let n = unsafe { GetLogicalDriveStringsW(buf.len() as u32, buf.as_mut_ptr()) };
if n == 0 {
return Err(eyre!("GetLogicalDriveStringsW failed: {}", std::io::Error::last_os_error()));
}
let buf = &buf[..n as usize];
let mut mounts = Vec::new();
let mut start = 0;
while start < buf.len() {
let end = buf[start..].iter().position(|&c| c == 0).map(|i| start + i).unwrap_or(buf.len());
if end == start {
break;
}
let wide: Vec<u16> = buf[start..end].to_vec();
let root = OsString::from_wide(&wide)
.to_string_lossy()
.into_owned();
if !root.is_empty() {
let trimmed = root.trim_end_matches('\\').to_string();
let device = if trimmed.len() == 2 && trimmed.ends_with(':') {
trimmed.clone()
} else {
trimmed.clone()
};
let fs_type = volume_fs_type(&root)?;
mounts.push((device, root, fs_type));
}
start = end + 1;
}
Ok(mounts)
}
pub fn volume_fs_type(root: &str) -> Result<String> {
let mut root_pb = PathBuf::from(root);
if !root.ends_with('\\') {
root_pb.push("");
}
let wide = to_wide_nul(root_pb.as_os_str());
let mut fs_name = vec![0u16; 32];
let ok = unsafe {
GetVolumeInformationW(
wide.as_ptr(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
fs_name.as_mut_ptr(),
fs_name.len() as u32,
)
};
if ok == 0 {
return Ok("unknown".to_string());
}
let len = fs_name.iter().position(|&x| x == 0).unwrap_or(fs_name.len());
Ok(String::from_utf16_lossy(&fs_name[..len]))
}
pub fn disk_space_for_root(root: &str) -> Result<(u64, u64, u64)> {
let mut root_pb = PathBuf::from(root);
if !root.ends_with('\\') {
root_pb.push("");
}
let wide = to_wide_nul(root_pb.as_os_str());
let mut avail = 0u64;
let mut total = 0u64;
let mut free = 0u64;
let ok = unsafe {
GetDiskFreeSpaceExW(
wide.as_ptr(),
&mut avail,
&mut total,
&mut free,
)
};
if ok == 0 {
return Err(eyre!("GetDiskFreeSpaceExW for '{}': {}", root, std::io::Error::last_os_error()));
}
Ok((total, free, avail))
}
pub fn volume_root_for_path(path: &Path) -> PathBuf {
use std::path::Prefix;
let mut c = path.components();
match c.next() {
Some(std::path::Component::Prefix(pref)) => match pref.kind() {
Prefix::VerbatimDisk(d) | Prefix::Disk(d) => {
PathBuf::from(format!("{}:\\", char::from(d)))
}
_ => path.to_path_buf(),
},
_ => {
let s = path.to_string_lossy();
if s.len() >= 2 && s.as_bytes()[1] == b':' {
PathBuf::from(format!("{}\\", &s[..2]))
} else {
path.to_path_buf()
}
}
}
}
}
#[cfg(unix)]
fn get_fs_stats(path: &str, inodes: bool) -> Result<DfFsStat> {
let stats = posix_statfs(path)
.map_err(|e| match e {
PosixError::Io(ioe) => eyre!("{}: {}", path, ioe),
PosixError::InvalidArgument(m) => eyre!("{}: {}", path, m),
PosixError::NotFound => eyre!("{}: not found", path),
})?;
Ok(statfs_to_df(stats, inodes))
}
#[cfg(windows)]
fn get_fs_stats(path: &str, inodes: bool) -> Result<DfFsStat> {
let _ = inodes;
let (total, free, avail) = win_df::disk_space_for_root(path)?;
let bsize = 512u64;
Ok(statfs_to_df_windows(total, free, avail, bsize, false))
}
#[cfg(windows)]
fn read_mount_table() -> Result<Vec<(String, String, String)>> {
win_df::read_mount_table()
}
fn calculate_percent(used: u64, total: u64) -> u64 {
if total == 0 {
0
} else {
let mut used_scaled = used as u128;
let mut total_scaled = total as u128;
while total_scaled >= u64::MAX as u128 / 100 {
used_scaled >>= 1;
total_scaled >>= 1;
}
let used_scaled_u64 = used_scaled as u64;
let total_scaled_u64 = total_scaled as u64;
(used_scaled_u64 * 100 + total_scaled_u64 / 2) / total_scaled_u64
}
}
fn df_resolve_display_block_size(options: &DfOptions) -> u64 {
let mut display_block_size = if options.kilo_bytes {
1024
} else if options.mega_bytes {
1024 * 1024
} else if let Some(bs) = options.block_size {
bs
} else if env::var("POSIXLY_CORRECT").is_ok() && !options.kilo_bytes {
512
} else {
1024
};
if options.inodes {
display_block_size = 1;
}
display_block_size
}
fn df_resolve_mounts(options: &DfOptions) -> Result<Vec<(String, String, String)>> {
if options.filesystems.is_empty() {
return read_mount_table();
}
let all_mounts = read_mount_table()?;
let mut filtered = Vec::new();
for fs in &options.filesystems {
let path = if Path::new(fs).is_absolute() {
Path::new(fs).to_path_buf()
} else {
env::current_dir()?.join(fs)
};
let mut found = None;
for (device, mount_point, fs_type) in &all_mounts {
#[cfg(unix)]
if fs == device || fs == mount_point {
// Exact match on device name or mount point
found = Some((device.clone(), mount_point.clone(), fs_type.clone()));
break;
}
#[cfg(unix)]
if path.exists() && path.starts_with(mount_point) {
// Path exists and is under this mount point
// Choose the longest (most specific) matching mount point
if found.as_ref().map(|(_, mp, _): &(_, String, _)| mp.len() < mount_point.len()).unwrap_or(true) {
found = Some((device.clone(), mount_point.clone(), fs_type.clone()));
}
}
#[cfg(windows)]
{
let fsl = fs.to_lowercase();
let mpl = mount_point.to_lowercase();
let pl = path.to_string_lossy().to_lowercase();
if fsl == device.to_lowercase() || fsl == mpl || pl.starts_with(&mpl.trim_end_matches('\\'))
|| pl.starts_with(&format!("{}\\", mpl.trim_end_matches('\\')).to_lowercase())
{
found = Some((device.clone(), mount_point.clone(), fs_type.clone()));
break;
}
}
}
if let Some(mount) = found {
filtered.push(mount);
} else {
#[cfg(windows)]
{
let root = win_df::volume_root_for_path(&path);
let mut root_str = root.to_string_lossy().into_owned();
if !root_str.ends_with('\\') {
root_str.push('\\');
}
let fs_type = win_df::volume_fs_type(&root_str).unwrap_or_else(|_| "unknown".to_string());
let dev = root_str.trim_end_matches('\\').to_string();
filtered.push((dev, root_str, fs_type));
}
#[cfg(not(windows))]
return Err(eyre!("{}: can't find mount point", fs));
}
}
Ok(filtered)
}
fn format_block_size_label(size: u64) -> String {
const M: u64 = 1024 * 1024;
const G: u64 = 1024 * 1024 * 1024;
match size {
1 => String::new(),
1024 => "1K".to_string(),
M => "1M".to_string(),
G => "1G".to_string(),
_ => format!("{}", size),
}
}
fn print_df_table_header(options: &DfOptions, display_block_size: u64) {
if options.posix_format {
print!("Filesystem ");
if options.show_fs_type {
print!("Type ");
}
print!(
"{}-blocks Used Available Capacity Mounted on\n",
format_block_size_label(display_block_size)
);
} else {
print!("Filesystem ");
if options.show_fs_type {
print!("Type ");
}
if options.human_readable || options.human_decimal {
print!(" Size Used Avail Use% ");
} else if display_block_size == 1 {
print!(" Inodes IUsed IFree IUse% ");
} else {
print!(
" {}-blocks Used Available Use% ",
format_block_size_label(display_block_size)
);
}
println!("Mounted on");
}
}
pub fn run(options: DfOptions) -> Result<()> {
#[cfg(windows)]
if options.inodes {
return Err(eyre!("df: inode counts (-i) are not supported on Windows"));
}
let display_block_size = df_resolve_display_block_size(&options);
let mounts = df_resolve_mounts(&options)?;
print_df_table_header(&options, display_block_size);
for (device, mount_point, fs_type) in mounts {
#[cfg(unix)]
if !options.all_filesystems && device == "rootfs" {
continue;
}
if let Some(filter_type) = &options.fs_type_filter {
if &fs_type != filter_type {
continue;
}
}
let stats = match get_fs_stats(&mount_point, options.inodes) {
Ok(stats) => stats,
Err(e) => {
// Skip inaccessible mount points when iterating all mounts.
// Only fail when user explicitly requested this filesystem.
if options.filesystems.is_empty() {
eprintln!("df: {}", e);
continue;
} else {
return Err(e);
}
}
};
if stats.f_blocks == 0 && !options.all_filesystems && options.filesystems.is_empty() {
continue;
}
let used_blocks = stats.f_blocks - stats.f_bfree;
let percent_used = calculate_percent(used_blocks, used_blocks + stats.f_bavail);
let device_display = if device.len() > 20 && !options.posix_format {
format!("{}\n{:20}", device, "")
} else {
format!("{:20}", device)
};
print!("{}", device_display);
if options.show_fs_type {
let type_display = if fs_type.len() > 10 && !options.posix_format {
format!(" {}\n{:31}", fs_type, "")
} else {
format!(" {:10}", fs_type)
};
print!("{}", type_display);
}
let total_display = format_blocks(
stats.f_blocks,
stats.f_bsize,
display_block_size,
options.human_readable,
options.human_decimal,
options.posix_format,
);
print!(" {:>9}", total_display);
let used_display = format_blocks(
used_blocks,
stats.f_bsize,
display_block_size,
options.human_readable,
options.human_decimal,
options.posix_format,
);
print!(" {:>9}", used_display);
let avail_display = format_blocks(
stats.f_bavail,
stats.f_bsize,
display_block_size,
options.human_readable,
options.human_decimal,
options.posix_format,
);
print!(" {:>9}", avail_display);
print!(" {:>3}%", percent_used);
println!(" {}", mount_point);
}
Ok(())
}