Skip to main content

stuffr_cli/
cli.rs

1//! The `stuffr` command's argument surface.
2//!
3//! Pulled out of `main.rs` and re-exported from `lib.rs` so an integration
4//! test can ask clap itself — via [`clap::CommandFactory`] — what flags and
5//! subcommands exist, rather than keeping a second, hand-maintained list that
6//! the examples page could silently drift out of sync with. `main.rs` still
7//! owns everything that isn't the argument shape: parsing, dispatch, and the
8//! examples text itself.
9
10use clap::{Args, Parser, Subcommand};
11
12#[derive(Parser)]
13#[command(
14    name = "stuffr",
15    version,
16    about = "Universal compression and archive toolkit"
17)]
18pub struct Cli {
19    #[command(subcommand)]
20    pub command: Option<Command>,
21    /// Print detailed usage examples covering every feature, then exit.
22    #[arg(long)]
23    pub examples: bool,
24}
25
26#[derive(Subcommand)]
27pub enum Command {
28    /// Compress a file, or collect several into an archive.
29    Pack {
30        /// Input paths, or `-` for stdin.
31        ///
32        /// More than one path needs an output that names a CONTAINER (`-o
33        /// bundle.tar`, or `--format tar`): a codec compresses one stream and
34        /// has nowhere to put a second.
35        #[arg(required = true, num_args = 1.., value_name = "PATHS")]
36        paths: Vec<String>,
37        /// Output path. Defaults to INPUT plus the format's extension.
38        #[arg(short, long)]
39        output: Option<String>,
40        /// Use this format instead of inferring one from the output name.
41        #[arg(long)]
42        format: Option<String>,
43        /// Compression level. gzip accepts 0-9.
44        #[arg(long)]
45        level: Option<i32>,
46        /// Overwrite an existing output.
47        #[arg(long)]
48        force: bool,
49        /// Skip the fsync that makes the output durable before it is published.
50        #[arg(long)]
51        no_sync: bool,
52        /// Use a weak fallback encoder that this build ships in place of the
53        /// format's real one. Worse ratio, and buffers the whole input.
54        #[arg(long)]
55        allow_weak_encoder: bool,
56        /// Encode with N worker threads. 0 means auto-detect.
57        ///
58        /// OMITTING this flag is not the same as passing 0: without it, stuffr
59        /// encodes single-threaded, so the same input always produces the same
60        /// bytes on any machine. Multi-threaded xz and zstd split the input
61        /// per worker, so their output depends on the worker count.
62        #[arg(long)]
63        threads: Option<usize>,
64        /// Use the full detected CPU budget, uncapped.
65        ///
66        /// Lifts the CPU cap only — `--memory-limit` still binds. Turbo means
67        /// "use my cores", not "ignore the OOM killer".
68        #[arg(long)]
69        turbo: bool,
70        /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
71        ///
72        /// On encode this bounds the worker count. Defaults to 25% of
73        /// available RAM, honouring cgroup limits.
74        #[arg(long, value_name = "SIZE")]
75        memory_limit: Option<String>,
76        /// Fail (exit 4) if anything was approximated or lost.
77        ///
78        /// On the pack side that means something the walk met and could not
79        /// store — a socket, an undecodable name, an unreadable directory —
80        /// or an entry shape this container has none of, such as a directory
81        /// or a symlink in an `ar`.
82        #[arg(long)]
83        strict_fidelity: bool,
84    },
85    /// Decompress a file, or extract an archive's entries.
86    Unpack {
87        /// Input path, or `-` for stdin.
88        input: String,
89        /// Entries to extract; every entry when none are given.
90        ///
91        /// An exact entry name, or a directory selecting everything beneath
92        /// it. Needs -C, which is what asks for entry-aware extraction.
93        #[arg(value_name = "PATTERNS")]
94        patterns: Vec<String>,
95        /// Extract the entry at this position instead of naming it. Repeatable.
96        ///
97        /// 0-BASED, counting in archive order — the same number `stuffr list`
98        /// prints in its first column. This is how you reach an entry whose
99        /// name you cannot type: `Makefile` and `makefile` from a
100        /// case-sensitive filesystem on a case-insensitive one. Needs -C,
101        /// like PATTERNS, and cannot be combined with them — they are two
102        /// ways of saying the same thing.
103        ///
104        /// It does NOT reach a record a zip's own index shadows: `unpack`
105        /// counts the entries the index enumerates, so a record a later one
106        /// hides is not at any index here — it has no number at all. `stuffr
107        /// salvage --list` finds those, and `salvage --index N` selects them
108        /// by SCAN position, which is a different numbering.
109        #[arg(long, value_name = "N")]
110        index: Vec<usize>,
111        /// Extract the archive's entries into this directory.
112        ///
113        /// Absolute entry paths, `..` traversal and symlinks escaping this
114        /// directory are refused (exit 7), never silently sanitised.
115        #[arg(short = 'C', long, value_name = "DIR")]
116        directory: Option<String>,
117        /// Restore archived permission bits exactly, not masked by the umask.
118        ///
119        /// By default an entry's mode is masked by the umask, as `tar` does
120        /// for a non-root user. setuid, setgid and the sticky bit are never
121        /// restored, and a directory that existed before the run keeps its
122        /// own mode either way. Needs -C.
123        #[arg(short = 'p', long)]
124        same_permissions: bool,
125        /// Fail (exit 4) if anything was approximated or lost.
126        ///
127        /// On extraction that means an entry whose mode or mtime could not be
128        /// restored, or one skipped for having no shape on disk.
129        #[arg(long)]
130        strict_fidelity: bool,
131        /// Output path. Defaults to INPUT with its extension removed.
132        #[arg(short, long)]
133        output: Option<String>,
134        /// Overwrite an existing output.
135        #[arg(long)]
136        force: bool,
137        /// Use this format instead of detecting one. Required for formats with
138        /// no magic bytes and no extension.
139        #[arg(long)]
140        format: Option<String>,
141        /// Refuse a decode expanding by more than this ratio.
142        #[arg(long)]
143        max_ratio: Option<u64>,
144        /// Skip the fsync that makes the output durable before it is published.
145        #[arg(long)]
146        no_sync: bool,
147        /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
148        ///
149        /// Defaults to 25% of available RAM, honouring cgroup limits.
150        #[arg(long, value_name = "SIZE")]
151        memory_limit: Option<String>,
152    },
153    /// Change a file's compression or its archive format, without unpacking
154    /// it to disk.
155    ///
156    /// Codec mode — the container is kept (`.tar.gz` -> `.tar.zst`, `.gz` ->
157    /// `.xz`, `.tar.xz` -> `.tar`): the container, or the plain stream, is
158    /// copied byte for byte and only the codec around it changes.
159    ///
160    /// Entry mode — the container changes (`.tgz` -> `.zip`, `.zip` ->
161    /// `.tar.xz`): every entry is read from the source and written into the
162    /// target under pack's rules, and what the target cannot hold (a
163    /// directory or symlink in an ar, a device anywhere) is skipped with a
164    /// fidelity warning. A hard link stays a link into tar and is written
165    /// as a copy of its target elsewhere, or skipped, named, when no copy
166    /// is available.
167    ///
168    /// The input's chain is detected by content, never by its name; the
169    /// target's comes from OUT's name or --format.
170    Convert {
171        /// Input path, or `-` for stdin.
172        input: String,
173        /// Output path; the same as -o. Give one or the other, not both.
174        #[arg(value_name = "OUT")]
175        output_pos: Option<String>,
176        /// Output path, or `-` for stdout (which needs --format).
177        #[arg(short, long)]
178        output: Option<String>,
179        /// Use this target format instead of inferring one from the output
180        /// name. A codec or a container; one the name contradicts is refused.
181        #[arg(long)]
182        format: Option<String>,
183        /// Compression level for the target codec. gzip accepts 0-9.
184        ///
185        /// Also what makes converting a file to the chain it already has
186        /// (`.tar.gz` -> `.tar.gz`) a re-encode rather than "nothing to
187        /// convert".
188        #[arg(long)]
189        level: Option<i32>,
190        /// Encode with N worker threads. 0 means auto-detect.
191        ///
192        /// OMITTING this flag is not the same as passing 0: without it, stuffr
193        /// encodes single-threaded, so the same input always produces the same
194        /// bytes on any machine. Multi-threaded xz and zstd split the input
195        /// per worker, so their output depends on the worker count.
196        #[arg(long)]
197        threads: Option<usize>,
198        /// Use the full detected CPU budget, uncapped.
199        ///
200        /// Lifts the CPU cap only — `--memory-limit` still binds. Turbo means
201        /// "use my cores", not "ignore the OOM killer".
202        #[arg(long)]
203        turbo: bool,
204        /// Use a weak fallback encoder that this build ships in place of the
205        /// format's real one. Worse ratio, and buffers the whole input.
206        #[arg(long)]
207        allow_weak_encoder: bool,
208        /// Overwrite an existing output.
209        #[arg(long)]
210        force: bool,
211        /// Fail (exit 4) if anything was approximated or lost.
212        ///
213        /// In codec mode the container is copied byte for byte and nothing
214        /// is lost. In entry mode every skipped entry or missing field is a
215        /// warning, and this turns any of them into exit 4 — with the output
216        /// still written, as with pack.
217        #[arg(long)]
218        strict_fidelity: bool,
219        /// Refuse a decode expanding by more than this ratio.
220        #[arg(long)]
221        max_ratio: Option<u64>,
222        /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
223        ///
224        /// Bounds every source codec layer's decoder and the encoder's worker
225        /// count. Defaults to 25% of available RAM, honouring cgroup limits.
226        #[arg(long, value_name = "SIZE")]
227        memory_limit: Option<String>,
228        /// Skip the fsync that makes the output durable before it is published.
229        #[arg(long)]
230        no_sync: bool,
231    },
232    /// Decompress a file, or stream an archive's entries, to stdout.
233    Cat {
234        /// Input path, or `-` for stdin.
235        input: String,
236        /// Entries whose bytes to stream, in archive order.
237        ///
238        /// An exact entry name, or a directory selecting everything beneath
239        /// it. Naming one is what asks for entry-aware streaming; without
240        /// any, `cat` decodes the input as a single stream.
241        #[arg(value_name = "PATTERNS")]
242        patterns: Vec<String>,
243        /// Stream the entry at this position instead of naming it. Repeatable.
244        ///
245        /// 0-BASED, counting in archive order — the same number `stuffr list`
246        /// prints in its first column. `stuffr cat a.zip --index 6 > out.sh`
247        /// is how you pull out an entry whose name is ambiguous or
248        /// untypeable. Like PATTERNS, naming one asks for entry-aware
249        /// streaming; the two cannot be combined.
250        #[arg(long, value_name = "N")]
251        index: Vec<usize>,
252        /// Use this format instead of detecting one. Required for formats with
253        /// no magic bytes and no extension.
254        #[arg(long)]
255        format: Option<String>,
256        /// Refuse a decode expanding by more than this ratio.
257        #[arg(long)]
258        max_ratio: Option<u64>,
259        /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
260        ///
261        /// Defaults to 25% of available RAM, honouring cgroup limits.
262        #[arg(long, value_name = "SIZE")]
263        memory_limit: Option<String>,
264        /// Exit 4 if anything was approximated or ignored (the output is still written).
265        ///
266        /// In entry mode the verdict covers only what was read; `stuffr test
267        /// --strict-fidelity` checks the whole file.
268        #[arg(long)]
269        strict_fidelity: bool,
270    },
271    /// Identify a stream, decoding at most its first 4 KiB.
272    Info {
273        /// Input path, or `-` for stdin.
274        input: String,
275        /// Emit machine-readable JSON instead of the human-readable report.
276        #[arg(long)]
277        json: bool,
278        /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
279        ///
280        /// Defaults to 25% of available RAM, honouring cgroup limits.
281        #[arg(long, value_name = "SIZE")]
282        memory_limit: Option<String>,
283    },
284    /// List the formats this build contains.
285    Formats,
286    /// Compare compression formats on your own files: size, ratio, and
287    /// compress and decompress time for each one.
288    ///
289    /// Every format runs under the same conditions: the same input (all in
290    /// memory, or all streamed from disk when it is over half of
291    /// --memory-limit), the same thread setting, and one untimed warm-up
292    /// read first. Only codec work is timed, except the temp file's writes
293    /// for a codec whose worst-case output is too large for memory; such a
294    /// row is marked. A format that fails is a row
295    /// marked failed, not an exit code. Each row is then run once more,
296    /// untimed, in a child process to measure its peak compress and
297    /// decompress memory (resident; see --no-memory). Timings need a
298    /// release build.
299    Bench(BenchArgs),
300    /// Create compatibility symlinks to this binary in a directory: every
301    /// compatibility name (`gzip`, `bzip2`, `xz`, `zstd`, `zip`, `unzip` and
302    /// more). Opt-in, never automatic.
303    InstallLinks {
304        /// Directory to put the links in. It must already exist.
305        #[arg(long, value_name = "DIR")]
306        dir: std::path::PathBuf,
307        /// Only these names, comma-separated or repeated. Default: all.
308        #[arg(long, value_name = "NAME", value_delimiter = ',')]
309        names: Vec<String>,
310        /// Replace an existing file or foreign link of the same name.
311        #[arg(long)]
312        force: bool,
313        /// Remove stuffr's links instead of creating them.
314        #[arg(long)]
315        remove: bool,
316        /// Say what would change, and change nothing.
317        #[arg(long)]
318        dry_run: bool,
319    },
320    /// List an archive's entries without extracting.
321    ///
322    /// The first column is the entry's 0-based position in archive order,
323    /// which is what `cat --index` and `unpack --index` select by.
324    List {
325        /// Archive path, or `-` for stdin.
326        input: String,
327        /// Emit machine-readable JSON instead of one entry per line.
328        #[arg(long)]
329        json: bool,
330        /// Refuse a codec layer beneath the container expanding by more
331        /// than this ratio (e.g. a `.tar.gz` bomb).
332        #[arg(long)]
333        max_ratio: Option<u64>,
334        /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
335        ///
336        /// Bounds the codec layer beneath the container, which `--max-ratio`
337        /// cannot see: a pure xz/lzma/lzip decoder sizes its dictionary from
338        /// a value the file declares in its own header, before producing any
339        /// output. Defaults to 25% of available RAM, honouring cgroup limits.
340        #[arg(long, value_name = "SIZE")]
341        memory_limit: Option<String>,
342        /// Fail (exit 4) if the read approximated or lost anything.
343        ///
344        /// A listing can be incomplete — a zip whose index shadows records
345        /// enumerates fewer entries than the archive declares — so `list`
346        /// gates on fidelity for the same reason `test` and `unpack -C` do.
347        #[arg(long)]
348        strict_fidelity: bool,
349    },
350    /// List what a folder holds that stuffr can open, or an archive's entries.
351    ///
352    /// Given a folder, or nothing (the current folder): one row per file
353    /// stuffr can open, newest first, as `FORMAT  SIZE  DATE  NAME`, plus
354    /// every subfolder as a `[FOLDER]` row. A file is listed when its header
355    /// checks out behind a readable format's signature; a compressed file's
356    /// inner layer is decoded from its first 4 KiB to name it. Apple disk
357    /// images are recognised and not listed. A footer `N of M files shown,
358    /// K not openable` counts the rest. A file or subfolder that cannot be
359    /// read is named on stderr and the exit is 4.
360    ///
361    /// Given an archive or compressed file, or `-`: exactly `stuffr list`,
362    /// with its options. `files` is a second name for the folder listing
363    /// only.
364    #[command(alias = "files")]
365    Ls(LsArgs),
366    /// Verify every entry's integrity without extracting.
367    Test {
368        /// Archive path, or `-` for stdin.
369        input: String,
370        /// Refuse a codec layer beneath the container expanding by more
371        /// than this ratio (e.g. a `.tar.gz` bomb).
372        #[arg(long)]
373        max_ratio: Option<u64>,
374        /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
375        ///
376        /// Bounds the codec layer beneath the container, which `--max-ratio`
377        /// cannot see: a pure xz/lzma/lzip decoder sizes its dictionary from
378        /// a value the file declares in its own header, before producing any
379        /// output. Defaults to 25% of available RAM, honouring cgroup limits.
380        #[arg(long, value_name = "SIZE")]
381        memory_limit: Option<String>,
382        /// Fail (exit 4) if the read approximated or lost anything.
383        #[arg(long)]
384        strict_fidelity: bool,
385    },
386    /// Recover what a damaged zip's scan can prove, even where its central
387    /// directory alone would show less — or would wrongly unify two records
388    /// that happen to share a name.
389    ///
390    /// The one recovery-biased verb in this tool. `list`, `cat`, `unpack` and
391    /// `test` all stay exactly as uncompromising as they already are, and it
392    /// is salvage's willingness to accept less-than-proven content that buys
393    /// them that freedom.
394    Salvage {
395        /// Input path.
396        ///
397        /// Salvage scans back and forth over the archive, which needs real
398        /// random access — unlike every other verb here, this one does not
399        /// accept `-` for stdin.
400        input: String,
401        /// Print every record the scan found, including ones this build
402        /// never writes (a shadow, a skipped partial, an unverified entry).
403        ///
404        /// A record that repeats an earlier record's name is marked one of
405        /// two ways, and they mean different things. `[shadowed: dup of #N]`
406        /// is a MEASUREMENT: name, declared length and checksum all agree
407        /// with record N, so nothing is lost by not writing this one.
408        /// `[name collision: #N uses this name too]` is the weaker fact:
409        /// the name repeats but the content was not proven identical, so
410        /// both records are worth having and `-C` writes the second one as
411        /// `name.salvaged-N`, never over the first. It is the marker
412        /// `stuffr list`'s own "repeat a name … and are shadowed" fidelity
413        /// warning corresponds to.
414        ///
415        /// `stuffr list` on the same archive can print FEWER rows than this:
416        /// a record a later one shadows is invisible to `list`, which only
417        /// ever sees what the central directory's own name index kept. Works
418        /// with no destination at all (neither `-C` nor `-o`) — listing is
419        /// not recovering, and this is the diagnostic half of the feature:
420        /// seeing what an archive holds without extracting anything.
421        #[arg(long)]
422        list: bool,
423        /// Recover entries into this directory.
424        ///
425        /// One of `-C`, `-o` or `--list` is required. Mutually exclusive
426        /// with `-o`: a directory tree and one named file are two different
427        /// destinations.
428        ///
429        /// A damaged archive can hold two records under one name. The
430        /// second one recovered lands as `name.salvaged-N`, where N is its
431        /// scan position, rather than over the first — both records exist,
432        /// both are worth having, and the run exits 4 so a script knows a
433        /// name had to be changed.
434        #[arg(short = 'C', long, value_name = "DIR")]
435        directory: Option<String>,
436        /// Recover exactly one entry's bytes to this file, naming it with
437        /// exactly one `--index N`.
438        ///
439        /// A `Partial` entry still lands under `FILE.partial`, never `FILE`
440        /// itself — the same rule `-C` follows, for the same reason: a
441        /// truncated file under the name you asked for would be
442        /// indistinguishable from a whole one. Refused (exit 2) with more
443        /// than one `--index`, or together with `-C`.
444        #[arg(short = 'o', long, value_name = "FILE")]
445        output: Option<String>,
446        /// Recover only the record at this SCAN position instead of every
447        /// one found. Repeatable with `-C`; `-o` takes exactly one.
448        ///
449        /// A scan position is 0-based and counts every record the scan
450        /// found, shadowed ones included — a DIFFERENT numbering from
451        /// `stuffr list`'s own index, which never counts a shadowed record at
452        /// all. On an archive where a name is duplicated, `list --index 6`
453        /// and `salvage --index 6` can name different entries, or `list`'s
454        /// may not exist at all. Always a scan position, never a list index.
455        #[arg(long, value_name = "N")]
456        index: Vec<usize>,
457        /// What to do with an entry whose payload could not be fully proven:
458        /// `keep` (default) writes it as `name.partial`, never under its
459        /// real name; `skip` recovers nothing for it; `ask` has no terminal
460        /// to ask from at this layer and is read conservatively as `skip`.
461        #[arg(long, value_name = "POLICY")]
462        partial: Option<String>,
463        /// Raise the structural ceiling on a single entry's declared length,
464        /// e.g. 8G. A declaration over the ceiling is refused before
465        /// anything is allocated on its say-so, either way.
466        #[arg(long, value_name = "SIZE")]
467        max_entry: Option<String>,
468        /// Demand proof: skip every partial entry regardless of `--partial`,
469        /// and every unattested one (a format with no checksum and no header
470        /// self-check, whose entries are otherwise written under their real
471        /// names). The strict path every other verb in this tool already
472        /// stands on, applied here on request instead of by default.
473        #[arg(long)]
474        strict: bool,
475        /// Use this format instead of detecting one.
476        ///
477        /// Selects among the formats this build's salvage can attempt to
478        /// scan — `zip`, `tar`, `ar`, `cpio`, `arc`, `zoo`, `lha`, `arj` — not
479        /// merely accepted or rejected as in an earlier stage. Naming one
480        /// this build has no salvage scanner for is refused at exit 3,
481        /// exactly as an undetected archive of that same format would be.
482        /// `ar`'s scanner walks members in order and cannot cross a damaged
483        /// header; it says on stderr where it stopped.
484        ///
485        /// Scanners exist for: zip, arc, zoo, lha, arj, tar, cpio, ar.
486        #[arg(long)]
487        format: Option<String>,
488    },
489}
490
491/// The arguments of `stuffr ls` (alias `files`).
492#[derive(Args, Debug)]
493pub struct LsArgs {
494    /// A folder (default: the current one), or an archive path or `-` to
495    /// list its entries as `stuffr list` does.
496    #[arg(value_name = "PATH")]
497    pub path: Option<String>,
498    /// Folder: also list inside subfolders, one block per folder that holds
499    /// something stuffr can open. Symlinked folders are not followed.
500    #[arg(short, long)]
501    pub recursive: bool,
502    /// Folder: include names starting with `.`.
503    #[arg(short, long)]
504    pub all: bool,
505    /// Folder: sort by name instead of newest first.
506    #[arg(short = 'n', long = "by-name")]
507    pub by_name: bool,
508    /// Emit machine-readable JSON: for a folder one object
509    /// `{entries, shown, skipped, warnings}`; for an archive what `list
510    /// --json` prints.
511    #[arg(long)]
512    pub json: bool,
513    /// Archive only: refuse a codec layer beneath the container expanding
514    /// by more than this ratio (e.g. a `.tar.gz` bomb).
515    #[arg(long)]
516    pub max_ratio: Option<u64>,
517    /// Archive only: cap the memory stuffr will ask for, e.g. 512M or 2G.
518    ///
519    /// Bounds the codec layer beneath the container, which `--max-ratio`
520    /// cannot see: a pure xz/lzma/lzip decoder sizes its dictionary from
521    /// a value the file declares in its own header, before producing any
522    /// output. Defaults to 25% of available RAM, honouring cgroup limits.
523    #[arg(long, value_name = "SIZE")]
524    pub memory_limit: Option<String>,
525    /// Archive only: fail (exit 4) if the read approximated or lost
526    /// anything.
527    #[arg(long)]
528    pub strict_fidelity: bool,
529}
530
531/// The arguments of `stuffr bench`.
532#[derive(Args, Debug)]
533pub struct BenchArgs {
534    /// Files to benchmark, or quoted glob patterns (`'logs/*.log'`).
535    ///
536    /// A path that exists is used as is; one that does not and contains
537    /// `*`, `?` or `[` is expanded, its matches sorted. A directory is
538    /// refused: name its files instead. So is a pipe or device: bench needs
539    /// each file's size, so write a pipe to a file first. A file it cannot
540    /// open, a dangling symlink or an unreadable directory on the way is
541    /// refused too, naming the path.
542    #[arg(required = true, num_args = 1.., value_name = "INPUTS")]
543    pub inputs: Vec<String>,
544    /// Only these formats, comma-separated or repeated. Default: every codec
545    /// this build can write with its real encoder.
546    #[arg(long, value_name = "FORMAT", value_delimiter = ',')]
547    pub formats: Vec<String>,
548    /// Run every level of each format instead of its default level only.
549    #[arg(long)]
550    pub levels: bool,
551    /// Compress each file as its own stream instead of all of them as one.
552    #[arg(long)]
553    pub per_file: bool,
554    /// Compress with N worker threads. 0 means auto-detect.
555    ///
556    /// Threads apply to compression only: decompression is always timed
557    /// single-threaded. OMITTING this flag is not the same as passing 0:
558    /// without it, stuffr compresses single-threaded, so the same input
559    /// always produces the same bytes on any machine. Multi-threaded xz and
560    /// zstd split the input per worker, so their output depends on the
561    /// worker count. bench ignores STUFFR_THREADS: threads come only from
562    /// this flag.
563    #[arg(long)]
564    pub threads: Option<usize>,
565    /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
566    ///
567    /// Input over half of it is streamed from disk rather than held in
568    /// memory. Each codec gets the output destination its own worst case
569    /// allows: a codec whose worst-case output is too large for the rest
570    /// writes to a temp file, whose writes then count in its compress time,
571    /// and its row is marked (`*`; `output: disk`). Defaults to 25%
572    /// of available RAM, honouring cgroup limits.
573    #[arg(long, value_name = "SIZE")]
574    pub memory_limit: Option<String>,
575    /// Time each row N times and report the medians.
576    #[arg(long, value_name = "N", default_value_t = 1,
577          value_parser = clap::value_parser!(u32).range(1..))]
578    pub repeat: u32,
579    /// Emit one machine-readable JSON object instead of the table.
580    #[arg(long, conflicts_with = "csv")]
581    pub json: bool,
582    /// Emit CSV, one line per row, instead of the table.
583    #[arg(long)]
584    pub csv: bool,
585    /// Print the table alone: no input line, no winners, no progress.
586    #[arg(short = 'q', long, conflicts_with_all = ["json", "csv"])]
587    pub table_only: bool,
588    /// Skip the memory columns: by default each row is run once more,
589    /// untimed, in a child process to measure its peak compress and
590    /// decompress memory (resident set).
591    #[arg(long)]
592    pub no_memory: bool,
593}