- platform::mode documents the on-disk mode-bit layout (bits 31..19 = d a l T L D p S u g c t ?, low 9 bits = permissions). - The snapshot module documents the LEB128 / zig-zag varints and the RFC 3339 JSON timestamps, and states that snapshots written by mapsize 1.x load unchanged. - Renames: GoTime -> JsonTime, go_duration -> fmt_duration, fixture testdata/go.msz -> testdata/v1.2.0.msz (a snapshot written by mapsize 1.2.0), loads_go_snapshot -> loads_1_2_snapshot. - --log / --log-level help says they are accepted and ignored.
248 lines
8.4 KiB
Rust
248 lines
8.4 KiB
Rust
//! mapsize: interactive terminal disk usage analyser.
|
|
|
|
use mapsize::app::{self, Config};
|
|
use mapsize::inventory::SizeMode;
|
|
use mapsize::{brand, config, scan, snapshot, textutil};
|
|
use std::path::Path;
|
|
|
|
/// Flags taking a value, written `-f v`, `-f=v`, `--f v` or `--f=v`.
|
|
const VALUE_FLAGS: &[&str] = &[
|
|
"workers",
|
|
"follow-symlinks",
|
|
"depth",
|
|
"top",
|
|
"largest-files",
|
|
"save",
|
|
"snapshot",
|
|
"load",
|
|
"scan",
|
|
"theme",
|
|
"color",
|
|
"log",
|
|
"log-level",
|
|
"exclude",
|
|
];
|
|
/// Boolean flags (`-f`, `-f=false`).
|
|
const BOOL_FLAGS: &[&str] = &[
|
|
"one-file-system",
|
|
"x",
|
|
"read-only",
|
|
"no-ui",
|
|
"json",
|
|
"csv",
|
|
"duplicates",
|
|
"compare",
|
|
"si",
|
|
"apparent",
|
|
"no-mouse",
|
|
"version",
|
|
];
|
|
|
|
fn usage() {
|
|
let n = brand::NAME;
|
|
let e = brand::SNAPSHOT_EXT;
|
|
eprint!(
|
|
"{n} — interactive terminal disk usage analyser
|
|
|
|
Usage:
|
|
{n} [flags] [PATH] scan PATH (default .) interactively
|
|
{n} [flags] SNAPSHOT{e} open a saved snapshot
|
|
{n} --compare OLD NEW compare two snapshots
|
|
{n} PATH --top 50 | --json | --csv | --no-ui --save FILE
|
|
|
|
Flags:
|
|
--workers MODE concurrency: conservative, balanced, aggressive or a number
|
|
--follow-symlinks MODE none, same-filesystem or all
|
|
-x, --one-file-system do not cross filesystem boundaries
|
|
--exclude PATTERN exclude PATTERN (glob on name, or path prefix if it contains a separator); repeatable
|
|
--read-only disable all modifying actions
|
|
--no-ui scan without the interactive interface and print a summary
|
|
--json, --csv print the inventory as JSON / CSV
|
|
--depth N maximum depth for --json/--csv (-1 = unlimited)
|
|
--top N print the N largest entries of the root
|
|
--largest-files N print the N largest files
|
|
--duplicates find duplicate files (non-interactive)
|
|
--save FILE save a snapshot to FILE after scanning (alias --snapshot)
|
|
--load FILE open a saved snapshot FILE instead of scanning
|
|
--scan PATH path to scan (alternative to a positional argument)
|
|
--compare compare two snapshots: --compare OLD NEW
|
|
--si use SI units (kB, MB) instead of IEC (KiB, MiB)
|
|
--apparent size by logical (apparent) size instead of disk usage
|
|
--theme NAME colour theme: default, dark, high-contrast, mono
|
|
--color MODE colour support: auto, truecolor, 256, 16, none
|
|
--no-mouse disable mouse support
|
|
--log FILE accepted for compatibility; ignored
|
|
--log-level LEVEL accepted for compatibility (error, warn, info, debug, trace); ignored
|
|
--version print version and exit
|
|
"
|
|
);
|
|
}
|
|
|
|
fn main() {
|
|
std::process::exit(run(std::env::args_os()
|
|
.skip(1)
|
|
.map(|a| a.to_string_lossy().into_owned())
|
|
.collect()));
|
|
}
|
|
|
|
fn fail(msg: impl std::fmt::Display) -> i32 {
|
|
eprintln!("{}: {msg}", brand::NAME);
|
|
2
|
|
}
|
|
|
|
fn run(args: Vec<String>) -> i32 {
|
|
let (settings, cfg_err) = config::load();
|
|
let mut vals: std::collections::HashMap<&str, String> = std::collections::HashMap::new();
|
|
let mut bools: std::collections::HashMap<&str, bool> = std::collections::HashMap::new();
|
|
let mut excludes = settings.excludes.clone();
|
|
let mut positional = Vec::new();
|
|
let mut it = args.into_iter();
|
|
while let Some(a) = it.next() {
|
|
if a == "--" {
|
|
positional.extend(it.by_ref());
|
|
break;
|
|
}
|
|
let Some(flag) = a
|
|
.strip_prefix("--")
|
|
.or_else(|| a.strip_prefix('-'))
|
|
.filter(|f| !f.is_empty() && a != "-")
|
|
else {
|
|
positional.push(a);
|
|
continue;
|
|
};
|
|
let (name, inline) = match flag.split_once('=') {
|
|
Some((n, v)) => (n, Some(v.to_string())),
|
|
None => (flag, None),
|
|
};
|
|
if name == "h" || name == "help" {
|
|
usage();
|
|
return 0;
|
|
}
|
|
if let Some(&n) = BOOL_FLAGS.iter().find(|&&f| f == name) {
|
|
match inline.as_deref().map(str::parse::<bool>) {
|
|
None => bools.insert(n, true),
|
|
Some(Ok(v)) => bools.insert(n, v),
|
|
Some(Err(_)) => {
|
|
return fail(format!(
|
|
"invalid boolean value {:?} for -{n}",
|
|
inline.unwrap()
|
|
))
|
|
}
|
|
};
|
|
} else if let Some(&n) = VALUE_FLAGS.iter().find(|&&f| f == name) {
|
|
let Some(v) = inline.or_else(|| it.next()) else {
|
|
return fail(format!("flag needs an argument: -{n}"));
|
|
};
|
|
if n == "exclude" {
|
|
excludes.push(v);
|
|
} else {
|
|
vals.insert(n, v);
|
|
}
|
|
} else {
|
|
usage();
|
|
return fail(format!("flag provided but not defined: -{name}"));
|
|
}
|
|
}
|
|
let b = |n: &str| bools.get(n).copied();
|
|
let v = |n: &str| vals.get(n).cloned();
|
|
let int = |n: &str, d: i64| -> Result<i64, String> {
|
|
v(n).map_or(Ok(d), |s| {
|
|
s.parse()
|
|
.map_err(|_| format!("invalid value {s:?} for flag -{n}: parse error"))
|
|
})
|
|
};
|
|
|
|
if b("version") == Some(true) {
|
|
println!(
|
|
"{} {} ({}/{})",
|
|
brand::NAME,
|
|
brand::VERSION,
|
|
std::env::consts::OS,
|
|
std::env::consts::ARCH
|
|
);
|
|
return 0;
|
|
}
|
|
if let Some(e) = cfg_err {
|
|
eprintln!("{}: warning: config: {e} (using defaults)", brand::NAME);
|
|
}
|
|
textutil::set_si(b("si").unwrap_or(settings.units == "si"));
|
|
if let Some(l) = v("log-level") {
|
|
if !["error", "warn", "info", "debug", "trace"].contains(&l.as_str()) {
|
|
return fail(format!("invalid --log-level {l:?}"));
|
|
}
|
|
}
|
|
// ponytail: --log and --log-level are accepted for compatibility and ignored; there is no debug logging.
|
|
|
|
let (depth, top, largest) = match (int("depth", -1), int("top", 0), int("largest-files", 0)) {
|
|
(Ok(d), Ok(t), Ok(l)) => (d, t.max(0) as usize, l.max(0) as usize),
|
|
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => return fail(e),
|
|
};
|
|
let mut cfg = Config {
|
|
no_ui: b("no-ui").unwrap_or(false),
|
|
json: b("json").unwrap_or(false),
|
|
csv: b("csv").unwrap_or(false),
|
|
depth,
|
|
top,
|
|
largest_files: largest,
|
|
duplicates: b("duplicates").unwrap_or(false),
|
|
read_only: b("read-only").unwrap_or(false),
|
|
theme: v("theme").unwrap_or_else(|| settings.theme.clone()),
|
|
mouse: !b("no-mouse").unwrap_or(!settings.mouse),
|
|
color: v("color").unwrap_or_else(|| "auto".into()),
|
|
load: v("load").unwrap_or_default(),
|
|
save: v("save").or_else(|| v("snapshot")).unwrap_or_default(),
|
|
size_mode: if b("apparent").unwrap_or(settings.size_mode == "logical") {
|
|
SizeMode::Logical
|
|
} else {
|
|
SizeMode::Allocated
|
|
},
|
|
..Config::new(settings.clone())
|
|
};
|
|
if let Some(p) = v("scan") {
|
|
positional.insert(0, p);
|
|
}
|
|
if b("compare") == Some(true) {
|
|
if positional.len() != 2 {
|
|
return fail("--compare needs exactly two snapshot files");
|
|
}
|
|
cfg.compare = Some((positional[0].clone(), positional[1].clone()));
|
|
} else {
|
|
if positional.len() > 1 {
|
|
return fail("only one path may be scanned at a time");
|
|
}
|
|
if positional.len() == 1
|
|
&& cfg.load.is_empty()
|
|
&& snapshot::is_snapshot(Path::new(&positional[0]))
|
|
{
|
|
cfg.load = positional.remove(0);
|
|
} else {
|
|
cfg.paths = positional;
|
|
}
|
|
}
|
|
let workers = match scan::workers(&v("workers").unwrap_or_else(|| settings.workers.clone())) {
|
|
Ok(w) => w,
|
|
Err(e) => return fail(e),
|
|
};
|
|
let follow = match scan::parse_follow(
|
|
&v("follow-symlinks").unwrap_or_else(|| settings.follow_symlinks.clone()),
|
|
) {
|
|
Ok(f) => f,
|
|
Err(e) => return fail(e),
|
|
};
|
|
cfg.scan_opts = scan::Options {
|
|
workers,
|
|
follow,
|
|
one_file_system: b("one-file-system").unwrap_or(settings.one_file_system)
|
|
|| b("x").unwrap_or(false),
|
|
excludes,
|
|
..Default::default()
|
|
};
|
|
match app::run(cfg) {
|
|
Ok(()) => 0,
|
|
Err(e) => {
|
|
eprintln!("{}: {}", brand::NAME, textutil::sanitize(&e.to_string()));
|
|
1
|
|
}
|
|
}
|
|
}
|