this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli): add --force / -f to overwrite existing output

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.

+111 -3
+8 -2
src/job.rs
··· 49 49 let mut output: Option<PathBuf> = match &args.output { 50 50 Some(output) => { 51 51 let path = PathBuf::from(output); 52 - if path.try_exists()? && !path.is_dir() { 53 - bail!("Specified output path already exists"); 52 + if !args.force && path.try_exists()? && !path.is_dir() { 53 + bail!("Specified output path already exists (use --force to overwrite)"); 54 54 } 55 55 Some(path) 56 56 } ··· 69 69 // Only treat an existing directory as the output when the user 70 70 // hinted extraction. In Compress/Unknown, we keep it as another 71 71 // input — this matches e.g. `cmprss tar dir1/ dir2/`. 72 + output = Some(path); 73 + io_list.pop(); 74 + } else if !path.is_dir() && args.force { 75 + // With --force, a trailing existing file is taken as the output 76 + // (to overwrite). Without --force we fall through to treating it 77 + // as another input. 72 78 output = Some(path); 73 79 io_list.pop(); 74 80 }
+4
src/utils.rs
··· 55 55 /// Ignore stdout when inferring I/O 56 56 #[arg(long)] 57 57 pub ignore_stdout: bool, 58 + 59 + /// Overwrite the output path if it already exists. 60 + #[arg(short, long)] 61 + pub force: bool, 58 62 } 59 63 60 64 /// Trait for validating compression levels for different compressors
+4 -1
tests/common/mod.rs
··· 1 + // Each `tests/*.rs` file compiles as its own crate, so any helper not used by 2 + // a particular file shows up as dead code in that crate. Allow it globally. 3 + #![allow(dead_code)] 4 + 1 5 use assert_fs::prelude::*; 2 6 use predicates::prelude::*; 3 7 ··· 14 18 Ok(assert_fs::TempDir::new()?) 15 19 } 16 20 17 - #[allow(dead_code)] 18 21 pub fn create_persistent_working_dir() -> Result<assert_fs::TempDir, Box<dyn std::error::Error>> { 19 22 Ok(assert_fs::TempDir::new()?.into_persistent()) 20 23 }
+95
tests/force.rs
··· 1 + use assert_cmd::prelude::*; 2 + use assert_fs::prelude::*; 3 + use predicates::prelude::*; 4 + use std::process::Command; 5 + 6 + mod common; 7 + use common::*; 8 + 9 + mod force { 10 + use super::*; 11 + 12 + /// Without --force, cmprss must refuse to overwrite an existing -o target. 13 + /// 14 + /// ``` bash 15 + /// echo 'sentinel' > output.gz 16 + /// cmprss gzip -o output.gz input.txt # fails, output.gz untouched 17 + /// ``` 18 + #[test] 19 + fn refuses_overwrite_without_force() -> Result<(), Box<dyn std::error::Error>> { 20 + let working_dir = create_working_dir()?; 21 + let input = working_dir.child("input.txt"); 22 + input.write_str("the real payload")?; 23 + 24 + let output = working_dir.child("output.gz"); 25 + output.write_str("sentinel — must not be clobbered")?; 26 + 27 + let mut cmd = Command::cargo_bin("cmprss")?; 28 + cmd.current_dir(&working_dir) 29 + .args(["gzip", "-o", "output.gz", "input.txt"]); 30 + cmd.assert() 31 + .failure() 32 + .stderr(predicate::str::contains("already exists")); 33 + 34 + // The existing file must be byte-for-byte unchanged. 35 + output.assert("sentinel — must not be clobbered"); 36 + Ok(()) 37 + } 38 + 39 + /// With --force, cmprss overwrites an existing -o target. 40 + #[test] 41 + fn overwrites_with_force_explicit_output() -> Result<(), Box<dyn std::error::Error>> { 42 + let working_dir = create_working_dir()?; 43 + let input = working_dir.child("input.txt"); 44 + input.write_str("the real payload")?; 45 + 46 + let output = working_dir.child("output.gz"); 47 + output.write_str("sentinel — should be clobbered")?; 48 + 49 + let mut cmd = Command::cargo_bin("cmprss")?; 50 + cmd.current_dir(&working_dir) 51 + .args(["gzip", "--force", "-o", "output.gz", "input.txt"]); 52 + cmd.assert().success(); 53 + 54 + // The file now contains real gzip output — confirm by round-tripping. 55 + let mut extract = Command::cargo_bin("cmprss")?; 56 + extract 57 + .current_dir(&working_dir) 58 + .args(["gzip", "--extract", "output.gz", "output.txt"]); 59 + extract.assert().success(); 60 + working_dir.child("output.txt").assert("the real payload"); 61 + Ok(()) 62 + } 63 + 64 + /// With --force, a trailing existing file in the positional io_list is 65 + /// taken as the output and overwritten. Without --force, that trailing 66 + /// file gets mistakenly pulled into the input list. 67 + #[test] 68 + fn overwrites_with_force_positional_output() -> Result<(), Box<dyn std::error::Error>> { 69 + let working_dir = create_working_dir()?; 70 + let input = working_dir.child("input.txt"); 71 + input.write_str("the real payload")?; 72 + 73 + let output = working_dir.child("input.txt.gz"); 74 + output.write_str("stale archive")?; 75 + 76 + let mut cmd = Command::cargo_bin("cmprss")?; 77 + cmd.current_dir(&working_dir) 78 + .args(["gzip", "--force", "input.txt", "input.txt.gz"]); 79 + cmd.assert().success(); 80 + 81 + // Round-trip the new archive to confirm it contains the fresh payload. 82 + let mut extract = Command::cargo_bin("cmprss")?; 83 + extract.current_dir(&working_dir).args([ 84 + "gzip", 85 + "--extract", 86 + "input.txt.gz", 87 + "roundtrip.txt", 88 + ]); 89 + extract.assert().success(); 90 + working_dir 91 + .child("roundtrip.txt") 92 + .assert("the real payload"); 93 + Ok(()) 94 + } 95 + }