gander / a JSON process list for scripts
The numbers the TUI shows, as one JSON document per sample — for cron jobs, monitors, and anything that would otherwise be parsing fixed-width ps output.
Use ps for one-shot column output; for that it remains the right tool. Use gander --json when you want memory percentages, full command lines and byte counts as data, on Linux, macOS and FreeBSD. For live CPU percentages, take at least two samples in the same invocation; the first sample has no CPU baseline.
ps prints one snapshot with space-aligned columns, which is why half the scripts on every server contain an awk invocation that breaks the day a column is renamed. Its CPU figure is the process's lifetime average, not what it is doing now, and getting both comm and the full command line without truncation takes flags that differ between procps and Darwin.
top has a batch mode (-b), but what it batches is the rendered curses screen — a text scrape of a display, not data. htop has no batch mode at all: there is no way to ask it for one sample of anything. btop is in the same position, and so is bottom.
That gap is why shell scripts end up re-reading /proc by hand, once per platform. gander's TUI and its batch mode read the same backends — /proc on Linux, sysctl/Mach/libproc on macOS, sysctl/libkvm on FreeBSD — so the numbers a script sees are the numbers the interactive view shows.
gander --json # one JSON sample, then exit
gander --json --limit 5 # at most five rows
gander --json --sort PERCENT_MEM # sorted like the TUI's F6
gander --json -F "postgres|nginx" # fixed-string filter, not regex
gander -n 60 -d 1000 --json # sixty samples, about one second apart
One invocation prints one JSON array of process rows and exits. With -n N it prints N arrays, one after another — a stream of documents, so jq -s 'add' flattens a run into one list. Sorting and filtering happen before JSON is produced, so --limit 5 means five rows, not a five-row slice of the whole table downloaded and thrown away.
cpu_percent needs two consecutive samples of a process. Every invocation starts with a fresh sampler, so its first sample reports zero CPU for every row; a newly observed process also starts at zero in later samples. Memory figures do not need this baseline. To get a CPU sample for a script or cron job, take two samples one second apart and keep only the second array:
gander -n 2 -d 1000 --json | jq -s '.[1]'
With jq, the top five consumers by memory, tab-separated:
gander --json --sort PERCENT_MEM --limit 5 |
jq -r '.[] | [.pid, .mem_percent, .rss, .cmdline] | @tsv'
The same row set, merged across a one-minute run, to catch what spikes and disappears:
gander -n 60 -d 1000 --json | jq -s 'add |
group_by(.comm) | map({comm: .[0].comm, max_rss: (map(.rss) | max)}) |
sort_by(-.max_rss) | .[0:10]'
A real row, from macOS:
{"pid":407,"ppid":1,"uid":0,"comm":"loginwindow","state":"S",
"cpu_percent":0.00,"mem_percent":0.18,"rss":15613952,
"vsize":446144643072,"nice":0,"priority":0,"threads":6,
"elapsed":479475,"is_thread":false,"identity":407,
"cmdline":"/System/Library/CoreServices/loginwindow.app/Contents/MacOS/loginwindow console",
"memory":null}
| Field | Meaning |
|---|---|
pid, ppid, uid | The ids, as the kernel reports them. |
comm | The executable's base name, the column the TUI sorts by default. |
cmdline | The full command line with arguments, never truncated. This is the field ps makes you fight for. |
state | The kernel's state character: R running, S sleeping, Z zombie, and so on. |
cpu_percent | CPU usage over the interval between consecutive samples of the process, not its lifetime average. Zero until a previous sample is available, including every row in the first sample of an invocation. |
mem_percent | Resident memory as a percentage of total physical memory at the time of the sample; available in the first sample too. |
rss, vsize | Resident and virtual size, in bytes. Not "KB, probably", which is the unit ambiguity that makes parsed ps output untrustworthy. |
nice, priority, threads | Scheduling figures as the kernel reports them. |
elapsed | Seconds since the process started. |
is_thread | True for rows that are threads of another process rather than processes; with --threads the per-thread rows are emitted too. |
identity | A numeric row identifier: the PID for a process row, or a thread-specific ID for a thread row. It distinguishes threads that share a PID on macOS; it is not a container identifier and is not null. |
memory | Platform-dependent nested memory and resource detail. Null on macOS and FreeBSD; on Linux, individual values are null when their optional reads were not requested or the data was unavailable. |
Sorting accepts the TUI's column names — PERCENT_CPU, PERCENT_MEM, M_RESIDENT, PID and the rest. These are not the JSON keys: to sort by rss, use --sort M_RESIDENT. -F matches fixed, case-insensitive strings against the command line, separated by |; it is deliberately not a regex, because a filter that runs on a schedule should not be one character away from matching everything.
It is not a monitoring agent. Each invocation is self-contained: no daemon, no storage, no dashboard, no dependency. A cron job can append gander --json to a file for memory snapshots; for CPU measurements, use the two-sample command above instead. The file remains readable by jq. If you need continuous collection with retention and alerting, point the tool you already run at this output rather than expecting gander to grow a server.
It is also not ps. One-shot column output, process-group selection and session ids are ps's strengths, and a script that already works with ps has no reason to change. The batch mode is for the cases where ps runs out: live percentages, byte-true sizes, and numbers that match what the interactive view shows.
brew install pandasir/tap/gander # macOS
cargo install gander-bin # any platform with Rust
Linux packages for Debian, Ubuntu, Fedora and RHEL are on a signed repository, and every release ships archives for macOS (Apple Silicon and Intel), Linux (x86_64 and arm64) and FreeBSD. See all install options.
There is no AUR package yet: AUR account registration has been closed since the June 2026 malicious-packages incident and there is no manual process to create one. Arch users build from the same PKGBUILD that would have been published — see Install.