commits
Add a List action alongside Compress/Extract, wire --list / -l to
short-circuit through get_job (no output slot needed), and introduce
a default Compressor::list that bails for stream codecs.
Real implementations land on Tar (iterates Archive entries), Zip
(iterates ZipArchive::file_names — tempfiles non-seekable input),
and Pipeline (reuses the multi-stage pipe plumbing from extract:
outer layers decompress through an in-memory pipe into the innermost
container format, which lists).
Pipelines such as tar.gz therefore list correctly; single stream
codecs like gzip correctly fail with a specific message.
Covered by four integration tests in tests/list.rs.
Add hidden `cmprss completions <shell>` and `cmprss manpage`
subcommands that emit to stdout. Packagers can pipe the output into
standard system locations during install:
cmprss completions bash > /usr/share/bash-completion/completions/cmprss
cmprss completions zsh > /usr/share/zsh/site-functions/_cmprss
cmprss manpage > /usr/share/man/man1/cmprss.1
Built on clap_complete and clap_mangen so the output tracks the real
clap derive — no hand-maintained scripts to drift.
Previously get_job hard-bailed on any existing -o target, and a trailing
existing file in the positional io_list silently fell through into the
input list (a long-standing footgun). --force now:
* relaxes the bail on explicit -o targets
* takes a trailing existing file as the output (overwrite) instead of
pulling it into the input list
Three integration tests in tests/force.rs cover refusal-without-force,
overwrite-via-`-o`, and overwrite-via-positional-output.
Action now cleanly models only the two real operations (Compress,
Extract); 'not yet resolved' is expressed as Option::None during
inference inside get_job. guess_from_filenames returns
Result<(Box<dyn Compressor>, Action)> and bails directly in the
previously-ambiguous cases instead of leaking Unknown upward.
Knock-on: main.rs's dispatch no longer needs its catch-all 'Unknown
action requested' bail — exhaustive matching on the two-variant enum.
Replace the tangled pair of nested matches with three explicit branches
keyed on how the output is determined:
1. Explicit output path (-o or trailing io_list item)
2. Stdout pipe
3. No output — invent a filename from the resolved compressor+action
Extract finalize_with_output, finalize_without_output, and
fill_missing_from_io as focused helpers. Each is ~linear; the previous
interleaving of output construction with as-side-effect action/compressor
updates is gone.
Behavior preserved; tests stay green.
Pull four leaf helpers out of get_job so its top now reads as a
phased pipeline:
let action = action_from_flags(args);
let (inputs, output) = partition_paths(args, action)?;
let input = resolve_input(inputs, args)?;
// ...resolve output + compressor + action
No behavior change; the remaining output/action inference logic is
unchanged and will be attacked in a follow-up.
Every backend with a compression level had the same 3-line pattern in
its new(): instantiate validator, pull args.level_args.level.level,
clamp. Fold that into LevelArgs::resolve(&validator) so the six
level-aware backends (gzip, xz, bzip2, zstd, brotli, lzma) get cleaner
single-expression constructors.
main.rs was 683 lines — 95% of it was inference heuristics (get_job,
guess_from_filenames, get_compressor_from_filename, expand_shortcut_ext,
get_input_filename, get_path), Action, Job, and their unit tests. Move
all of that to a new job module, leaving main.rs at 96 lines containing
only the CmprssArgs/Format CLI shell and the thin command dispatch.
Behavior is unchanged; this is a pure reorganization commit.
FILE/DIRECTORY → File/Directory. Matches Rust's enum variant naming
convention and clippy's screaming_snake_case_ctor lint.
Every single-stream backend (gzip, xz, bzip2, zstd, lz4, brotli, snappy,
lzma) had the same ~40 lines of boilerplate per compress/extract for
resolving CmprssInput into a Read, resolving CmprssOutput into a Write,
and rejecting directory inputs/outputs. Consolidate that into a new
stream module and have each backend call open_input, open_output, and
guard_file_output.
Net -323 lines across the eight backends. Also normalizes the
directory-rejection error messages and extends the directory-input
guard to xz and bzip2, which previously only surfaced the lower-level
'Is a directory' error from File::open.
GITHUB_TOKEN events don't trigger downstream workflows, so the
release-plz workflow now dispatches publish.yml directly after
creating a release.
chore: release v0.3.0
Add MultiLevelCompressor that chains compressors together using
threaded pipelines for formats like tar.gz. Includes PipeReader/
PipeWriter for inter-stage streaming, Reader/Writer variants on
CmprssInput/CmprssOutput, and multi-level filename detection in
get_compressor_from_filename.
Add a List action alongside Compress/Extract, wire --list / -l to
short-circuit through get_job (no output slot needed), and introduce
a default Compressor::list that bails for stream codecs.
Real implementations land on Tar (iterates Archive entries), Zip
(iterates ZipArchive::file_names — tempfiles non-seekable input),
and Pipeline (reuses the multi-stage pipe plumbing from extract:
outer layers decompress through an in-memory pipe into the innermost
container format, which lists).
Pipelines such as tar.gz therefore list correctly; single stream
codecs like gzip correctly fail with a specific message.
Covered by four integration tests in tests/list.rs.
Add hidden `cmprss completions <shell>` and `cmprss manpage`
subcommands that emit to stdout. Packagers can pipe the output into
standard system locations during install:
cmprss completions bash > /usr/share/bash-completion/completions/cmprss
cmprss completions zsh > /usr/share/zsh/site-functions/_cmprss
cmprss manpage > /usr/share/man/man1/cmprss.1
Built on clap_complete and clap_mangen so the output tracks the real
clap derive — no hand-maintained scripts to drift.
Previously get_job hard-bailed on any existing -o target, and a trailing
existing file in the positional io_list silently fell through into the
input list (a long-standing footgun). --force now:
* relaxes the bail on explicit -o targets
* takes a trailing existing file as the output (overwrite) instead of
pulling it into the input list
Three integration tests in tests/force.rs cover refusal-without-force,
overwrite-via-`-o`, and overwrite-via-positional-output.
Action now cleanly models only the two real operations (Compress,
Extract); 'not yet resolved' is expressed as Option::None during
inference inside get_job. guess_from_filenames returns
Result<(Box<dyn Compressor>, Action)> and bails directly in the
previously-ambiguous cases instead of leaking Unknown upward.
Knock-on: main.rs's dispatch no longer needs its catch-all 'Unknown
action requested' bail — exhaustive matching on the two-variant enum.
Replace the tangled pair of nested matches with three explicit branches
keyed on how the output is determined:
1. Explicit output path (-o or trailing io_list item)
2. Stdout pipe
3. No output — invent a filename from the resolved compressor+action
Extract finalize_with_output, finalize_without_output, and
fill_missing_from_io as focused helpers. Each is ~linear; the previous
interleaving of output construction with as-side-effect action/compressor
updates is gone.
Behavior preserved; tests stay green.
Pull four leaf helpers out of get_job so its top now reads as a
phased pipeline:
let action = action_from_flags(args);
let (inputs, output) = partition_paths(args, action)?;
let input = resolve_input(inputs, args)?;
// ...resolve output + compressor + action
No behavior change; the remaining output/action inference logic is
unchanged and will be attacked in a follow-up.
Every backend with a compression level had the same 3-line pattern in
its new(): instantiate validator, pull args.level_args.level.level,
clamp. Fold that into LevelArgs::resolve(&validator) so the six
level-aware backends (gzip, xz, bzip2, zstd, brotli, lzma) get cleaner
single-expression constructors.
main.rs was 683 lines — 95% of it was inference heuristics (get_job,
guess_from_filenames, get_compressor_from_filename, expand_shortcut_ext,
get_input_filename, get_path), Action, Job, and their unit tests. Move
all of that to a new job module, leaving main.rs at 96 lines containing
only the CmprssArgs/Format CLI shell and the thin command dispatch.
Behavior is unchanged; this is a pure reorganization commit.
Every single-stream backend (gzip, xz, bzip2, zstd, lz4, brotli, snappy,
lzma) had the same ~40 lines of boilerplate per compress/extract for
resolving CmprssInput into a Read, resolving CmprssOutput into a Write,
and rejecting directory inputs/outputs. Consolidate that into a new
stream module and have each backend call open_input, open_output, and
guard_file_output.
Net -323 lines across the eight backends. Also normalizes the
directory-rejection error messages and extends the directory-input
guard to xz and bzip2, which previously only surfaced the lower-level
'Is a directory' error from File::open.