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 (`bzip2`, `bunzip2`, `bzcat`,
301 /// `bzip2recover`) to this binary in a directory. Opt-in, never automatic.
302 InstallLinks {
303 /// Directory to put the links in. It must already exist.
304 #[arg(long, value_name = "DIR")]
305 dir: std::path::PathBuf,
306 /// Only these names, comma-separated or repeated. Default: all.
307 #[arg(long, value_name = "NAME", value_delimiter = ',')]
308 names: Vec<String>,
309 /// Replace an existing file or foreign link of the same name.
310 #[arg(long)]
311 force: bool,
312 /// Remove stuffr's links instead of creating them.
313 #[arg(long)]
314 remove: bool,
315 /// Say what would change, and change nothing.
316 #[arg(long)]
317 dry_run: bool,
318 },
319 /// List an archive's entries without extracting.
320 ///
321 /// The first column is the entry's 0-based position in archive order,
322 /// which is what `cat --index` and `unpack --index` select by.
323 List {
324 /// Archive path, or `-` for stdin.
325 input: String,
326 /// Emit machine-readable JSON instead of one entry per line.
327 #[arg(long)]
328 json: bool,
329 /// Refuse a codec layer beneath the container expanding by more
330 /// than this ratio (e.g. a `.tar.gz` bomb).
331 #[arg(long)]
332 max_ratio: Option<u64>,
333 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
334 ///
335 /// Bounds the codec layer beneath the container, which `--max-ratio`
336 /// cannot see: a pure xz/lzma/lzip decoder sizes its dictionary from
337 /// a value the file declares in its own header, before producing any
338 /// output. Defaults to 25% of available RAM, honouring cgroup limits.
339 #[arg(long, value_name = "SIZE")]
340 memory_limit: Option<String>,
341 /// Fail (exit 4) if the read approximated or lost anything.
342 ///
343 /// A listing can be incomplete — a zip whose index shadows records
344 /// enumerates fewer entries than the archive declares — so `list`
345 /// gates on fidelity for the same reason `test` and `unpack -C` do.
346 #[arg(long)]
347 strict_fidelity: bool,
348 },
349 /// List what a folder holds that stuffr can open, or an archive's entries.
350 ///
351 /// Given a folder, or nothing (the current folder): one row per file
352 /// stuffr can open, newest first, as `FORMAT SIZE DATE NAME`, plus
353 /// every subfolder as a `[FOLDER]` row. A file is listed when its header
354 /// checks out behind a readable format's signature; a compressed file's
355 /// inner layer is decoded from its first 4 KiB to name it. Apple disk
356 /// images are recognised and not listed. A footer `N of M files shown,
357 /// K not openable` counts the rest. A file or subfolder that cannot be
358 /// read is named on stderr and the exit is 4.
359 ///
360 /// Given an archive or compressed file, or `-`: exactly `stuffr list`,
361 /// with its options. `files` is a second name for the folder listing
362 /// only.
363 #[command(alias = "files")]
364 Ls(LsArgs),
365 /// Verify every entry's integrity without extracting.
366 Test {
367 /// Archive path, or `-` for stdin.
368 input: String,
369 /// Refuse a codec layer beneath the container expanding by more
370 /// than this ratio (e.g. a `.tar.gz` bomb).
371 #[arg(long)]
372 max_ratio: Option<u64>,
373 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
374 ///
375 /// Bounds the codec layer beneath the container, which `--max-ratio`
376 /// cannot see: a pure xz/lzma/lzip decoder sizes its dictionary from
377 /// a value the file declares in its own header, before producing any
378 /// output. Defaults to 25% of available RAM, honouring cgroup limits.
379 #[arg(long, value_name = "SIZE")]
380 memory_limit: Option<String>,
381 /// Fail (exit 4) if the read approximated or lost anything.
382 #[arg(long)]
383 strict_fidelity: bool,
384 },
385 /// Recover what a damaged zip's scan can prove, even where its central
386 /// directory alone would show less — or would wrongly unify two records
387 /// that happen to share a name.
388 ///
389 /// The one recovery-biased verb in this tool. `list`, `cat`, `unpack` and
390 /// `test` all stay exactly as uncompromising as they already are, and it
391 /// is salvage's willingness to accept less-than-proven content that buys
392 /// them that freedom.
393 Salvage {
394 /// Input path.
395 ///
396 /// Salvage scans back and forth over the archive, which needs real
397 /// random access — unlike every other verb here, this one does not
398 /// accept `-` for stdin.
399 input: String,
400 /// Print every record the scan found, including ones this build
401 /// never writes (a shadow, a skipped partial, an unverified entry).
402 ///
403 /// A record that repeats an earlier record's name is marked one of
404 /// two ways, and they mean different things. `[shadowed: dup of #N]`
405 /// is a MEASUREMENT: name, declared length and checksum all agree
406 /// with record N, so nothing is lost by not writing this one.
407 /// `[name collision: #N uses this name too]` is the weaker fact:
408 /// the name repeats but the content was not proven identical, so
409 /// both records are worth having and `-C` writes the second one as
410 /// `name.salvaged-N`, never over the first. It is the marker
411 /// `stuffr list`'s own "repeat a name … and are shadowed" fidelity
412 /// warning corresponds to.
413 ///
414 /// `stuffr list` on the same archive can print FEWER rows than this:
415 /// a record a later one shadows is invisible to `list`, which only
416 /// ever sees what the central directory's own name index kept. Works
417 /// with no destination at all (neither `-C` nor `-o`) — listing is
418 /// not recovering, and this is the diagnostic half of the feature:
419 /// seeing what an archive holds without extracting anything.
420 #[arg(long)]
421 list: bool,
422 /// Recover entries into this directory.
423 ///
424 /// One of `-C`, `-o` or `--list` is required. Mutually exclusive
425 /// with `-o`: a directory tree and one named file are two different
426 /// destinations.
427 ///
428 /// A damaged archive can hold two records under one name. The
429 /// second one recovered lands as `name.salvaged-N`, where N is its
430 /// scan position, rather than over the first — both records exist,
431 /// both are worth having, and the run exits 4 so a script knows a
432 /// name had to be changed.
433 #[arg(short = 'C', long, value_name = "DIR")]
434 directory: Option<String>,
435 /// Recover exactly one entry's bytes to this file, naming it with
436 /// exactly one `--index N`.
437 ///
438 /// A `Partial` entry still lands under `FILE.partial`, never `FILE`
439 /// itself — the same rule `-C` follows, for the same reason: a
440 /// truncated file under the name you asked for would be
441 /// indistinguishable from a whole one. Refused (exit 2) with more
442 /// than one `--index`, or together with `-C`.
443 #[arg(short = 'o', long, value_name = "FILE")]
444 output: Option<String>,
445 /// Recover only the record at this SCAN position instead of every
446 /// one found. Repeatable with `-C`; `-o` takes exactly one.
447 ///
448 /// A scan position is 0-based and counts every record the scan
449 /// found, shadowed ones included — a DIFFERENT numbering from
450 /// `stuffr list`'s own index, which never counts a shadowed record at
451 /// all. On an archive where a name is duplicated, `list --index 6`
452 /// and `salvage --index 6` can name different entries, or `list`'s
453 /// may not exist at all. Always a scan position, never a list index.
454 #[arg(long, value_name = "N")]
455 index: Vec<usize>,
456 /// What to do with an entry whose payload could not be fully proven:
457 /// `keep` (default) writes it as `name.partial`, never under its
458 /// real name; `skip` recovers nothing for it; `ask` has no terminal
459 /// to ask from at this layer and is read conservatively as `skip`.
460 #[arg(long, value_name = "POLICY")]
461 partial: Option<String>,
462 /// Raise the structural ceiling on a single entry's declared length,
463 /// e.g. 8G. A declaration over the ceiling is refused before
464 /// anything is allocated on its say-so, either way.
465 #[arg(long, value_name = "SIZE")]
466 max_entry: Option<String>,
467 /// Demand proof: skip every partial entry regardless of `--partial`,
468 /// and every unattested one (a format with no checksum and no header
469 /// self-check, whose entries are otherwise written under their real
470 /// names). The strict path every other verb in this tool already
471 /// stands on, applied here on request instead of by default.
472 #[arg(long)]
473 strict: bool,
474 /// Use this format instead of detecting one.
475 ///
476 /// Selects among the formats this build's salvage can attempt to
477 /// scan — `zip`, `tar`, `ar`, `cpio`, `arc`, `zoo`, `lha`, `arj` — not
478 /// merely accepted or rejected as in an earlier stage. Naming one
479 /// this build has no salvage scanner for is refused at exit 3,
480 /// exactly as an undetected archive of that same format would be.
481 /// `ar`'s scanner walks members in order and cannot cross a damaged
482 /// header; it says on stderr where it stopped.
483 ///
484 /// Scanners exist for: zip, arc, zoo, lha, arj, tar, cpio, ar.
485 #[arg(long)]
486 format: Option<String>,
487 },
488}
489
490/// The arguments of `stuffr ls` (alias `files`).
491#[derive(Args, Debug)]
492pub struct LsArgs {
493 /// A folder (default: the current one), or an archive path or `-` to
494 /// list its entries as `stuffr list` does.
495 #[arg(value_name = "PATH")]
496 pub path: Option<String>,
497 /// Folder: also list inside subfolders, one block per folder that holds
498 /// something stuffr can open. Symlinked folders are not followed.
499 #[arg(short, long)]
500 pub recursive: bool,
501 /// Folder: include names starting with `.`.
502 #[arg(short, long)]
503 pub all: bool,
504 /// Folder: sort by name instead of newest first.
505 #[arg(short = 'n', long = "by-name")]
506 pub by_name: bool,
507 /// Emit machine-readable JSON: for a folder one object
508 /// `{entries, shown, skipped, warnings}`; for an archive what `list
509 /// --json` prints.
510 #[arg(long)]
511 pub json: bool,
512 /// Archive only: refuse a codec layer beneath the container expanding
513 /// by more than this ratio (e.g. a `.tar.gz` bomb).
514 #[arg(long)]
515 pub max_ratio: Option<u64>,
516 /// Archive only: cap the memory stuffr will ask for, e.g. 512M or 2G.
517 ///
518 /// Bounds the codec layer beneath the container, which `--max-ratio`
519 /// cannot see: a pure xz/lzma/lzip decoder sizes its dictionary from
520 /// a value the file declares in its own header, before producing any
521 /// output. Defaults to 25% of available RAM, honouring cgroup limits.
522 #[arg(long, value_name = "SIZE")]
523 pub memory_limit: Option<String>,
524 /// Archive only: fail (exit 4) if the read approximated or lost
525 /// anything.
526 #[arg(long)]
527 pub strict_fidelity: bool,
528}
529
530/// The arguments of `stuffr bench`.
531#[derive(Args, Debug)]
532pub struct BenchArgs {
533 /// Files to benchmark, or quoted glob patterns (`'logs/*.log'`).
534 ///
535 /// A path that exists is used as is; one that does not and contains
536 /// `*`, `?` or `[` is expanded, its matches sorted. A directory is
537 /// refused: name its files instead. So is a pipe or device: bench needs
538 /// each file's size, so write a pipe to a file first. A file it cannot
539 /// open, a dangling symlink or an unreadable directory on the way is
540 /// refused too, naming the path.
541 #[arg(required = true, num_args = 1.., value_name = "INPUTS")]
542 pub inputs: Vec<String>,
543 /// Only these formats, comma-separated or repeated. Default: every codec
544 /// this build can write with its real encoder.
545 #[arg(long, value_name = "FORMAT", value_delimiter = ',')]
546 pub formats: Vec<String>,
547 /// Run every level of each format instead of its default level only.
548 #[arg(long)]
549 pub levels: bool,
550 /// Compress each file as its own stream instead of all of them as one.
551 #[arg(long)]
552 pub per_file: bool,
553 /// Compress with N worker threads. 0 means auto-detect.
554 ///
555 /// Threads apply to compression only: decompression is always timed
556 /// single-threaded. OMITTING this flag is not the same as passing 0:
557 /// without it, stuffr compresses single-threaded, so the same input
558 /// always produces the same bytes on any machine. Multi-threaded xz and
559 /// zstd split the input per worker, so their output depends on the
560 /// worker count. bench ignores STUFFR_THREADS: threads come only from
561 /// this flag.
562 #[arg(long)]
563 pub threads: Option<usize>,
564 /// Cap the memory stuffr will ask for, e.g. 512M or 2G.
565 ///
566 /// Input over half of it is streamed from disk rather than held in
567 /// memory. Each codec gets the output destination its own worst case
568 /// allows: a codec whose worst-case output is too large for the rest
569 /// writes to a temp file, whose writes then count in its compress time,
570 /// and its row is marked (`*`; `output: disk`). Defaults to 25%
571 /// of available RAM, honouring cgroup limits.
572 #[arg(long, value_name = "SIZE")]
573 pub memory_limit: Option<String>,
574 /// Time each row N times and report the medians.
575 #[arg(long, value_name = "N", default_value_t = 1,
576 value_parser = clap::value_parser!(u32).range(1..))]
577 pub repeat: u32,
578 /// Emit one machine-readable JSON object instead of the table.
579 #[arg(long, conflicts_with = "csv")]
580 pub json: bool,
581 /// Emit CSV, one line per row, instead of the table.
582 #[arg(long)]
583 pub csv: bool,
584 /// Print the table alone: no input line, no winners, no progress.
585 #[arg(short = 'q', long, conflicts_with_all = ["json", "csv"])]
586 pub table_only: bool,
587 /// Skip the memory columns: by default each row is run once more,
588 /// untimed, in a child process to measure its peak compress and
589 /// decompress memory (resident set).
590 #[arg(long)]
591 pub no_memory: bool,
592}