sonar

Scan files at memory bandwidth speed.
Log | Files | Refs

commit 956dcddfe266dbecb479a07c5901848031460c80
parent d8d1bb6166350dff4811a0c6e942ca9e10ad1b21
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Sun, 20 Sep 2026 14:26:05 -0300

main: parse the command line with core:flags

Every flag was matched by hand against a prefix, which meant the usage line was
written separately from the flags it described and drifted from them twice: once
losing --walk-workers entirely, once keeping a default the code no longer used.

A struct with usage tags is the one description now, and the help text is printed
from it. The flags, their names and the exit codes are unchanged; UNIX style also
accepts `--flag value` beside `--flag=value`, which the old parser refused. The
home directory default moves to the temp allocator so it is freed with everything
else.

Diffstat:
Mmain.odin | 100++++++++++++++++++++++++++++++++++---------------------------------------------
1 file changed, 43 insertions(+), 57 deletions(-)

diff --git a/main.odin b/main.odin @@ -2,7 +2,7 @@ sonar: find out what is using the disk. Usage: - sonar [drive-or-image] default C: + sonar [drive-or-image] [flags] -h lists the flags Reads the volume's MFT directly and prints the largest files and directories. Reading a live volume needs an elevated prompt. @@ -13,8 +13,6 @@ import "core:debug/trace" import "core:flags" import "core:fmt" import "core:os" -import "core:strconv" -import "core:strings" import "core:time" @(require) import "debug" @@ -25,6 +23,26 @@ import "walk" // Largest N files/folders to print. TOP_N :: 20 +PROGRAM :: "sonar" + +/* +What the command line can set. + +Read in UNIX style, so `--flag`, `--flag=value` and `--flag value` all work, and `-h` +prints this. A count left at zero means the reader chooses its own, rather than +having a second set of defaults to keep in step here. +*/ +Options :: struct { + target: string `args:"pos=0" usage:"Drive, device, image or directory to scan. Defaults to your home directory."`, + live: bool `usage:"Draw the answer as it is found rather than once at the end."`, + buffered: bool `usage:"Read the volume through the system cache rather than around it."`, + no_skip: bool `usage:"Read runs of dead MFT records rather than jumping over them."`, + min_skip: int `usage:"Shortest run of dead records worth jumping over, in bytes."`, + workers: int `usage:"MFT readers to run at once."`, + walk_workers: int `usage:"Directory readers to run at once."`, + chunk: int `usage:"Bytes per volume read."`, +} + main :: proc() { os.exit(run()) } @@ -42,50 +60,28 @@ run :: proc() -> int { context.assertion_failure_proc = trace.assertion_failure_proc - target := os.user_home_dir(context.allocator) or_else "/" - opts: ntfs.Read_Options - wcfg: walk.Config - live := false + opt: Options + if err := flags.parse(&opt, os.args[1:], .Unix); err != nil { + // print_errors writes usage for a help request and the reason for anything + // else, so the only decision left here is what to exit with. + flags.print_errors(Options, err, PROGRAM, .Unix) + _, asked_for_help := err.(flags.Help_Request) + return 0 if asked_for_help else 1 + } - for arg in os.args[1:] { - switch arg { - case "-h", "--help", "/?": - fmt.println( - "usage: sonar [drive] [--live] [--buffered] [--no-skip] [--min-skip=N] [--workers=N] [--walk-workers=N] [--chunk=N]", - ) - return 0 - case "--live": - live = true - case "--buffered": - opts.io_mode = .Buffered - case "--no-skip": - opts.min_skip = -1 - case: - // --min-skip=<bytes> tunes how long a run of dead records must be before - // breaking the sequential read to jump over it pays for itself. - ok := true - switch { - case strings.has_prefix(arg, "--min-skip="): - opts.min_skip, ok = number(arg) - case strings.has_prefix(arg, "--workers="): - opts.workers, ok = number(arg) - case strings.has_prefix(arg, "--walk-workers="): - wcfg.workers, ok = number(arg) - case strings.has_prefix(arg, "--chunk="): - opts.chunk_size, ok = number(arg) - case strings.has_prefix(arg, "-"): - // Anything else starting with a dash is a mistyped option. Taking it - // for the target would report that the volume cannot be found. - fmt.eprintfln("error: unknown option %s", arg) - return 1 - case: - target = arg - } - if !ok { - fmt.eprintfln("error: %s needs a number", arg) - return 1 - } - } + target := opt.target + if target == "" { + target = os.user_home_dir(context.temp_allocator) or_else "/" + } + opts := ntfs.Read_Options { + chunk_size = opt.chunk, + io_mode = .Buffered if opt.buffered else .Unbuffered, + // Negative reads every record; zero leaves the reader its own default. + min_skip = -1 if opt.no_skip else opt.min_skip, + workers = opt.workers, + } + wcfg := walk.Config { + workers = opt.walk_workers, } // Resolve once, so the reader is chosen from what the OS says rather than from @@ -114,7 +110,7 @@ run :: proc() -> int { return 1 } - if live { + if opt.live { if choice.engine != .Mft { fmt.eprintln("error: live mode needs the MFT reader") return 1 @@ -197,16 +193,6 @@ run :: proc() -> int { return 0 } -// The value of a `--name=value` option. -@(private = "file") -number :: proc(arg: string) -> (int, bool) { - i := strings.index_byte(arg, '=') - if i < 0 { - return 0, false - } - return strconv.parse_int(arg[i + 1:]) -} - /* Credit every node's bytes to each of its ancestors.